mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
Display server error message on server list (#1741)
## Description On the switch server page, display error message at the bottom for the focused server. This includes unsupported server version. ### Related issues Jellyfin 10.11/12.0 support discussion: #1640 ### Testing Emulator ## Screenshots <img width="1280" height="720" alt="server_error Large" src="https://github.com/user-attachments/assets/1227e7e7-a364-48d9-9a77-cb3f78d134a1" /> ## AI or LLM usage None
This commit is contained in:
parent
4171d0b861
commit
e7b0be2660
7 changed files with 200 additions and 34 deletions
|
|
@ -15,6 +15,8 @@ import org.acra.ReportField
|
|||
import org.acra.config.dialog
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -120,5 +122,7 @@ class WholphinApplication :
|
|||
companion object {
|
||||
lateinit var instance: WholphinApplication
|
||||
private set
|
||||
|
||||
val minimumServerVersion: ServerVersion = Jellyfin.minimumVersion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ fun rememberIdColor(
|
|||
@Composable
|
||||
fun ServerIconCard(
|
||||
server: JellyfinServer,
|
||||
serverVersionSupported: ServerVersionSupported,
|
||||
connectionStatus: ServerConnectionStatus,
|
||||
isCurrentServer: Boolean,
|
||||
onClick: () -> Unit,
|
||||
|
|
@ -166,28 +167,8 @@ fun ServerIconCard(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
// Show connection status indicator or server name/letter
|
||||
when (connectionStatus) {
|
||||
is ServerConnectionStatus.Success -> {
|
||||
// Show server name/letter
|
||||
|
||||
Text(
|
||||
text = displayText,
|
||||
style =
|
||||
MaterialTheme.typography.displayLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
ServerConnectionStatus.Pending -> {
|
||||
CircularProgress(
|
||||
modifier = Modifier.size(cardSize * 0.4f),
|
||||
)
|
||||
}
|
||||
|
||||
is ServerConnectionStatus.Error -> {
|
||||
when {
|
||||
connectionStatus is ServerConnectionStatus.Error || serverVersionSupported != ServerVersionSupported.SUPPORTED -> {
|
||||
// Show warning icon with server letter below
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
@ -195,7 +176,7 @@ fun ServerIconCard(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = connectionStatus.message,
|
||||
contentDescription = (connectionStatus as? ServerConnectionStatus.Error)?.message,
|
||||
tint = MaterialTheme.colorScheme.errorContainer,
|
||||
modifier = Modifier.size(cardSize * 0.3f),
|
||||
)
|
||||
|
|
@ -210,6 +191,26 @@ fun ServerIconCard(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
connectionStatus is ServerConnectionStatus.Success -> {
|
||||
// Show server name/letter
|
||||
|
||||
Text(
|
||||
text = displayText,
|
||||
style =
|
||||
MaterialTheme.typography.displayLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
connectionStatus == ServerConnectionStatus.Pending -> {
|
||||
CircularProgress(
|
||||
modifier = Modifier.size(cardSize * 0.4f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.setup
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -30,12 +35,16 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
|
|
@ -55,6 +64,7 @@ import com.github.damontecres.wholphin.ui.components.TextButton
|
|||
import com.github.damontecres.wholphin.ui.dimAndBlur
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
||||
|
|
@ -85,8 +95,10 @@ private fun SwitchServerContentInternal(
|
|||
viewModel: SwitchServerViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
var showAddServer by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) }
|
||||
var focusedIndex by rememberInt(0)
|
||||
Box(
|
||||
modifier = modifier.dimAndBlur(showAddServer || showDeleteDialog != null),
|
||||
) {
|
||||
|
|
@ -125,8 +137,13 @@ private fun SwitchServerContentInternal(
|
|||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val firstServerFocus = remember { FocusRequester() }
|
||||
if (state.servers.isNotEmpty()) {
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
LaunchedEffect(state.loading) {
|
||||
val state = state
|
||||
if (state.loading == LoadingState.Success && state.servers.isNotEmpty()) {
|
||||
firstServerFocus.tryRequestFocus()
|
||||
} else if (state.loading == LoadingState.Pending) {
|
||||
firstServerFocus.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
|
|
@ -141,6 +158,7 @@ private fun SwitchServerContentInternal(
|
|||
ServerIconCard(
|
||||
server = server.server,
|
||||
connectionStatus = server.status,
|
||||
serverVersionSupported = server.versionSupported,
|
||||
isCurrentServer = false, // TODO: Determine current server if needed
|
||||
onClick = {
|
||||
when (server.status) {
|
||||
|
|
@ -162,10 +180,13 @@ private fun SwitchServerContentInternal(
|
|||
},
|
||||
allowDelete = true,
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
index == 0,
|
||||
Modifier.focusRequester(firstServerFocus),
|
||||
),
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) focusedIndex = index
|
||||
}.ifElse(
|
||||
index == 0,
|
||||
Modifier.focusRequester(firstServerFocus),
|
||||
),
|
||||
)
|
||||
}
|
||||
// Add Server card - always rightmost
|
||||
|
|
@ -173,10 +194,13 @@ private fun SwitchServerContentInternal(
|
|||
AddServerCard(
|
||||
onClick = { showAddServer = true },
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
state.servers.isEmpty(),
|
||||
Modifier.focusRequester(firstServerFocus),
|
||||
),
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) focusedIndex = -1
|
||||
}.ifElse(
|
||||
state.servers.isEmpty(),
|
||||
Modifier.focusRequester(firstServerFocus),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -191,6 +215,37 @@ private fun SwitchServerContentInternal(
|
|||
)
|
||||
}
|
||||
|
||||
val errorMessage =
|
||||
remember(resources, focusedIndex, state.servers) {
|
||||
val server = state.servers.getOrNull(focusedIndex)
|
||||
serverErrorMessage(server, resources)
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = errorMessage,
|
||||
transitionSpec = {
|
||||
slideInVertically { it / 2 } togetherWith slideOutVertically { it }
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
) { message ->
|
||||
|
||||
Text(
|
||||
text = message ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete server dialog
|
||||
showDeleteDialog?.let { server ->
|
||||
DialogPopup(
|
||||
|
|
@ -411,3 +466,26 @@ private fun SwitchServerContentInternal(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun serverErrorMessage(
|
||||
server: ServerState?,
|
||||
resources: Resources,
|
||||
): String? =
|
||||
when {
|
||||
server?.status is ServerConnectionStatus.Error -> {
|
||||
server.status.message
|
||||
}
|
||||
|
||||
server?.status is ServerConnectionStatus.Success &&
|
||||
server.versionSupported == ServerVersionSupported.NOT_SUPPORTED -> {
|
||||
resources.getString(R.string.server_version_not_supported) + ": ${server.status.systemInfo.version}"
|
||||
}
|
||||
|
||||
server?.versionSupported == ServerVersionSupported.NOT_SUPPORTED -> {
|
||||
resources.getString(R.string.server_version_not_supported)
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.widget.Toast
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
|
|
@ -24,6 +25,7 @@ import org.jellyfin.sdk.api.client.HttpClientOptions
|
|||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerIssue
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
|
@ -73,11 +75,30 @@ class SwitchServerViewModel
|
|||
server: JellyfinServer,
|
||||
result: ServerConnectionStatus,
|
||||
) {
|
||||
val supported =
|
||||
if (result is ServerConnectionStatus.Success) {
|
||||
val serverVersion =
|
||||
result.systemInfo.version?.let { ServerVersion.fromString(it) }
|
||||
if (serverVersion == null || serverVersion < WholphinApplication.minimumServerVersion) {
|
||||
ServerVersionSupported.NOT_SUPPORTED
|
||||
} else {
|
||||
ServerVersionSupported.SUPPORTED
|
||||
}
|
||||
} else {
|
||||
ServerVersionSupported.UNKNOWN
|
||||
}
|
||||
_state.update {
|
||||
val servers =
|
||||
it.servers.toMutableList().apply {
|
||||
val index = indexOfFirst { it.server.id == server.id }
|
||||
set(index, get(index).copy(server = server, status = result))
|
||||
set(
|
||||
index,
|
||||
get(index).copy(
|
||||
server = server,
|
||||
status = result,
|
||||
versionSupported = supported,
|
||||
),
|
||||
)
|
||||
}
|
||||
it.copy(servers = servers)
|
||||
}
|
||||
|
|
@ -245,4 +266,11 @@ data class ServerState(
|
|||
val server: JellyfinServer,
|
||||
val status: ServerConnectionStatus = ServerConnectionStatus.Pending,
|
||||
val quickConnect: Boolean = false,
|
||||
val versionSupported: ServerVersionSupported = ServerVersionSupported.UNKNOWN,
|
||||
)
|
||||
|
||||
enum class ServerVersionSupported {
|
||||
SUPPORTED,
|
||||
NOT_SUPPORTED,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,13 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
|
|
@ -169,6 +171,37 @@ fun SwitchUserContent(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
when (state.serverVersionSupported) {
|
||||
ServerVersionSupported.SUPPORTED -> {}
|
||||
|
||||
ServerVersionSupported.NOT_SUPPORTED,
|
||||
ServerVersionSupported.UNKNOWN,
|
||||
-> {
|
||||
val resources = LocalResources.current
|
||||
val message =
|
||||
remember(resources) {
|
||||
if (state.serverVersion.isNotNullOrBlank()) {
|
||||
resources.getString(R.string.server_version_not_supported) + ": ${state.serverVersion}"
|
||||
} else {
|
||||
resources.getString(R.string.server_version_not_supported) + ": " +
|
||||
resources.getString(R.string.unknown)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.setup
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
|
|
@ -33,7 +34,9 @@ import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
|||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.api.QuickConnectDto
|
||||
import org.jellyfin.sdk.model.api.QuickConnectResult
|
||||
import timber.log.Timber
|
||||
|
|
@ -78,10 +81,13 @@ class SwitchUserViewModel
|
|||
_state.update { SwitchUserState() }
|
||||
try {
|
||||
val serverUsers = getUsers()
|
||||
val (serverVersion, supported) = checkServerVersion()
|
||||
_state.update {
|
||||
it.copy(
|
||||
loading = LoadingState.Success,
|
||||
users = serverUsers,
|
||||
serverVersion = serverVersion,
|
||||
serverVersionSupported = supported,
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
|
@ -272,6 +278,19 @@ class SwitchUserViewModel
|
|||
knownUsers + publicUsers
|
||||
}
|
||||
|
||||
private suspend fun checkServerVersion(): Pair<String?, ServerVersionSupported> {
|
||||
val api = jellyfin.createApi(server.url)
|
||||
val systemInfo by api.systemApi.getPublicSystemInfo()
|
||||
val serverVersion = systemInfo.version?.let { ServerVersion.fromString(it) }
|
||||
return if (serverVersion == null) {
|
||||
systemInfo.version to ServerVersionSupported.UNKNOWN
|
||||
} else if (serverVersion < WholphinApplication.minimumServerVersion) {
|
||||
systemInfo.version to ServerVersionSupported.NOT_SUPPORTED
|
||||
} else {
|
||||
systemInfo.version to ServerVersionSupported.SUPPORTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun setError(
|
||||
msg: String? = null,
|
||||
ex: Exception? = null,
|
||||
|
|
@ -300,4 +319,6 @@ data class SwitchUserState(
|
|||
// LoadingState for while adding/switching users
|
||||
val switchUserState: LoadingState = LoadingState.Pending,
|
||||
val loginAttempts: Int = 0,
|
||||
val serverVersion: String? = null,
|
||||
val serverVersionSupported: ServerVersionSupported = ServerVersionSupported.UNKNOWN,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -898,4 +898,5 @@
|
|||
<string name="user_views">User views</string>
|
||||
<string name="books">Books</string>
|
||||
<string name="audio_books">Audio books</string>
|
||||
<string name="server_version_not_supported">Server version not supported</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue