From 3109b4c0034945f8844b8045dd7931f20152397c Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 4 Oct 2025 21:11:56 -0400 Subject: [PATCH] Lots of UI tweaks --- .../damontecres/dolphin/ui/Extensions.kt | 4 ++ .../dolphin/ui/cards/ChapterCard.kt | 16 +++-- .../dolphin/ui/detail/movie/MovieDetails.kt | 2 +- .../ui/detail/series/FocusedEpisodeHeader.kt | 65 +++++++++---------- .../ui/detail/series/SeriesOverviewContent.kt | 2 +- .../dolphin/ui/playback/PlaybackControls.kt | 9 +-- .../dolphin/ui/playback/PlaybackOverlay.kt | 25 ++++--- .../dolphin/ui/playback/PlaybackViewModel.kt | 38 +++++------ .../dolphin/util/profile/PlaybackListener.kt | 60 ----------------- app/src/main/res/values/themes.xml | 6 +- 10 files changed, 87 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/com/github/damontecres/dolphin/util/profile/PlaybackListener.kt diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/Extensions.kt index c7b2469b..e4b7c8a3 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/Extensions.kt @@ -27,6 +27,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -174,6 +175,9 @@ fun playOnClickSound( val Duration.roundMinutes: Duration get() = (this + 30.seconds).inWholeMinutes.minutes +val Duration.roundSeconds: Duration + get() = (this + 30.milliseconds).inWholeSeconds.seconds + val BaseItemDto.timeRemaining: Duration? get() = userData?.playbackPositionTicks?.let { diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ChapterCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ChapterCard.kt index 4729280c..5803e23a 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ChapterCard.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ChapterCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -17,6 +18,7 @@ import androidx.tv.material3.Card import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.dolphin.ui.AppColors +import com.github.damontecres.dolphin.ui.roundSeconds import kotlin.time.Duration @Composable @@ -44,21 +46,23 @@ fun ChapterCard( contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize(), ) - Column( + Box( modifier = Modifier .align(Alignment.BottomStart) .fillMaxWidth() .background(AppColors.TransparentBlack50), ) { - name?.let { + Column(modifier = Modifier.padding(4.dp)) { + name?.let { + Text( + text = it, + ) + } Text( - text = it, + text = position.roundSeconds.toString(), ) } - Text( - text = position.toString(), - ) } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt index efb32b1e..2b065931 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt @@ -194,7 +194,7 @@ fun MovieDetailsContent( modifier = Modifier .bringIntoViewRequester(bringIntoViewRequester) - .padding(bottom = 120.dp), + .padding(bottom = 56.dp), ) { MovieDetailsHeader( movie = movie, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt index 7b79b88d..b334cf61 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -94,41 +94,40 @@ fun FocusedEpisodeHeader( } } } - dto.overview?.let { overview -> - val interactionSource = remember { MutableInteractionSource() } - val isFocused = interactionSource.collectIsFocusedAsState().value - val bgColor = - if (isFocused) { - MaterialTheme.colorScheme.onPrimary.copy(alpha = .4f) - } else { - Color.Unspecified - } - Box( + + val interactionSource = remember { MutableInteractionSource() } + val isFocused = interactionSource.collectIsFocusedAsState().value + val bgColor = + if (isFocused) { + MaterialTheme.colorScheme.onPrimary.copy(alpha = .4f) + } else { + Color.Unspecified + } + Box( + modifier = + Modifier + .background(bgColor, shape = RoundedCornerShape(8.dp)) + .playSoundOnFocus(true) + .clickable( + enabled = true, + interactionSource = interactionSource, + indication = LocalIndication.current, + ) { + playOnClickSound(context) + overviewOnClick.invoke() + }, + ) { + Text( + text = dto.overview ?: "", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 3, + overflow = TextOverflow.Ellipsis, modifier = Modifier - .background(bgColor, shape = RoundedCornerShape(8.dp)) - .playSoundOnFocus(true) - .clickable( - enabled = true, - interactionSource = interactionSource, - indication = LocalIndication.current, - ) { - playOnClickSound(context) - overviewOnClick.invoke() - }, - ) { - Text( - text = overview, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .padding(8.dp) - .height(60.dp), - ) - } + .padding(8.dp) + .height(60.dp), + ) } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt index b4940da2..768aab59 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt @@ -188,7 +188,7 @@ fun SeriesOverviewContent( LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(start = 16.dp), + contentPadding = PaddingValues(horizontal = 16.dp), modifier = Modifier .focusRestorer(firstItemFocusRequester) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt index 32ff20d5..d44a7b38 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt @@ -142,6 +142,7 @@ fun PlaybackControls( Column( modifier = modifier.bringIntoViewRequester(bringIntoViewRequester), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(0.dp), ) { SeekBar( player = playerControls, @@ -152,7 +153,7 @@ fun PlaybackControls( intervals = seekBarIntervals, modifier = Modifier - .padding(vertical = 8.dp) + .padding(vertical = 0.dp) .fillMaxWidth(.95f), ) Box( @@ -516,11 +517,11 @@ fun PlaybackButton( containerColor = AppColors.TransparentBlack25, focusedContainerColor = selectedColor, ), - contentPadding = PaddingValues(8.dp), + contentPadding = PaddingValues(4.dp), modifier = modifier - .padding(8.dp) - .size(56.dp, 56.dp) + .padding(4.dp) + .size(44.dp, 44.dp) .onFocusChanged { onControllerInteraction.invoke() }, ) { Icon( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt index 5d58b2b0..fc721695 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.media3.common.Player import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -81,32 +82,33 @@ fun PlaybackOverlay( // val chapterRowFocused = chapterInteractionSources.any { it.collectIsFocusedAsState().value } var chapterRowFocused by remember { mutableStateOf(false) } - val titleTextStyle = MaterialTheme.typography.displaySmall - val subtitleTextStyle = MaterialTheme.typography.headlineMedium + val titleTextSize = 28.sp + val subtitleTextSize = 18.sp val density = LocalDensity.current val titleHeight = - if (title.isNotNullOrBlank()) with(density) { titleTextStyle.fontSize.toDp() } else 0.dp + if (title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp val subtitleHeight = - if (subtitle.isNotNullOrBlank()) with(density) { subtitleTextStyle.fontSize.toDp() } else 0.dp + if (subtitle.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp // Calculate height based on content // Base height (with or w/o chapters) + title + subtitle // The extra 8dp is for padding between title, subtitle, and playback controls // When chapter row is focused, the title/subtitle/playback controls will be hidden, but need extra height for the chapter images val height by animateDpAsState( - (if (chapters.isNotEmpty()) 184.dp else 140.dp) + - (if (chapterRowFocused) 40.dp else 0.dp) + + 96.dp + + (if (chapters.isNotEmpty()) 40.dp else 0.dp) + + (if (chapterRowFocused) 80.dp else 0.dp) + ( if (!chapterRowFocused && title.isNotNullOrBlank()) { - titleHeight + 8.dp + titleHeight + 12.dp } else { 0.dp } ) + ( if (!chapterRowFocused && subtitle.isNotNullOrBlank()) { - subtitleHeight + 8.dp + subtitleHeight + 12.dp } else { 0.dp } @@ -132,6 +134,7 @@ fun PlaybackOverlay( modifier = Modifier, ) { Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(start = 16.dp) @@ -140,13 +143,15 @@ fun PlaybackOverlay( title?.let { Text( text = it, - style = titleTextStyle, + style = MaterialTheme.typography.titleLarge, + fontSize = titleTextSize, ) } subtitle?.let { Text( text = it, - style = subtitleTextStyle, + style = MaterialTheme.typography.titleMedium, + fontSize = subtitleTextSize, ) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt index 40ee49b1..1ab4658d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt @@ -21,7 +21,7 @@ import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener import com.github.damontecres.dolphin.util.TrackSupport import com.github.damontecres.dolphin.util.checkForSupport -import com.github.damontecres.dolphin.util.profile.PlaybackListener +import com.github.damontecres.dolphin.util.formatDateTime import com.github.damontecres.dolphin.util.subtitleMimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -57,15 +57,7 @@ data class StreamDecision( val itemId: UUID, val type: TranscodeType, val url: String, -) { - val mediaItem: MediaItem - get() = - MediaItem - .Builder() - .setMediaId(itemId.toString()) - .setUri(url.toUri()) - .build() -} +) @HiltViewModel class PlaybackViewModel @@ -112,25 +104,26 @@ class PlaybackViewModel val base = item?.data ?: api.userLibraryApi.getItem(itemId).content dto = base - val title = base.name + val title = + if (base.type == BaseItemKind.EPISODE) { + base.seriesName + } else { + base.name + } val subtitle = if (base.type == BaseItemKind.EPISODE) { val season = base.parentIndexNumber?.toString()?.padStart(2, '0') val episode = base.indexNumber?.toString()?.padStart(2, '0') // TODO multi episode support - if (season != null && episode != null) { - buildString { - if (base.seriesName != null) { - append(base.seriesName) - append(" - ") - } - append("S${season}E$episode") + buildList { + if (season != null && episode != null) { + add("S${season}E$episode") } - } else { - null - } + add(base.name) + add(base.premiereDate?.let { formatDateTime(it) }) + }.filterNotNull().joinToString(" - ") } else { - null + base.productionYear?.toString() } withContext(Dispatchers.Main) { this@PlaybackViewModel.title.value = title @@ -186,7 +179,6 @@ class PlaybackViewModel TrackActivityPlaybackListener(api, itemId, player) addCloseable { activityListener.release() } player.addListener(activityListener) - player.addListener(PlaybackListener()) changeStreams( itemId, audioIndex, diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/profile/PlaybackListener.kt b/app/src/main/java/com/github/damontecres/dolphin/util/profile/PlaybackListener.kt deleted file mode 100644 index 3d61cb41..00000000 --- a/app/src/main/java/com/github/damontecres/dolphin/util/profile/PlaybackListener.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.damontecres.dolphin.util.profile - -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.Tracks -import androidx.media3.common.text.CueGroup -import com.github.damontecres.dolphin.ui.indexOfFirstOrNull -import com.github.damontecres.dolphin.util.TrackSupportReason -import com.github.damontecres.dolphin.util.TrackType -import com.github.damontecres.dolphin.util.checkForSupport -import timber.log.Timber -import java.util.Locale - -class PlaybackListener : Player.Listener { - var mediaIndexSubtitlesActivated = -1 - var currentPlaylistIndex = -2 - - override fun onCues(cueGroup: CueGroup) { - val subtitles = cueGroup.cues.ifEmpty { null } - } - - override fun onTracksChanged(tracks: Tracks) { - val trackInfo = checkForSupport(tracks) - Timber.v("Track info: $trackInfo") - val audioTracks = - trackInfo - .filter { it.type == TrackType.AUDIO && it.supported == TrackSupportReason.HANDLED } - val audioIndex = audioTracks.indexOfFirstOrNull { it.selected } - val audioOptions = - audioTracks.map { it.labels.joinToString(", ").ifBlank { "Default" } } - val captions = - trackInfo.filter { it.type == TrackType.TEXT && it.supported == TrackSupportReason.HANDLED } - - // TODO user preference - val captionsByDefault = true - if (captionsByDefault && captions.isNotEmpty()) { - // TODO Captions will be empty when transitioning to new media item - // Only want to activate subtitles once in case the user turns them off - mediaIndexSubtitlesActivated = currentPlaylistIndex - val languageCode = Locale.getDefault().language - captions.indexOfFirstOrNull { it.format.language == languageCode }?.let { - Timber.v("Found default subtitle track for $languageCode: $it") -// if (toggleSubtitles(player, null, it)) { -// subtitleIndex = it -// } - } - } - } - - override fun onMediaItemTransition( - mediaItem: MediaItem?, - reason: Int, - ) { - } - - override fun onPlayerError(error: PlaybackException) { - Timber.e(error, "Playback error") - } -} diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 09a7a336..ddc052a3 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,6 @@ - +