From 89b936480795c29c6196ac838648a02080a59765 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 26 Oct 2025 18:17:47 -0400 Subject: [PATCH] Dependency updates & removing unused code (#70) Just bumping dependencies and deleting some unused code. No user facing changes --- .../wholphin/WholphinApplication.kt | 5 +- .../damontecres/wholphin/data/AppDatabase.kt | 2 + .../wholphin/data/ItemPlaybackDao.kt | 1 + ...JellyfinServer.kt => JellyfinServerDao.kt} | 57 +----- .../wholphin/data/ServerPreferencesDao.kt | 1 + .../wholphin/data/ServerRepository.kt | 2 + .../wholphin/data/model/ItemPlayback.kt | 11 +- .../wholphin/data/model/JellyfinServer.kt | 53 +++++ .../wholphin/data/model/ServerPreferences.kt | 1 - .../{ui/data => data/model}/Trailer.kt | 4 +- .../wholphin/ui/components/LicenseInfo.kt | 4 +- .../wholphin/ui/components/PlayButton.kt | 134 ------------ .../wholphin/ui/components/PlayButtons.kt | 175 ++++++++-------- .../components/details/VideoDetailsHeader.kt | 191 ------------------ .../wholphin/ui/detail/VideoDetails.kt | 107 ---------- .../wholphin/ui/detail/movie/MovieDetails.kt | 172 +--------------- .../ui/detail/movie/MovieViewModel.kt | 177 ++++++++++++++++ .../ui/detail/{ => series}/SeriesDetails.kt | 2 +- .../ui/detail/series/SeriesOverview.kt | 3 - .../ui/detail/series/SeriesOverviewContent.kt | 1 - .../ui/detail/{ => series}/SeriesViewModel.kt | 3 +- .../wholphin/ui/nav/ApplicationContent.kt | 4 +- .../wholphin/ui/nav/DestinationContent.kt | 2 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 4 +- .../wholphin/ui/playback/PlaybackViewModel.kt | 50 ----- .../wholphin/ui/setup/ServerList.kt | 2 +- .../wholphin/ui/setup/SwitchUserViewModel.kt | 4 +- .../damontecres/wholphin/ui/setup/UserList.kt | 2 +- .../damontecres/wholphin/util/LazyList.kt | 11 - gradle/libs.versions.toml | 26 +-- 30 files changed, 367 insertions(+), 844 deletions(-) rename app/src/main/java/com/github/damontecres/wholphin/data/{JellyfinServer.kt => JellyfinServerDao.kt} (55%) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt rename app/src/main/java/com/github/damontecres/wholphin/{ui/data => data/model}/Trailer.kt (72%) delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButton.kt delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/details/VideoDetailsHeader.kt delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/detail/VideoDetails.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt rename app/src/main/java/com/github/damontecres/wholphin/ui/detail/{ => series}/SeriesDetails.kt (99%) rename app/src/main/java/com/github/damontecres/wholphin/ui/detail/{ => series}/SeriesViewModel.kt (99%) delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/LazyList.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt index 81f9265d..7418dbc5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt @@ -5,7 +5,6 @@ import android.os.Build import android.util.Log import androidx.compose.runtime.Composer import androidx.compose.runtime.ExperimentalComposeRuntimeApi -import androidx.compose.runtime.tooling.ComposeStackTraceMode import dagger.hilt.android.HiltAndroidApp import org.acra.ACRA import org.acra.ReportField @@ -42,9 +41,7 @@ class WholphinApplication : Application() { ) } - Composer.setDiagnosticStackTraceMode( - if (BuildConfig.DEBUG) ComposeStackTraceMode.SourceInformation else ComposeStackTraceMode.GroupKeys, - ) + Composer.setDiagnosticStackTraceEnabled(BuildConfig.DEBUG) } override fun onCreate() { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt index f825891c..ad9fb3a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt @@ -8,6 +8,8 @@ import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase 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 org.jellyfin.sdk.model.serializer.toUUID import java.util.UUID diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackDao.kt index 92f0eb87..e0743040 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackDao.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ItemPlaybackDao.kt @@ -5,6 +5,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.github.damontecres.wholphin.data.model.ItemPlayback +import com.github.damontecres.wholphin.data.model.JellyfinUser import java.util.UUID @Dao diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServerDao.kt similarity index 55% rename from app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServer.kt rename to app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServerDao.kt index ebfa77d4..e44db6f1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/JellyfinServerDao.kt @@ -1,66 +1,19 @@ package com.github.damontecres.wholphin.data -import androidx.room.ColumnInfo 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.OnConflictStrategy -import androidx.room.PrimaryKey import androidx.room.Query -import androidx.room.Relation import androidx.room.Transaction 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 -@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, -) - @Dao interface JellyfinServerDao { - @Insert(onConflict = OnConflictStrategy.IGNORE) + @Insert(onConflict = OnConflictStrategy.Companion.IGNORE) fun addServer(server: JellyfinServer): Long @Update @@ -74,7 +27,7 @@ interface JellyfinServerDao { } } - @Insert(onConflict = OnConflictStrategy.IGNORE) + @Insert(onConflict = OnConflictStrategy.Companion.IGNORE) fun addUser(user: JellyfinUser): Long @Update diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerPreferencesDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerPreferencesDao.kt index 870086d0..351fabfe 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerPreferencesDao.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerPreferencesDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem @Dao diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index 13bb3106..8975579f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.edit 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.ui.toServerString import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt index 5c2ba906..10bbaea7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt @@ -4,13 +4,14 @@ package com.github.damontecres.wholphin.data.model import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey -import com.github.damontecres.wholphin.data.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSourceInfo @@ -43,9 +44,13 @@ data class ItemPlayback( val audioIndex: 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 } /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt new file mode 100644 index 00000000..af3377cd --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -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, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt index 15ef8003..f3f52585 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt @@ -2,7 +2,6 @@ package com.github.damontecres.wholphin.data.model import androidx.room.Entity import androidx.room.ForeignKey -import com.github.damontecres.wholphin.data.JellyfinUser enum class NavPinType { PINNED, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/Trailer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt similarity index 72% rename from app/src/main/java/com/github/damontecres/wholphin/ui/data/Trailer.kt rename to app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt index d0637d9b..bf82bbf3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/Trailer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt @@ -1,6 +1,4 @@ -package com.github.damontecres.wholphin.ui.data - -import com.github.damontecres.wholphin.data.model.BaseItem +package com.github.damontecres.wholphin.data.model sealed interface Trailer { val name: String diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt index eda224a7..67c56f12 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt @@ -3,12 +3,12 @@ package com.github.damontecres.wholphin.ui.components import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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 @Composable fun LicenseInfo(modifier: Modifier = Modifier) { - val libraries by rememberLibraries() + val libraries by produceLibraries() LibrariesContainer(libraries, modifier) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButton.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButton.kt deleted file mode 100644 index 2c7e0c4e..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButton.kt +++ /dev/null @@ -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), - ) - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index 402eae1d..6790e627 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -1,9 +1,15 @@ 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.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues 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.lazy.LazyRow 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.onFocusChanged 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.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.ifElse import com.github.damontecres.wholphin.ui.theme.WholphinTheme import kotlin.time.Duration 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 */ @@ -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 @Composable private fun ExpandablePlayButtonsPreview() { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/details/VideoDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/details/VideoDetailsHeader.kt deleted file mode 100644 index 8c52735c..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/details/VideoDetailsHeader.kt +++ /dev/null @@ -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, - moreDetails: SortedMap, - 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, - ) - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/VideoDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/VideoDetails.kt deleted file mode 100644 index fc852e58..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/VideoDetails.kt +++ /dev/null @@ -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") - } - } - } - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 263e8f96..3015d903 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -39,23 +39,20 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R 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.data.model.chooseSource import com.github.damontecres.wholphin.preferences.UserPreferences 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.ItemRow 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.data.ItemDetailsDialog 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.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.NavigationManager -import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler 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.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.serializer.toUUID -import java.util.UUID -import javax.inject.Inject 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>(listOf()) - val people = MutableLiveData>(listOf()) - val chapters = MutableLiveData>(listOf()) - val similar = MutableLiveData>(listOf()) - val chosenStreams = MutableLiveData(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 fun MovieDetails( preferences: UserPreferences, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt new file mode 100644 index 00000000..2e30d5b3 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -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>(listOf()) + val people = MutableLiveData>(listOf()) + val chapters = MutableLiveData>(listOf()) + val similar = MutableLiveData>(listOf()) + val chosenStreams = MutableLiveData(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 + } + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt similarity index 99% rename from app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt rename to app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index e27a6080..746871e0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -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.Box diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 199f19c5..4844caf9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -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.data.ItemDetailsDialog 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.nav.Destination import com.github.damontecres.wholphin.ui.tryRequestFocus diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 48b21607..51495cac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -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.components.ErrorMessage 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.isNotNullOrBlank import com.github.damontecres.wholphin.ui.tryRequestFocus diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt similarity index 99% rename from app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt rename to app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index c6a7ab52..8ad3232d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -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.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.UserPreferences 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.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index b8c0dcbd..14476e70 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -7,8 +7,8 @@ import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDe import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay -import com.github.damontecres.wholphin.data.JellyfinServer -import com.github.damontecres.wholphin.data.JellyfinUser +import com.github.damontecres.wholphin.data.model.JellyfinServer +import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences import org.jellyfin.sdk.model.api.DeviceProfile import timber.log.Timber diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 2093ff14..feba06f2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -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.FavoritesPage 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.series.SeriesDetails import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview import com.github.damontecres.wholphin.ui.main.HomePage import com.github.damontecres.wholphin.ui.main.SearchPage diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index a9ad45a6..aa5dc2c3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -59,9 +59,9 @@ import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import androidx.tv.material3.surfaceColorAtElevation 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.model.JellyfinServer +import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.ifElse diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index e6905b32..a4ff435c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -864,53 +864,3 @@ private fun applyTrackSelections( } } } - -private fun determineSubtitleIndex( - subtitleStreams: List, - 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 - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt index 1f35436f..8dadb4fb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/ServerList.kt @@ -23,7 +23,7 @@ import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text 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.DialogItem import com.github.damontecres.wholphin.ui.components.DialogPopup diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index cade75f5..bc69a50c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -3,10 +3,10 @@ package com.github.damontecres.wholphin.ui.setup import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.data.JellyfinServer 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.model.JellyfinServer +import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt index 777e0032..37bc69c0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt @@ -21,7 +21,7 @@ import androidx.tv.material3.Icon import androidx.tv.material3.ListItem import androidx.tv.material3.Text 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.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogPopup diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/LazyList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/LazyList.kt deleted file mode 100644 index e6eb2a98..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/LazyList.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.damontecres.wholphin.util - -class LazyList( - private val source: List, - private val transform: (Source) -> Transform, -) : AbstractList() { - override fun get(index: Int): Transform = transform.invoke(source[index]) - - override val size: Int - get() = source.size -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e71e2e3..11fdc815 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,17 @@ [versions] -aboutLibraries = "12.2.4" +aboutLibraries = "13.1.0" acra = "5.13.1" agp = "8.13.0" auto-service = "1.1.1" autoServiceKsp = "1.2.0" desugar_jdk_libs = "2.1.5" hiltNavigationCompose = "1.3.0" -kotlin = "2.2.20" -ksp = "2.2.20-2.0.4" +kotlin = "2.2.21" +ksp = "2.3.0" coreKtx = "1.17.0" appcompat = "1.7.1" -composeBom = "2025.10.00" -compose-runtime = "1.9.3" +composeBom = "2025.10.01" +compose-runtime = "1.9.4" multiplatformMarkdownRenderer = "0.37.0" programguide = "1.6.0" timber = "5.0.1" @@ -22,19 +22,17 @@ activityCompose = "1.11.0" androidx-media3 = "1.8.0" coil = "3.3.0" jellyfin-sdk = "1.7.1" -nav3Core = "1.0.0-alpha11" -lifecycleViewmodelNav3 = "2.10.0-alpha05" +nav3Core = "1.0.0-beta01" +lifecycleViewmodelNav3 = "2.10.0-beta01" material3AdaptiveNav3 = "1.0.0-alpha03" protobuf = "0.9.5" datastore = "1.1.7" -kotlinx-coroutines-android = "1.10.2" kotlinx-serialization = "1.9.0" -protobuf-javalite = "4.32.1" +protobuf-javalite = "4.33.0" hilt = "2.57.2" -room = "2.8.2" +room = "2.8.3" material3 = "1.4.0" preferenceKtx = "1.2.1" -roomTesting = "2.8.2" [libraries] 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" } programguide = { module = "io.github.oleksandrbalan:programguide", version.ref = "programguide" } 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-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-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-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-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" } 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] 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" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 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" }