This commit is contained in:
ponzischeme89
2026-08-22 18:40:50 +12:00
parent 9858e44710
commit 7d77700c7b
36 changed files with 480 additions and 119 deletions
@@ -109,6 +109,7 @@ data class RemoteFeatures(
data class RemotePresentation(
val navigationRailExpandedWidthDp: Int = 184,
val navigationContentShiftDp: Int = 112,
val fontFamily: String = "system",
)
@Serializable
@@ -7,6 +7,8 @@ import android.service.dreams.DreamService
import android.view.KeyEvent
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@@ -18,6 +20,7 @@ import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.ui.player.PlayerActivity
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
@@ -60,7 +63,8 @@ class MembyDreamService : DreamService() {
setViewTreeSavedStateRegistryOwner(lifecycleOwner)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MembyTheme {
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsState()
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
ScreensaverContent(
onPlay = ::launchTrailer,
onExit = { finish() },
@@ -0,0 +1,36 @@
package com.ponzischeme89.memby.ui
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
/** A restrained pull-back that gives large hero artwork some life without repeating. */
@Composable
internal fun Modifier.cinematicBackdropPullBack(
artworkKey: Any?,
alpha: () -> Float = { 1f },
): Modifier {
val scale = remember(artworkKey) { Animatable(CinematicBackdropInitialScale) }
LaunchedEffect(artworkKey) {
scale.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = CinematicBackdropPullBackMs,
easing = LinearOutSlowInEasing,
),
)
}
return graphicsLayer {
scaleX = scale.value
scaleY = scale.value
this.alpha = alpha().coerceIn(0f, 1f)
}
}
private const val CinematicBackdropInitialScale = 1.08f
private const val CinematicBackdropPullBackMs = 10_000
@@ -114,6 +114,7 @@ import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
import com.ponzischeme89.memby.ui.theme.MembyQuietText
import com.ponzischeme89.memby.ui.theme.MembySurface
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
import com.ponzischeme89.memby.ui.theme.ValueSeparator
// The detail page's names for the shared tokens. The neutrals used to be a shade darker
@@ -269,7 +270,7 @@ internal fun DetailBackdrop(
alignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
.graphicsLayer { alpha = artworkAlpha().coerceIn(0f, 1f) },
.cinematicBackdropPullBack(artwork, artworkAlpha),
)
}
Box(
@@ -637,6 +638,7 @@ private fun DetailHero(
// height instead would re-flow the title, the synopsis and the action row on every frame
// of the transition — which is the "suddenly resize a focused item" failure, sixty times
// a second.
MembyTrialTypography {
Box(
Modifier
.fillMaxWidth()
@@ -837,6 +839,7 @@ private fun DetailHero(
)
}
}
}
}
/**
@@ -1369,6 +1372,7 @@ internal fun DetailFocusablePane(
@Composable
internal fun DetailMetaRows(rows: List<TechnicalSpec>, modifier: Modifier = Modifier, labelWidth: androidx.compose.ui.unit.Dp = 110.dp) {
MembyTrialTypography {
Column(modifier, verticalArrangement = Arrangement.spacedBy(9.dp)) {
rows.forEach { row ->
Row(Modifier.fillMaxWidth()) {
@@ -1377,6 +1381,7 @@ internal fun DetailMetaRows(rows: List<TechnicalSpec>, modifier: Modifier = Modi
}
}
}
}
}
/**
@@ -1394,6 +1399,7 @@ internal fun DetailOverviewPane(
modifier: Modifier = Modifier,
supportingText: String? = null,
) {
MembyTrialTypography {
DetailFocusablePane(focusRequester, modifier) {
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
Column(Modifier.weight(1.5f)) {
@@ -1426,6 +1432,7 @@ internal fun DetailOverviewPane(
DetailMetaRows(credits, Modifier.weight(1f), labelWidth = 96.dp)
}
}
}
}
/**
@@ -1451,6 +1458,7 @@ internal fun DetailDetailsPane(
val alreadySaid = rows.map { it.value }.toSet()
specs.filterNot { it.value in alreadySaid }
}
MembyTrialTypography {
DetailFocusablePane(focusRequester, modifier) {
if (rows.isEmpty() && specs.isEmpty()) {
Text(
@@ -1477,6 +1485,7 @@ internal fun DetailDetailsPane(
}
}
}
}
}
/**
@@ -17,7 +17,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsStateWithLifecycle()
MembyTheme {
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
AppRoot(
remoteConfig = remoteConfig,
onCloseSettings = ::finish,
@@ -1,8 +1,5 @@
package com.ponzischeme89.memby.ui
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
@@ -10,14 +7,12 @@ 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.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@@ -26,6 +21,7 @@ import coil.request.ImageRequest
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.ui.theme.MembySurface
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
/**
* The quiet surface between the focused title details and the scrolling shelves.
@@ -44,51 +40,53 @@ internal fun MetadataHero(
isContinueWatchingItem: Boolean = false,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clipToBounds()
.background(MembySurface),
) {
MetadataHeroArtwork(
item = item,
modifier = Modifier.fillMaxSize(),
)
MembyTrialTypography {
Box(
Modifier.fillMaxSize().background(
Brush.horizontalGradient(
0f to MembySurface,
0.30f to MembySurface,
0.48f to MembySurface.copy(alpha = 0.80f),
0.62f to MembySurface.copy(alpha = 0.40f),
0.78f to MembySurface.copy(alpha = 0.10f),
1f to Color.Transparent,
modifier = modifier
.clipToBounds()
.background(MembySurface),
) {
MetadataHeroArtwork(
item = item,
modifier = Modifier.fillMaxSize(),
)
Box(
Modifier.fillMaxSize().background(
Brush.horizontalGradient(
0f to MembySurface,
0.30f to MembySurface,
0.48f to MembySurface.copy(alpha = 0.80f),
0.62f to MembySurface.copy(alpha = 0.40f),
0.78f to MembySurface.copy(alpha = 0.10f),
1f to Color.Transparent,
),
),
),
)
Box(
Modifier.fillMaxSize().background(
Brush.verticalGradient(
0f to MembySurface.copy(alpha = 0.18f),
0.58f to Color.Transparent,
1f to MembySurface.copy(alpha = 0.20f),
)
Box(
Modifier.fillMaxSize().background(
Brush.verticalGradient(
0f to MembySurface.copy(alpha = 0.18f),
0.58f to Color.Transparent,
1f to MembySurface.copy(alpha = 0.20f),
),
),
),
)
MediaMetadataPanel(
item = item,
loading = loading,
contentOrder = contentOrder,
timeRemainingColour = timeRemainingColour,
isContinueWatchingItem = isContinueWatchingItem,
modifier = Modifier
.fillMaxSize()
.padding(
start = HomeContentHorizontalInset,
end = HomeContentHorizontalInset,
top = 28.dp,
bottom = 18.dp,
),
)
)
MediaMetadataPanel(
item = item,
loading = loading,
contentOrder = contentOrder,
timeRemainingColour = timeRemainingColour,
isContinueWatchingItem = isContinueWatchingItem,
modifier = Modifier
.fillMaxSize()
.padding(
start = HomeContentHorizontalInset,
end = HomeContentHorizontalInset,
top = 28.dp,
bottom = 18.dp,
),
)
}
}
}
@@ -119,16 +117,6 @@ private fun MetadataHeroArtwork(
}
}
if (request == null) return
val artworkScale = remember(artwork) { Animatable(MetadataHeroArtworkInitialScale) }
LaunchedEffect(artwork) {
artworkScale.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = MetadataHeroArtworkZoomOutMs,
easing = LinearOutSlowInEasing,
),
)
}
Box(modifier) {
Box(
modifier = Modifier
@@ -143,10 +131,7 @@ private fun MetadataHeroArtwork(
alignment = Alignment.CenterEnd,
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX = artworkScale.value
scaleY = artworkScale.value
},
.cinematicBackdropPullBack(artwork),
)
Box(
Modifier.fillMaxSize().background(
@@ -173,6 +158,4 @@ private fun MetadataHeroArtwork(
}
private const val MetadataHeroArtworkCrossfadeMs = 180
private const val MetadataHeroArtworkInitialScale = 1.05f
private const val MetadataHeroArtworkZoomOutMs = 10_000
private const val MetadataHeroArtworkWidthFraction = 0.68f
@@ -32,9 +32,10 @@ annotation class TvPreview
@Composable
fun PreviewSurface(
alignment: Alignment = Alignment.Center,
fontFamilyName: String = "system",
content: @Composable () -> Unit,
) {
MembyTheme {
MembyTheme(fontFamilyName = fontFamilyName) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -19,14 +19,12 @@ import com.ponzischeme89.memby.ui.theme.MembyAccent
import com.ponzischeme89.memby.ui.theme.MembyAccentBright
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
import com.ponzischeme89.memby.ui.theme.MembyAccentMuted
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
import com.ponzischeme89.memby.ui.theme.MembyHairline
import com.ponzischeme89.memby.ui.theme.MembyMutedText
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
import com.ponzischeme89.memby.ui.theme.MembyOutline
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
import com.ponzischeme89.memby.ui.theme.MembyQuietText
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
/**
* One answer in a television settings panel. The panel owns its invariant chrome while
@@ -204,7 +202,7 @@ fun tvSettingsOptionView(
fun render(focused: Boolean) {
val fill = when {
focused -> MembyControlSurface.toArgb()
focused -> PlayerMenuFocusedSurface
prominent -> MembyAccentMuted.toArgb()
selected -> MembyAccentMuted.toArgb()
else -> android.graphics.Color.TRANSPARENT
@@ -243,7 +241,7 @@ fun applyTvSettingsPanelSurface(view: View) {
val context = view.context
view.background = roundedBackground(
context,
MembySurfaceRaised.toArgb(),
PlayerMenuBackground,
MembyOutline.toArgb(),
1,
MembyPanelCorner.value.toInt(),
@@ -259,10 +257,10 @@ private data class TvSettingsPanel(
private fun tvSettingsPanel(context: Context, title: String, description: String): TvSettingsPanel {
val root = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(context.dp(28), context.dp(25), context.dp(28), context.dp(25))
setPadding(context.dp(24), context.dp(22), context.dp(24), context.dp(22))
background = roundedBackground(
context,
MembySurfaceRaised.toArgb(),
PlayerMenuBackground,
MembyOutline.toArgb(),
1,
MembyPanelCorner.value.toInt(),
@@ -272,7 +270,7 @@ private fun tvSettingsPanel(context: Context, title: String, description: String
root.addView(TextView(context).apply {
text = title
setTextColor(MembyOnSurface.toArgb())
textSize = 24f
textSize = 22f
typeface = Typeface.create("sans-serif", Typeface.BOLD)
})
if (description.isNotBlank()) {
@@ -304,7 +302,7 @@ private fun tvSettingsDialog(context: Context, onDismiss: () -> Unit): Dialog =
window?.apply {
setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes = attributes.apply { dimAmount = 0.72f }
attributes = attributes.apply { dimAmount = 0.80f }
setGravity(Gravity.CENTER)
}
setOnDismissListener { onDismiss() }
@@ -320,7 +318,7 @@ private fun finishTvSettingsDialog(
val context = root.context
val frame = FrameLayout(context).apply {
setPadding(context.dp(8), context.dp(8), context.dp(8), context.dp(8))
addView(root, FrameLayout.LayoutParams(context.dp(560), ViewGroup.LayoutParams.WRAP_CONTENT))
addView(root, FrameLayout.LayoutParams(context.dp(480), ViewGroup.LayoutParams.WRAP_CONTENT))
}
dialog.setContentView(frame)
val scroll = options.parent as ScrollView
@@ -354,7 +352,7 @@ private fun tvSettingsAction(context: Context, label: String, primary: Boolean,
context,
when {
focused && primary -> MembyAccent.toArgb()
focused -> MembyControlSurface.toArgb()
focused -> PlayerMenuFocusedSurface
primary -> MembyAccentMuted.toArgb()
else -> android.graphics.Color.TRANSPARENT
},
@@ -385,3 +383,6 @@ private fun roundedBackground(
}
private fun Context.dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
private val PlayerMenuBackground = android.graphics.Color.BLACK
private val PlayerMenuFocusedSurface = 0xFF151719.toInt()
@@ -9,6 +9,7 @@ import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.ui.ResumableMediaCard
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
@Composable
fun ContinueWatchingCard(
@@ -46,13 +47,15 @@ fun ContinueWatchingCard(
primaryUrl = repository.primaryUrl(item, 480),
)
}
ResumableMediaCard(
model = model,
width = width,
portraitArtwork = portraitArtwork,
onFocused = onFocused,
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier
)
MembyTrialTypography {
ResumableMediaCard(
model = model,
width = width,
portraitArtwork = portraitArtwork,
onFocused = onFocused,
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier,
)
}
}
@@ -476,7 +476,7 @@ private fun MetadataTimeRemaining(
label,
color = if (colour.equals("white", ignoreCase = true)) Color.White else EmbyGreen,
fontSize = 12.sp,
lineHeight = 16.sp,
lineHeight = 18.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
@@ -81,6 +81,7 @@ import com.ponzischeme89.memby.ui.theme.MembyMutedText
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
import com.ponzischeme89.memby.ui.theme.MembyQuietText
import com.ponzischeme89.memby.ui.theme.MembySurface
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
import com.ponzischeme89.memby.data.remoteconfig.BundledRemoteConfig
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
@@ -197,6 +198,7 @@ fun TvNavigationRail(
animationSpec = tween(160),
label = "navigation-rail-width",
)
MembyTrialTypography {
Box(
modifier = modifier
.width(TvRailCollapsedWidth)
@@ -362,6 +364,7 @@ fun TvNavigationRail(
}
}
}
}
}
private fun configuredNavigationLabel(
@@ -4553,7 +4553,7 @@ class PlayerActivity : ComponentActivity() {
title = "Playback options",
description = "Fine-tune this video without leaving playback.",
options = options,
onDismiss = { playerView?.showController() },
onDismiss = { restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings) },
)
}
@@ -4579,7 +4579,7 @@ class PlayerActivity : ComponentActivity() {
showManualPassthroughPicker(current.codecs)
},
),
onDismiss = { playerView?.showController() },
onDismiss = { restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings) },
)
}
@@ -4598,7 +4598,7 @@ class PlayerActivity : ComponentActivity() {
SurroundCodec.entries.filterIndexed { index, _ -> index in selected }.toSet(),
)
},
onDismiss = { playerView?.showController() },
onDismiss = { restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings) },
)
}
@@ -4632,7 +4632,7 @@ class PlayerActivity : ComponentActivity() {
},
)
},
onDismiss = { playerView?.showController() },
onDismiss = { restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings) },
)
}
@@ -5197,7 +5197,7 @@ class PlayerActivity : ComponentActivity() {
private fun hideSubtitleOverlay() {
subtitleOverlay?.visibility = View.GONE
playerView?.showController()
restorePlayerControlFocus(androidx.media3.ui.R.id.exo_subtitle)
}
/** A completed choice returns directly to the programme, without reopening transport. */
@@ -5354,14 +5354,33 @@ class PlayerActivity : ComponentActivity() {
)
subtitleAutoSelectionAttempted = true
}
if (trackType == C.TRACK_TYPE_AUDIO) playerView?.showController()
if (trackType == C.TRACK_TYPE_AUDIO) {
restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings)
}
},
)
},
onDismiss = {
restorePlayerControlFocus(
if (trackType == C.TRACK_TYPE_TEXT) {
androidx.media3.ui.R.id.exo_subtitle
} else {
androidx.media3.ui.R.id.exo_settings
},
)
},
onDismiss = { playerView?.showController() },
)
}
/** Reopens the transport and returns the remote to the control that launched a menu. */
private fun restorePlayerControlFocus(controlId: Int) {
val view = playerView ?: return
view.showController()
view.post {
if (view.findViewById<View>(controlId)?.requestFocus() != true) view.requestFocus()
}
}
@OptIn(UnstableApi::class)
private fun selectedTrackLabel(trackType: Int): String {
val playback = player ?: return ""
@@ -5,6 +5,9 @@ import android.os.Build
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.ui.player.PlayerActivity
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
import com.ponzischeme89.memby.ui.theme.MembyTheme
@@ -24,7 +27,8 @@ class ScreensaverActivity : ComponentActivity() {
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
}
setContent {
MembyTheme {
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsStateWithLifecycle()
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
ScreensaverContent(
onPlay = { item ->
startActivity(
@@ -73,13 +73,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalTextStyle
import androidx.tv.material3.Text
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ponzischeme89.memby.R
@@ -103,6 +103,7 @@ import com.ponzischeme89.memby.data.parseThemeColor
import com.ponzischeme89.memby.ui.PreviewSurface
import com.ponzischeme89.memby.ui.TvPreview
import com.ponzischeme89.memby.ui.WelcomeQuoteStyle
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
import com.ponzischeme89.memby.ui.theme.MembyAccent
import com.ponzischeme89.memby.ui.theme.MembyAccentBright
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
@@ -694,6 +695,7 @@ fun SettingsSheet(
},
)
MembyTrialTypography {
Box(modifier = modifier.fillMaxSize()) {
if (overlay) {
Box(
@@ -755,6 +757,7 @@ fun SettingsSheet(
}
}
}
}
@Composable
internal fun SettingsPanelContent(
@@ -786,6 +789,7 @@ internal fun SettingsPanelContent(
contentScrollState.scrollTo(0)
}
MembyTrialTypography {
Row(
modifier = modifier
.then(if (overlay) Modifier.width(760.dp) else Modifier.fillMaxWidth())
@@ -1109,6 +1113,8 @@ internal fun SettingsPanelContent(
Spacer(Modifier.height(12.dp))
}
}
}
}
}
/**
@@ -1269,7 +1275,7 @@ private fun DeviceRenameDialog(
onValueChange = { if (it.length <= 80) name = it },
singleLine = true,
enabled = !busy,
textStyle = TextStyle(color = TextPrimary, fontSize = 18.sp),
textStyle = LocalTextStyle.current.copy(color = TextPrimary, fontSize = 18.sp),
cursorBrush = SolidColor(EmbyGreen),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { if (valid && !busy) onSave(name) }),
@@ -2,12 +2,34 @@ package com.ponzischeme89.memby.ui.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.tv.material3.LocalTextStyle
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.darkColorScheme
import com.ponzischeme89.memby.R
internal enum class AppFontFamily {
SYSTEM,
INTER,
}
internal fun appFontFamily(value: String?): AppFontFamily = when (value?.trim()?.lowercase()) {
"inter" -> AppFontFamily.INTER
else -> AppFontFamily.SYSTEM
}
private val InterFontFamily = FontFamily(
Font(R.font.inter_regular, FontWeight.Normal),
Font(R.font.inter_medium, FontWeight.Medium),
Font(R.font.inter_semibold, FontWeight.SemiBold),
Font(R.font.inter_bold, FontWeight.Bold),
)
private val LocalTrialFontFamily = compositionLocalOf { FontFamily.SansSerif }
// The scheme is the same near-blacks the screens actually paint (see DesignTokens.kt), so
// a component that falls back to a theme colour lands on the surface it is sitting on
@@ -25,7 +47,10 @@ private fun embyColors() = darkColorScheme(
)
@Composable
fun MembyTheme(content: @Composable () -> Unit) {
fun MembyTheme(
fontFamilyName: String = "system",
content: @Composable () -> Unit,
) {
// Use Android's complete Roboto-backed sans family rather than pinning every label
// to sans-serif-medium. The generic family lets Compose select real regular,
// medium, semibold and bold faces from each Text's FontWeight, restoring hierarchy
@@ -41,9 +66,23 @@ fun MembyTheme(content: @Composable () -> Unit) {
letterSpacing = 0.006.em,
lineHeight = 1.22.em,
)
val trialFontFamily = when (appFontFamily(fontFamilyName)) {
AppFontFamily.SYSTEM -> FontFamily.SansSerif
AppFontFamily.INTER -> InterFontFamily
}
MaterialTheme(colorScheme = embyColors()) {
CompositionLocalProvider(LocalTextStyle provides appTextStyle) {
CompositionLocalProvider(
LocalTextStyle provides appTextStyle,
LocalTrialFontFamily provides trialFontFamily,
) {
content()
}
}
}
/** Applies the server-selected family only to the surfaces included in the visual trial. */
@Composable
fun MembyTrialTypography(content: @Composable () -> Unit) {
val textStyle = LocalTextStyle.current.copy(fontFamily = LocalTrialFontFamily.current)
CompositionLocalProvider(LocalTextStyle provides textStyle, content = content)
}