Few minor UI fixes (#343)

- Keep the currently selected nav drawer item in view and focus on when
entering
- Adjust star rating height so that the row is consistent preventing
resizing when moving between episodes with & without a rating
- Simplify checks when switching to a new server and use more specific
keys to prevent needing to switch servers twice
This commit is contained in:
damontecres 2025-11-28 21:31:02 -05:00 committed by GitHub
parent 13a3c6c831
commit ff91fa87e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 125 additions and 39 deletions

View file

@ -110,15 +110,15 @@ class MainActivity : AppCompatActivity() {
} }
isRestoringSession = false isRestoringSession = false
} }
val server by serverRepository.currentServer.observeAsState() val current by serverRepository.current.observeAsState()
val user by serverRepository.currentUser.observeAsState()
val userDto by serverRepository.currentUserDto.observeAsState()
val preferences = val preferences =
UserPreferences( remember(current) {
appPreferences, UserPreferences(
userDto?.configuration ?: DefaultUserConfiguration, appPreferences,
) current?.userDto?.configuration ?: DefaultUserConfiguration,
)
}
if (isRestoringSession) { if (isRestoringSession) {
Box( Box(
@ -131,9 +131,9 @@ class MainActivity : AppCompatActivity() {
) )
} }
} else { } else {
key(server, user) { key(current) {
val initialDestination = val initialDestination =
if (server != null && user != null) { if (current != null) {
Destination.Home() Destination.Home()
} else { } else {
Destination.ServerList Destination.ServerList
@ -150,16 +150,16 @@ class MainActivity : AppCompatActivity() {
} }
} }
val deviceProfile = val deviceProfile =
remember(server, appPreferences) { remember(current, appPreferences) {
createDeviceProfile( createDeviceProfile(
this@MainActivity, this@MainActivity,
preferences, preferences,
server?.serverVersion, current?.server?.serverVersion,
) )
} }
ApplicationContent( ApplicationContent(
user = user, user = current?.user,
server = server, server = current?.server,
navigationManager = navigationManager, navigationManager = navigationManager,
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile, deviceProfile = deviceProfile,

View file

@ -18,6 +18,7 @@ package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -78,10 +79,19 @@ fun DotSeparatedRow(
@Composable @Composable
private fun DotSeparatedRowPreview() { private fun DotSeparatedRowPreview() {
WholphinTheme { WholphinTheme {
DotSeparatedRow( Column {
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"), DotSeparatedRow(
rating = 7.5f, texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
modifier = Modifier, rating = 7.5f,
) modifier = Modifier,
textStyle = MaterialTheme.typography.titleMedium,
)
DotSeparatedRow(
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left 7.5"),
rating = 7.5f,
modifier = Modifier,
textStyle = MaterialTheme.typography.titleLarge,
)
}
} }
} }

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import android.R.attr.textStyle
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -35,10 +36,12 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.LocalTextStyle
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
@ -100,11 +103,12 @@ fun SimpleStarRating(
text = text, text = text,
modifier = Modifier, modifier = Modifier,
) )
val height = with(LocalDensity.current) { LocalTextStyle.current.fontSize.toDp() }
Icon( Icon(
imageVector = Icons.Filled.Star, imageVector = Icons.Filled.Star,
tint = starColor, tint = starColor,
contentDescription = null, contentDescription = null,
modifier = Modifier, modifier = Modifier.height(height),
) )
} }
} }

View file

