Fixes to v1.0.9 (#2072)

## Description
As usual, a PR to fix various smaller bugs in the latest release.

- Fixes not showing the Videos tab on favorites page
- Fixes the Random button not working for photos
- Long nav drawer item names use a marquee scroll if needed
- Fixes a crash when clicking "View More" on a studios rows on the home
page

### Related issues
Fixes #2070
Fixes #2069
Fixes #2135

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-28 15:17:24 -04:00 • committed by GitHub
parent 811eaed87e
commit 5f2f9a8106
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 120 additions and 52 deletions

View file

@ -260,6 +260,7 @@ class HomeDataService
Success( Success(
title, title,
studios, studios,
rowType = row,
viewOptions = row.viewOptions, viewOptions = row.viewOptions,
showViewMore = studios.size >= limit, showViewMore = studios.size >= limit,
) )

View file

@ -198,10 +198,10 @@ val CollectionType.baseItemKinds: List<BaseItemKind>
listOf(BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE) listOf(BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE)
} }
CollectionType.HOMEVIDEOS -> { CollectionType.PHOTOS,
listOf(BaseItemKind.VIDEO) CollectionType.HOMEVIDEOS,
listOf(BaseItemKind.PHOTO) -> {
listOf(BaseItemKind.PHOTO_ALBUM) listOf(BaseItemKind.VIDEO, BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM)
} }
CollectionType.MUSIC -> { CollectionType.MUSIC -> {
@ -224,10 +224,6 @@ val CollectionType.baseItemKinds: List<BaseItemKind>
listOf(BaseItemKind.PLAYLIST) listOf(BaseItemKind.PLAYLIST)
} }
CollectionType.PHOTOS -> {
listOf(BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM)
}
CollectionType.LIVETV -> { CollectionType.LIVETV -> {
listOf( listOf(
BaseItemKind.TV_CHANNEL, BaseItemKind.TV_CHANNEL,

View file

@ -67,6 +67,7 @@ import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
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.ui.util.goToRandom
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.BlockingList
import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DataLoadingState
@ -302,11 +303,19 @@ class CollectionFolderViewModel
override fun onClickRandom() { override fun onClickRandom() {
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
val random = val state = state.value
(state.value.items.successValue as? BlockingList<BaseItem?>)?.randomBlocking() val list = state.items.successValue as? BlockingList<BaseItem?>
Timber.d("Got random item: %s", random?.id) if (list != null) {
random?.destination()?.let { goToRandom(
navigateTo(random.destination()) navigateTo = ::navigateTo,
parentId = itemId,
filter = state.filter,
sortAndDirection = state.sortAndDirection,
recursive = recursive,
list = list,
)
} else {
Timber.w("state.value.items is not successful: %s", state.items)
} }
} catch (ex: CancellationException) { } catch (ex: CancellationException) {
throw ex throw ex

View file

@ -43,6 +43,7 @@ import com.github.damontecres.wholphin.ui.main.settings.Library
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.util.goToRandom
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.BlockingList
import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DataLoadingState
@ -700,11 +701,25 @@ class FavoritesViewModel
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
collectionStateFor(type)?.let { collectionState -> collectionStateFor(type)?.let { collectionState ->
val random = val list =
(collectionState.items.successValue as? BlockingList<BaseItem?>)?.randomBlocking() collectionState.items.successValue as? BlockingList<BaseItem?>
Timber.d("Got random item: %s", random?.id) if (list != null) {
random?.destination()?.let { goToRandom(
navigateTo(random.destination()) navigateTo = ::navigateTo,
parentId = libraryDisplayItemId(type),
filter =
collectionState.filter.copy(
includeItemTypes = listOf(type),
),
sortAndDirection = collectionState.sortAndDirection,
recursive = true,
list = list,
)
} else {
Timber.w(
"state.value.items is not successful: %s",
collectionState.items,
)
} }
} }
} catch (ex: CancellationException) { } catch (ex: CancellationException) {

View file

@ -134,7 +134,7 @@ sealed class Destination(
@Serializable @Serializable
data class Slideshow( data class Slideshow(
val parentId: UUID, val parentId: UUID?,
val index: Int, val index: Int,
val filter: CollectionFolderFilter, val filter: CollectionFolderFilter,
val sortAndDirection: SortAndDirection, val sortAndDirection: SortAndDirection,

View file

@ -80,6 +80,7 @@ import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.components.TimeDisplay import com.github.damontecres.wholphin.ui.components.TimeDisplay
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
@ -573,6 +574,7 @@ fun NavigationDrawerScope.ProfileIcon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val focused by interactionSource.collectIsFocusedAsState()
NavigationDrawerItem( NavigationDrawerItem(
modifier = modifier, modifier = modifier,
selected = false, selected = false,
@ -595,7 +597,7 @@ fun NavigationDrawerScope.ProfileIcon(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
modifier = Modifier, modifier = Modifier.enableMarquee(focused),
text = user.name ?: user.id.toString(), text = user.name ?: user.id.toString(),
maxLines = 1, maxLines = 1,
) )
@ -639,7 +641,7 @@ fun NavigationDrawerScope.IconNavItem(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
modifier = Modifier, modifier = Modifier.enableMarquee(focused),
text = text, text = text,
maxLines = 1, maxLines = 1,
) )
@ -733,7 +735,7 @@ fun NavigationDrawerScope.NavItem(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
modifier = Modifier, modifier = Modifier.enableMarquee(focused),
text = library.name(context), text = library.name(context),
maxLines = 1, maxLines = 1,
) )

View file

@ -67,7 +67,6 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TextButton import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.dimAndBlur 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.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.serverSupportedMessage import com.github.damontecres.wholphin.ui.serverSupportedMessage
@ -104,6 +103,8 @@ private fun SwitchServerContentInternal(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val resources = LocalResources.current val resources = LocalResources.current
val focusRequesters =
remember(state.servers.size) { List(state.servers.size + 1) { FocusRequester() } }
var showAddServer by remember { mutableStateOf(false) } var showAddServer by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) } var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) }
var focusedIndex by rememberInt(0) var focusedIndex by rememberInt(0)
@ -147,10 +148,13 @@ private fun SwitchServerContentInternal(
val firstServerFocus = remember { FocusRequester() } val firstServerFocus = remember { FocusRequester() }
LaunchedEffect(state.loading) { LaunchedEffect(state.loading) {
val state = state val state = state
if (state.loading == LoadingState.Success && state.servers.isNotEmpty()) { when (state.loading) {
firstServerFocus.tryRequestFocus() is LoadingState.Error,
} else if (state.loading == LoadingState.Pending) { LoadingState.Loading,
firstServerFocus.tryRequestFocus() LoadingState.Pending,
-> Unit
LoadingState.Success -> focusRequesters.firstOrNull()?.tryRequestFocus()
} }
} }
LazyRow( LazyRow(
@ -191,10 +195,7 @@ private fun SwitchServerContentInternal(
Modifier Modifier
.onFocusChanged { .onFocusChanged {
if (it.isFocused) focusedIndex = index if (it.isFocused) focusedIndex = index
}.ifElse( }.focusRequester(focusRequesters[index]),
index == 0,
Modifier.focusRequester(firstServerFocus),
),
) )
} }
// Add Server card - always rightmost // Add Server card - always rightmost
@ -205,10 +206,7 @@ private fun SwitchServerContentInternal(
Modifier Modifier
.onFocusChanged { .onFocusChanged {
if (it.isFocused) focusedIndex = -1 if (it.isFocused) focusedIndex = -1
}.ifElse( }.focusRequester(focusRequesters.last()),
state.servers.isEmpty(),
Modifier.focusRequester(firstServerFocus),
),
) )
} }
} }
@ -317,12 +315,12 @@ private fun AddServerDialog(
} }
} }
val firstDiscoveredServerFocusRequester = remember { FocusRequester() } val buttonFocusRequester = remember { FocusRequester() }
// Default focus to first discovered server if available // Default focus to first discovered server if available
LaunchedEffect(state.discoveredServers.isNotEmpty(), showEnterAddress) { LaunchedEffect(showEnterAddress) {
if (!showEnterAddress && state.discoveredServers.isNotEmpty()) { if (!showEnterAddress) {
firstDiscoveredServerFocusRequester.tryRequestFocus() buttonFocusRequester.tryRequestFocus()
} }
} }
@ -369,12 +367,8 @@ private fun AddServerDialog(
key = { state.discoveredServers[it].url }, key = { state.discoveredServers[it].url },
) { index -> ) { index ->
val server = state.discoveredServers[index] val server = state.discoveredServers[index]
val focusRequester = val focusRequester = remember { FocusRequester() }
if (index == 0) { LaunchedEffect(Unit) { if (index == 0) focusRequester.tryRequestFocus() }
firstDiscoveredServerFocusRequester
} else {
remember { FocusRequester() }
}
ListItem( ListItem(
enabled = true, enabled = true,

View file

@ -143,13 +143,16 @@ class SlideshowViewModel
), ),
) )
serverRepository.currentUser?.let { user -> serverRepository.currentUser?.let { user ->
val filter = val filter =
playbackEffectDao slideshowSettings.parentId?.let {
.getPlaybackEffect( playbackEffectDao
user.rowId, .getPlaybackEffect(
slideshowSettings.parentId, user.rowId,
BaseItemKind.PHOTO_ALBUM, slideshowSettings.parentId,
)?.videoFilter BaseItemKind.PHOTO_ALBUM,
)?.videoFilter
}
if (filter != null) { if (filter != null) {
Timber.v("Got filter for album %s", slideshowSettings.parentId) Timber.v("Got filter for album %s", slideshowSettings.parentId)
albumImageFilter = filter albumImageFilter = filter
@ -441,7 +444,7 @@ class SlideshowViewModel
fun saveGalleryFilter() { fun saveGalleryFilter() {
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
val vf = _imageFilter.value val vf = _imageFilter.value
if (vf != null) { if (slideshowSettings.parentId != null) {
albumImageFilter = vf albumImageFilter = vf
serverRepository.currentUser?.let { user -> serverRepository.currentUser?.let { user ->
playbackEffectDao playbackEffectDao

View file

@ -0,0 +1,48 @@
package com.github.damontecres.wholphin.ui.util
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.BlockingList
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import kotlin.random.Random
/**
* Navigate to a random item in the given list
*/
suspend fun goToRandom(
navigateTo: (Destination) -> Unit,
parentId: String,
filter: GetItemsFilter,
sortAndDirection: SortAndDirection,
recursive: Boolean,
list: BlockingList<BaseItem?>,
) {
val randomIndex = Random.nextInt(list.size)
val random = list.getBlocking(randomIndex)
Timber.d("Got random item #%s: %s", randomIndex, random?.id)
if (random != null) {
val destination =
if (random.type == BaseItemKind.PHOTO) {
Destination.Slideshow(
parentId = parentId.toUUIDOrNull(),
index = randomIndex,
filter =
CollectionFolderFilter(
filter = filter,
),
sortAndDirection = sortAndDirection,
recursive = recursive,
startSlideshow = false,
)
} else {
random.destination()
}
navigateTo(destination)
}
}