Merge branch 'main' into develop/mpv

This commit is contained in:
Damontecres 2025-11-08 19:57:44 -05:00
commit 6d585a2a99
No known key found for this signature in database
16 changed files with 612 additions and 97 deletions

View file

@ -668,6 +668,12 @@ sealed interface AppPreference<T> {
summaryOff = R.string.disabled, summaryOff = R.string.disabled,
) )
val SubtitleStyle =
AppDestinationPreference(
title = R.string.subtitle_style,
destination = Destination.Settings(PreferenceScreenOption.SUBTITLES),
)
val PlayerBackendPref = val PlayerBackendPref =
AppChoicePreference<PlayerBackend>( AppChoicePreference<PlayerBackend>(
title = R.string.player_backend, title = R.string.player_backend,
@ -704,6 +710,7 @@ val basicPreferences =
AppPreference.RewatchNextUp, AppPreference.RewatchNextUp,
AppPreference.PlayThemeMusic, AppPreference.PlayThemeMusic,
AppPreference.RememberSelectedTab, AppPreference.RememberSelectedTab,
AppPreference.SubtitleStyle,
AppPreference.ThemeColors, AppPreference.ThemeColors,
), ),
), ),

View file

@ -1,7 +1,9 @@
package com.github.damontecres.wholphin.preferences package com.github.damontecres.wholphin.preferences
import androidx.compose.ui.graphics.toArgb
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -83,6 +85,13 @@ class AppPreferencesSerializer
navDrawerSwitchOnFocus = navDrawerSwitchOnFocus =
AppPreference.NavDrawerSwitchOnFocus.defaultValue AppPreference.NavDrawerSwitchOnFocus.defaultValue
showClock = AppPreference.ShowClock.defaultValue showClock = AppPreference.ShowClock.defaultValue
subtitlesPreferences =
SubtitlePreferences
.newBuilder()
.apply {
resetSubtitles()
}.build()
}.build() }.build()
}.build() }.build()
@ -126,3 +135,21 @@ inline fun AppPreferences.updateInterfacePreferences(block: InterfacePreferences
update { update {
interfacePreferences = interfacePreferences.toBuilder().apply(block).build() interfacePreferences = interfacePreferences.toBuilder().apply(block).build()
} }
inline fun AppPreferences.updateSubtitlePreferences(block: SubtitlePreferences.Builder.() -> Unit): AppPreferences =
updateInterfacePreferences {
subtitlesPreferences = subtitlesPreferences.toBuilder().apply(block).build()
}
fun SubtitlePreferences.Builder.resetSubtitles() {
fontSize = SubtitleSettings.FontSize.defaultValue.toInt()
fontColor = SubtitleSettings.FontColor.defaultValue.toArgb()
fontBold = SubtitleSettings.FontBold.defaultValue
fontItalic = SubtitleSettings.FontItalic.defaultValue
fontOpacity = SubtitleSettings.FontOpacity.defaultValue.toInt()
edgeColor = SubtitleSettings.EdgeColor.defaultValue.toArgb()
edgeStyle = SubtitleSettings.EdgeStylePref.defaultValue
backgroundColor = SubtitleSettings.BackgroundColor.defaultValue.toArgb()
backgroundOpacity = SubtitleSettings.BackgroundOpacity.defaultValue.toInt()
backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue
}

View file

@ -44,7 +44,7 @@ fun SliderBar(
targetValue = 6.dp.times((if (isFocused) 2f else 1f)), targetValue = 6.dp.times((if (isFocused) 2f else 1f)),
) )
var currentValue by remember(value) { mutableLongStateOf(value) } var currentValue by remember(value) { mutableLongStateOf(value) }
val percent = currentValue.toFloat() / (max - min) val percent = (currentValue - min).toFloat() / (max - min)
val handleSeekEventModifier = val handleSeekEventModifier =
Modifier.handleDPadKeyEvents( Modifier.handleDPadKeyEvents(

View file

@ -342,37 +342,50 @@ fun PlaybackOverlay(
(playerControls.currentPosition.toFloat() / playerControls.duration) (playerControls.currentPosition.toFloat() / playerControls.duration)
} }
} }
if (trickplayInfo != null) {
AnimatedVisibility(
seekProgressPercent >= 0 && seekBarFocused,
) {
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
val index =
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
val imageUrl = trickplayUrlFor(index)
if (imageUrl != null) { AnimatedVisibility(
Box( seekProgressPercent >= 0 && seekBarFocused,
modifier = ) {
Modifier Box(
.align(Alignment.Center) modifier =
.fillMaxWidth(.95f), Modifier
) { .align(Alignment.Center)
SeekPreviewImage( .fillMaxWidth(.95f),
modifier = ) {
Modifier Column(
.align(Alignment.BottomStart) verticalArrangement = Arrangement.spacedBy(8.dp),
.offsetByPercent( horizontalAlignment = Alignment.CenterHorizontally,
xPercentage = seekProgressPercent.coerceIn(0f, 1f), modifier =
).padding(bottom = controllerHeight - titleHeight - subtitleHeight), Modifier
previewImageUrl = imageUrl, .align(Alignment.BottomStart)
duration = playerControls.duration, .offsetByPercent(
seekProgressMs = seekProgressMs, xPercentage = seekProgressPercent.coerceIn(0f, 1f),
videoWidth = trickplayInfo.width, ).padding(bottom = controllerHeight - titleHeight - subtitleHeight),
videoHeight = trickplayInfo.height, ) {
trickPlayInfo = trickplayInfo, if (trickplayInfo != null) {
) val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
val index =
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
val imageUrl = trickplayUrlFor(index)
if (imageUrl != null) {
SeekPreviewImage(
modifier =
Modifier,
previewImageUrl = imageUrl,
duration = playerControls.duration,
seekProgressMs = seekProgressMs,
videoWidth = trickplayInfo.width,
videoHeight = trickplayInfo.height,
trickPlayInfo = trickplayInfo,
)
}
} }
Text(
text = (seekProgressMs / 1000L).seconds.toString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge,
)
} }
} }
} }

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.playback package com.github.damontecres.wholphin.ui.playback
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.Dimension
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@ -70,6 +71,7 @@ import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
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.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
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
@ -285,8 +287,10 @@ fun PlaybackPage(
AndroidView( AndroidView(
factory = { context -> factory = { context ->
SubtitleView(context).apply { SubtitleView(context).apply {
setUserDefaultStyle() preferences.appPreferences.interfacePreferences.subtitlesPreferences.let {
setUserDefaultTextSize() setStyle(it.toSubtitleStyle())
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
}
} }
}, },
update = { update = {

View file

@ -2,12 +2,9 @@ package com.github.damontecres.wholphin.ui.playback
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
@ -17,7 +14,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
@ -25,7 +21,6 @@ import coil3.request.transformations
import com.github.damontecres.wholphin.ui.CoilTrickplayTransformation import com.github.damontecres.wholphin.ui.CoilTrickplayTransformation
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import org.jellyfin.sdk.model.api.TrickplayInfo import org.jellyfin.sdk.model.api.TrickplayInfo
import kotlin.time.Duration.Companion.seconds
fun Modifier.offsetByPercent( fun Modifier.offsetByPercent(
xPercentage: Float, xPercentage: Float,
@ -85,55 +80,44 @@ fun SeekPreviewImage(
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column( if (previewImageUrl.isNotNullOrBlank() &&
modifier = modifier, videoWidth != null &&
verticalArrangement = Arrangement.spacedBy(8.dp), videoHeight != null
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
if (previewImageUrl.isNotNullOrBlank() && val height = 160.dp
videoWidth != null && val width = height * (videoWidth.toFloat() / videoHeight)
videoHeight != null val heightPx = with(LocalDensity.current) { height.toPx().toInt() }
) { val widthPx = with(LocalDensity.current) { width.toPx().toInt() }
val height = 160.dp
val width = height * (videoWidth.toFloat() / videoHeight)
val heightPx = with(LocalDensity.current) { height.toPx().toInt() }
val widthPx = with(LocalDensity.current) { width.toPx().toInt() }
val index = (seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Which tile val index = (seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Which tile
val numberOfTitlesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth val numberOfTitlesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth
val imageIndex = index % numberOfTitlesPerImage val imageIndex = index % numberOfTitlesPerImage
AsyncImage( AsyncImage(
modifier = modifier =
Modifier modifier
.width(width) .width(width)
.height(height) .height(height)
.background(Color.Black) .background(Color.Black)
.border(1.5.dp, color = MaterialTheme.colorScheme.border), .border(1.5.dp, color = MaterialTheme.colorScheme.border),
model = model =
ImageRequest ImageRequest
.Builder(context) .Builder(context)
.data(previewImageUrl) .data(previewImageUrl)
.memoryCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED)
.transformations( .transformations(
CoilTrickplayTransformation( CoilTrickplayTransformation(
widthPx, widthPx,
heightPx, heightPx,
trickPlayInfo.tileHeight, trickPlayInfo.tileHeight,
trickPlayInfo.tileWidth, trickPlayInfo.tileWidth,
imageIndex, imageIndex,
index, index,
), ),
).build(), ).build(),
contentScale = ContentScale.None, contentScale = ContentScale.None,
contentDescription = null, contentDescription = null,
placeholder = placeHolder, placeholder = placeHolder,
)
}
Text(
text = (seekProgressMs / 1000L).seconds.toString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge,
) )
} }
} }

View file

@ -35,6 +35,7 @@ enum class PreferenceScreenOption {
BASIC, BASIC,
ADVANCED, ADVANCED,
USER_INTERFACE, USER_INTERFACE,
SUBTITLES,
; ;
companion object { companion object {

View file

@ -50,6 +50,8 @@ import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playOnClickSound import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
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
@ -94,12 +96,14 @@ fun PreferencesContent(
PreferenceScreenOption.BASIC -> basicPreferences PreferenceScreenOption.BASIC -> basicPreferences
PreferenceScreenOption.ADVANCED -> advancedPreferences PreferenceScreenOption.ADVANCED -> advancedPreferences
PreferenceScreenOption.USER_INTERFACE -> uiPreferences PreferenceScreenOption.USER_INTERFACE -> uiPreferences
PreferenceScreenOption.SUBTITLES -> SubtitleSettings.preferences
} }
val screenTitle = val screenTitle =
when (preferenceScreenOption) { when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> R.string.settings PreferenceScreenOption.BASIC -> R.string.settings
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface
PreferenceScreenOption.SUBTITLES -> R.string.subtitle_style
} }
var visible by remember { mutableStateOf(false) } var visible by remember { mutableStateOf(false) }
@ -302,6 +306,19 @@ fun PreferencesContent(
) )
} }
SubtitleSettings.Reset -> {
ClickPreference(
title = stringResource(pref.title),
onClick = {
viewModel.resetSubtitleSettings()
},
modifier = Modifier,
summary = pref.summary(context, null),
onLongClick = {},
interactionSource = interactionSource,
)
}
else -> { else -> {
val value = pref.getter.invoke(preferences) val value = pref.getter.invoke(preferences)
ComposablePreference( ComposablePreference(
@ -357,13 +374,24 @@ fun PreferencesPage(
Box( Box(
modifier = modifier.background(MaterialTheme.colorScheme.background), modifier = modifier.background(MaterialTheme.colorScheme.background),
) { ) {
PreferencesContent( when (preferenceScreenOption) {
initialPreferences, PreferenceScreenOption.BASIC,
preferenceScreenOption, PreferenceScreenOption.ADVANCED,
Modifier PreferenceScreenOption.USER_INTERFACE,
.fillMaxWidth(.4f) ->
.fillMaxHeight() PreferencesContent(
.align(Alignment.TopEnd), initialPreferences,
) preferenceScreenOption,
Modifier
.fillMaxWidth(.4f)
.fillMaxHeight()
.align(Alignment.TopEnd),
)
PreferenceScreenOption.SUBTITLES ->
SubtitleStylePage(
initialPreferences,
)
}
} }
} }

View file

@ -12,6 +12,8 @@ import com.github.damontecres.wholphin.data.isPinned
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import com.github.damontecres.wholphin.data.model.NavPinType import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.resetSubtitles
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
@ -91,4 +93,20 @@ class PreferencesViewModel
fun sendAppLogs() { fun sendAppLogs() {
sendAppLogs(context, api, clientInfo, deviceInfo) sendAppLogs(context, api, clientInfo, deviceInfo)
} }
fun resetSubtitleSettings() {
viewModelScope.launchIO {
resetSubtitleSettings(preferenceDataStore)
}
}
companion object {
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
appPreferences.updateData {
it.updateSubtitlePreferences {
resetSubtitles()
}
}
}
}
} }

View file

@ -0,0 +1,248 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.graphics.Typeface
import androidx.annotation.OptIn
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.CaptionStyleCompat
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppClickablePreference
import com.github.damontecres.wholphin.preferences.AppSliderPreference
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
import com.github.damontecres.wholphin.preferences.BackgroundStyle
import com.github.damontecres.wholphin.preferences.EdgeStyle
import com.github.damontecres.wholphin.preferences.SubtitlePreferences
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
object SubtitleSettings {
val FontSize =
AppSliderPreference(
title = R.string.font_size,
defaultValue = 24,
min = 8,
max = 70,
interval = 2,
getter = {
it.interfacePreferences.subtitlesPreferences.fontSize
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontSize = value.toInt() }
},
summarizer = { value -> value?.toString() },
)
private val colorList =
listOf(
Color.White,
Color.Black,
Color.LightGray,
Color.DarkGray,
Color.Red,
Color.Yellow,
Color.Green,
Color.Cyan,
Color.Blue,
Color.Magenta,
)
val FontColor =
AppChoicePreference<Color>(
title = R.string.font_color,
defaultValue = Color.White,
getter = { Color(it.interfacePreferences.subtitlesPreferences.fontColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
valueToIndex = { value ->
val color = value.toArgb().and(0x00FFFFFF)
colorList.indexOfFirstOrNull { color == it.toArgb().and(0x00FFFFFF) } ?: 0
},
)
val FontBold =
AppSwitchPreference(
title = R.string.bold_font,
defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontBold },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontBold = value }
},
)
val FontItalic =
AppSwitchPreference(
title = R.string.italic_font,
defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontItalic },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontItalic = value }
},
)
val FontOpacity =
AppSliderPreference(
title = R.string.font_opacity,
defaultValue = 100,
min = 10,
max = 100,
interval = 10,
getter = {
it.interfacePreferences.subtitlesPreferences.fontOpacity
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontOpacity = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val EdgeStylePref =
AppChoicePreference<EdgeStyle>(
title =
R.string.edge_style,
defaultValue = EdgeStyle.EDGE_SOLID,
getter = { it.interfacePreferences.subtitlesPreferences.edgeStyle },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeStyle = value }
},
displayValues = R.array.subtitle_edge,
indexToValue = { EdgeStyle.forNumber(it) },
valueToIndex = { it.number },
)
val EdgeColor =
AppChoicePreference<Color>(
title = R.string.edge_color,
defaultValue = Color.Black,
getter = { Color(it.interfacePreferences.subtitlesPreferences.edgeColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
valueToIndex = { value ->
val color = value.toArgb().and(0x00FFFFFF)
colorList.indexOfFirstOrNull { color == it.toArgb().and(0x00FFFFFF) } ?: 0
},
)
val BackgroundColor =
AppChoicePreference<Color>(
title = R.string.background_color,
defaultValue = Color.Transparent,
getter = { Color(it.interfacePreferences.subtitlesPreferences.backgroundColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
valueToIndex = { value ->
val color = value.toArgb().and(0x00FFFFFF)
colorList.indexOfFirstOrNull { color == it.toArgb().and(0x00FFFFFF) } ?: 0
},
)
val BackgroundOpacity =
AppSliderPreference(
title = R.string.background_opacity,
defaultValue = 50,
min = 10,
max = 100,
interval = 10,
getter = {
it.interfacePreferences.subtitlesPreferences.backgroundOpacity
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundOpacity = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val BackgroundStylePref =
AppChoicePreference<BackgroundStyle>(
title =
R.string.background_style,
defaultValue = BackgroundStyle.BG_NONE,
getter = { it.interfacePreferences.subtitlesPreferences.backgroundStyle },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundStyle = value }
},
displayValues = R.array.background_style,
indexToValue = { BackgroundStyle.forNumber(it) },
valueToIndex = { it.number },
)
val Reset =
AppClickablePreference(
title = R.string.reset,
getter = { },
setter = { prefs, _ -> prefs },
)
val preferences =
listOf(
PreferenceGroup(
title = R.string.font,
preferences =
listOf(
FontSize,
FontColor,
FontBold,
FontItalic,
FontOpacity,
),
),
PreferenceGroup(
title = R.string.edge_style,
preferences =
listOf(
EdgeStylePref,
EdgeColor,
),
),
PreferenceGroup(
title = R.string.background,
preferences =
listOf(
BackgroundStylePref,
BackgroundColor,
BackgroundOpacity,
),
),
PreferenceGroup(
title = R.string.more,
preferences =
listOf(Reset),
),
)
@OptIn(UnstableApi::class)
fun SubtitlePreferences.toSubtitleStyle(): CaptionStyleCompat {
val fo = (fontOpacity / 100.0 * 255).toInt().shl(24)
val bg = (backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor)
return CaptionStyleCompat(
fo.or(fontColor),
if (backgroundStyle == BackgroundStyle.BG_WRAP)bg else 0,
if (backgroundStyle == BackgroundStyle.BG_BOXED) bg else 0,
when (edgeStyle) {
EdgeStyle.EDGE_NONE, EdgeStyle.UNRECOGNIZED -> CaptionStyleCompat.EDGE_TYPE_NONE
EdgeStyle.EDGE_SOLID -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
EdgeStyle.EDGE_SHADOW -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
},
fo.or(edgeColor),
when {
fontBold && fontItalic -> Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)
fontBold -> Typeface.defaultFromStyle(Typeface.BOLD)
fontItalic -> Typeface.defaultFromStyle(Typeface.ITALIC)
else -> Typeface.DEFAULT
},
)
}
}

View file

@ -0,0 +1,116 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle
import androidx.annotation.Dimension
import androidx.annotation.OptIn
import androidx.compose.foundation.Image
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.media3.common.text.Cue
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.SubtitleView
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import com.github.damontecres.wholphin.ui.preferences.PreferencesContent
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import timber.log.Timber
@OptIn(UnstableApi::class)
@Composable
fun SubtitleStylePage(
initialPreferences: AppPreferences,
modifier: Modifier = Modifier,
viewModel: PreferencesViewModel = hiltViewModel(),
) {
var preferences by remember { mutableStateOf(initialPreferences) }
LaunchedEffect(Unit) {
viewModel.preferenceDataStore.data.collect {
preferences = it
}
}
Timber.v("SubtitleStylePage")
val prefs = preferences.interfacePreferences.subtitlesPreferences
Row(
modifier = modifier,
) {
Box(
contentAlignment = Alignment.BottomCenter,
modifier =
Modifier
.fillMaxSize()
.weight(1f),
) {
Image(
painter = painterResource(R.mipmap.eclipse),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier =
Modifier
.fillMaxSize(),
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier
.padding(16.dp)
.fillMaxSize(),
) {
val examples =
mapOf(
"Subtitles will look like this" to 48.dp,
"This is another example" to 24.dp,
"Longer multi line subtitles will\nlook like this" to 0.dp,
)
examples.forEach { (text, padding) ->
AndroidView(
factory = { context ->
SubtitleView(context)
},
update = {
it.setStyle(prefs.toSubtitleStyle())
it.setFixedTextSize(Dimension.SP, prefs.fontSize.toFloat())
it.setCues(
listOf(
Cue.Builder().setText(text).build(),
),
)
},
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.padding(bottom = padding),
)
}
}
}
PreferencesContent(
initialPreferences = preferences,
preferenceScreenOption = PreferenceScreenOption.SUBTITLES,
modifier =
Modifier
.fillMaxHeight()
.fillMaxWidth(.25f),
)
}
}

View file

@ -10,6 +10,7 @@ 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.updateInterfacePreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -115,4 +116,7 @@ suspend fun upgradeApp(
} }
} }
} }
if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) {
PreferencesViewModel.resetSubtitleSettings(appPreferences)
}
} }

View file

@ -94,6 +94,31 @@ enum AppThemeColors {
ORANGE = 3; ORANGE = 3;
} }
enum EdgeStyle{
EDGE_NONE = 0;
EDGE_SOLID = 1;
EDGE_SHADOW = 2;
}
enum BackgroundStyle{
BG_NONE = 0;
BG_WRAP = 1;
BG_BOXED = 2;
}
message SubtitlePreferences{
int32 font_size = 1;
int32 font_color = 2;
int32 font_opacity = 3;
int32 edge_color = 4;
EdgeStyle edge_style = 5;
int32 background_color = 6;
int32 background_opacity = 7;
BackgroundStyle background_style = 8;
bool font_bold = 9;
bool font_italic = 10;
}
message InterfacePreferences { message InterfacePreferences {
ThemeSongVolume play_theme_songs = 1; ThemeSongVolume play_theme_songs = 1;
bool remember_selected_tab = 2; bool remember_selected_tab = 2;
@ -101,6 +126,7 @@ message InterfacePreferences {
AppThemeColors app_theme_colors = 4; AppThemeColors app_theme_colors = 4;
bool nav_drawer_switch_on_focus = 5; bool nav_drawer_switch_on_focus = 5;
bool show_clock = 6; bool show_clock = 6;
SubtitlePreferences subtitles_preferences = 7;
} }
message AppPreferences { message AppPreferences {

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

View file

@ -47,6 +47,23 @@
<string name="update_available">Update available</string> <string name="update_available">Update available</string>
<string name="update_url_summary">URL used to check for app updates</string> <string name="update_url_summary">URL used to check for app updates</string>
<string name="update_url">Update URL</string> <string name="update_url">Update URL</string>
<string name="font_size">Font size</string>
<string name="font_color">Font color</string>
<string name="font_opacity">Font opacity</string>
<string name="edge_style">Edge style</string>
<string name="edge_color">Edge color</string>
<string name="background_opacity">Background opacity</string>
<string name="background_style">Background style</string>
<string name="subtitle_style">Subtitle style</string>
<string name="background_color">Background color</string>
<string name="reset">Reset</string>
<string name="bold_font">Bold font</string>
<string name="player_backend">Playback Backend</string>
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string>
<string name="disable_if_crash">Disable if you experience crashes</string>
<string name="mpv_options">MPV Options</string>
<string name="exoplayer_options">ExoPlayer Options</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>
@ -90,9 +107,33 @@
<item>During end credits/outro</item> <item>During end credits/outro</item>
</string-array> </string-array>
<string-array name="font_colors">
<item>White</item>
<item>Black</item>
<item>Light Gray</item>
<item>Dark Gray</item>
<item>Red</item>
<item>Yellow</item>
<item>Green</item>
<item>Cyan</item>
<item>Blue</item>
<item>Magenta</item>
</string-array>
<string-array name="subtitle_edge">
<item>None</item>
<item>Outline</item>
<item>Shadow</item>
</string-array>
<string-array name="background_style">
<item>None</item>
<item>Wrap</item>
<item>Boxed</item>
</string-array>
<string-array name="player_backend_options"> <string-array name="player_backend_options">
<item>ExoPlayer (default)</item> <item>ExoPlayer (default)</item>
<item>MPV (Experimental)</item> <item>MPV (Experimental)</item>
</string-array> </string-array>
</resources> </resources>

View file

@ -144,11 +144,9 @@
<string name="add_to_playlist">Add to playlist</string> <string name="add_to_playlist">Add to playlist</string>
<string name="one_click_pause">Pause with one click</string> <string name="one_click_pause">Pause with one click</string>
<string name="one_click_pause_summary_on">Press D-Pad center to pause/play</string> <string name="one_click_pause_summary_on">Press D-Pad center to pause/play</string>
<string name="player_backend">Playback Backend</string> <string name="italic_font">Italicize font</string>
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string> <string name="font">Font</string>
<string name="disable_if_crash">Disable if you experience crashes</string> <string name="background">Background</string>
<string name="mpv_options">MPV Options</string>
<string name="exoplayer_options">ExoPlayer Options</string>
<plurals name="downloads"> <plurals name="downloads">
<item quantity="zero">%s downloads</item> <item quantity="zero">%s downloads</item>