Scale hardcoded fontSize sites by Text size level

Adds a scaledSp(baseSp: Int) composable helper that returns a TextUnit
scaled by the active text-size level. The lookup is a hand-mapped
DisplaySizeLevel.scaledSp(Int) table for the known base sp values
(integer outputs at every cell, no fractional sp). Unknown base sp
values fall back to integer-rounded percent math instead of throwing,
so the helper is safe to call from anywhere.

scaledSp reads from LocalInterfaceCustomization.current.textSizeLevel
so the same composition local that already carries the user's interface
preferences also drives text scaling.

33 hardcoded fontSize = N.sp call sites across cards, components,
playback overlay, nav, setup, and livetv go through scaledSp. This
covers both plain text and FontAwesome icon-glyph sites: at the Default
level the lookup returns the exact base sp, so today's rendered sizes
are byte-identical. PlaybackController's file-level titleTextSize and
subtitleTextSize constants are inlined as scaledSp(28) and scaledSp(18),
keeping the rendering and PlaybackOverlay's height-offset math in lockstep
at every level.

The v1 scaledDensity helper in InterfaceHelpers.kt and its tests are
deleted - they had no callers after MainContent's density override was
removed. The InterfaceCustomization derived-getter doc comment is
retuned to v2-valid percent values.
This commit is contained in:
Justin Visser 2026-05-28 12:49:55 +02:00
parent f039be0495
commit ca8ed52cf0
26 changed files with 138 additions and 113 deletions

View file

@ -1,5 +1,8 @@
package com.github.damontecres.wholphin.preferences
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
val displaySizeLevelDisplayOrder: List<DisplaySizeLevel> =
listOf(
DisplaySizeLevel.EXTRA_SMALL,
@ -8,6 +11,70 @@ val displaySizeLevelDisplayOrder: List<DisplaySizeLevel> =
DisplaySizeLevel.LARGE,
)
fun DisplaySizeLevel.scaledSp(baseSp: Int): TextUnit {
if (this == DisplaySizeLevel.DEFAULT || this == DisplaySizeLevel.UNRECOGNIZED) {
return baseSp.sp
}
val mapped =
when (this) {
DisplaySizeLevel.EXTRA_SMALL -> {
when (baseSp) {
10 -> 9
12 -> 11
13 -> 11
14 -> 12
16 -> 14
18 -> 16
20 -> 18
24 -> 21
28 -> 25
56 -> 49
64 -> 56
else -> null
}
}
DisplaySizeLevel.SMALL -> {
when (baseSp) {
10 -> 9
12 -> 11
13 -> 12
14 -> 13
16 -> 15
18 -> 17
20 -> 19
24 -> 23
28 -> 26
56 -> 53
64 -> 60
else -> null
}
}
DisplaySizeLevel.LARGE -> {
when (baseSp) {
10 -> 11
12 -> 13
13 -> 14
14 -> 15
16 -> 17
18 -> 19
20 -> 22
24 -> 26
28 -> 30
56 -> 60
64 -> 69
else -> null
}
}
else -> {
null
}
}
return mapped?.sp ?: ((baseSp * textPercent() + 50) / 100).sp
}
fun DisplaySizeLevel.textPercent(): Int =
when (this) {
DisplaySizeLevel.EXTRA_SMALL -> 88

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme
@ -48,6 +47,7 @@ import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.util.scaledSp
import org.jellyfin.sdk.model.api.ImageType
/**
@ -179,7 +179,7 @@ fun BannerCard(
.padding(8.dp),
color = colorResource(android.R.color.holo_red_light),
text = stringResource(R.string.fa_heart),
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
)
}

View file

@ -30,7 +30,6 @@ import androidx.compose.ui.text.font.FontWeight
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.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme
@ -46,6 +45,7 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlinx.coroutines.delay
@Composable
@ -157,7 +157,7 @@ fun DiscoverItemCard(
Text(
text = pluralStringResource(it, 1),
style = MaterialTheme.typography.bodySmall,
fontSize = 10.sp,
fontSize = scaledSp(10),
modifier =
Modifier
.align(Alignment.TopStart)
@ -224,7 +224,7 @@ fun PendingIndicator(modifier: Modifier = Modifier) {
Text(
text = stringResource(R.string.fa_bell),
fontFamily = FontAwesome,
fontSize = 10.sp,
fontSize = scaledSp(10),
color = AppColors.Discover.Yellow,
modifier = Modifier.align(Alignment.Center),
)
@ -249,7 +249,7 @@ fun AvailableIndicator(modifier: Modifier = Modifier) {
Text(
text = stringResource(R.string.fa_check),
fontFamily = FontAwesome,
fontSize = 10.sp,
fontSize = scaledSp(10),
color = Color.White,
modifier = Modifier.align(Alignment.Center),
)

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
@ -41,6 +40,7 @@ import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.logCoilError
import com.github.damontecres.wholphin.ui.util.scaledSp
import org.jellyfin.sdk.model.api.ImageType
/**
@ -166,7 +166,7 @@ fun BoxScope.ItemCardImageFallback(
Text(
text = name,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 14.sp,
fontSize = scaledSp(14),
modifier =
Modifier
.padding(8.dp)
@ -222,7 +222,6 @@ fun ItemCardImageOverlay(
text = numberOfVersions.toString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
// fontSize = 16.sp,
modifier = Modifier.padding(4.dp),
)
}
@ -255,7 +254,6 @@ fun ItemCardImageOverlay(
text = unwatchedCount.toString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
// fontSize = 16.sp,
modifier = Modifier.padding(4.dp),
)
}
@ -280,7 +278,7 @@ fun ItemCardImageOverlay(
@Composable
fun FavoriteIndicator(
modifier: Modifier = Modifier,
fontSize: TextUnit = 20.sp,
fontSize: TextUnit = scaledSp(20),
) = Text(
color = colorResource(android.R.color.holo_red_light),
text = stringResource(R.string.fa_heart),

View file

@ -32,7 +32,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Border
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
@ -45,6 +44,7 @@ import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.PersonKind
@ -153,7 +153,7 @@ fun PersonCard(
text = stringResource(R.string.fa_user),
fontFamily = FontAwesome,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 64.sp,
fontSize = scaledSp(64),
textAlign = TextAlign.Center,
modifier =
Modifier

View file

@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
@ -54,6 +53,7 @@ import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.scaledSp
import java.util.UUID
/**
@ -399,7 +399,7 @@ fun ExpandableFilterButton(
text = stringResource(R.string.fa_filter),
style = MaterialTheme.typography.titleSmall,
color = iconColor,
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center),

View file

@ -46,7 +46,6 @@ import androidx.compose.ui.res.painterResource
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.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -59,6 +58,7 @@ import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import kotlin.time.Duration
@ -329,7 +329,7 @@ fun ExpandableFaButton(
text = stringResource(iconStringRes),
style = MaterialTheme.typography.titleSmall,
color = iconColor,
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center),

View file

@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.util.scaledSp
import org.jellyfin.sdk.model.api.BaseItemDto
@Composable
@ -34,7 +34,7 @@ fun EpisodeName(
text = episodeName ?: "",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
fontSize = 20.sp,
fontSize = scaledSp(20),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier,

View file

@ -36,12 +36,12 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.scaledSp
import timber.log.Timber
@Composable
@ -147,7 +147,7 @@ fun Tab(
) {
Text(
text = title,
fontSize = 16.sp,
fontSize = scaledSp(16),
color = contentColor,
modifier = Modifier.padding(horizontal = 16.dp),
)

View file

@ -7,10 +7,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.util.LocalClock
import com.github.damontecres.wholphin.ui.util.scaledSp
/**
* Displays the [LocalClock] in the upper right corner of the parent [androidx.compose.foundation.layout.Box]
@ -20,7 +20,7 @@ fun BoxScope.TimeDisplay(modifier: Modifier = Modifier) {
val timeString by LocalClock.current.timeString
Text(
text = timeString,
fontSize = 18.sp,
fontSize = scaledSp(18),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
modifier =

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Text
@ -41,6 +40,7 @@ import com.github.damontecres.wholphin.ui.util.StreamFormatting.formatAudioCodec
import com.github.damontecres.wholphin.ui.util.StreamFormatting.formatSubtitleCodec
import com.github.damontecres.wholphin.ui.util.StreamFormatting.formatVideoRange
import com.github.damontecres.wholphin.ui.util.StreamFormatting.resolutionString
import com.github.damontecres.wholphin.ui.util.scaledSp
import com.github.damontecres.wholphin.util.languageName
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaStream
@ -185,7 +185,7 @@ fun StreamLabel(
ProvideTextStyle(
TextStyle(
color = MaterialTheme.colorScheme.onSurface,
fontSize = 12.sp,
fontSize = scaledSp(12),
fontWeight = FontWeight.SemiBold,
),
) {

View file

@ -43,7 +43,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.ButtonDefaults
@ -54,13 +53,12 @@ import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlinx.coroutines.delay
private const val ERROR_AUTO_DISMISS_DELAY_MS = 3000L
private const val SOUND_LEVEL_SCALE_FACTOR = 0.15f
private val BUBBLE_SIZE = 160.dp
private val MIC_ICON_FONT_SIZE = 56.sp
private val BUTTON_ICON_FONT_SIZE = 20.sp
private val CONTENT_SPACING = 48.dp
private val HORIZONTAL_PADDING = 64.dp
private val DISMISS_HINT_BOTTOM_PADDING = 32.dp
@ -174,7 +172,7 @@ fun VoiceSearchButton(
Text(
text = stringResource(R.string.fa_microphone),
fontFamily = FontAwesome,
fontSize = BUTTON_ICON_FONT_SIZE,
fontSize = scaledSp(20),
textAlign = TextAlign.Center,
modifier = Modifier.semantics { contentDescription = voiceSearchDesc },
)
@ -347,7 +345,7 @@ private fun VoiceSearchOverlay(
Text(
text = stringResource(R.string.fa_microphone),
fontFamily = FontAwesome,
fontSize = MIC_ICON_FONT_SIZE,
fontSize = scaledSp(56),
color = onPrimaryColor,
textAlign = TextAlign.Center,
modifier = Modifier.semantics { contentDescription = voiceSearchDesc },

View file

@ -19,7 +19,6 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -29,6 +28,7 @@ import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.util.scaledSp
import java.time.LocalDateTime
@Composable
@ -87,7 +87,7 @@ fun Program(
text = stringResource(R.string.fa_caret_left),
fontFamily = FontAwesome,
color = LocalContentColor.current,
fontSize = 16.sp,
fontSize = scaledSp(16),
modifier =
Modifier
.align(Alignment.CenterVertically)
@ -104,7 +104,7 @@ fun Program(
Text(
text = title,
color = LocalContentColor.current,
fontSize = 16.sp,
fontSize = scaledSp(16),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
@ -120,7 +120,7 @@ fun Program(
Text(
text = it,
color = LocalContentColor.current,
fontSize = 14.sp,
fontSize = scaledSp(14),
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
@ -197,7 +197,7 @@ fun Channel(
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
fontSize = if (showImage) 10.sp else 16.sp,
fontSize = if (showImage) scaledSp(10) else scaledSp(16),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
@ -209,7 +209,7 @@ fun Channel(
Text(
color = colorResource(android.R.color.holo_red_light),
text = stringResource(R.string.fa_heart),
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
modifier =
Modifier

View file

@ -24,7 +24,6 @@ import androidx.compose.ui.focus.onFocusChanged
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.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.state.rememberNextButtonState
@ -48,6 +47,7 @@ import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackFaButton
import com.github.damontecres.wholphin.ui.playback.overlay.buttonSpacing
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlin.time.Duration.Companion.seconds
@OptIn(UnstableApi::class)
@ -176,7 +176,7 @@ fun ShuffleButton(
) {
Text(
text = stringResource(R.string.fa_shuffle),
fontSize = 18.sp,
fontSize = scaledSp(18),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
color =
@ -228,7 +228,7 @@ fun RepeatButton(
) {
Text(
text = stringResource(R.string.fa_repeat),
fontSize = 18.sp,
fontSize = scaledSp(18),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
color =
@ -253,7 +253,7 @@ fun RepeatButton(
if (repeatMode == Player.REPEAT_MODE_ONE) {
Text(
text = "1",
fontSize = 10.sp,
fontSize = scaledSp(10),
color = MaterialTheme.colorScheme.surface,
modifier =
Modifier

View file

@ -53,7 +53,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.findViewTreeViewModelStoreOwner
@ -89,6 +88,7 @@ import com.github.damontecres.wholphin.ui.spacedByWithFooter
import com.github.damontecres.wholphin.ui.theme.LocalTheme
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.scaledSp
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -717,7 +717,7 @@ fun NavigationDrawerScope.NavItem(
Text(
text = stringResource(icon),
textAlign = TextAlign.Center,
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
color = color,
modifier = Modifier,

View file

@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.Player
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
@ -35,6 +34,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.getTimeFormatter
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.PlaybackDialogType
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import org.jellyfin.sdk.model.api.MediaSegmentDto
@ -121,9 +121,6 @@ fun PlaybackController(
}
}
internal val titleTextSize = 28.sp
internal val subtitleTextSize = 18.sp
/**
* A wrapper for the playback controls to show title and other information, plus the actual controls
*
@ -175,7 +172,7 @@ fun Controller(
Text(
text = it,
style = MaterialTheme.typography.titleLarge,
fontSize = titleTextSize,
fontSize = scaledSp(28),
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
modifier = Modifier.fillMaxWidth(.75f),
@ -192,7 +189,7 @@ fun Controller(
Text(
text = it,
style = MaterialTheme.typography.titleMedium,
fontSize = subtitleTextSize,
fontSize = scaledSp(18),
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
modifier = Modifier.fillMaxWidth(.75f),

View file

@ -49,7 +49,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
@ -79,6 +78,7 @@ import com.github.damontecres.wholphin.ui.skipStringRes
import com.github.damontecres.wholphin.ui.theme.LocalTheme
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.scaledSp
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@ -497,7 +497,7 @@ fun PlaybackFaButton(
) {
Text(
text = stringResource(iconRes),
fontSize = 18.sp,
fontSize = scaledSp(18),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
color =

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import androidx.tv.material3.MaterialTheme
@ -33,6 +32,7 @@ import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.playback.AnalyticsState
import com.github.damontecres.wholphin.ui.playback.CurrentPlayback
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import com.github.damontecres.wholphin.util.TrackSupport
import com.github.damontecres.wholphin.util.TrackSupportReason
import com.github.damontecres.wholphin.util.TrackType
@ -85,7 +85,7 @@ fun PlaybackDebugOverlay(
}
val textStyle =
MaterialTheme.typography.bodySmall.copy(
fontSize = 10.sp,
fontSize = scaledSp(10),
color = MaterialTheme.colorScheme.onSurface,
)

View file

@ -61,6 +61,7 @@ import com.github.damontecres.wholphin.ui.playback.AnalyticsState
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.CurrentPlayback
import com.github.damontecres.wholphin.ui.playback.PlaybackDialogType
import com.github.damontecres.wholphin.ui.util.scaledSp
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.MediaSegmentDto
import org.jellyfin.sdk.model.api.TrickplayInfo
@ -105,13 +106,15 @@ fun PlaybackOverlay(
val density = LocalDensity.current
val titleSp = scaledSp(28)
val subtitleSp = scaledSp(18)
val titleHeight =
remember(item?.title) {
if (item?.title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp
remember(item?.title, titleSp) {
if (item?.title.isNotNullOrBlank()) with(density) { titleSp.toDp() } else 0.dp
}
val subtitleHeight =
remember(item?.subtitleLong) {
if (item?.subtitleLong.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp
remember(item?.subtitleLong, subtitleSp) {
if (item?.subtitleLong.isNotNullOrBlank()) with(density) { subtitleSp.toDp() } else 0.dp
}
// This will be calculated after composition

View file

@ -18,11 +18,11 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.util.scaledSp
import kotlin.math.abs
/**
@ -63,7 +63,7 @@ fun SkipIndicator(
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onBackground,
fontSize = 13.sp,
fontSize = scaledSp(13),
style = MaterialTheme.typography.bodySmall,
text = abs(durationMs / 1000).toString(),
)

View file

@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
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.datastore.core.DataStore
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
@ -59,6 +58,7 @@ import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.hilt.android.lifecycle.HiltViewModel
@ -275,7 +275,7 @@ fun MoveButton(
) {
Text(
text = stringResource(icon),
fontSize = 16.sp,
fontSize = scaledSp(16),
fontFamily = FontAwesome,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),

View file

@ -36,7 +36,6 @@ import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -60,6 +59,7 @@ import com.github.damontecres.wholphin.ui.dimAndBlur
import com.github.damontecres.wholphin.ui.formatBytes
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.Version
@ -369,7 +369,7 @@ fun DownloadDialog(
) {
Text(
text = stringResource(R.string.downloading),
fontSize = 24.sp,
fontSize = scaledSp(24),
color = MaterialTheme.colorScheme.onSurface,
)
if (progress != null) {
@ -396,7 +396,7 @@ fun DownloadDialog(
val size = formatBytes(contentLength)
Text(
text = "$bytes / $size",
fontSize = 18.sp,
fontSize = scaledSp(18),
color = MaterialTheme.colorScheme.onSurface,
)
}

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
@ -38,6 +37,7 @@ import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.playback.isEnterKey
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.scaledSp
@Composable
fun PinEntry(
@ -101,7 +101,7 @@ fun PinEntry(
)
Text(
text = stringResource(R.string.will_remove_pin),
fontSize = 10.sp,
fontSize = scaledSp(10),
)
}
}

View file

@ -39,7 +39,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Border
import androidx.tv.material3.Button
@ -60,6 +59,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.scaledSp
import java.util.UUID
/**
@ -333,7 +333,7 @@ fun UserIconCardImage(
Text(
text = firstLetter,
style = MaterialTheme.typography.bodyLarge,
fontSize = 14.sp,
fontSize = scaledSp(14),
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,

View file

@ -1,11 +1,13 @@
package com.github.damontecres.wholphin.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Density
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppSliderPreference
import com.github.damontecres.wholphin.preferences.scaledSp
import com.github.damontecres.wholphin.ui.AspectRatio
import com.github.damontecres.wholphin.ui.Cards
@ -26,8 +28,8 @@ internal fun naturalCardBaseDp(aspectRatio: AspectRatio): Int =
AspectRatio.TALL, AspectRatio.SQUARE, AspectRatio.FOUR_THREE -> Cards.HEIGHT_2X3_DP
}
// The global Card slider, the per-row multiplier, and any preset-set absolute heightDp
// all stack. Preset 148dp + global 150% + per-row 100% = 222dp. The base is the row's
// The global Card size, the per-row multiplier, and any preset-set absolute heightDp
// all stack. Preset 148dp + global 116% + per-row 110% = ~189dp. The base is the row's
// preset if present, otherwise the natural height for its aspect.
internal fun resolveCardHeight(
heightDp: Int,
@ -40,19 +42,9 @@ internal fun resolveCardHeight(
return base * globalCardSizePercent / 100 * cardSizeMultiplier / 100
}
// Scales density (and therefore both dp layouts and sp text) linearly by uiScalePercent.
// fontScale is left at the user's system value so Android's accessibility setting still
// applies on top - scaling both would render text at factor^2 instead of factor.
internal fun scaledDensity(
base: Density,
uiScalePercent: Int,
): Density {
val factor = uiScalePercent / 100f
return Density(
density = base.density * factor,
fontScale = base.fontScale,
)
}
@Composable
@ReadOnlyComposable
internal fun scaledSp(baseSp: Int): TextUnit = LocalInterfaceCustomization.current.textSizeLevel.scaledSp(baseSp)
internal fun Int.withinBoundsOrDefault(preference: AppSliderPreference<*>): Int {
val min = preference.min.toInt()

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.ui.util
import androidx.compose.ui.unit.Density
import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppPreference
@ -102,35 +101,6 @@ class InterfaceHelpersTest {
assertEquals(Cards.HEIGHT_2X3_DP, naturalCardBaseDp(AspectRatio.FOUR_THREE))
}
@Test
fun `scaledDensity scales density linearly and leaves fontScale alone`() {
val base = Density(density = 2.0f, fontScale = 1.0f)
val scaled = scaledDensity(base, 150)
// density grows by 1.5 - so dp layouts scale 1.5x.
assertEquals(3.0f, scaled.density, 0.001f)
// fontScale stays put - sp text is (density * fontScale) so it also scales linearly.
// Scaling fontScale too would render text at 1.5^2 = 2.25x.
assertEquals(1.0f, scaled.fontScale, 0.001f)
}
@Test
fun `scaledDensity at 100 percent returns identity`() {
val base = Density(density = 2.0f, fontScale = 1.2f)
val scaled = scaledDensity(base, 100)
assertEquals(base.density, scaled.density, 0.001f)
assertEquals(base.fontScale, scaled.fontScale, 0.001f)
}
@Test
fun `scaledDensity preserves user accessibility fontScale across UI scale changes`() {
// A user with system fontScale 1.3x (accessibility setting) bumping UI scale to 75%
// should keep fontScale = 1.3 - the UI scale slider is independent of accessibility.
val base = Density(density = 2.0f, fontScale = 1.3f)
val scaled = scaledDensity(base, 75)
assertEquals(1.5f, scaled.density, 0.001f)
assertEquals(1.3f, scaled.fontScale, 0.001f)
}
@Test
fun `InterfaceCustomization cardHeightDp at DEFAULT level matches natural base`() {
val customization = InterfaceCustomization(prefs = AppPreferences.getDefaultInstance())