Updates to series detail page

This commit is contained in:
Damontecres 2025-10-07 14:46:37 -04:00
parent 993798d14b
commit 8cda98d6aa
No known key found for this signature in database
10 changed files with 243 additions and 31 deletions

View file

@ -127,6 +127,7 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.preference.ktx)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)

View file

@ -59,7 +59,7 @@ fun ItemCard(
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp? = null, cardWidth: Dp? = null,
cardHeight: Dp = 200.dp * .85f, cardHeight: Dp? = 200.dp * .85f,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val hideOverlayDelay = 750L val hideOverlayDelay = 750L
@ -112,7 +112,7 @@ fun ItemCard(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(cardHeight), .ifElse(cardHeight != null, { Modifier.height(cardHeight!!) }),
) )
Column( Column(
verticalArrangement = Arrangement.spacedBy(0.dp), verticalArrangement = Arrangement.spacedBy(0.dp),

View file

@ -16,7 +16,7 @@ import com.github.damontecres.dolphin.ui.ifElse
fun NullCard( fun NullCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp? = null, cardWidth: Dp? = null,
cardHeight: Dp = 200.dp * .75f, cardHeight: Dp? = 200.dp * .75f,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
) { ) {
Card( Card(
@ -27,7 +27,7 @@ fun NullCard(
Column( Column(
modifier = modifier =
Modifier Modifier
.height(cardHeight) .ifElse(cardHeight != null, { Modifier.height(cardHeight!!) })
.ifElse(cardWidth != null, { Modifier.width(cardWidth!!) }), .ifElse(cardWidth != null, { Modifier.width(cardWidth!!) }),
) { ) {
Text( Text(

View file

@ -9,12 +9,16 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.focusRestorer import androidx.compose.ui.focus.focusRestorer
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.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.Person import com.github.damontecres.dolphin.data.model.Person
import com.github.damontecres.dolphin.ui.ifElse
@Composable @Composable
fun PersonRow( fun PersonRow(
@ -23,6 +27,7 @@ fun PersonRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onLongClick: ((Person) -> Unit)? = null, onLongClick: ((Person) -> Unit)? = null,
) { ) {
val firstFocus = remember { FocusRequester() }
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, modifier = modifier,
@ -40,14 +45,14 @@ fun PersonRow(
Modifier Modifier
.padding(start = 16.dp) .padding(start = 16.dp)
.fillMaxWidth() .fillMaxWidth()
.focusRestorer(), .focusRestorer(firstFocus),
) { ) {
itemsIndexed(people) { index, item -> itemsIndexed(people) { index, item ->
PersonCard( PersonCard(
item = item, item = item,
onClick = { onClick.invoke(item) }, onClick = { onClick.invoke(item) },
onLongClick = { onLongClick?.invoke(item) }, onLongClick = { onLongClick?.invoke(item) },
modifier = Modifier, modifier = Modifier.ifElse(index == 0, Modifier.focusRequester(firstFocus)),
) )
} }
} }

View file

@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -38,16 +40,19 @@ import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
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 com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.FontAwesome import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -282,6 +287,7 @@ fun ScrollableDialog(
fun BasicDialog( fun BasicDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Dialog( Dialog(
@ -292,7 +298,7 @@ fun BasicDialog(
modifier = modifier =
Modifier Modifier
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), MaterialTheme.colorScheme.surfaceColorAtElevation(elevation),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
), ),
) { ) {
@ -300,3 +306,75 @@ fun BasicDialog(
} }
} }
} }
@Composable
fun ConfirmDialog(
title: String,
body: String?,
onCancel: () -> Unit,
onConfirm: () -> Unit,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp,
) = BasicDialog(
onDismissRequest = onCancel,
properties = properties,
elevation = elevation,
content = {
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier)
},
)
@Composable
fun ConfirmDialogContent(
title: String,
body: String?,
onCancel: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp),
modifier = modifier,
) {
item {
Text(
text = title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
)
}
body?.let {
item {
Text(
text = body,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
item {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
) {
Button(
onClick = onCancel,
) {
Text(
text = stringResource(R.string.cancel),
)
}
Button(
onClick = onConfirm,
) {
Text(
text = stringResource(R.string.confirm),
)
}
}
}
}
}

View file

@ -319,6 +319,7 @@ fun CardGrid(
item = item, item = item,
onClick = { if (item != null) itemOnClick.invoke(item) }, onClick = { if (item != null) itemOnClick.invoke(item) },
onLongClick = { if (item != null) longClicker.invoke(item) }, onLongClick = { if (item != null) longClicker.invoke(item) },
cardHeight = null,
) )
} }
} }

View file

@ -8,18 +8,26 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -36,16 +44,22 @@ 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 coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Person import com.github.damontecres.dolphin.data.model.Person
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.cards.PersonRow import com.github.damontecres.dolphin.ui.cards.PersonRow
import com.github.damontecres.dolphin.ui.components.ConfirmDialog
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.ExpandableFaButton
import com.github.damontecres.dolphin.ui.components.ExpandablePlayButton
import com.github.damontecres.dolphin.ui.components.LoadingPage import com.github.damontecres.dolphin.ui.components.LoadingPage
import com.github.damontecres.dolphin.ui.components.StarRating import com.github.damontecres.dolphin.ui.components.StarRating
import com.github.damontecres.dolphin.ui.components.StarRatingPrecision import com.github.damontecres.dolphin.ui.components.StarRatingPrecision
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialog
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.letNotEmpty import com.github.damontecres.dolphin.ui.letNotEmpty
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
@ -56,6 +70,7 @@ import com.github.damontecres.dolphin.ui.roundMinutes
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.LoadingState import com.github.damontecres.dolphin.util.LoadingState
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@Composable @Composable
fun SeriesDetails( fun SeriesDetails(
@ -74,24 +89,58 @@ fun SeriesDetails(
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty()) val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
val people by viewModel.people.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf())
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showWatchConfirmation by remember { mutableStateOf(false) }
when (val state = loading) { when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state) is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading -> LoadingPage() LoadingState.Loading -> LoadingPage()
LoadingState.Success -> { LoadingState.Success -> {
item?.let { item -> item?.let { item ->
val played = item.data.userData?.played ?: false
SeriesDetailsContent( SeriesDetailsContent(
preferences = preferences, preferences = preferences,
navigationManager = navigationManager, navigationManager = navigationManager,
series = item, series = item,
seasons = seasons, seasons = seasons,
people = people, people = people,
played = played,
modifier = modifier, modifier = modifier,
overviewOnClick = {}, // TODO overviewOnClick = {
overviewDialog =
ItemDetailsDialogInfo(
title = item.name ?: "Unknown",
overview = item.data.overview,
files = listOf(),
)
},
playOnClick = { viewModel.playNextUp(navigationManager) },
watchOnClick = { showWatchConfirmation = true },
)
if (showWatchConfirmation) {
ConfirmDialog(
title = item.name ?: "",
body = if (played) "Mark entire series as unplayed?" else "Mark entire series as played?",
onCancel = {
showWatchConfirmation = false
},
onConfirm = {
viewModel.setWatchedSeries(!played)
showWatchConfirmation = false
},
) )
} }
} }
} }
} }
overviewDialog?.let { info ->
ItemDetailsDialog(
info = info,
onDismissRequest = { overviewDialog = null },
modifier = Modifier,
)
}
}
@Composable @Composable
fun SeriesDetailsContent( fun SeriesDetailsContent(
@ -100,18 +149,16 @@ fun SeriesDetailsContent(
series: BaseItem, series: BaseItem,
seasons: ItemListAndMapping, seasons: ItemListAndMapping,
people: List<Person>, people: List<Person>,
played: Boolean,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
playOnClick: () -> Unit,
watchOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val bringIntoViewRequester = remember { BringIntoViewRequester() }
val dto = series.data
val seasonsFocusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
if (seasons.items.isNotEmpty()) { LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
LaunchedEffect(Unit) {
seasonsFocusRequester.tryRequestFocus()
}
}
Box( Box(
modifier = modifier, modifier = modifier,
@ -153,14 +200,23 @@ fun SeriesDetailsContent(
.fillMaxSize(), .fillMaxSize(),
) { ) {
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(bottom = 80.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier, modifier = modifier,
) { ) {
item { item {
SeriesDetailsHeader( SeriesDetailsHeader(
series = series, series = series,
played = played,
overviewOnClick = overviewOnClick, overviewOnClick = overviewOnClick,
modifier = Modifier.fillMaxWidth(.7f), playOnClick = playOnClick,
watchOnClick = watchOnClick,
modifier =
Modifier
.fillMaxWidth(.6f)
.bringIntoViewRequester(bringIntoViewRequester)
.focusRequester(focusRequester)
.padding(bottom = 80.dp),
) )
} }
item { item {
@ -169,10 +225,16 @@ fun SeriesDetailsContent(
items = seasons.items, items = seasons.items,
onClickItem = { navigationManager.navigateTo(it.destination()) }, onClickItem = { navigationManager.navigateTo(it.destination()) },
onLongClickItem = { }, onLongClickItem = { },
cardOnFocus = { isFocused, index ->
// if (isFocused) {
// scope.launch(ExceptionHandler()) {
// bringIntoViewRequester.bringIntoView()
// }
// }
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(seasonsFocusRequester),
) )
} }
if (people.isNotEmpty()) { if (people.isNotEmpty()) {
@ -193,7 +255,10 @@ fun SeriesDetailsContent(
@Composable @Composable
fun SeriesDetailsHeader( fun SeriesDetailsHeader(
series: BaseItem, series: BaseItem,
played: Boolean,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
playOnClick: () -> Unit,
watchOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -205,6 +270,7 @@ fun SeriesDetailsHeader(
?.ticks ?.ticks
?.roundMinutes ?.roundMinutes
?.let { add(it.toString()) } ?.let { add(it.toString()) }
dto.officialRating?.let(::add)
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@ -212,12 +278,12 @@ fun SeriesDetailsHeader(
) { ) {
Text( Text(
text = series.name ?: "Unknown", text = series.name ?: "Unknown",
style = MaterialTheme.typography.displayMedium, style = MaterialTheme.typography.displaySmall,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
DotSeparatedRow( DotSeparatedRow(
texts = details, texts = details,
textStyle = MaterialTheme.typography.headlineSmall, textStyle = MaterialTheme.typography.titleMedium,
) )
dto.genres?.letNotEmpty { dto.genres?.letNotEmpty {
@ -237,7 +303,7 @@ fun SeriesDetailsHeader(
enabled = false, enabled = false,
precision = StarRatingPrecision.HALF, precision = StarRatingPrecision.HALF,
playSoundOnFocus = true, playSoundOnFocus = true,
modifier = Modifier.height(32.dp), modifier = Modifier.height(24.dp),
) )
} }
} }
@ -278,5 +344,23 @@ fun SeriesDetailsHeader(
) )
} }
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(start = 16.dp),
) {
ExpandablePlayButton(
title = R.string.play,
resume = Duration.ZERO,
icon = Icons.Default.PlayArrow,
onClick = { playOnClick.invoke() },
modifier = Modifier,
)
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,
)
}
} }
} }

View file

@ -1,6 +1,7 @@
package com.github.damontecres.dolphin.ui.detail package com.github.damontecres.dolphin.ui.detail
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -17,6 +18,8 @@ import com.github.damontecres.dolphin.hilt.AuthOkHttpClient
import com.github.damontecres.dolphin.preferences.ThemeSongVolume import com.github.damontecres.dolphin.preferences.ThemeSongVolume
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.letNotEmpty import com.github.damontecres.dolphin.ui.letNotEmpty
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ApiRequestPager import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
@ -33,6 +36,7 @@ import okhttp3.OkHttpClient
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.universalAudioApi import org.jellyfin.sdk.api.client.extensions.universalAudioApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -55,6 +59,7 @@ class SeriesViewModel
) : ItemViewModel<Video>(api) { ) : ItemViewModel<Video>(api) {
private var player: Player? = null private var player: Player? = null
private lateinit var seriesId: UUID private lateinit var seriesId: UUID
private lateinit var prefs: UserPreferences
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty()) val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
val episodes = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty()) val episodes = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
@ -68,6 +73,7 @@ class SeriesViewModel
episode: Int?, episode: Int?,
) { ) {
this.seriesId = itemId this.seriesId = itemId
this.prefs = prefs
viewModelScope.launch( viewModelScope.launch(
LoadingExceptionHandler( LoadingExceptionHandler(
loading, loading,
@ -241,13 +247,24 @@ class SeriesViewModel
refreshEpisode(itemId, listIndex) refreshEpisode(itemId, listIndex)
} }
fun setWatchedSeries(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(seriesId)
} else {
api.playStateApi.markUnplayedItem(seriesId)
}
init(prefs, seriesId, null, null, null)
}
fun refreshEpisode( fun refreshEpisode(
itemId: UUID, itemId: UUID,
listIndex: Int, listIndex: Int,
) = viewModelScope.launch(ExceptionHandler()) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val base = api.userLibraryApi.getItem(itemId).content val base = api.userLibraryApi.getItem(itemId).content
val item = BaseItem.Companion.from(base, api) val item = BaseItem.Companion.from(base, api)
val eps = episodes.value!! val eps = episodes.value!!
withContext(Dispatchers.Main) {
episodes.value = episodes.value =
eps.copy( eps.copy(
items = items =
@ -258,6 +275,27 @@ class SeriesViewModel
} }
} }
fun playNextUp(nav: NavigationManager) {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val result by api.tvShowsApi.getNextUp(seriesId = seriesId)
val nextUp =
result.items.firstOrNull() ?: api.tvShowsApi
.getEpisodes(
seriesId,
limit = 1,
).content.items
.firstOrNull()
if (nextUp != null) {
nav.navigateTo(Destination.Playback(nextUp.id, 0L))
} else {
Toast
.makeText(context, "Could not find an episode to play", Toast.LENGTH_SHORT)
.show()
}
}
}
}
data class ItemListAndMapping( data class ItemListAndMapping(
val items: List<BaseItem?>, val items: List<BaseItem?>,
val numberToIndex: Map<Int, Int>, val numberToIndex: Map<Int, Int>,

View file

@ -56,5 +56,8 @@
<string name="sort_by_date_played">Date Played</string> <string name="sort_by_date_played">Date Played</string>
<string name="sort_by_date_released">Date Released</string> <string name="sort_by_date_released">Date Released</string>
<string name="skip_back_on_resume_preference">Skip back when resuming playback</string> <string name="skip_back_on_resume_preference">Skip back when resuming playback</string>
<string name="pref_key_update_last_check_threshold">preference.update.threshold</string>
<string name="pref_key_update_last_check">preference.update.lastTimestamp</string>
<string name="confirm">Confirm</string>
</resources> </resources>

View file

@ -27,6 +27,7 @@ protobuf-javalite = "4.32.1"
hilt = "2.57.2" hilt = "2.57.2"
room = "2.8.1" room = "2.8.1"
material3 = "1.4.0" material3 = "1.4.0"
preferenceKtx = "1.2.1"
[libraries] [libraries]
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
@ -85,6 +86,7 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }