diff --git a/NOTICE b/NOTICE index 72fec62..170ec18 100644 --- a/NOTICE +++ b/NOTICE @@ -51,6 +51,13 @@ contributors. Third-party software -------------------- +Inter +----- + +Memby bundles the Inter font family, copyright 2016 The Inter Project Authors +(https://github.com/rsms/inter), under the SIL Open Font License, Version 1.1. +The complete font licence is packaged at assets/licenses/inter_ofl.txt. + The Android application uses AndroidX, Jetpack Compose, Media3, Coil, Retrofit, OkHttp, kotlinx.serialization, and other open-source libraries. Their copyright notices and licence metadata remain in their respective diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da972ca..ba99f33 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,7 +62,7 @@ val projectNoticeText = rootProject.file("NOTICE").readText() .replace("https://g.sublogue.com/admin/memby", membySourceUrl) -val defaultVersionName = "0.3.02" +val defaultVersionName = "0.3.03" val membyVersionName: String = (project.findProperty("memby.versionName") as String?) ?.trim() diff --git a/app/src/main/assets/licenses/inter_ofl.txt b/app/src/main/assets/licenses/inter_ofl.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/app/src/main/assets/licenses/inter_ofl.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/src/main/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfig.kt b/app/src/main/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfig.kt index c2a905c..fe0e60e 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfig.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfig.kt @@ -109,6 +109,7 @@ data class RemoteFeatures( data class RemotePresentation( val navigationRailExpandedWidthDp: Int = 184, val navigationContentShiftDp: Int = 112, + val fontFamily: String = "system", ) @Serializable diff --git a/app/src/main/java/com/ponzischeme89/memby/screensaver/MembyDreamService.kt b/app/src/main/java/com/ponzischeme89/memby/screensaver/MembyDreamService.kt index db37475..85a869e 100644 --- a/app/src/main/java/com/ponzischeme89/memby/screensaver/MembyDreamService.kt +++ b/app/src/main/java/com/ponzischeme89/memby/screensaver/MembyDreamService.kt @@ -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() }, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/CinematicBackdropMotion.kt b/app/src/main/java/com/ponzischeme89/memby/ui/CinematicBackdropMotion.kt new file mode 100644 index 0000000..edced3f --- /dev/null +++ b/app/src/main/java/com/ponzischeme89/memby/ui/CinematicBackdropMotion.kt @@ -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 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/DetailPageComponents.kt b/app/src/main/java/com/ponzischeme89/memby/ui/DetailPageComponents.kt index c73f305..faeefa9 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/DetailPageComponents.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/DetailPageComponents.kt @@ -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, 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, 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( } } } + } } /** diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/MainActivity.kt b/app/src/main/java/com/ponzischeme89/memby/ui/MainActivity.kt index a79e6b4..3365613 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/MainActivity.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/MainActivity.kt @@ -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, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt b/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt index 868988e..f67bd00 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt @@ -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 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/PreviewSupport.kt b/app/src/main/java/com/ponzischeme89/memby/ui/PreviewSupport.kt index d8588b5..93d6e0d 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/PreviewSupport.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/PreviewSupport.kt @@ -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() diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/TvSettingsComponents.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/TvSettingsComponents.kt index 261ca3d..993bf44 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/TvSettingsComponents.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/TvSettingsComponents.kt @@ -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() diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt index d29e9cf..4b11985 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt @@ -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, + ) + } } diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt index 918810c..e5a06f4 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt @@ -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, ) diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeNavigation.kt b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeNavigation.kt index 0b51024..8f8fa7d 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeNavigation.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeNavigation.kt @@ -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( diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt index 5b8014d..d651d95 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt @@ -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(controlId)?.requestFocus() != true) view.requestFocus() + } + } + @OptIn(UnstableApi::class) private fun selectedTrackLabel(trackType: Int): String { val playback = player ?: return "" diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/screensaver/ScreensaverActivity.kt b/app/src/main/java/com/ponzischeme89/memby/ui/screensaver/ScreensaverActivity.kt index 828427c..28077f4 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/screensaver/ScreensaverActivity.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/screensaver/ScreensaverActivity.kt @@ -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( diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt b/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt index 664975f..5d2e770 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt @@ -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) }), diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/theme/Theme.kt b/app/src/main/java/com/ponzischeme89/memby/ui/theme/Theme.kt index 8d14140..0498dab 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/theme/Theme.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/theme/Theme.kt @@ -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) +} diff --git a/app/src/main/res/drawable/ic_player_settings.xml b/app/src/main/res/drawable/ic_player_settings.xml index 904da2d..0d37703 100644 --- a/app/src/main/res/drawable/ic_player_settings.xml +++ b/app/src/main/res/drawable/ic_player_settings.xml @@ -1,16 +1,12 @@ - + + android:fillColor="#FFFFFFFF" + android:fillType="evenOdd" + android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.65 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.37,-0.31 -0.60,-0.22l-2.49,1c-0.52,-0.40 -1.08,-0.73 -1.69,-0.98L14.5,2.42C14.47,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42L9.13,5.07c-0.61,0.25 -1.18,0.58 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.48,0 -0.60,0.22l-2,3.46c-0.12,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.08,0.66 -0.08,0.98s0.04,0.66 0.08,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.37,0.31 0.60,0.22l2.49,-1c0.51,0.40 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.47,-0.18 0.50,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.58 1.69,-0.98l2.49,1c0.23,0.09 0.48,0 0.60,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64zM12,8.5a3.5,3.5 0,1 0,0 7a3.5,3.5 0,1 0,0 -7z" /> diff --git a/app/src/main/res/drawable/player_menu_background.xml b/app/src/main/res/drawable/player_menu_background.xml index 047a0c3..ca3f60f 100644 --- a/app/src/main/res/drawable/player_menu_background.xml +++ b/app/src/main/res/drawable/player_menu_background.xml @@ -4,7 +4,7 @@ the only lit thing on it. --> - + diff --git a/app/src/main/res/font/inter_bold.otf b/app/src/main/res/font/inter_bold.otf new file mode 100644 index 0000000..07cd5f0 Binary files /dev/null and b/app/src/main/res/font/inter_bold.otf differ diff --git a/app/src/main/res/font/inter_medium.otf b/app/src/main/res/font/inter_medium.otf new file mode 100644 index 0000000..8108180 Binary files /dev/null and b/app/src/main/res/font/inter_medium.otf differ diff --git a/app/src/main/res/font/inter_regular.otf b/app/src/main/res/font/inter_regular.otf new file mode 100644 index 0000000..13c3ec4 Binary files /dev/null and b/app/src/main/res/font/inter_regular.otf differ diff --git a/app/src/main/res/font/inter_semibold.otf b/app/src/main/res/font/inter_semibold.otf new file mode 100644 index 0000000..b433abb Binary files /dev/null and b/app/src/main/res/font/inter_semibold.otf differ diff --git a/app/src/test/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfigTest.kt b/app/src/test/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfigTest.kt index 8d6a2c4..52d275a 100644 --- a/app/src/test/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfigTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/data/remoteconfig/MembyRemoteConfigTest.kt @@ -13,6 +13,7 @@ class MembyRemoteConfigTest { assertEquals(0, bundled.configVersion) assertEquals("Favourites", bundled.navigation.labels.favourites) assertEquals(184, bundled.navigation.expandedWidthDp) + assertEquals("system", bundled.presentation.fontFamily) } @Test @@ -31,6 +32,17 @@ class MembyRemoteConfigTest { assertEquals(true, document.continueWatching.enabled) assertEquals(50, document.search.maxResults) assertEquals("automatic", document.ui.artworkStyle) + assertEquals("system", document.presentation.fontFamily) + } + + @Test + fun interFontFamilyDecodesFromTheServerDocument() { + val raw = validDocument.replace( + "\"navigationContentShiftDp\": 120", + "\"navigationContentShiftDp\": 120, \"fontFamily\": \"inter\"", + ) + + assertEquals("inter", decodeRemoteConfig(raw)!!.presentation.fontFamily) } @Test diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingFontScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingFontScreenshotTest.kt new file mode 100644 index 0000000..d0465f9 --- /dev/null +++ b/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingFontScreenshotTest.kt @@ -0,0 +1,72 @@ +package com.ponzischeme89.memby.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import androidx.test.core.app.ApplicationProvider +import com.github.takahirom.roborazzi.captureRoboImage +import com.ponzischeme89.memby.ServiceLocator +import com.ponzischeme89.memby.data.model.BaseItem +import com.ponzischeme89.memby.data.model.UserItemData +import com.ponzischeme89.memby.ui.components.media.ContinueWatchingCard +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [34], qualifiers = "w960dp-h540dp-television-xhdpi") +class ContinueWatchingFontScreenshotTest { + @get:Rule + val compose = createComposeRule() + + @Before + fun locator() { + ServiceLocator.init(ApplicationProvider.getApplicationContext()) + } + + @Test + fun `Inter keeps continue watching metadata within the card`() { + compose.setContent { + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { + Box(Modifier.fillMaxSize().padding(48.dp)) { + ContinueWatchingCard( + item = BaseItem( + id = "episode-1", + name = "A Harbour at the Edge of Morning", + type = "Episode", + seriesName = "The Longest Northbound Winter", + parentIndexNumber = 3, + indexNumber = 7, + runTimeTicks = 52L * 60L * 10_000_000L, + userData = UserItemData( + playbackPositionTicks = 19L * 60L * 10_000_000L, + ), + ), + availableWidth = 864.dp, + portraitArtwork = false, + onFocused = {}, + onClick = {}, + onLongClick = {}, + ) + } + } + } + + compose.onNodeWithText("The Longest Northbound Winter").assertExists() + compose.onNodeWithText("S03E07 · A Harbour at the Edge of Morning").assertExists() + compose.onRoot().captureRoboImage( + "build/screenshots/typography/continue-watching-inter.png", + ) + } +} diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/DetailPageScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/DetailPageScreenshotTest.kt index d3da0e3..1e011c4 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/DetailPageScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/DetailPageScreenshotTest.kt @@ -279,7 +279,7 @@ class DetailPageScreenshotTest { private fun capturePane(name: String, pane: @Composable () -> Unit) { compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { Box( Modifier .fillMaxWidth() @@ -293,14 +293,14 @@ class DetailPageScreenshotTest { private fun capture(name: String, content: @Composable () -> Unit) { compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { content() } + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { content() } } compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png") } private fun captureTab(name: String, tab: String, content: @Composable () -> Unit) { compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { content() } + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { content() } } compose.onNodeWithText(tab).performClick() compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png") diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt index e870743..c176612 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt @@ -165,7 +165,7 @@ class HomeMovieHeroScreenshotTest { ) compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { Box(Modifier.fillMaxSize()) { Image( bitmap = artwork, @@ -262,7 +262,7 @@ class HomeMovieHeroScreenshotTest { ) compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { Box(Modifier.fillMaxSize()) { Image( bitmap = artwork, @@ -293,7 +293,7 @@ class HomeMovieHeroScreenshotTest { val railFocus = FocusRequester() compose.setContent { - PreviewSurface(alignment = Alignment.TopStart) { + PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { Row(Modifier.fillMaxSize()) { TvNavigationRail( selected = BrowseDestination.HOME, diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/settings/SettingsSheetScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/settings/SettingsSheetScreenshotTest.kt index 2eb66ec..08c3e8f 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/settings/SettingsSheetScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/settings/SettingsSheetScreenshotTest.kt @@ -179,7 +179,7 @@ class SettingsSheetScreenshotTest { fun record(title: String, enabled: Boolean) { if (enabled) onToggled(title) } - PreviewSurface { + PreviewSurface(fontFamilyName = "inter") { SettingsPanelContent( state = SettingsPanelState( showLogo = showLogo, @@ -237,7 +237,10 @@ class SettingsSheetScreenshotTest { ) { val firstFocus = remember { FocusRequester() } LaunchedEffect(Unit) { firstFocus.requestFocus() } - PreviewSurface(alignment = if (overlay) Alignment.CenterEnd else Alignment.Center) { + PreviewSurface( + alignment = if (overlay) Alignment.CenterEnd else Alignment.Center, + fontFamilyName = "inter", + ) { SettingsPanelContent( state = SettingsPanelState( showLogo = true, diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/theme/AppFontFamilyTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/theme/AppFontFamilyTest.kt new file mode 100644 index 0000000..adbe729 --- /dev/null +++ b/app/src/test/java/com/ponzischeme89/memby/ui/theme/AppFontFamilyTest.kt @@ -0,0 +1,19 @@ +package com.ponzischeme89.memby.ui.theme + +import org.junit.Assert.assertEquals +import org.junit.Test + +class AppFontFamilyTest { + @Test + fun `system and inter are supported`() { + assertEquals(AppFontFamily.SYSTEM, appFontFamily("system")) + assertEquals(AppFontFamily.INTER, appFontFamily("inter")) + } + + @Test + fun `missing and unknown values fall back to system`() { + assertEquals(AppFontFamily.SYSTEM, appFontFamily(null)) + assertEquals(AppFontFamily.SYSTEM, appFontFamily("")) + assertEquals(AppFontFamily.SYSTEM, appFontFamily("something-new")) + } +} diff --git a/server/internal/api/features.go b/server/internal/api/features.go index dc831e1..a9eb9f7 100644 --- a/server/internal/api/features.go +++ b/server/internal/api/features.go @@ -62,6 +62,7 @@ var configurationCatalogue = []configurationDefinition{ {Key: "continueWatching.enabled", Name: "Continue Watching", Description: "Show the Continue Watching row.", Type: "boolean", Scopes: []string{"global", "user", "device"}, Default: true}, {Key: "continueWatching.showNextUp", Name: "Continue Watching: Next Up", Description: "Include an unstarted next episode in Continue Watching.", Type: "boolean", Scopes: []string{"global", "user", "device"}, Default: true}, {Key: "continueWatching.progressColour", Name: "Progress bar colour", Description: "Choose the progress bar treatment.", Type: "enum", Scopes: []string{"global", "user", "device"}, Default: "emby", Options: []string{"emby", "white"}}, + {Key: "presentation.fontFamily", Name: "App font family", Description: "Choose the bundled font used in Memby’s typography trial areas.", Type: "enum", Scopes: []string{"global"}, Default: "system", Options: []string{"system", "inter"}}, {Key: "ratings.enabled", Name: "Ratings", Description: "Show ratings throughout the catalogue.", Type: "boolean", Scopes: []string{"global", "user", "device"}, Default: true}, {Key: "genres.enabled", Name: "Genres", Description: "Show genre browsing controls.", Type: "boolean", Scopes: []string{"global", "user", "device"}, Default: true}, {Key: "trailers.enabled", Name: "Trailers", Description: "Offer trailers where available.", Type: "boolean", Scopes: []string{"global", "device", "experimental"}, Default: true}, diff --git a/server/internal/api/features_test.go b/server/internal/api/features_test.go index 1d60d80..7a139e0 100644 --- a/server/internal/api/features_test.go +++ b/server/internal/api/features_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -75,6 +76,19 @@ func TestContinueWatchingIsServerControlledAndOnByDefault(t *testing.T) { } } +func TestAppFontFamilyIsAValidatedGlobalConfiguration(t *testing.T) { + definition, ok := configurationDefinitionFor("presentation.fontFamily") + if !ok || definition.Type != "enum" || len(definition.Scopes) != 1 || definition.Scopes[0] != "global" { + t.Fatalf("font family definition = %+v, found=%v", definition, ok) + } + if err := validateConfigurationValue(definition, json.RawMessage(`"inter"`)); err != nil { + t.Fatalf("Inter was rejected: %v", err) + } + if err := validateConfigurationValue(definition, json.RawMessage(`"unknown"`)); err == nil { + t.Fatal("unknown font family was accepted") + } +} + func TestClientCapabilitiesAreNormalizedAndBounded(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/v1/status", nil) req.Header.Set("X-Memby-Capabilities", " Sonarr_Preroll_V1,server_features_v1,sonarr_preroll_v1,"+ diff --git a/server/internal/api/remote_config.go b/server/internal/api/remote_config.go index 7f0ef40..cf68adf 100644 --- a/server/internal/api/remote_config.go +++ b/server/internal/api/remote_config.go @@ -107,6 +107,10 @@ func applyGlobalConfiguration(document *config.RemoteConfig, policy store.Featur if v, ok := value.(string); ok { document.ContinueWatching.ProgressColour = v } + case "presentation.fontFamily": + if v, ok := value.(string); ok && (v == "system" || v == "inter") { + document.Presentation.FontFamily = v + } case "home.heroRefreshSeconds": if v, ok := value.(float64); ok { document.Home.HeroRefreshSeconds = int(v) diff --git a/server/internal/api/remote_config_test.go b/server/internal/api/remote_config_test.go index ccc8dd7..e848a0f 100644 --- a/server/internal/api/remote_config_test.go +++ b/server/internal/api/remote_config_test.go @@ -1,11 +1,13 @@ package api import ( + "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/ponzischeme89/memby/server/internal/config" + "github.com/ponzischeme89/memby/server/internal/store" ) func TestRemoteConfigSupportsETagRevalidationWithoutAuthentication(t *testing.T) { @@ -31,3 +33,15 @@ func TestRemoteConfigSupportsETagRevalidationWithoutAuthentication(t *testing.T) t.Fatalf("revalidation status = %d", second.Code) } } + +func TestGlobalFontFamilyConfigurationIsDelivered(t *testing.T) { + document := config.DefaultRemoteConfig() + policy := store.DefaultFeaturePolicy() + policy.Values["presentation.fontFamily"] = json.RawMessage(`"inter"`) + + applyGlobalConfiguration(&document, policy) + + if document.Presentation.FontFamily != "inter" { + t.Fatalf("font family = %q, want inter", document.Presentation.FontFamily) + } +} diff --git a/server/internal/config/remote_config.go b/server/internal/config/remote_config.go index 634e86b..3e0e9e0 100644 --- a/server/internal/config/remote_config.go +++ b/server/internal/config/remote_config.go @@ -56,8 +56,9 @@ type RemoteConfigFeatures struct { } type RemoteConfigPresentation struct { - NavigationRailExpandedWidthDp int `json:"navigationRailExpandedWidthDp"` - NavigationContentShiftDp int `json:"navigationContentShiftDp"` + NavigationRailExpandedWidthDp int `json:"navigationRailExpandedWidthDp"` + NavigationContentShiftDp int `json:"navigationContentShiftDp"` + FontFamily string `json:"fontFamily"` } // The remainder of the document is deliberately declarative. A new server-side row or @@ -160,6 +161,7 @@ func DefaultRemoteConfig() RemoteConfig { Presentation: RemoteConfigPresentation{ NavigationRailExpandedWidthDp: 184, NavigationContentShiftDp: 112, + FontFamily: "system", }, Home: RemoteHomeConfig{ Sections: []string{"continue", "for-you", "favorites", "latest-movies"}, @@ -262,6 +264,9 @@ func validateRemoteConfig(document RemoteConfig) error { if shift < 80 || shift > 160 || shift >= width { return fmt.Errorf("navigationContentShiftDp must be between 80 and 160 and less than the rail width") } + if document.Presentation.FontFamily != "system" && document.Presentation.FontFamily != "inter" { + return fmt.Errorf("fontFamily must be system or inter") + } if err := validateRemoteConfigSections(document); err != nil { return err } diff --git a/server/internal/config/remote_config_test.go b/server/internal/config/remote_config_test.go index e0ce58f..8e12ec0 100644 --- a/server/internal/config/remote_config_test.go +++ b/server/internal/config/remote_config_test.go @@ -19,6 +19,9 @@ func TestRemoteConfigDefaultsAreComplete(t *testing.T) { if !document.ContinueWatching.IncludeNextUp || len(document.Home.Sections) == 0 { t.Fatalf("central defaults are incomplete: %+v", document) } + if document.Presentation.FontFamily != "system" { + t.Fatalf("font family = %q, want system", document.Presentation.FontFamily) + } } func TestRemoteConfigDefaultsMissingNewSectionsForOlderDocuments(t *testing.T) { @@ -47,6 +50,16 @@ func TestRemoteConfigRejectsMalformedAndUnsafeDocuments(t *testing.T) { t.Fatal("unknown fields were accepted") } + document = DefaultRemoteConfig() + document.Presentation.FontFamily = "comic-sans" + raw, err = json.Marshal(document) + if err != nil { + t.Fatal(err) + } + if _, err := loadRemoteConfig(string(raw)); err == nil { + t.Fatal("unknown font family was accepted") + } + document = DefaultRemoteConfig() document.MinimumAppVersion = "0.3.0" document.MaximumAppVersion = "0.2.54"