Fix focus issues & add clear image cache option

This commit is contained in:
Damontecres 2025-10-12 22:31:09 -04:00
parent 370cd2c146
commit 4113f990b2
No known key found for this signature in database
8 changed files with 50 additions and 22 deletions

View file

@ -439,6 +439,13 @@ sealed interface AppPreference<T> {
indexToValue = { SkipSegmentBehavior.forNumber(it) }, indexToValue = { SkipSegmentBehavior.forNumber(it) },
valueToIndex = { it.number }, valueToIndex = { it.number },
) )
val ClearImageCache =
AppClickablePreference(
title = R.string.clear_image_cache,
getter = { },
setter = { prefs, _ -> prefs },
)
} }
} }
@ -513,6 +520,7 @@ val advancedPreferences =
title = R.string.more, title = R.string.more,
preferences = preferences =
listOf( listOf(
AppPreference.ClearImageCache,
AppPreference.OssLicenseInfo, AppPreference.OssLicenseInfo,
), ),
), ),

View file

@ -15,7 +15,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
@ -29,7 +28,6 @@ import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.PreviewTvSpec import com.github.damontecres.dolphin.ui.PreviewTvSpec
import com.github.damontecres.dolphin.ui.theme.DolphinTheme import com.github.damontecres.dolphin.ui.theme.DolphinTheme
import com.github.damontecres.dolphin.ui.tryRequestFocus
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -139,14 +137,10 @@ fun ExpandablePlayButtons(
modifier = modifier =
modifier modifier
.focusGroup() .focusGroup()
.focusProperties { .focusRestorer(firstFocus),
onEnter = {
firstFocus.tryRequestFocus()
}
},
) { ) {
if (resumePosition > Duration.ZERO) { if (resumePosition > Duration.ZERO) {
item { item("play") {
ExpandablePlayButton( ExpandablePlayButton(
R.string.resume, R.string.resume,
resumePosition, resumePosition,
@ -157,7 +151,7 @@ fun ExpandablePlayButtons(
.focusRequester(firstFocus), .focusRequester(firstFocus),
) )
} }
item { item("restart") {
ExpandablePlayButton( ExpandablePlayButton(
R.string.restart, R.string.restart,
Duration.ZERO, Duration.ZERO,
@ -168,7 +162,7 @@ fun ExpandablePlayButtons(
) )
} }
} else { } else {
item { item("play") {
ExpandablePlayButton( ExpandablePlayButton(
R.string.play, R.string.play,
Duration.ZERO, Duration.ZERO,
@ -182,7 +176,7 @@ fun ExpandablePlayButtons(
} }
// Watched button // Watched button
item { item("watched") {
ExpandableFaButton( ExpandableFaButton(
title = if (watched) R.string.mark_unwatched else R.string.mark_watched, title = if (watched) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash, iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
@ -192,7 +186,7 @@ fun ExpandablePlayButtons(
} }
// More button // More button
item { item("more") {
ExpandablePlayButton( ExpandablePlayButton(
R.string.more, R.string.more,
Duration.ZERO, Duration.ZERO,

View file

@ -50,6 +50,8 @@ fun FocusedEpisodeHeader(
Text( Text(
text = dto.episodeTitle ?: dto.name ?: "", text = dto.episodeTitle ?: dto.name ?: "",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier, modifier = Modifier,
) )
Row( Row(

View file

@ -16,7 +16,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -34,6 +33,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab import androidx.tv.material3.Tab
@ -77,8 +77,6 @@ fun SeriesOverviewContent(
val tabRowFocusRequester = remember { FocusRequester() } val tabRowFocusRequester = remember { FocusRequester() }
val focusedEpisode = episodes.getOrNull(position.episodeRowIndex) val focusedEpisode = episodes.getOrNull(position.episodeRowIndex)
LaunchedEffect(position) {
}
Box( Box(
modifier = modifier =
@ -184,6 +182,8 @@ fun SeriesOverviewContent(
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier, modifier = Modifier,
) )
} }

View file

@ -249,12 +249,16 @@ fun MainPageHeader(
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
subtitle?.let { subtitle?.let {
Text( Text(
text = subtitle, text = subtitle,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
if (details.isNotEmpty()) { if (details.isNotEmpty()) {

View file

@ -280,10 +280,11 @@ class PlaybackViewModel
Timber.i("No change in playback for changeStreams") Timber.i("No change in playback for changeStreams")
return return
} }
// TODO if the new audio or subtitle index is already in the streams (eg direct play), should toggle in the player instead
val maxBitrate = val maxBitrate =
preferences.appPreferences.playbackPreferences.maxBitrate preferences.appPreferences.playbackPreferences.maxBitrate
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE .takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
val response = val response by
api.mediaInfoApi api.mediaInfoApi
.getPostedPlaybackInfo( .getPostedPlaybackInfo(
itemId, itemId,
@ -302,7 +303,7 @@ class PlaybackViewModel
alwaysBurnInSubtitleWhenTranscoding = null, alwaysBurnInSubtitleWhenTranscoding = null,
maxStreamingBitrate = maxBitrate.toInt(), maxStreamingBitrate = maxBitrate.toInt(),
), ),
).content )
val source = response.mediaSources.firstOrNull() val source = response.mediaSources.firstOrNull()
source?.let { source -> source?.let { source ->
val mediaUrl = val mediaUrl =
@ -335,8 +336,8 @@ class PlaybackViewModel
?.let { ?.let {
it.deliveryUrl?.let { deliveryUrl -> it.deliveryUrl?.let { deliveryUrl ->
var flags = 0 var flags = 0
if (it.isForced) flags = flags.and(C.SELECTION_FLAG_FORCED) if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED)
if (it.isDefault) flags = flags.and(C.SELECTION_FLAG_DEFAULT) if (it.isDefault) flags = flags.or(C.SELECTION_FLAG_DEFAULT)
MediaItem.SubtitleConfiguration MediaItem.SubtitleConfiguration
.Builder( .Builder(
api.createUrl(deliveryUrl).toUri(), api.createUrl(deliveryUrl).toUri(),
@ -368,6 +369,7 @@ class PlaybackViewModel
) )
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// TODO, don't need to release & recreate when switching streams
this@PlaybackViewModel.activityListener?.let { this@PlaybackViewModel.activityListener?.let {
it.release() it.release()
player.removeListener(it) player.removeListener(it)
@ -389,7 +391,7 @@ class PlaybackViewModel
positionMs, positionMs,
) )
if (audioIndex != null || subtitleIndex != null) { if (audioIndex != null || subtitleIndex != null) {
val trackActivationListener = val onTracksChangedListener =
object : Player.Listener { object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
Timber.v("onTracksChanged: $tracks") Timber.v("onTracksChanged: $tracks")
@ -408,7 +410,7 @@ class PlaybackViewModel
} }
} }
} }
player.addListener(trackActivationListener) player.addListener(onTracksChangedListener)
} }
} }
val trickPlayInfo = val trickPlayInfo =
@ -416,7 +418,7 @@ class PlaybackViewModel
?.get(source.id) ?.get(source.id)
?.values ?.values
?.firstOrNull() ?.firstOrNull()
Timber.v("Trickplay info: $trickPlayInfo") // Timber.v("Trickplay info: $trickPlayInfo")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
trickplay.value = trickPlayInfo trickplay.value = trickPlayInfo
} }

View file

@ -39,6 +39,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.SingletonImageLoader
import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.preferences.AppPreference import com.github.damontecres.dolphin.preferences.AppPreference
import com.github.damontecres.dolphin.preferences.AppPreferences import com.github.damontecres.dolphin.preferences.AppPreferences
@ -245,6 +246,22 @@ fun PreferencesContent(
) )
} }
AppPreference.ClearImageCache -> {
ClickPreference(
title = stringResource(pref.title),
onClick = {
SingletonImageLoader.get(context).let {
it.memoryCache?.clear()
it.diskCache?.clear()
}
},
modifier = Modifier,
summary = null,
onLongClick = {},
interactionSource = interactionSource,
)
}
else -> { else -> {
val value = pref.getter.invoke(preferences) val value = pref.getter.invoke(preferences)
ComposablePreference( ComposablePreference(

View file

@ -74,5 +74,6 @@
<string name="update_url_summary">URL used to check for app updates</string> <string name="update_url_summary">URL used to check for app updates</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="no_update_available">No update available</string> <string name="no_update_available">No update available</string>
<string name="clear_image_cache">Clear image cache</string>
</resources> </resources>