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(
title,
studios,
rowType = row,
viewOptions = row.viewOptions,
showViewMore = studios.size >= limit,
)

View file

@ -198,10 +198,10 @@ val CollectionType.baseItemKinds: List<BaseItemKind>
listOf(BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE)
}
CollectionType.HOMEVIDEOS -> {
listOf(BaseItemKind.VIDEO)
listOf(BaseItemKind.PHOTO)
listOf(BaseItemKind.PHOTO_ALBUM)
CollectionType.PHOTOS,
CollectionType.HOMEVIDEOS,
-> {
listOf(BaseItemKind.VIDEO, BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM)
}
CollectionType.MUSIC -> {
@ -224,10 +224,6 @@ val CollectionType.baseItemKinds: List<BaseItemKind>
listOf(BaseItemKind.PLAYLIST)
}
CollectionType.PHOTOS -> {
listOf(BaseItemKind.PHOTO, BaseItemKind.PHOTO_ALBUM)
}
CollectionType.LIVETV -> {
listOf(
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.toServerString
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.BlockingList
import com.github.damontecres.wholphin.util.DataLoadingState
@ -302,11 +303,19 @@ class CollectionFolderViewModel
override fun onClickRandom() {
viewModelScope.launchIO {
try {
val random =
(state.value.items.successValue as? BlockingList<BaseItem?>)?.randomBlocking()
Timber.d("Got random item: %s", random?.id)
random?.destination()?.let {
navigateTo(random.destination())
val state = state.value
val list = state.items.successValue as? BlockingList<BaseItem?>
if (list != null) {
goToRandom(
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) {
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.NavDrawerItem
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.BlockingList
import com.github.damontecres.wholphin.util.DataLoadingState
@ -700,11 +701,25 @@ class FavoritesViewModel
viewModelScope.launchIO {
try {
collectionStateFor(type)?.let { collectionState ->
val random =
(collectionState.items.successValue as? BlockingList<BaseItem?>)?.randomBlocking()
Timber.d("Got random item: %s", random?.id)
random?.destination()?.let {
navigateTo(random.destination())
val list =
collectionState.items.successValue as? BlockingList<BaseItem?>
if (list != null) {
goToRandom(
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) {

View file

@ -134,7 +134,7 @@ sealed class Destination(
@Serializable
data class Slideshow(
val parentId: UUID,
val parentId: UUID?,
val index: Int,
val filter: CollectionFolderFilter,
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.ui.FontAwesome
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.launchDefault
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
@ -573,6 +574,7 @@ fun NavigationDrawerScope.ProfileIcon(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val focused by interactionSource.collectIsFocusedAsState()
NavigationDrawerItem(
modifier = modifier,
selected = false,
@ -595,7 +597,7 @@ fun NavigationDrawerScope.ProfileIcon(
interactionSource = interactionSource,
) {
Text(
modifier = Modifier,
modifier = Modifier.enableMarquee(focused),
text = user.name ?: user.id.toString(),
maxLines = 1,
)
@ -639,7 +641,7 @@ fun NavigationDrawerScope.IconNavItem(
interactionSource = interactionSource,
) {
Text(
modifier = Modifier,
modifier = Modifier.enableMarquee(focused),
text = text,
maxLines = 1,
)
@ -733,7 +735,7 @@ fun NavigationDrawerScope.NavItem(
interactionSource = interactionSource,
) {
Text(
modifier = Modifier,
modifier = Modifier.enableMarquee(focused),
text = library.name(context),
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.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.serverSupportedMessage
@ -104,6 +103,8 @@ private fun SwitchServerContentInternal(
modifier: Modifier = Modifier,
) {
val resources = LocalResources.current
val focusRequesters =
remember(state.servers.size) { List(state.servers.size + 1) { FocusRequester() } }
var showAddServer by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) }
var focusedIndex by rememberInt(0)
@ -147,10 +148,13 @@ private fun SwitchServerContentInternal(
val firstServerFocus = remember { FocusRequester() }
LaunchedEffect(state.loading) {
val state = state
if (state.loading == LoadingState.Success && state.servers.isNotEmpty()) {
firstServerFocus.tryRequestFocus()
} else if (state.loading == LoadingState.Pending) {
firstServerFocus.tryRequestFocus()
when (state.loading) {
is LoadingState.Error,
LoadingState.Loading,
LoadingState.Pending,
-> Unit
LoadingState.Success -> focusRequesters.firstOrNull()?.tryRequestFocus()
}
}
LazyRow(
@ -191,10 +195,7 @@ private fun SwitchServerContentInternal(
Modifier
.onFocusChanged {
if (it.isFocused) focusedIndex = index
}.ifElse(
index == 0,
Modifier.focusRequester(firstServerFocus),
),
}.focusRequester(focusRequesters[index]),
)
}
// Add Server card - always rightmost
@ -205,10 +206,7 @@ private fun SwitchServerContentInternal(
Modifier
.onFocusChanged {
if (it.isFocused) focusedIndex = -1
}.ifElse(
state.servers.isEmpty(),
Modifier.focusRequester(firstServerFocus),
),
}.focusRequester(focusRequesters.last()),
)
}
}
@ -317,12 +315,12 @@ private fun AddServerDialog(
}
}
val firstDiscoveredServerFocusRequester = remember { FocusRequester() }
val buttonFocusRequester = remember { FocusRequester() }
// Default focus to first discovered server if available
LaunchedEffect(state.discoveredServers.isNotEmpty(), showEnterAddress) {
if (!showEnterAddress && state.discoveredServers.isNotEmpty()) {
firstDiscoveredServerFocusRequester.tryRequestFocus()
LaunchedEffect(showEnterAddress) {
if (!showEnterAddress) {
buttonFocusRequester.tryRequestFocus()
}
}
@ -369,12 +367,8 @@ private fun AddServerDialog(
key = { state.discoveredServers[it].url },
) { index ->
val server = state.discoveredServers[index]
val focusRequester =
if (index == 0) {
firstDiscoveredServerFocusRequester
} else {
remember { FocusRequester() }
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { if (index == 0) focusRequester.tryRequestFocus() }
ListItem(
enabled = true,

View file

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