Various bug fixes (#2010)

## Description
Collecting minor bug fixes in one PR:
- If error occurs fetching a series, show the error on the UI instead of
loading forever
- Use song's artists on Now Playing page
- Fixes settings wrap around scrolling when an update is available
- Fix backdrop not clearing for items without backdrop
- Fix not picking an ABI-specific APK when updating a regular release
(maybe related to #1966)
- Add "Go To" for album context menus
- Fix handling of next/previous media remote button presses during video
playback

### Related issues
Fixes #1990 
Fixes #1965

### Testing
Emulator mostly

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-08 08:09:20 -04:00 • committed by GitHub
parent a38eb3e3d8
commit b99f21efa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 453 additions and 27 deletions

View file

@ -42,7 +42,7 @@ data class AudioItem(
?.id,
title = item.title,
albumTitle = item.data.album,
artistNames = item.data.albumArtist,
artistNames = item.data.artists?.joinToString(", "),
runtime = item.data.runTimeTicks?.ticks,
imageUrl = imageUrl,
hasLyrics = item.data.hasLyrics == true,

View file

@ -77,7 +77,7 @@ class BackdropService
itemId: String,
imageUrl: String?,
) = withContext(WholphinDispatchers.IO) {
if (backdropFlow.firstOrNull()?.imageUrl != imageUrl) {
if (backdropFlow.firstOrNull()?.imageUrl != imageUrl || imageUrl == null) {
_backdropFlow.update {
it.copy(
itemId = itemId,
@ -110,7 +110,7 @@ class BackdropService
val dynamicEnabled =
backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR ||
backdropStyle == BackdropStyle.UNRECOGNIZED
val (primaryColor, secondaryColor, tertiaryColor) =
val colors =
if (dynamicEnabled) {
extractColorsFromBackdrop(imageUrl)
} else {
@ -121,9 +121,9 @@ class BackdropService
BackdropResult(
itemId = itemId,
imageUrl = imageUrl,
primaryColor = primaryColor,
secondaryColor = secondaryColor,
tertiaryColor = tertiaryColor,
primaryColor = colors?.primary ?: it.primaryColor,
secondaryColor = colors?.secondary ?: it.secondaryColor,
tertiaryColor = colors?.tertiary ?: it.tertiaryColor,
)
} else {
it
@ -131,10 +131,10 @@ class BackdropService
}
}
suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors =
suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors? =
withContext(WholphinDispatchers.IO) {
if (imageUrl.isNullOrBlank()) {
return@withContext ExtractedColors.DEFAULT
return@withContext null
}
extractedColorCache.get(imageUrl)?.let {
return@withContext it

View file

@ -210,9 +210,7 @@ class UpdateChecker
okHttpClient.newCall(request).execute().use {
if (it.isSuccessful) {
Timber.v("Request successful for ${release.downloadUrl}")
withContext(WholphinDispatchers.Main) {
callback.contentLength(it.body.contentLength())
}
callback.contentLength(it.body.contentLength())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues =
ContentValues().apply {
@ -397,9 +395,7 @@ suspend fun copyTo(
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
withContext(WholphinDispatchers.Main) {
callback.bytesDownloaded(bytesCopied)
}
callback.bytesDownloaded(bytesCopied)
bytes = input.read(buffer)
}
return@withContext bytesCopied
@ -416,7 +412,12 @@ fun getDownloadUrl(
buildList {
add("$ASSET_NAME${releaseSuffix}$abiSuffix.apk")
add("$ASSET_NAME$releaseSuffix.apk")
if (!debug) add("$ASSET_NAME.apk")
if (!debug) {
if (abiSuffix.isNotNullOrBlank()) {
add("$ASSET_NAME$abiSuffix.apk")
}
add("$ASSET_NAME.apk")
}
}
var preferredAsset: JsonObject? = null
outer@ for (name in preferredNames) {

View file

@ -66,6 +66,7 @@ sealed interface ContextMenu {
val canRemoveFromQueue: Boolean,
val actions: MusicContextActions,
val showRemoveFromPlaylist: Boolean = false,
val showGoTo: Boolean = false,
) : ContextMenu
data class ForQueue(
@ -758,6 +759,18 @@ fun buildContextForMusic(
buildList {
val item = music.item
val index = music.index
// Songs should not show Go to
if (music.showGoTo && item.type != BaseItemKind.AUDIO) {
add(
DialogItem(
resources.getString(R.string.go_to),
Icons.Default.ArrowForward,
dismissOnClick = true,
) {
actions.navigateTo(item.destination())
},
)
}
add(
DialogItem(
resources.getString(R.string.play),

View file

@ -609,6 +609,7 @@ fun AlbumDetailsPage(
),
canRemoveFromQueue = false,
actions = moreDialogActions,
showGoTo = true,
)
},
cardContent = { index: Int, item: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->

View file

@ -568,6 +568,7 @@ fun ArtistDetailsPage(
),
canRemoveFromQueue = false,
actions = moreDialogActions,
showGoTo = true,
)
},
cardContent = { index: Int, album: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -606,6 +607,7 @@ fun ArtistDetailsPage(
),
canRemoveFromQueue = false,
actions = moreDialogActions,
showGoTo = true,
)
},
cardContent = { index: Int, album: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->
@ -704,6 +706,7 @@ fun ArtistDetailsPage(
),
canRemoveFromQueue = false,
actions = moreDialogActions,
showGoTo = true,
)
},
cardContent = { index: Int, item: BaseItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit ->

View file

@ -48,6 +48,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.rememberCurrentMediaItemState
import androidx.tv.material3.Button
@ -127,6 +128,12 @@ fun NowPlayingPage(
onPlaybackDialogTypeClick = { },
getDurationMs = { player.duration },
dpadSeekMode = preferences.playbackPreferences.dpadSeekMode,
onSeekToNext = {
if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)) player.seekToNext()
},
onPSeekToPrevious = {
if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)) player.seekToPrevious()
},
)
}

View file

@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
import com.github.damontecres.wholphin.services.BackdropResult
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.ExtractedColors
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.MusicService
import com.github.damontecres.wholphin.services.NavigationManager
@ -278,7 +279,7 @@ class NowPlayingViewModel
val (primaryColor, secondaryColor, tertiaryColor) =
backdropService.extractColorsFromBackdrop(
imageUrl,
)
) ?: ExtractedColors.DEFAULT
val backdropResult =
BackdropResult(
itemId = itemId.toString(),

View file

@ -124,10 +124,18 @@ class SeriesViewModel
Timber.v("Start")
addCloseable { themeSongPlayer.stop() }
val series =
api.userLibraryApi
.getItem(seriesId)
.content
.let { BaseItem(it) }
try {
api.userLibraryApi
.getItem(seriesId)
.content
.let { BaseItem(it) }
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(ex, "Error fetching series %s", seriesId)
_state.update { it.copy(series = DataLoadingState.Error(ex)) }
return@launchIO
}
viewModelScope.launchDefault {
mediaManagementService.collectCanDelete(flowOf(series)) { canDelete ->
_state.update { it.copy(canDeleteSeries = canDelete) }

View file

@ -33,6 +33,8 @@ class PlaybackKeyHandler(
private val isDpadSeekVisible: () -> Boolean = { false },
private val onDpadSeek: (Long) -> Unit = { },
private val dpadSeekMode: DpadSeekMode,
private val onSeekToNext: () -> Unit,
private val onPSeekToPrevious: () -> Unit,
) {
private var leftHandledByRepeat = false
private var rightHandledByRepeat = false
@ -89,11 +91,11 @@ class PlaybackKeyHandler(
}
Key.MediaNext -> {
if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)) player.seekToNext()
onSeekToNext.invoke()
}
Key.MediaPrevious -> {
if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)) player.seekToPrevious()
onPSeekToPrevious.invoke()
}
Key.Captions -> {

View file

@ -269,6 +269,8 @@ fun PlaybackPageContent(
},
onDpadSeek = onDpadSeek,
dpadSeekMode = prefs.dpadSeekMode,
onSeekToNext = viewModel::playNextUp,
onPSeekToPrevious = viewModel::playPrevious,
)
}

View file

@ -226,6 +226,8 @@ fun PreferencesContent(
) {
if (showUpdate) {
item {
val interactionSource = remember { MutableInteractionSource() }
val focused = interactionSource.collectIsFocusedAsState().value
val updateFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if (focusedIndex.first == 0 && focusedIndex.second == 0) {
@ -240,11 +242,20 @@ fun PreferencesContent(
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
},
summary = release?.version?.toString(),
interactionSource = interactionSource,
modifier =
Modifier
.focusRequester(updateFocusRequester)
.focusRequester(firstFocusRequester)
.playSoundOnFocus(movementSounds),
.playSoundOnFocus(movementSounds)
.lazyListWrapScrolling(
state = state,
focused = focused,
isFirst = true,
isLast = false,
firstFocusRequester = firstFocusRequester,
lastFocusRequester = lastFocusRequester,
),
)
}
}