@ -41,9 +41,12 @@ import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -85,12 +88,15 @@ import com.github.damontecres.wholphin.ui.spacedByWithFooter
import com.github.damontecres.wholphin.ui.theme.LocalTheme import com.github.damontecres.wholphin.ui.theme.LocalTheme
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -303,6 +309,28 @@ fun NavDrawer(
}, },
) )
val spacedBy = 4.dp val spacedBy = 4.dp
val config = LocalConfiguration.current
val density = LocalDensity.current
val heightInPx = remember { with(density) { config.screenHeightDp.dp.roundToPx() } }
suspend fun scrollToSelected() {
val target = selectedIndex + 2
try {
if (target !in
listState.firstVisibleItemIndex..<listState.layoutInfo.visibleItemsInfo.lastIndex
) {
val mult = if ((target - 2) < listState.layoutInfo.totalItemsCount / 2) -1 else 1
listState.animateScrollToItem(selectedIndex + 2, mult * (heightInPx / 2))
}
} catch (ex: Exception) {
Timber.w(ex, "Error scrolling to %s", target)
}
}
LaunchedEffect(selectedIndex) {
scrollToSelected()
}
NavigationDrawer( NavigationDrawer(
modifier = modifier, modifier = modifier,
drawerState = drawerState, drawerState = drawerState,
@ -315,7 +343,11 @@ fun NavDrawer(
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.width(drawerWidth) .width(drawerWidth)
.background(drawerBackground), .background(drawerBackground)
.onFocusChanged {
if (!it.hasFocus) {
}
},
) { ) {
// Even though some must be clicked, focusing on it should clear other focused items // Even though some must be clicked, focusing on it should clear other focused items
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -353,6 +385,11 @@ fun NavDrawer(
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
} }
} }
onExit = {
scope.launch(ExceptionHandler()) {
scrollToSelected()
}
}
}.fillMaxHeight() }.fillMaxHeight()
.padding(start = drawerPadding), .padding(start = drawerPadding),
) { ) {

View file

@ -29,10 +29,13 @@ import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import org.jellyfin.sdk.model.api.PublicSystemInfo
import java.util.UUID import java.util.UUID
sealed interface ServerConnectionStatus { sealed interface ServerConnectionStatus {
object Success : ServerConnectionStatus data class Success(
val systemInfo: PublicSystemInfo,
) : ServerConnectionStatus
object Pending : ServerConnectionStatus object Pending : ServerConnectionStatus
@ -68,7 +71,7 @@ fun ServerList(
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) }, supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
leadingContent = { leadingContent = {
when (status) { when (status) {
ServerConnectionStatus.Success -> {} is ServerConnectionStatus.Success -> {}
ServerConnectionStatus.Pending -> { ServerConnectionStatus.Pending -> {
CircularProgress( CircularProgress(
Modifier.size(IconButtonDefaults.MediumIconSize), Modifier.size(IconButtonDefaults.MediumIconSize),
@ -85,7 +88,7 @@ fun ServerList(
}, },
onClick = { onClick = {
when (status) { when (status) {
ServerConnectionStatus.Success -> onSwitchServer.invoke(server) is ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
ServerConnectionStatus.Pending -> {} ServerConnectionStatus.Pending -> {}
is ServerConnectionStatus.Error -> onTestServer.invoke(server) is ServerConnectionStatus.Error -> onTestServer.invoke(server)
} }

View file

@ -39,6 +39,7 @@ import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import org.jellyfin.sdk.model.api.PublicSystemInfo
@Composable @Composable
fun SwitchServerContent( fun SwitchServerContent(
@ -88,7 +89,7 @@ fun SwitchServerContent(
servers = servers, servers = servers,
connectionStatus = serverStatus, connectionStatus = serverStatus,
onSwitchServer = { onSwitchServer = {
viewModel.addServer(it.url) viewModel.switchServer(it)
}, },
onTestServer = { onTestServer = {
viewModel.testServer(it) viewModel.testServer(it)
@ -141,7 +142,7 @@ fun SwitchServerContent(
connectionStatus = connectionStatus =
discoveredServers discoveredServers
.map { it.id } .map { it.id }
.associateWith { ServerConnectionStatus.Success }, .associateWith { ServerConnectionStatus.Success(PublicSystemInfo()) },
onSwitchServer = { onSwitchServer = {
viewModel.addServer(it.url) viewModel.addServer(it.url)
}, },

View file

@ -87,7 +87,7 @@ class SwitchServerViewModel
viewModelScope.launchIO { viewModelScope.launchIO {
delay(1000) delay(1000)
val result = internalTestServer(server) val result = internalTestServer(server)
if (result == ServerConnectionStatus.Success) { if (result is ServerConnectionStatus.Success) {
showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT) showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT)
} else if (result is ServerConnectionStatus.Error) { } else if (result is ServerConnectionStatus.Error) {
showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT) showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT)
@ -97,27 +97,30 @@ class SwitchServerViewModel
private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus = private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus =
try { try {
jellyfin val systemInfo =
.createApi( jellyfin
server.url, .createApi(
httpClientOptions = server.url,
HttpClientOptions( httpClientOptions =
requestTimeout = 6.seconds, HttpClientOptions(
connectTimeout = 6.seconds, requestTimeout = 6.seconds,
socketTimeout = 6.seconds, connectTimeout = 6.seconds,
), socketTimeout = 6.seconds,
).systemApi ),
.getPublicSystemInfo() ).systemApi
.getPublicSystemInfo()
.content
val result = ServerConnectionStatus.Success(systemInfo)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
serverStatus.value = serverStatus.value =
serverStatus.value!!.toMutableMap().apply { serverStatus.value!!.toMutableMap().apply {
put( put(
server.id, server.id,
ServerConnectionStatus.Success, result,
) )
} }
} }
ServerConnectionStatus.Success result
} catch (ex: Exception) { } catch (ex: Exception) {
val status = ServerConnectionStatus.Error(ex.localizedMessage) val status = ServerConnectionStatus.Error(ex.localizedMessage)
Timber.w(ex, "Error checking server ${server.url}") Timber.w(ex, "Error checking server ${server.url}")
@ -133,6 +136,34 @@ class SwitchServerViewModel
status status
} }
fun switchServer(server: JellyfinServer) {
viewModelScope.launchIO {
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Pending,
)
}
}
val result = internalTestServer(server)
if (result is ServerConnectionStatus.Success) {
val updatedServer =
server.copy(
name = result.systemInfo.serverName,
version = result.systemInfo.version,
)
serverRepository.addAndChangeServer(updatedServer)
withContext(Dispatchers.Main) {
navigationManager.navigateTo(Destination.UserList(server))
}
} else if (result is ServerConnectionStatus.Error) {
showToast(context, "Error connecting: $${result.message}")
}
}
}
fun addServer(inputUrl: String) { fun addServer(inputUrl: String) {
addServerState.value = LoadingState.Loading addServerState.value = LoadingState.Loading
viewModelScope.launchIO { viewModelScope.launchIO {