mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Dependency updates & removing unused code (#70)
Just bumping dependencies and deleting some unused code. No user facing changes
This commit is contained in:
parent
1c1e64a5b9
commit
89b9364807
30 changed files with 367 additions and 844 deletions
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue