diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt index 7e05744020..7babba3374 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel +import androidx.media3.common.C.DEFAULT_SEEK_BACK_INCREMENT_MS import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -38,11 +39,14 @@ import dev.jdtech.jellyfin.core.R import dev.jdtech.jellyfin.models.Preference import dev.jdtech.jellyfin.models.PreferenceCategory import dev.jdtech.jellyfin.models.PreferenceCategoryLabel +import dev.jdtech.jellyfin.models.PreferenceLong import dev.jdtech.jellyfin.models.PreferenceSelect import dev.jdtech.jellyfin.models.PreferenceSwitch import dev.jdtech.jellyfin.ui.components.SettingsCategoryCard import dev.jdtech.jellyfin.ui.components.SettingsCategoryLabel +import dev.jdtech.jellyfin.ui.components.SettingsDetailsLongCard import dev.jdtech.jellyfin.ui.components.SettingsDetailsSelectCard +import dev.jdtech.jellyfin.ui.components.SettingsLongCard import dev.jdtech.jellyfin.ui.components.SettingsSelectCard import dev.jdtech.jellyfin.ui.components.SettingsSwitchCard import dev.jdtech.jellyfin.ui.theme.FindroidTheme @@ -88,6 +92,9 @@ fun SettingsSubScreen( is PreferenceSelect -> { settingsViewModel.setString(preference.backendName, preference.value) } + is PreferenceLong -> { + settingsViewModel.setString(preference.backendName, preference.value.toString()) + } } settingsViewModel.loadPreferences(indexes) } @@ -190,25 +197,50 @@ private fun SettingsSubScreenLayout( } } } + is PreferenceLong -> { + SettingsLongCard( + preference = preference, + modifier = Modifier.onFocusChanged { + if (it.isFocused) { + focusedPreference = preference + } + }, + ) { + onUpdate(preference.copy(value = preference.value)) + } + } } } } Box( modifier = Modifier.weight(2f), ) { - (focusedPreference as? PreferenceSelect)?.let { - SettingsDetailsSelectCard( - preference = it, - modifier = Modifier - .fillMaxSize() - .padding(bottom = MaterialTheme.spacings.large), - onOptionSelected = { value -> - println(value) - val newPreference = it.copy(value = value) - onUpdate(newPreference) - focusedPreference = newPreference - }, - ) + focusedPreference.let { + when (it) { + is PreferenceSelect -> SettingsDetailsSelectCard( + preference = it, + modifier = Modifier + .fillMaxSize() + .padding(bottom = MaterialTheme.spacings.large), + onOptionSelected = { value -> + println(value) + val newPreference = it.copy(value = value) + onUpdate(newPreference) + focusedPreference = newPreference + }, + ) + is PreferenceLong -> SettingsDetailsLongCard( + preference = it, + modifier = Modifier + .fillMaxSize() + .padding(bottom = MaterialTheme.spacings.large), + onValueUpdate = { value -> + val newPreference = it.copy(value = value) + onUpdate(newPreference) + focusedPreference = newPreference + } + ) + } } } } @@ -238,6 +270,13 @@ private fun SettingsSubScreenLayoutPreview() { options = CoreR.array.mpv_hwdec, optionValues = CoreR.array.mpv_hwdec, ), + PreferenceCategoryLabel(nameStringResource = R.string.seeking), + PreferenceLong( + nameStringResource = R.string.seek_back_increment, + backendName = Constants.PREF_PLAYER_SEEK_BACK_INC, + backendDefaultValue = DEFAULT_SEEK_BACK_INCREMENT_MS, + value = DEFAULT_SEEK_BACK_INCREMENT_MS, + ), ), ), title = CoreR.string.settings_category_player, diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsLongCard.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsLongCard.kt new file mode 100644 index 0000000000..7d32c958a2 --- /dev/null +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsLongCard.kt @@ -0,0 +1,108 @@ +package dev.jdtech.jellyfin.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.OutlinedTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.text.isDigitsOnly +import androidx.media3.common.C.DEFAULT_SEEK_BACK_INCREMENT_MS +import androidx.tv.material3.Button +import androidx.tv.material3.ButtonScale +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import dev.jdtech.jellyfin.Constants +import dev.jdtech.jellyfin.models.PreferenceLong +import dev.jdtech.jellyfin.ui.theme.FindroidTheme +import dev.jdtech.jellyfin.ui.theme.spacings +import dev.jdtech.jellyfin.core.R as CoreR + +@Composable +fun SettingsDetailsLongCard( + preference: PreferenceLong, + modifier: Modifier = Modifier, + onValueUpdate: (Long) -> Unit, +) { + Surface(modifier = modifier) { + Column( + modifier = Modifier.padding( + horizontal = MaterialTheme.spacings.default, + vertical = MaterialTheme.spacings.medium, + ), + ) { + Text( + text = stringResource(id = preference.nameStringResource), + style = MaterialTheme.typography.headlineMedium + ) + preference.descriptionStringRes?.let { + Spacer(modifier = Modifier.height(MaterialTheme.spacings.small)) + Text(text = stringResource(id = it), style = MaterialTheme.typography.bodyMedium) + } + Spacer(modifier = Modifier.height(MaterialTheme.spacings.default)) + Column(modifier = Modifier.padding(vertical = MaterialTheme.spacings.medium)) { + var text by remember(preference.value) { mutableStateOf(preference.value.toString()) } + OutlinedTextField( + value = text, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { + if (it.isDigitsOnly()) { + text = it + } + }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(MaterialTheme.spacings.medium)) + Box { + Button( + onClick = { + onValueUpdate(text.toLongOrNull() ?: return@Button) + }, + enabled = text.isNotEmpty(), + scale = ButtonScale.None, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(id = android.R.string.ok), + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + + } + } + } +} + +@Preview +@Composable +private fun SettingsDetailLongCardPreview() { + FindroidTheme { + SettingsDetailsLongCard( + preference = PreferenceLong( + nameStringResource = CoreR.string.seek_back_increment, + backendName = Constants.PREF_PLAYER_SEEK_BACK_INC, + backendDefaultValue = DEFAULT_SEEK_BACK_INCREMENT_MS, + value = DEFAULT_SEEK_BACK_INCREMENT_MS, + ), + onValueUpdate = {}, + ) + } +} diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsLongCard.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsLongCard.kt new file mode 100644 index 0000000000..9766f90202 --- /dev/null +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsLongCard.kt @@ -0,0 +1,105 @@ +package dev.jdtech.jellyfin.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.media3.common.C.DEFAULT_SEEK_BACK_INCREMENT_MS +import androidx.tv.material3.Border +import androidx.tv.material3.ClickableSurfaceDefaults +import androidx.tv.material3.ClickableSurfaceScale +import androidx.tv.material3.Icon +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import dev.jdtech.jellyfin.Constants +import dev.jdtech.jellyfin.models.PreferenceLong +import dev.jdtech.jellyfin.ui.theme.FindroidTheme +import dev.jdtech.jellyfin.ui.theme.spacings +import dev.jdtech.jellyfin.core.R as CoreR + +@Composable +fun SettingsLongCard( + preference: PreferenceLong, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Surface( + onClick = onClick, + enabled = preference.enabled, + shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(10.dp)), + colors = ClickableSurfaceDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface, + focusedContainerColor = MaterialTheme.colorScheme.surface, + ), + border = ClickableSurfaceDefaults.border( + focusedBorder = Border( + BorderStroke( + 4.dp, + Color.White, + ), + shape = RoundedCornerShape(10.dp), + ), + ), + scale = ClickableSurfaceScale.None, + modifier = modifier + .fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(MaterialTheme.spacings.default), + verticalAlignment = Alignment.CenterVertically, + ) { + if (preference.iconDrawableId != null) { + Icon( + painter = painterResource(id = preference.iconDrawableId!!), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(24.dp)) + } + + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = preference.nameStringResource), + style = MaterialTheme.typography.titleMedium, + ) + + Spacer(modifier = Modifier.height(MaterialTheme.spacings.extraSmall)) + Text( + text = preference.value.toString(), + style = MaterialTheme.typography.labelMedium, + ) + } + } + } +} + +@Preview +@Composable +private fun SettingsLongCardPreview() { + FindroidTheme { + SettingsLongCard( + preference = PreferenceLong( + nameStringResource = CoreR.string.seek_back_increment, + backendName = Constants.PREF_PLAYER_SEEK_BACK_INC, + backendDefaultValue = DEFAULT_SEEK_BACK_INCREMENT_MS, + value = DEFAULT_SEEK_BACK_INCREMENT_MS, + ), + onClick = {}, + ) + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f38277b956..8bb972d9ba 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { // implementation(composeBom) implementation(libs.androidx.compose.ui) implementation(libs.androidx.core) + implementation(libs.androidx.media3.common) implementation(libs.androidx.hilt.work) ksp(libs.androidx.hilt.compiler) implementation(libs.androidx.lifecycle.viewmodel) diff --git a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/SettingsViewModel.kt b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/SettingsViewModel.kt index 7aa9c240d0..6553760624 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/SettingsViewModel.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/SettingsViewModel.kt @@ -2,6 +2,8 @@ package dev.jdtech.jellyfin.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.media3.common.C.DEFAULT_SEEK_BACK_INCREMENT_MS +import androidx.media3.common.C.DEFAULT_SEEK_FORWARD_INCREMENT_MS import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.Constants @@ -9,6 +11,7 @@ import dev.jdtech.jellyfin.core.R import dev.jdtech.jellyfin.models.Preference import dev.jdtech.jellyfin.models.PreferenceCategory import dev.jdtech.jellyfin.models.PreferenceCategoryLabel +import dev.jdtech.jellyfin.models.PreferenceLong import dev.jdtech.jellyfin.models.PreferenceSelect import dev.jdtech.jellyfin.models.PreferenceSwitch import kotlinx.coroutines.channels.Channel @@ -111,6 +114,17 @@ constructor( options = R.array.mpv_aos, optionValues = R.array.mpv_aos, ), + PreferenceCategoryLabel(nameStringResource = R.string.seeking), + PreferenceLong( + nameStringResource = R.string.seek_back_increment, + backendName = Constants.PREF_PLAYER_SEEK_BACK_INC, + backendDefaultValue = DEFAULT_SEEK_BACK_INCREMENT_MS, + ), + PreferenceLong( + nameStringResource = R.string.seek_forward_increment, + backendName = Constants.PREF_PLAYER_SEEK_FORWARD_INC, + backendDefaultValue = DEFAULT_SEEK_FORWARD_INCREMENT_MS, + ), ), ), PreferenceCategory( @@ -192,6 +206,15 @@ constructor( value = getString(preference.backendName, preference.backendDefaultValue), ) } + is PreferenceLong -> { + preference.copy( + enabled = preference.dependencies.all { getBoolean(it, false) }, + value = getString( + preference.backendName, + preference.backendDefaultValue.toString() + )!!.toLongOrNull() ?: preference.backendDefaultValue + ) + } else -> preference } } diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/models/PreferenceLong.kt b/preferences/src/main/java/dev/jdtech/jellyfin/models/PreferenceLong.kt new file mode 100644 index 0000000000..62ba195030 --- /dev/null +++ b/preferences/src/main/java/dev/jdtech/jellyfin/models/PreferenceLong.kt @@ -0,0 +1,16 @@ +package dev.jdtech.jellyfin.models + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +data class PreferenceLong( + @StringRes override val nameStringResource: Int, + @StringRes override val descriptionStringRes: Int? = null, + @DrawableRes override val iconDrawableId: Int? = null, + override val enabled: Boolean = true, + override val dependencies: List = emptyList(), + val onClick: (Preference) -> Unit = {}, + val backendName: String, + val backendDefaultValue: Long, + val value: Long = 0L, +) : Preference