Merge branch 'main' into fea/customize-home

This commit is contained in:
Damontecres 2026-01-29 14:27:19 -05:00
commit 01bdf2cefa
No known key found for this signature in database
10 changed files with 57 additions and 35 deletions

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />

View file

@ -752,7 +752,7 @@ sealed interface AppPreference<Pref, T> {
val PlayerBackendPref = val PlayerBackendPref =
AppChoicePreference<AppPreferences, PlayerBackend>( AppChoicePreference<AppPreferences, PlayerBackend>(
title = R.string.player_backend, title = R.string.player_backend,
defaultValue = PlayerBackend.PREFER_MPV, defaultValue = PlayerBackend.EXO_PLAYER,
getter = { it.playbackPreferences.playerBackend }, getter = { it.playbackPreferences.playerBackend },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updatePlaybackPreferences { playerBackend = value } prefs.updatePlaybackPreferences { playerBackend = value }

View file

@ -2,26 +2,21 @@ package com.github.damontecres.wholphin.services
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.WholphinApplication
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.preferences.update import com.github.damontecres.wholphin.preferences.update
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.preferences.updateLiveTvPreferences import com.github.damontecres.wholphin.preferences.updateLiveTvPreferences
import com.github.damontecres.wholphin.preferences.updateMpvOptions import com.github.damontecres.wholphin.preferences.updateMpvOptions
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.Version import com.github.damontecres.wholphin.util.Version
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber import timber.log.Timber
@ -203,12 +198,13 @@ suspend fun upgradeApp(
} }
} }
if (previous.isEqualOrBefore(Version.fromString("0.4.0-1-g0"))) { // TODO temporarily disabled until some MPV bugs are fixed
appPreferences.updateData { // if (previous.isEqualOrBefore(Version.fromString("0.4.0-1-g0"))) {
it.updatePlaybackPreferences { playerBackend = PlayerBackend.PREFER_MPV } // appPreferences.updateData {
} // it.updatePlaybackPreferences { playerBackend = PlayerBackend.PREFER_MPV }
showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG) // }
} // showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG)
// }
if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) {
appPreferences.updateData { appPreferences.updateData {

View file

@ -276,10 +276,16 @@ class StreamChoiceService
SubtitlePlaybackMode.SMART -> { SubtitlePlaybackMode.SMART -> {
if (subtitleLanguage.isNotNullOrBlank()) { if (subtitleLanguage.isNotNullOrBlank()) {
val audioLanguage = userConfig?.audioLanguagePreference val audioLanguage = userConfig?.audioLanguagePreference
if (audioLanguage.isNullOrBlank() || audioLanguage != audioStreamLang) { if (
// Has preferred subtitle lang & preferred audio, so only show subtitles if actual audio is different
(audioLanguage.isNotNullOrBlank() && audioLanguage != audioStreamLang) ||
// Has preferred subtitle lang, but no preferred audio lang, so show subtitle if subtitle lang is different from actual audio
(audioLanguage.isNullOrBlank() && subtitleLanguage != audioStreamLang)
) {
candidates.firstOrNull { it.language == subtitleLanguage } candidates.firstOrNull { it.language == subtitleLanguage }
?: candidates.firstOrNull { it.language.isUnknown } ?: candidates.firstOrNull { it.language.isUnknown }
} else { } else {
// Otherwise, show forced subtitles in preferred lang
candidates.firstOrNull { it.isForced && it.language == subtitleLanguage } candidates.firstOrNull { it.isForced && it.language == subtitleLanguage }
?: candidates.firstOrNull { it.isForced && it.language.isUnknown } ?: candidates.firstOrNull { it.isForced && it.language.isUnknown }
} }

View file

@ -56,7 +56,6 @@ import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.min import kotlin.math.min
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -234,7 +233,7 @@ val Duration.roundMinutes: Duration
* Rounds a [Duration] to nearest whole second * Rounds a [Duration] to nearest whole second
*/ */
val Duration.roundSeconds: Duration val Duration.roundSeconds: Duration
get() = (this + 30.milliseconds).inWholeSeconds.seconds get() = (this + .5.seconds).inWholeSeconds.seconds
/** /**
* Gets the user's playback position as a [Duration] * Gets the user's playback position as a [Duration]

View file

@ -32,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -183,7 +184,7 @@ fun DialogPopup(
dismissOnClick: Boolean = true, dismissOnClick: Boolean = true,
waitToLoad: Boolean = true, waitToLoad: Boolean = true,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp, elevation: Dp = 8.dp,
) { ) {
var waiting by remember { mutableStateOf(waitToLoad) } var waiting by remember { mutableStateOf(waitToLoad) }
if (showDialog) { if (showDialog) {
@ -238,7 +239,7 @@ fun DialogPopupContent(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
dismissOnClick: Boolean = true, dismissOnClick: Boolean = true,
elevation: Dp = 3.dp, elevation: Dp = 8.dp,
) { ) {
val elevatedContainerColor = val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation) MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
@ -246,6 +247,7 @@ fun DialogPopupContent(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
modifier modifier
.shadow(elevation = elevation, shape = RoundedCornerShape(28.0.dp))
.graphicsLayer { .graphicsLayer {
this.clip = true this.clip = true
this.shape = RoundedCornerShape(28.0.dp) this.shape = RoundedCornerShape(28.0.dp)
@ -296,7 +298,7 @@ fun DialogPopup(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
dismissOnClick: Boolean = true, dismissOnClick: Boolean = true,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp, elevation: Dp = 8.dp,
) = DialogPopup( ) = DialogPopup(
showDialog = true, showDialog = true,
waitToLoad = params.fromLongClick, waitToLoad = params.fromLongClick,
@ -345,6 +347,7 @@ fun ScrollableDialog(
.width(width) .width(width)
.heightIn(max = maxHeight) .heightIn(max = maxHeight)
.focusable() .focusable()
.shadow(elevation = 8.dp, shape = RoundedCornerShape(8.dp))
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
@ -373,7 +376,7 @@ fun ScrollableDialog(
fun BasicDialog( fun BasicDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp, elevation: Dp = 8.dp,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Dialog( Dialog(
@ -383,6 +386,7 @@ fun BasicDialog(
Box( Box(
modifier = modifier =
Modifier Modifier
.shadow(elevation = elevation, shape = RoundedCornerShape(8.dp))
.background( .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation), MaterialTheme.colorScheme.surfaceColorAtElevation(elevation),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
@ -403,7 +407,7 @@ fun ConfirmDialog(
onCancel: () -> Unit, onCancel: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp, elevation: Dp = 8.dp,
) = BasicDialog( ) = BasicDialog(
onDismissRequest = onCancel, onDismissRequest = onCancel,
properties = properties, properties = properties,

View file

@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.GenreText import com.github.damontecres.wholphin.ui.components.GenreText
import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.listToDotString import com.github.damontecres.wholphin.ui.listToDotString
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
@ -103,7 +104,8 @@ fun DiscoverMovieDetailsHeader(
GenreText(it, Modifier.padding(bottom = padding)) GenreText(it, Modifier.padding(bottom = padding))
} }
movie.tagline?.let { tagline -> val tagline = remember { movie.tagline?.takeIf { it.isNotNullOrBlank() } }
tagline?.let {
Text( Text(
text = tagline, text = tagline,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@ -134,8 +136,9 @@ fun DiscoverMovieDetailsHeader(
remember(movie.credits?.crew) { remember(movie.credits?.crew) {
movie.credits movie.credits
?.crew ?.crew
?.filter { it.job == "Directing" } ?.filter { it.job == "Director" && it.name.isNotNullOrBlank() }
?.joinToString(", ") { it.name!! } ?.joinToString(", ") { it.name!! }
?.takeIf { it.isNotNullOrBlank() }
} }
directorName directorName

View file

@ -284,6 +284,7 @@ fun DestinationContent(
} }
SeerrItemType.PERSON -> { SeerrItemType.PERSON -> {
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
DiscoverPersonPage( DiscoverPersonPage(
person = destination.item, person = destination.item,
modifier = modifier, modifier = modifier,

View file

@ -355,7 +355,7 @@ class TestStreamChoiceServiceSmart(
userAudioLang = null, userAudioLang = null,
), ),
TestInput( TestInput(
1, null,
SubtitlePlaybackMode.SMART, SubtitlePlaybackMode.SMART,
subtitles = subtitles =
listOf( listOf(
@ -367,6 +367,18 @@ class TestStreamChoiceServiceSmart(
userSubtitleLang = "eng", userSubtitleLang = "eng",
userAudioLang = null, userAudioLang = null,
), ),
TestInput(
null,
SubtitlePlaybackMode.SMART,
subtitles =
listOf(
subtitle(0, "eng"),
subtitle(1, "spa"),
),
streamAudioLang = "spa",
userSubtitleLang = "spa",
userAudioLang = null,
),
) )
} }
} }
@ -498,7 +510,7 @@ class TestStreamChoiceServiceMultipleChoices(
), ),
), ),
TestInput( TestInput(
2, 0,
SubtitlePlaybackMode.SMART, SubtitlePlaybackMode.SMART,
subtitles = subtitles =
listOf( listOf(

View file

@ -13,10 +13,10 @@ kotlinxCoroutinesCore = "1.10.2"
ksp = "2.3.0" ksp = "2.3.0"
coreKtx = "1.17.0" coreKtx = "1.17.0"
appcompat = "1.7.1" appcompat = "1.7.1"
composeBom = "2025.12.01" composeBom = "2026.01.01"
mockk = "1.14.7" mockk = "1.14.7"
robolectric = "4.14.1" robolectric = "4.16.1"
multiplatformMarkdownRenderer = "0.39.0" multiplatformMarkdownRenderer = "0.39.1"
okhttpBom = "5.3.2" okhttpBom = "5.3.2"
programguide = "1.6.0" programguide = "1.6.0"
slf4j2Timber = "1.2" slf4j2Timber = "1.2"
@ -24,8 +24,8 @@ timber = "5.0.1"
tvFoundation = "1.0.0-alpha12" tvFoundation = "1.0.0-alpha12"
tvMaterial = "1.0.1" tvMaterial = "1.0.1"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.2" activityCompose = "1.12.3"
androidx-media3 = "1.9.0" androidx-media3 = "1.9.1"
coil = "3.3.0" coil = "3.3.0"
jellyfin-sdk = "1.7.1" jellyfin-sdk = "1.7.1"
nav3Core = "1.0.0" nav3Core = "1.0.0"
@ -33,15 +33,15 @@ lifecycleViewmodelNav3 = "2.10.0"
material3AdaptiveNav3 = "1.0.0-alpha03" material3AdaptiveNav3 = "1.0.0-alpha03"
protobuf = "0.9.6" protobuf = "0.9.6"
datastore = "1.2.0" datastore = "1.2.0"
kotlinx-serialization = "1.9.0" kotlinx-serialization = "1.10.0"
protobuf-javalite = "4.33.2" protobuf-javalite = "4.33.4"
hilt = "2.57.2" hilt = "2.58"
room = "2.8.4" room = "2.8.4"
preferenceKtx = "1.2.1" preferenceKtx = "1.2.1"
tvprovider = "1.1.0" tvprovider = "1.1.0"
workRuntimeKtx = "2.11.0" workRuntimeKtx = "2.11.1"
paletteKtx = "1.0.0" paletteKtx = "1.0.0"
openapi-generator = "7.18.0" openapi-generator = "7.19.0"
[libraries] [libraries]
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }