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

View file

@ -18,6 +18,7 @@ package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -78,10 +79,19 @@ fun DotSeparatedRow(
@Composable
private fun DotSeparatedRowPreview() {
WholphinTheme {
Column {
DotSeparatedRow(
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
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
import android.R.attr.textStyle
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
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.Color
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.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.LocalTextStyle
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.AppColors
@ -100,11 +103,12 @@ fun SimpleStarRating(
text = text,
modifier = Modifier,
)
val height = with(LocalDensity.current) { LocalTextStyle.current.fontSize.toDp() }
Icon(
imageVector = Icons.Filled.Star,
tint = starColor,
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.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
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.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -303,6 +309,28 @@ fun NavDrawer(
},
)
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(
modifier = modifier,
drawerState = drawerState,
@ -315,7 +343,11 @@ fun NavDrawer(
Modifier
.fillMaxHeight()
.width(drawerWidth)
.background(drawerBackground),
.background(drawerBackground)
.onFocusChanged {
if (!it.hasFocus) {
}
},
) {
// Even though some must be clicked, focusing on it should clear other focused items
val interactionSource = remember { MutableInteractionSource() }
@ -353,6 +385,11 @@ fun NavDrawer(
focusRequester.tryRequestFocus()
}
}
onExit = {
scope.launch(ExceptionHandler()) {
scrollToSelected()
}
}
}.fillMaxHeight()
.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.DialogPopup
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import org.jellyfin.sdk.model.api.PublicSystemInfo
import java.util.UUID
sealed interface ServerConnectionStatus {
object Success : ServerConnectionStatus
data class Success(
val systemInfo: PublicSystemInfo,
) : ServerConnectionStatus
object Pending : ServerConnectionStatus
@ -68,7 +71,7 @@ fun ServerList(
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
leadingContent = {
when (status) {
ServerConnectionStatus.Success -> {}
is ServerConnectionStatus.Success -> {}
ServerConnectionStatus.Pending -> {
CircularProgress(
Modifier.size(IconButtonDefaults.MediumIconSize),
@ -85,7 +88,7 @@ fun ServerList(
},
onClick = {
when (status) {
ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
is ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
ServerConnectionStatus.Pending -> {}
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.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState
import org.jellyfin.sdk.model.api.PublicSystemInfo
@Composable
fun SwitchServerContent(
@ -88,7 +89,7 @@ fun SwitchServerContent(
servers = servers,
connectionStatus = serverStatus,
onSwitchServer = {
viewModel.addServer(it.url)
viewModel.switchServer(it)
},
onTestServer = {
viewModel.testServer(it)
@ -141,7 +142,7 @@ fun SwitchServerContent(
connectionStatus =
discoveredServers
.map { it.id }
.associateWith { ServerConnectionStatus.Success },
.associateWith { ServerConnectionStatus.Success(PublicSystemInfo()) },
onSwitchServer = {
viewModel.addServer(it.url)
},

View file

@ -87,7 +87,7 @@ class SwitchServerViewModel
viewModelScope.launchIO {
delay(1000)
val result = internalTestServer(server)
if (result == ServerConnectionStatus.Success) {
if (result is ServerConnectionStatus.Success) {
showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT)
} else if (result is ServerConnectionStatus.Error) {
showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT)
@ -97,6 +97,7 @@ class SwitchServerViewModel
private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus =
try {
val systemInfo =
jellyfin
.createApi(
server.url,
@ -108,16 +109,18 @@ class SwitchServerViewModel
),
).systemApi
.getPublicSystemInfo()
.content
val result = ServerConnectionStatus.Success(systemInfo)
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Success,
result,
)
}
}
ServerConnectionStatus.Success
result
} catch (ex: Exception) {
val status = ServerConnectionStatus.Error(ex.localizedMessage)
Timber.w(ex, "Error checking server ${server.url}")
@ -133,6 +136,34 @@ class SwitchServerViewModel
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) {
addServerState.value = LoadingState.Loading
viewModelScope.launchIO {