Better Jellyseerr connection error handling (#933)

## Description
Show Jellyseerr connection errors in settings. If an error occurred
trying to login, in setting it will show "Server error" and clicking
gives more error info and option to remove the server.

### Related issues
Fixes #907 

### Testing
Emulator with simulated errors

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-20 14:29:58 -05:00 • committed by GitHub
parent dfd9acf561
commit 4f8f69c438
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 63 deletions

View file

@ -408,12 +408,13 @@ fun ConfirmDialog(
onConfirm: () -> Unit,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 8.dp,
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
) = BasicDialog(
onDismissRequest = onCancel,
properties = properties,
elevation = elevation,
content = {
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier)
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor)
},
)
@ -427,6 +428,7 @@ fun ConfirmDialogContent(
onCancel: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -446,7 +448,7 @@ fun ConfirmDialogContent(
item {
Text(
text = body,
color = MaterialTheme.colorScheme.onSurface,
color = bodyColor,
)
}
}

View file

@ -73,7 +73,8 @@ class DiscoverMovieViewModel
val canCancelRequest = MutableStateFlow(false)
val userConfig = seerrServerRepository.current.map { it?.config }
val request4kEnabled = seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
val request4kEnabled =
seerrServerRepository.current.map { it?.request4kMovieEnabled ?: false }
init {
init()

View file

@ -64,7 +64,7 @@ class SeerrRequestsViewModel
viewModelScope.launchIO {
backdropService.clearBackdrop()
}
seerrServerRepository.current
seerrServerRepository.connection
.onEach { user ->
state.update { it.copy(requests = DataLoadingState.Loading) }
if (user != null) {

View file

@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.preferences.advancedPreferences
import com.github.damontecres.wholphin.preferences.basicPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.ifElse
@ -90,7 +91,7 @@ fun PreferencesContent(
var showPinFlow by remember { mutableStateOf(false) }
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false)
val seerrConnection by viewModel.seerrConnection.collectAsState()
var seerrDialogMode by remember { mutableStateOf<SeerrDialogMode>(SeerrDialogMode.None) }
var showQuickConnectDialog by remember { mutableStateOf(false) }
@ -385,19 +386,29 @@ fun PreferencesContent(
ClickPreference(
title = stringResource(pref.title),
onClick = {
if (seerrIntegrationEnabled) {
seerrDialogMode = SeerrDialogMode.Remove
} else {
seerrVm.resetStatus()
seerrDialogMode = SeerrDialogMode.Add
}
seerrDialogMode =
when (val conn = seerrConnection) {
is SeerrConnectionStatus.Error -> {
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
}
SeerrConnectionStatus.NotConfigured -> {
SeerrDialogMode.Add
}
is SeerrConnectionStatus.Success -> {
SeerrDialogMode.Remove(
conn.current.server.url,
)
}
}
},
modifier = Modifier,
summary =
if (seerrIntegrationEnabled) {
stringResource(R.string.enabled)
} else {
null
when (seerrConnection) {
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
},
onLongClick = {},
interactionSource = interactionSource,
@ -479,11 +490,11 @@ fun PreferencesContent(
)
}
}
when (seerrDialogMode) {
SeerrDialogMode.Remove -> {
when (val mode = seerrDialogMode) {
is SeerrDialogMode.Remove -> {
ConfirmDialog(
title = stringResource(R.string.remove_seerr_server),
body = currentServer?.url ?: "",
body = mode.serverUrl,
onCancel = { seerrDialogMode = SeerrDialogMode.None },
onConfirm = {
seerrVm.removeServer()
@ -510,6 +521,28 @@ fun PreferencesContent(
)
}
is SeerrDialogMode.Error -> {
val errorStr = stringResource(R.string.voice_error_server)
val body =
remember(mode) {
"""
${mode.serverUrl}
$errorStr: ${mode.ex.localizedMessage}
""".trimIndent()
}
ConfirmDialog(
title = stringResource(R.string.remove_seerr_server),
body = body,
onCancel = { seerrDialogMode = SeerrDialogMode.None },
onConfirm = {
seerrVm.removeServer()
seerrDialogMode = SeerrDialogMode.None
},
bodyColor = MaterialTheme.colorScheme.error,
)
}
SeerrDialogMode.None -> {}
}
}
@ -580,10 +613,17 @@ data class CacheUsage(
val imageDiskUsed: Long,
)
private sealed class SeerrDialogMode {
data object None : SeerrDialogMode()
private sealed interface SeerrDialogMode {
data object None : SeerrDialogMode
data object Add : SeerrDialogMode()
data object Add : SeerrDialogMode
data object Remove : SeerrDialogMode()
data class Remove(
val serverUrl: String,
) : SeerrDialogMode
data class Error(
val serverUrl: String,
val ex: Exception,
) : SeerrDialogMode
}

View file

@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.ui.preferences
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinUser
@ -22,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
@ -46,10 +44,7 @@ class PreferencesViewModel
RememberTabManager by rememberTabManager {
val currentUser get() = serverRepository.currentUser
val seerrEnabled =
seerrServerRepository.currentUser.combine(currentUser.asFlow()) { seerrUser, jellyfinUser ->
seerrUser != null && jellyfinUser != null && seerrUser.jellyfinUserRowId == jellyfinUser.rowId
}
val seerrConnection = seerrServerRepository.connection
private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus