Update movie details

This commit is contained in:
Damontecres 2025-10-02 23:20:26 -04:00
parent 6e42cd3c65
commit bdfe143308
No known key found for this signature in database
9 changed files with 514 additions and 200 deletions

View file

@ -24,6 +24,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@ -186,3 +187,11 @@ val BaseItemDto.timeRemaining: Duration?
fun Player.seekBack(amount: Duration) = seekTo((currentPosition - amount.inWholeMilliseconds).coerceAtLeast(0L))
fun Player.seekForward(amount: Duration) = seekTo((currentPosition + amount.inWholeMilliseconds).coerceAtMost(duration))
@OptIn(ExperimentalContracts::class)
inline fun <T : Collection<*>, R> T.letNotEmpty(block: (T) -> R): R? {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return if (this.isNotEmpty()) block(this) else null
}

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
@ -26,6 +27,7 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.tryRequestFocus
import kotlin.time.Duration
@Composable
@ -109,3 +111,84 @@ fun PlayButtons(
}
}
}
@Composable
fun ExpandablePlayButtons(
resumePosition: Duration,
watched: Boolean,
playOnClick: (position: Duration) -> Unit,
watchOnClick: () -> Unit,
moreOnClick: () -> Unit,
buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier,
) {
val firstFocus = remember { FocusRequester() }
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
modifier =
modifier
.focusGroup()
.focusProperties {
onEnter = {
firstFocus.tryRequestFocus()
}
},
) {
if (resumePosition > Duration.ZERO) {
item {
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
ExpandablePlayButton(
R.string.resume,
resumePosition,
Icons.Default.PlayArrow,
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus),
)
}
item {
ExpandablePlayButton(
R.string.restart,
Duration.ZERO,
Icons.Default.Refresh,
playOnClick,
Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
} else {
item {
ExpandablePlayButton(
R.string.play,
Duration.ZERO,
Icons.Default.PlayArrow,
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus),
)
}
}
// Watched button
item {
ExpandableFaButton(
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,
onClick = watchOnClick,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
// More button
item {
ExpandablePlayButton(
R.string.more,
Duration.ZERO,
Icons.Default.MoreVert,
{ moreOnClick.invoke() },
Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
}
}

View file

@ -1,105 +0,0 @@
package com.github.damontecres.dolphin.ui.detail
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.Video
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.LoadingPage
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.extensions.ticks
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class MovieViewModel
@Inject
constructor(
api: ApiClient,
) : LoadingItemViewModel<Video>(api)
@Composable
fun MovieDetails(
preferences: UserPreferences,
navigationManager: NavigationManager,
destination: Destination.MediaItem,
modifier: Modifier = Modifier,
viewModel: MovieViewModel = hiltViewModel(),
) {
LaunchedEffect(Unit) {
viewModel.init(destination.itemId, destination.item)
}
val item by viewModel.item.observeAsState()
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading -> LoadingPage()
LoadingState.Success -> {
item?.let { item ->
val dto = item.data
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(32.dp),
modifier = modifier,
) {
item {
Text(text = item.name ?: "Unknown")
}
dto.overview?.let {
item {
Text(text = it)
}
}
dto.userData?.playbackPositionTicks?.ticks?.let {
if (it > 60.seconds) {
item {
Button(
onClick = {
navigationManager.navigateTo(
Destination.Playback(
item.id,
it.inWholeMilliseconds,
item,
),
)
},
) {
Text(text = "Resume")
}
}
}
}
item {
Button(
onClick = {
navigationManager.navigateTo(
Destination.Playback(
item.id,
0L,
item,
),
)
},
) {
Text(text = "Play")
}
}
}
}
}
}
}

View file

@ -16,6 +16,7 @@ import com.github.damontecres.dolphin.data.model.Video
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.LoadingPage
import com.github.damontecres.dolphin.ui.detail.movie.MovieViewModel
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.LoadingState

View file

@ -0,0 +1,176 @@
package com.github.damontecres.dolphin.ui.detail.movie
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Video
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.dolphin.ui.components.LoadingPage
import com.github.damontecres.dolphin.ui.detail.LoadingItemViewModel
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.extensions.ticks
import javax.inject.Inject
import kotlin.time.Duration
@HiltViewModel
class MovieViewModel
@Inject
constructor(
api: ApiClient,
) : LoadingItemViewModel<Video>(api)
@Composable
fun MovieDetails(
preferences: UserPreferences,
navigationManager: NavigationManager,
destination: Destination.MediaItem,
modifier: Modifier = Modifier,
viewModel: MovieViewModel = hiltViewModel(),
) {
LaunchedEffect(Unit) {
viewModel.init(destination.itemId, destination.item)
}
val item by viewModel.item.observeAsState()
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading -> LoadingPage()
LoadingState.Success -> {
item?.let {
MovieDetailsContent(
preferences,
navigationManager,
it,
modifier,
)
}
}
}
}
@Composable
fun MovieDetailsContent(
preferences: UserPreferences,
navigationManager: NavigationManager,
movie: BaseItem,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
val dto = movie.data
val backdropImageUrl = movie.backdropImageUrl
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
val bringIntoViewRequester = remember { BringIntoViewRequester() }
LaunchedEffect(Unit) {
bringIntoViewRequester.bringIntoView()
focusRequester.tryRequestFocus()
}
Box(modifier = modifier) {
if (backdropImageUrl.isNotNullOrBlank()) {
val gradientColor = MaterialTheme.colorScheme.background
AsyncImage(
model = backdropImageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
alignment = Alignment.TopEnd,
modifier =
Modifier
.fillMaxHeight(.75f)
.alpha(.5f)
.drawWithContent {
drawContent()
drawRect(
Brush.verticalGradient(
colors = listOf(Color.Transparent, gradientColor),
startY = size.height * .5f,
),
)
drawRect(
Brush.horizontalGradient(
colors = listOf(Color.Transparent, gradientColor),
endX = 0f,
startX = size.width * .75f,
),
)
},
)
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(32.dp),
modifier = Modifier.fillMaxWidth(),
) {
item {
MovieDetailsHeader(
movie = movie,
bringIntoViewRequester = bringIntoViewRequester,
overviewOnClick = {},
Modifier
.fillMaxWidth(.7f),
)
}
item {
ExpandablePlayButtons(
resumePosition = resumePosition,
watched = dto.userData?.played ?: false,
playOnClick = {
navigationManager.navigateTo(
Destination.Playback(
movie.id,
it.inWholeMilliseconds,
movie,
),
)
},
moreOnClick = {
// TODO
},
watchOnClick = {
// TODO
},
buttonOnFocusChanged = {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
},
modifier = Modifier.focusRequester(focusRequester),
)
}
}
}
}

View file

@ -0,0 +1,218 @@
package com.github.damontecres.dolphin.ui.detail.movie
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
import com.github.damontecres.dolphin.ui.components.StarRating
import com.github.damontecres.dolphin.ui.components.StarRatingPrecision
import com.github.damontecres.dolphin.ui.components.TitleValueText
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.letNotEmpty
import com.github.damontecres.dolphin.ui.playOnClickSound
import com.github.damontecres.dolphin.ui.playSoundOnFocus
import com.github.damontecres.dolphin.ui.roundMinutes
import com.github.damontecres.dolphin.ui.timeRemaining
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.extensions.ticks
@Composable
fun MovieDetailsHeader(
movie: BaseItem,
bringIntoViewRequester: BringIntoViewRequester,
overviewOnClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val dto = movie.data
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = modifier,
) {
// Title
Text(
text = movie.name ?: "",
color = MaterialTheme.colorScheme.onSurface,
style =
MaterialTheme.typography.displayMedium.copy(
shadow =
Shadow(
color = Color.DarkGray,
offset = Offset(5f, 2f),
blurRadius = 2f,
),
),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.alpha(0.75f),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val details =
buildList {
dto.productionYear?.let { add(it.toString()) }
val duration = dto.runTimeTicks?.ticks
duration
?.roundMinutes
?.toString()
?.let {
add(it)
}
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
}
DotSeparatedRow(
texts = details,
textStyle = MaterialTheme.typography.titleLarge,
)
}
// TODO ratings?
dto.communityRating?.let {
if (it > 0f) {
StarRating(
rating100 = (it * 10).toInt(),
onRatingChange = {},
enabled = false,
precision = StarRatingPrecision.HALF,
playSoundOnFocus = true,
modifier = Modifier.height(40.dp),
)
} else {
Spacer(Modifier.height(40.dp))
}
}
// Description
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(
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),
)
}
}
// Key-Values
Row(
modifier =
Modifier
.padding(top = 8.dp, start = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
dto.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.displayTitle?.let {
TitleValueText(
stringResource(R.string.video),
it,
modifier = Modifier,
)
}
dto.mediaStreams
?.firstOrNull { it.type == MediaStreamType.AUDIO }
?.displayTitle
?.let {
// TODO probably a cleaner way to do this
// Removes part of "5.1 Surround - English - AAC - Default"
it
.replace(" - Default", "")
.ifBlank { null }
?.let {
TitleValueText(
stringResource(R.string.audio),
it,
)
}
}
dto.mediaStreams
?.filter { it.type == MediaStreamType.SUBTITLE && it.language.isNotNullOrBlank() }
?.mapNotNull { it.language }
?.joinToString(", ")
?.let {
if (it.isNotNullOrBlank()) {
TitleValueText(
"Subtitles",
it,
modifier = Modifier.widthIn(max = 64.dp),
)
}
}
// TODO director
// TODO writers
dto.studios?.letNotEmpty {
TitleValueText(
stringResource(R.string.studios),
it.joinToString(", ") { s -> s.name ?: "" },
modifier = Modifier.widthIn(max = 80.dp),
)
}
dto.genres?.letNotEmpty {
TitleValueText(
stringResource(R.string.studios),
it.joinToString(", "),
modifier = Modifier.widthIn(max = 80.dp),
)
}
}
}
}
}

View file

@ -1,30 +1,20 @@
package com.github.damontecres.dolphin.ui.detail.series
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.components.ExpandableFaButton
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButton
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.dolphin.ui.components.TitleValueText
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.tryRequestFocus
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@ -45,102 +35,41 @@ fun FocusedEpisodeFooter(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
modifier =
Modifier
.focusGroup()
.focusProperties {
onEnter = {
firstFocus.tryRequestFocus()
}
},
) {
if (resumePosition > Duration.ZERO) {
item {
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
ExpandablePlayButton(
R.string.resume,
resumePosition,
Icons.Default.PlayArrow,
playOnClick,
Modifier.focusRequester(firstFocus),
// .onFocusChanged(buttonOnFocusChanged),
)
}
item {
ExpandablePlayButton(
R.string.restart,
Duration.ZERO,
Icons.Default.Refresh,
playOnClick,
Modifier,
// .onFocusChanged(buttonOnFocusChanged),
)
}
} else {
item {
ExpandablePlayButton(
R.string.play,
Duration.ZERO,
Icons.Default.PlayArrow,
playOnClick,
Modifier.focusRequester(firstFocus),
// .onFocusChanged(buttonOnFocusChanged)
)
}
}
val played = dto.userData?.played ?: false
// Played button
item {
ExpandableFaButton(
title = if (played) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick,
modifier = Modifier,
)
}
// More button
item {
ExpandablePlayButton(
R.string.more,
Duration.ZERO,
Icons.Default.MoreVert,
{ moreOnClick.invoke() },
Modifier,
// .onFocusChanged(buttonOnFocusChanged)
)
}
}
ExpandablePlayButtons(
resumePosition = resumePosition,
watched = dto.userData?.played ?: false,
playOnClick = playOnClick,
moreOnClick = moreOnClick,
watchOnClick = watchOnClick,
buttonOnFocusChanged = {},
modifier = Modifier,
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
dto.mediaStreams
?.firstOrNull { it.type == MediaStreamType.VIDEO }
?.let { stream ->
stream.displayTitle?.let {
TitleValueText(
"Video",
it,
)
}
?.displayTitle
?.let {
TitleValueText(
stringResource(R.string.video),
it,
)
}
dto.mediaStreams
?.firstOrNull { it.type == MediaStreamType.AUDIO }
?.let { stream ->
?.displayTitle
?.let {
// TODO probably a cleaner way to do this
// Removes part of "5.1 Surround - English - AAC - Default"
stream.displayTitle
?.replace(" - Default", "")
?.ifBlank { null }
it
.replace(" - Default", "")
.ifBlank { null }
?.let {
TitleValueText(
"Audio",
stringResource(R.string.audio),
it,
)
}

View file

@ -6,9 +6,9 @@ import androidx.tv.material3.Text
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.detail.CollectionFolderDetails
import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
import com.github.damontecres.dolphin.ui.detail.MovieDetails
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
import com.github.damontecres.dolphin.ui.detail.VideoDetails
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
import com.github.damontecres.dolphin.ui.main.MainPage

View file

@ -38,4 +38,7 @@
<string name="seek_bar_steps">Seek bar steps</string>
<string name="max_homepage_items">Max items on home page rows</string>
<string name="rewatch_next_up">Enable rewatching in next up</string>
<string name="studios">Studios</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
</resources>