mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Updates to series detail page
This commit is contained in:
parent
993798d14b
commit
8cda98d6aa
10 changed files with 243 additions and 31 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,23 +89,57 @@ 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
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,20 +247,52 @@ 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!!
|
||||||
episodes.value =
|
withContext(Dispatchers.Main) {
|
||||||
eps.copy(
|
episodes.value =
|
||||||
items =
|
eps.copy(
|
||||||
eps.items.toMutableList().apply {
|
items =
|
||||||
this[listIndex] = item
|
eps.items.toMutableList().apply {
|
||||||
},
|
this[listIndex] = item
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue