mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Lots of UI tweaks
This commit is contained in:
parent
81d0279cce
commit
3109b4c003
10 changed files with 87 additions and 140 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,22 +46,24 @@ fun ChapterCard(
|
|||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Column(
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.TransparentBlack50),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
name?.let {
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = position.toString(),
|
||||
text = position.roundSeconds.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ fun MovieDetailsContent(
|
|||
modifier =
|
||||
Modifier
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.padding(bottom = 120.dp),
|
||||
.padding(bottom = 56.dp),
|
||||
) {
|
||||
MovieDetailsHeader(
|
||||
movie = movie,
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ fun FocusedEpisodeHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
dto.overview?.let { overview ->
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val bgColor =
|
||||
|
|
@ -118,7 +118,7 @@ fun FocusedEpisodeHeader(
|
|||
},
|
||||
) {
|
||||
Text(
|
||||
text = overview,
|
||||
text = dto.overview ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 3,
|
||||
|
|
@ -130,5 +130,4 @@ fun FocusedEpisodeHeader(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
buildList {
|
||||
if (season != null && episode != null) {
|
||||
buildString {
|
||||
if (base.seriesName != null) {
|
||||
append(base.seriesName)
|
||||
append(" - ")
|
||||
}
|
||||
append("S${season}E$episode")
|
||||
add("S${season}E$episode")
|
||||
}
|
||||
add(base.name)
|
||||
add(base.premiereDate?.let { formatDateTime(it) })
|
||||
}.filterNotNull().joinToString(" - ")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} 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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
<resources>
|
||||
|
||||
<style name="Theme.Dolphin" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||
<style name="Theme.Dolphin" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:colorBackground">@android:color/background_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue