mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
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:
parent
13a3c6c831
commit
ff91fa87e2
7 changed files with 125 additions and 39 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue