Dependency updates & removing unused code (#70)

Just bumping dependencies and deleting some unused code. No user facing
changes
This commit is contained in:
damontecres 2025-10-26 18:17:47 -04:00 committed by GitHub
parent 1c1e64a5b9
commit 89b9364807
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 367 additions and 844 deletions

View file

@ -5,7 +5,6 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composer import androidx.compose.runtime.Composer
import androidx.compose.runtime.ExperimentalComposeRuntimeApi import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA import org.acra.ACRA
import org.acra.ReportField import org.acra.ReportField
@ -42,9 +41,7 @@ class WholphinApplication : Application() {
) )
} }
Composer.setDiagnosticStackTraceMode( Composer.setDiagnosticStackTraceEnabled(BuildConfig.DEBUG)
if (BuildConfig.DEBUG) ComposeStackTraceMode.SourceInformation else ComposeStackTraceMode.GroupKeys,
)
} }
override fun onCreate() { override fun onCreate() {

View file

@ -8,6 +8,8 @@ import androidx.room.TypeConverters
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID import java.util.UUID

View file

@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinUser
import java.util.UUID import java.util.UUID
@Dao @Dao

View file

@ -1,66 +1,19 @@
package com.github.damontecres.wholphin.data package com.github.damontecres.wholphin.data
import androidx.room.ColumnInfo
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import androidx.room.Relation
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
import com.github.damontecres.wholphin.data.model.JellyfinUser
import java.util.UUID import java.util.UUID
@Entity(tableName = "servers")
data class JellyfinServer(
@PrimaryKey val id: UUID,
val name: String?,
val url: String,
)
@Entity(
tableName = "users",
foreignKeys = [
ForeignKey(
entity = JellyfinServer::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("serverId"),
onDelete = ForeignKey.CASCADE,
),
],
indices = [Index("id", "serverId", unique = true)],
)
data class JellyfinUser(
@PrimaryKey(autoGenerate = true)
val rowId: Int = 0,
@ColumnInfo(index = true)
val id: UUID,
val name: String?,
@ColumnInfo(index = true)
val serverId: UUID,
val accessToken: String?,
) {
override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken=${accessToken.isNotNullOrBlank()})"
}
data class JellyfinServerUsers(
@Embedded val server: JellyfinServer,
@Relation(
parentColumn = "id",
entityColumn = "serverId",
)
val users: List<JellyfinUser>,
)
@Dao @Dao
interface JellyfinServerDao { interface JellyfinServerDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
fun addServer(server: JellyfinServer): Long fun addServer(server: JellyfinServer): Long
@Update @Update
@ -74,7 +27,7 @@ interface JellyfinServerDao {
} }
} }
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
fun addUser(user: JellyfinUser): Long fun addUser(user: JellyfinUser): Long
@Update @Update

View file

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
@Dao @Dao

View file

@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.edit import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext

View file

@ -4,13 +4,14 @@ package com.github.damontecres.wholphin.data.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.damontecres.wholphin.data.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaSourceInfo
@ -43,9 +44,13 @@ data class ItemPlayback(
val audioIndex: Int = TrackIndex.UNSPECIFIED, val audioIndex: Int = TrackIndex.UNSPECIFIED,
val subtitleIndex: Int = TrackIndex.UNSPECIFIED, val subtitleIndex: Int = TrackIndex.UNSPECIFIED,
) { ) {
@Transient val audioIndexEnabled = audioIndex >= 0 @Transient
@Ignore
val audioIndexEnabled = audioIndex >= 0
@Transient val subtitleIndexEnabled = subtitleIndex >= 0 @Transient
@Ignore
val subtitleIndexEnabled = subtitleIndex >= 0
} }
/** /**

View file

@ -0,0 +1,53 @@
package com.github.damontecres.wholphin.data.model
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import java.util.UUID
@Entity(tableName = "servers")
data class JellyfinServer(
@PrimaryKey val id: UUID,
val name: String?,
val url: String,
)
@Entity(
tableName = "users",
foreignKeys = [
ForeignKey(
entity = JellyfinServer::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("serverId"),
onDelete = ForeignKey.CASCADE,
),
],
indices = [Index("id", "serverId", unique = true)],
)
data class JellyfinUser(
@PrimaryKey(autoGenerate = true)
val rowId: Int = 0,
@ColumnInfo(index = true)
val id: UUID,
val name: String?,
@ColumnInfo(index = true)
val serverId: UUID,
val accessToken: String?,
) {
override fun toString(): String =
"JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken=${accessToken.isNotNullOrBlank()})"
}
data class JellyfinServerUsers(
@Embedded val server: JellyfinServer,
@Relation(
parentColumn = "id",
entityColumn = "serverId",
)
val users: List<JellyfinUser>,
)

View file

@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.data.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import com.github.damontecres.wholphin.data.JellyfinUser
enum class NavPinType { enum class NavPinType {
PINNED, PINNED,

View file

@ -1,6 +1,4 @@
package com.github.damontecres.wholphin.ui.data package com.github.damontecres.wholphin.data.model
import com.github.damontecres.wholphin.data.model.BaseItem
sealed interface Trailer { sealed interface Trailer {
val name: String val name: String

View file

@ -3,12 +3,12 @@ package com.github.damontecres.wholphin.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.mikepenz.aboutlibraries.ui.compose.android.rememberLibraries import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
@Composable @Composable
fun LicenseInfo(modifier: Modifier = Modifier) { fun LicenseInfo(modifier: Modifier = Modifier) {
val libraries by rememberLibraries() val libraries by produceLibraries()
LibrariesContainer(libraries, modifier) LibrariesContainer(libraries, modifier)
} }

View file

@ -1,134 +0,0 @@
package com.github.damontecres.wholphin.ui.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.ButtonDefaults
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.ifElse
import kotlin.time.Duration
/**
* An icon button typically used in a row for playing media
*/
@Composable
fun PlayButton(
@StringRes title: Int,
resume: Duration,
icon: ImageVector,
onClick: (position: Duration) -> Unit,
modifier: Modifier = Modifier,
mirrorIcon: Boolean = false,
) {
Button(
onClick = { onClick.invoke(resume) },
modifier = modifier,
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
)
}
}
/**
* An icon button typically used in a row for playing media
*
* Only shows the icon until focused when it expands to show the title
*/
@Composable
fun ExpandablePlayButton(
@StringRes title: Int,
resume: Duration,
icon: ImageVector,
onClick: (position: Duration) -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
mirrorIcon: Boolean = false,
) {
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = { onClick.invoke(resume) },
modifier = modifier,
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
AnimatedVisibility(isFocused) {
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
)
}
}
}
/**
* Similar to [ExpandablePlayButton], but uses a [FontAwesome] string instead of an Icon
*/
@Composable
fun ExpandableFaButton(
@StringRes title: Int,
@StringRes iconStringRes: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified,
) {
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = onClick,
modifier = modifier.heightIn(min = 40.dp),
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Text(
text = stringResource(iconStringRes),
style = MaterialTheme.typography.titleSmall,
color = iconColor,
fontSize = 16.sp,
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier,
)
AnimatedVisibility(isFocused) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}

View file

@ -1,9 +1,15 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -19,105 +25,25 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
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.sp
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.ButtonDefaults
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.DefaultButtonPadding
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/**
* Standard row of [PlayButton] including Play (or Resume & Restart) & More
*/
@Composable
fun PlayButtons(
resumePosition: Duration,
playOnClick: (position: Duration) -> Unit,
moreOnClick: () -> Unit,
buttonOnFocusChanged: (FocusState) -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
val firstFocus = remember { FocusRequester() }
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
modifier =
modifier
.focusGroup()
.focusRestorer(firstFocus),
) {
if (resumePosition > Duration.ZERO) {
item {
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
PlayButton(
R.string.resume,
resumePosition,
Icons.Default.PlayArrow,
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus)
.focusRequester(focusRequester),
)
}
item {
PlayButton(
R.string.restart,
Duration.ZERO,
Icons.Default.Refresh,
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged),
mirrorIcon = true,
)
}
} else {
item {
// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
PlayButton(
R.string.play,
Duration.ZERO,
Icons.Default.PlayArrow,
playOnClick,
Modifier
.onFocusChanged(buttonOnFocusChanged)
.focusRequester(firstFocus)
.focusRequester(focusRequester),
)
}
}
// More button
item {
Button(
onClick = moreOnClick,
onLongClick = {},
modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
)
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(R.string.more),
style = MaterialTheme.typography.titleSmall,
)
}
}
}
}
/** /**
* Standard row of [ExpandablePlayButton] including Play (or Resume & Restart), Mark played, & More * Standard row of [ExpandablePlayButton] including Play (or Resume & Restart), Mark played, & More
*/ */
@ -212,6 +138,81 @@ fun ExpandablePlayButtons(
} }
} }
/**
* An icon button typically used in a row for playing media
*
* Only shows the icon until focused when it expands to show the title
*/
@Composable
fun ExpandablePlayButton(
@StringRes title: Int,
resume: Duration,
icon: ImageVector,
onClick: (position: Duration) -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
mirrorIcon: Boolean = false,
) {
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = { onClick.invoke(resume) },
modifier = modifier,
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.ifElse(mirrorIcon, Modifier.graphicsLayer { scaleX = -1f }),
)
AnimatedVisibility(isFocused) {
Spacer(Modifier.size(8.dp))
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
)
}
}
}
/**
* Similar to [ExpandablePlayButton], but uses a [FontAwesome] string instead of an Icon
*/
@Composable
fun ExpandableFaButton(
@StringRes title: Int,
@StringRes iconStringRes: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified,
) {
val isFocused = interactionSource.collectIsFocusedAsState().value
Button(
onClick = onClick,
modifier = modifier.heightIn(min = 40.dp),
contentPadding = DefaultButtonPadding,
interactionSource = interactionSource,
) {
Text(
text = stringResource(iconStringRes),
style = MaterialTheme.typography.titleSmall,
color = iconColor,
fontSize = 16.sp,
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier,
)
AnimatedVisibility(isFocused) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
@PreviewTvSpec @PreviewTvSpec
@Composable @Composable
private fun ExpandablePlayButtonsPreview() { private fun ExpandablePlayButtonsPreview() {

View file

@ -1,191 +0,0 @@
package com.github.damontecres.wholphin.ui.components.details
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.focus.FocusRequester
import androidx.compose.ui.focus.onFocusChanged
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.text.font.FontWeight
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.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.PlayButtons
import com.github.damontecres.wholphin.ui.components.StarRating
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
import com.github.damontecres.wholphin.ui.components.TitleValueText
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import java.util.SortedMap
import kotlin.time.Duration
@Composable
fun VideoDetailsHeader(
title: String,
subtitle: String?,
description: String?,
details: List<String>,
moreDetails: SortedMap<String, String>,
rating: Float?,
resumeTime: Duration?,
watched: Boolean,
favorite: Boolean,
bringIntoViewRequester: BringIntoViewRequester,
descriptionOnClick: () -> Unit,
moreOnClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
Column(
modifier = modifier,
) {
// Title
Text(
text = title,
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,
)
// Subtitle
if (subtitle.isNotNullOrBlank()) {
// Title
Text(
text = subtitle,
color = MaterialTheme.colorScheme.onSurface,
style =
MaterialTheme.typography.displaySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Column(
modifier = Modifier.alpha(0.75f),
) {
// Rating
if (rating != null) {
StarRating(
rating100 = (rating * 100).toInt(),
precision = StarRatingPrecision.HALF,
onRatingChange = {
// TODO
},
enabled = false,
modifier =
Modifier
.height(40.dp)
.padding(start = 12.dp),
playSoundOnFocus = false,
)
}
// Quick info
if (details.isNotEmpty()) {
DotSeparatedRow(
modifier = Modifier.padding(top = 6.dp, start = 8.dp),
textStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
texts = details,
)
}
// Description
if (description.isNotNullOrBlank()) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused = interactionSource.collectIsFocusedAsState().value
val bgColor =
if (isFocused) {
MaterialTheme.colorScheme.onPrimary.copy(alpha = .75f)
} else {
Color.Unspecified
}
Box(
modifier =
Modifier
.background(bgColor, shape = RoundedCornerShape(8.dp))
.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() }
}
}.playSoundOnFocus(true)
.clickable(
enabled = true,
interactionSource = interactionSource,
indication = LocalIndication.current,
) {
playOnClickSound(context)
descriptionOnClick.invoke()
},
) {
Text(
text = description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(8.dp),
)
}
}
// Key-Values
if (moreDetails.isNotEmpty()) {
Row(
modifier =
Modifier
.padding(top = 8.dp, start = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
moreDetails.forEach { (key, value) ->
TitleValueText(key, value)
}
}
}
}
PlayButtons(
resumePosition = resumeTime ?: Duration.ZERO,
playOnClick = { position ->
// TODO
},
moreOnClick = { moreOnClick.invoke() },
buttonOnFocusChanged = {
scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() }
},
focusRequester = focusRequester,
modifier = Modifier,
)
}
}

View file

@ -1,107 +0,0 @@
package com.github.damontecres.wholphin.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.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.movie.MovieViewModel
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.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 VideoViewModel
@Inject
constructor(
api: ApiClient,
val navigationManager: NavigationManager,
) : LoadingItemViewModel(api)
@Composable
fun VideoDetails(
preferences: UserPreferences,
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,
LoadingState.Pending,
-> 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 = {
viewModel.navigationManager.navigateTo(
Destination.Playback(
item.id,
it.inWholeMilliseconds,
item,
),
)
},
) {
Text(text = "Resume")
}
}
}
}
item {
Button(
onClick = {
viewModel.navigationManager.navigateTo(
Destination.Playback(
item.id,
0L,
item,
),
)
},
) {
Text(text = "Play")
}
}
}
}
}
}
}

View file

@ -39,23 +39,20 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
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.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.ChapterRow import com.github.damontecres.wholphin.ui.cards.ChapterRow
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.PersonRow import com.github.damontecres.wholphin.ui.cards.PersonRow
@ -69,180 +66,17 @@ import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.LocalTrailer
import com.github.damontecres.wholphin.ui.data.RemoteTrailer
import com.github.damontecres.wholphin.ui.data.Trailer
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration import kotlin.time.Duration
@HiltViewModel
class MovieViewModel
@Inject
constructor(
api: ApiClient,
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
) : LoadingItemViewModel(api) {
private lateinit var itemId: UUID
val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf())
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
override fun init(
itemId: UUID,
potential: BaseItem?,
): Job? {
this.itemId = itemId
return viewModelScope.launch(ExceptionHandler()) {
super.init(itemId, potential)?.join()
item.value?.let { item ->
viewModelScope.launchIO {
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
withContext(Dispatchers.Main) {
chosenStreams.value = result
}
val remoteTrailers =
item.data.remoteTrailers
?.mapNotNull { t ->
t.url?.let { url ->
val name =
t.name
// TODO would be nice to clean up the trailer name
// ?.replace(item.name ?: "", "")
// ?.removePrefix(" - ")
?: "Trailer"
RemoteTrailer(name, url)
}
}.orEmpty()
.sortedBy { it.name }
val localTrailerCount = item.data.localTrailerCount ?: 0
val localTrailers =
if (localTrailerCount > 0) {
api.userLibraryApi.getLocalTrailers(itemId).content.map {
LocalTrailer(BaseItem.from(it, api))
}
} else {
listOf()
}
withContext(Dispatchers.Main) {
this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
}
}
withContext(Dispatchers.Main) {
people.value =
item.data.people
?.letNotEmpty { people ->
people.map { Person.fromDto(it, api) }
}.orEmpty()
chapters.value = Chapter.fromDto(item.data, api)
}
val similar =
api.libraryApi
.getSimilarItems(
GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id,
itemId = itemId,
fields = SlimItemFields,
limit = 25,
),
).content.items
.map { BaseItem.from(it, api) }
this@MovieViewModel.similar.setValueOnMain(similar)
}
}
}
fun setWatched(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
fetchAndSetItem(itemId)
}
fun setFavorite(favorite: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
fetchAndSetItem(itemId)
}
fun savePlayVersion(
item: BaseItem,
sourceId: UUID,
) {
viewModelScope.launchIO {
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
val chosen =
result?.let {
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
}
withContext(Dispatchers.Main) {
chosenStreams.value = chosen
}
}
}
fun saveTrackSelection(
item: BaseItem,
itemPlayback: ItemPlayback?,
trackIndex: Int,
type: MediaStreamType,
) {
viewModelScope.launchIO {
val result =
itemPlaybackRepository.saveTrackSelection(
item = item,
itemPlayback = itemPlayback,
trackIndex = trackIndex,
type = type,
)
val chosen =
result?.let {
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
}
withContext(Dispatchers.Main) {
chosenStreams.value = chosen
}
}
}
}
@Composable @Composable
fun MovieDetails( fun MovieDetails(
preferences: UserPreferences, preferences: UserPreferences,

View file

@ -0,0 +1,177 @@
package com.github.damontecres.wholphin.ui.detail.movie
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
class MovieViewModel
@Inject
constructor(
api: ApiClient,
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
) : LoadingItemViewModel(api) {
private lateinit var itemId: UUID
val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf())
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
override fun init(
itemId: UUID,
potential: BaseItem?,
): Job? {
this.itemId = itemId
return viewModelScope.launch(ExceptionHandler()) {
super.init(itemId, potential)?.join()
item.value?.let { item ->
viewModelScope.launchIO {
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
withContext(Dispatchers.Main) {
chosenStreams.value = result
}
val remoteTrailers =
item.data.remoteTrailers
?.mapNotNull { t ->
t.url?.let { url ->
val name =
t.name
// TODO would be nice to clean up the trailer name
// ?.replace(item.name ?: "", "")
// ?.removePrefix(" - ")
?: "Trailer"
RemoteTrailer(name, url)
}
}.orEmpty()
.sortedBy { it.name }
val localTrailerCount = item.data.localTrailerCount ?: 0
val localTrailers =
if (localTrailerCount > 0) {
api.userLibraryApi.getLocalTrailers(itemId).content.map {
LocalTrailer(BaseItem.Companion.from(it, api))
}
} else {
listOf()
}
withContext(Dispatchers.Main) {
this@MovieViewModel.trailers.value = localTrailers + remoteTrailers
}
}
withContext(Dispatchers.Main) {
people.value =
item.data.people
?.letNotEmpty { people ->
people.map { Person.Companion.fromDto(it, api) }
}.orEmpty()
chapters.value = Chapter.Companion.fromDto(item.data, api)
}
val similar =
api.libraryApi
.getSimilarItems(
GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id,
itemId = itemId,
fields = SlimItemFields,
limit = 25,
),
).content.items
.map { BaseItem.Companion.from(it, api) }
this@MovieViewModel.similar.setValueOnMain(similar)
}
}
}
fun setWatched(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
fetchAndSetItem(itemId)
}
fun setFavorite(favorite: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
fetchAndSetItem(itemId)
}
fun savePlayVersion(
item: BaseItem,
sourceId: UUID,
) {
viewModelScope.launchIO {
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
val chosen =
result?.let {
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
}
withContext(Dispatchers.Main) {
chosenStreams.value = chosen
}
}
}
fun saveTrackSelection(
item: BaseItem,
itemPlayback: ItemPlayback?,
trackIndex: Int,
type: MediaStreamType,
) {
viewModelScope.launchIO {
val result =
itemPlaybackRepository.saveTrackSelection(
item = item,
itemPlayback = itemPlayback,
trackIndex = trackIndex,
type = type,
)
val chosen =
result?.let {
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
}
withContext(Dispatchers.Main) {
chosenStreams.value = chosen
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.github.damontecres.wholphin.ui.detail package com.github.damontecres.wholphin.ui.detail.series
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box

View file

@ -25,9 +25,6 @@ import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.EpisodeList
import com.github.damontecres.wholphin.ui.detail.ItemListAndMapping
import com.github.damontecres.wholphin.ui.detail.SeriesViewModel
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus

View file

@ -50,7 +50,6 @@ import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.EpisodeList
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus

View file

@ -1,4 +1,4 @@
package com.github.damontecres.wholphin.ui.detail package com.github.damontecres.wholphin.ui.detail.series
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination

View file

@ -7,8 +7,8 @@ import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDe
import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import com.github.damontecres.wholphin.data.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber import timber.log.Timber

View file

@ -12,8 +12,8 @@ import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv
import com.github.damontecres.wholphin.ui.detail.DebugPage import com.github.damontecres.wholphin.ui.detail.DebugPage
import com.github.damontecres.wholphin.ui.detail.FavoritesPage import com.github.damontecres.wholphin.ui.detail.FavoritesPage
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
import com.github.damontecres.wholphin.ui.detail.SeriesDetails
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
import com.github.damontecres.wholphin.ui.detail.series.SeriesDetails
import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview
import com.github.damontecres.wholphin.ui.main.HomePage import com.github.damontecres.wholphin.ui.main.HomePage
import com.github.damontecres.wholphin.ui.main.SearchPage import com.github.damontecres.wholphin.ui.main.SearchPage

View file

@ -59,9 +59,9 @@ import androidx.tv.material3.Text
import androidx.tv.material3.rememberDrawerState import androidx.tv.material3.rememberDrawerState
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.JellyfinServer
import com.github.damontecres.wholphin.data.JellyfinUser
import com.github.damontecres.wholphin.data.NavDrawerItemRepository import com.github.damontecres.wholphin.data.NavDrawerItemRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse

View file

@ -864,53 +864,3 @@ private fun applyTrackSelections(
} }
} }
} }
private fun determineSubtitleIndex(
subtitleStreams: List<SubtitleStream>,
subtitleMode: SubtitlePlaybackMode,
audioLanguage: String?,
subtitleLanguage: String?,
playbackConfig: ItemPlayback,
): Int? {
if (playbackConfig.subtitleIndex == TrackIndex.DISABLED) {
return null
} else if (playbackConfig.subtitleIndexEnabled) {
return playbackConfig.subtitleIndex
} else {
return when (subtitleMode) {
SubtitlePlaybackMode.ALWAYS -> {
if (subtitleLanguage != null) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
} else {
subtitleStreams.firstOrNull()?.index
}
}
SubtitlePlaybackMode.ONLY_FORCED ->
if (subtitleLanguage != null) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index
} else {
subtitleStreams.firstOrNull { it.forced }?.index
}
SubtitlePlaybackMode.SMART -> {
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
} else {
null
}
}
SubtitlePlaybackMode.DEFAULT -> {
// TODO check for language?
(
subtitleStreams.firstOrNull { it.default && it.forced }
?: subtitleStreams.firstOrNull { it.default }
?: subtitleStreams.firstOrNull { it.forced }
)?.index
}
SubtitlePlaybackMode.NONE -> null
}
}
}

View file

@ -23,7 +23,7 @@ import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup

View file

@ -3,10 +3,10 @@ package com.github.damontecres.wholphin.ui.setup
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.JellyfinServer
import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.JellyfinUser
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager

View file

@ -21,7 +21,7 @@ import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.DialogPopup

View file

@ -1,11 +0,0 @@
package com.github.damontecres.wholphin.util
class LazyList<Source, Transform>(
private val source: List<Source>,
private val transform: (Source) -> Transform,
) : AbstractList<Transform>() {
override fun get(index: Int): Transform = transform.invoke(source[index])
override val size: Int
get() = source.size
}

View file

@ -1,17 +1,17 @@
[versions] [versions]
aboutLibraries = "12.2.4" aboutLibraries = "13.1.0"
acra = "5.13.1" acra = "5.13.1"
agp = "8.13.0" agp = "8.13.0"
auto-service = "1.1.1" auto-service = "1.1.1"
autoServiceKsp = "1.2.0" autoServiceKsp = "1.2.0"
desugar_jdk_libs = "2.1.5" desugar_jdk_libs = "2.1.5"
hiltNavigationCompose = "1.3.0" hiltNavigationCompose = "1.3.0"
kotlin = "2.2.20" kotlin = "2.2.21"
ksp = "2.2.20-2.0.4" ksp = "2.3.0"
coreKtx = "1.17.0" coreKtx = "1.17.0"
appcompat = "1.7.1" appcompat = "1.7.1"
composeBom = "2025.10.00" composeBom = "2025.10.01"
compose-runtime = "1.9.3" compose-runtime = "1.9.4"
multiplatformMarkdownRenderer = "0.37.0" multiplatformMarkdownRenderer = "0.37.0"
programguide = "1.6.0" programguide = "1.6.0"
timber = "5.0.1" timber = "5.0.1"
@ -22,19 +22,17 @@ activityCompose = "1.11.0"
androidx-media3 = "1.8.0" androidx-media3 = "1.8.0"
coil = "3.3.0" coil = "3.3.0"
jellyfin-sdk = "1.7.1" jellyfin-sdk = "1.7.1"
nav3Core = "1.0.0-alpha11" nav3Core = "1.0.0-beta01"
lifecycleViewmodelNav3 = "2.10.0-alpha05" lifecycleViewmodelNav3 = "2.10.0-beta01"
material3AdaptiveNav3 = "1.0.0-alpha03" material3AdaptiveNav3 = "1.0.0-alpha03"
protobuf = "0.9.5" protobuf = "0.9.5"
datastore = "1.1.7" datastore = "1.1.7"
kotlinx-coroutines-android = "1.10.2"
kotlinx-serialization = "1.9.0" kotlinx-serialization = "1.9.0"
protobuf-javalite = "4.32.1" protobuf-javalite = "4.33.0"
hilt = "2.57.2" hilt = "2.57.2"
room = "2.8.2" room = "2.8.3"
material3 = "1.4.0" material3 = "1.4.0"
preferenceKtx = "1.2.1" preferenceKtx = "1.2.1"
roomTesting = "2.8.2"
[libraries] [libraries]
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
@ -70,14 +68,12 @@ multiplatform-markdown-renderer = { module = "com.mikepenz:multiplatform-markdow
multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRenderer" } multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRenderer" }
programguide = { module = "io.github.oleksandrbalan:programguide", version.ref = "programguide" } programguide = { module = "io.github.oleksandrbalan:programguide", version.ref = "programguide" }
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" } androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" } androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" } androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" }
@ -103,7 +99,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
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" } androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "roomTesting" } androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
@ -115,4 +111,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
room = { id = "androidx.room", version.ref = "room" } room = { id = "androidx.room", version.ref = "room" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }