0.3.03
This commit is contained in:
@@ -51,6 +51,13 @@ contributors.
|
|||||||
Third-party software
|
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,
|
The Android application uses AndroidX, Jetpack Compose, Media3, Coil,
|
||||||
Retrofit, OkHttp, kotlinx.serialization, and other open-source libraries.
|
Retrofit, OkHttp, kotlinx.serialization, and other open-source libraries.
|
||||||
Their copyright notices and licence metadata remain in their respective
|
Their copyright notices and licence metadata remain in their respective
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ val projectNoticeText =
|
|||||||
rootProject.file("NOTICE").readText()
|
rootProject.file("NOTICE").readText()
|
||||||
.replace("https://g.sublogue.com/admin/memby", membySourceUrl)
|
.replace("https://g.sublogue.com/admin/memby", membySourceUrl)
|
||||||
|
|
||||||
val defaultVersionName = "0.3.02"
|
val defaultVersionName = "0.3.03"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -109,6 +109,7 @@ data class RemoteFeatures(
|
|||||||
data class RemotePresentation(
|
data class RemotePresentation(
|
||||||
val navigationRailExpandedWidthDp: Int = 184,
|
val navigationRailExpandedWidthDp: Int = 184,
|
||||||
val navigationContentShiftDp: Int = 112,
|
val navigationContentShiftDp: Int = 112,
|
||||||
|
val fontFamily: String = "system",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.service.dreams.DreamService
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
@@ -18,6 +20,7 @@ import androidx.savedstate.SavedStateRegistry
|
|||||||
import androidx.savedstate.SavedStateRegistryController
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import com.ponzischeme89.memby.ServiceLocator
|
||||||
import com.ponzischeme89.memby.ui.player.PlayerActivity
|
import com.ponzischeme89.memby.ui.player.PlayerActivity
|
||||||
import com.ponzischeme89.memby.data.model.BaseItem
|
import com.ponzischeme89.memby.data.model.BaseItem
|
||||||
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
|
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
|
||||||
@@ -60,7 +63,8 @@ class MembyDreamService : DreamService() {
|
|||||||
setViewTreeSavedStateRegistryOwner(lifecycleOwner)
|
setViewTreeSavedStateRegistryOwner(lifecycleOwner)
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
setContent {
|
setContent {
|
||||||
MembyTheme {
|
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsState()
|
||||||
|
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
|
||||||
ScreensaverContent(
|
ScreensaverContent(
|
||||||
onPlay = ::launchTrailer,
|
onPlay = ::launchTrailer,
|
||||||
onExit = { finish() },
|
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.MembyQuietText
|
||||||
import com.ponzischeme89.memby.ui.theme.MembySurface
|
import com.ponzischeme89.memby.ui.theme.MembySurface
|
||||||
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
|
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
|
||||||
|
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
|
||||||
import com.ponzischeme89.memby.ui.theme.ValueSeparator
|
import com.ponzischeme89.memby.ui.theme.ValueSeparator
|
||||||
|
|
||||||
// The detail page's names for the shared tokens. The neutrals used to be a shade darker
|
// 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,
|
alignment = Alignment.TopCenter,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer { alpha = artworkAlpha().coerceIn(0f, 1f) },
|
.cinematicBackdropPullBack(artwork, artworkAlpha),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -637,6 +638,7 @@ private fun DetailHero(
|
|||||||
// height instead would re-flow the title, the synopsis and the action row on every frame
|
// 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
|
// of the transition — which is the "suddenly resize a focused item" failure, sixty times
|
||||||
// a second.
|
// a second.
|
||||||
|
MembyTrialTypography {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -837,6 +839,7 @@ private fun DetailHero(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1369,6 +1372,7 @@ internal fun DetailFocusablePane(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DetailMetaRows(rows: List<TechnicalSpec>, modifier: Modifier = Modifier, labelWidth: androidx.compose.ui.unit.Dp = 110.dp) {
|
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)) {
|
Column(modifier, verticalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||||
rows.forEach { row ->
|
rows.forEach { row ->
|
||||||
Row(Modifier.fillMaxWidth()) {
|
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,
|
modifier: Modifier = Modifier,
|
||||||
supportingText: String? = null,
|
supportingText: String? = null,
|
||||||
) {
|
) {
|
||||||
|
MembyTrialTypography {
|
||||||
DetailFocusablePane(focusRequester, modifier) {
|
DetailFocusablePane(focusRequester, modifier) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
|
||||||
Column(Modifier.weight(1.5f)) {
|
Column(Modifier.weight(1.5f)) {
|
||||||
@@ -1426,6 +1432,7 @@ internal fun DetailOverviewPane(
|
|||||||
DetailMetaRows(credits, Modifier.weight(1f), labelWidth = 96.dp)
|
DetailMetaRows(credits, Modifier.weight(1f), labelWidth = 96.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1451,6 +1458,7 @@ internal fun DetailDetailsPane(
|
|||||||
val alreadySaid = rows.map { it.value }.toSet()
|
val alreadySaid = rows.map { it.value }.toSet()
|
||||||
specs.filterNot { it.value in alreadySaid }
|
specs.filterNot { it.value in alreadySaid }
|
||||||
}
|
}
|
||||||
|
MembyTrialTypography {
|
||||||
DetailFocusablePane(focusRequester, modifier) {
|
DetailFocusablePane(focusRequester, modifier) {
|
||||||
if (rows.isEmpty() && specs.isEmpty()) {
|
if (rows.isEmpty() && specs.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -1477,6 +1485,7 @@ internal fun DetailDetailsPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsStateWithLifecycle()
|
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsStateWithLifecycle()
|
||||||
MembyTheme {
|
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
|
||||||
AppRoot(
|
AppRoot(
|
||||||
remoteConfig = remoteConfig,
|
remoteConfig = remoteConfig,
|
||||||
onCloseSettings = ::finish,
|
onCloseSettings = ::finish,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.ponzischeme89.memby.ui
|
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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -26,6 +21,7 @@ import coil.request.ImageRequest
|
|||||||
import com.ponzischeme89.memby.ServiceLocator
|
import com.ponzischeme89.memby.ServiceLocator
|
||||||
import com.ponzischeme89.memby.data.model.BaseItem
|
import com.ponzischeme89.memby.data.model.BaseItem
|
||||||
import com.ponzischeme89.memby.ui.theme.MembySurface
|
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.
|
* The quiet surface between the focused title details and the scrolling shelves.
|
||||||
@@ -44,51 +40,53 @@ internal fun MetadataHero(
|
|||||||
isContinueWatchingItem: Boolean = false,
|
isContinueWatchingItem: Boolean = false,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
MembyTrialTypography {
|
||||||
modifier = modifier
|
|
||||||
.clipToBounds()
|
|
||||||
.background(MembySurface),
|
|
||||||
) {
|
|
||||||
MetadataHeroArtwork(
|
|
||||||
item = item,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize().background(
|
modifier = modifier
|
||||||
Brush.horizontalGradient(
|
.clipToBounds()
|
||||||
0f to MembySurface,
|
.background(MembySurface),
|
||||||
0.30f to MembySurface,
|
) {
|
||||||
0.48f to MembySurface.copy(alpha = 0.80f),
|
MetadataHeroArtwork(
|
||||||
0.62f to MembySurface.copy(alpha = 0.40f),
|
item = item,
|
||||||
0.78f to MembySurface.copy(alpha = 0.10f),
|
modifier = Modifier.fillMaxSize(),
|
||||||
1f to Color.Transparent,
|
)
|
||||||
|
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(
|
||||||
Box(
|
Modifier.fillMaxSize().background(
|
||||||
Modifier.fillMaxSize().background(
|
Brush.verticalGradient(
|
||||||
Brush.verticalGradient(
|
0f to MembySurface.copy(alpha = 0.18f),
|
||||||
0f to MembySurface.copy(alpha = 0.18f),
|
0.58f to Color.Transparent,
|
||||||
0.58f to Color.Transparent,
|
1f to MembySurface.copy(alpha = 0.20f),
|
||||||
1f to MembySurface.copy(alpha = 0.20f),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
MediaMetadataPanel(
|
||||||
MediaMetadataPanel(
|
item = item,
|
||||||
item = item,
|
loading = loading,
|
||||||
loading = loading,
|
contentOrder = contentOrder,
|
||||||
contentOrder = contentOrder,
|
timeRemainingColour = timeRemainingColour,
|
||||||
timeRemainingColour = timeRemainingColour,
|
isContinueWatchingItem = isContinueWatchingItem,
|
||||||
isContinueWatchingItem = isContinueWatchingItem,
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxSize()
|
||||||
.fillMaxSize()
|
.padding(
|
||||||
.padding(
|
start = HomeContentHorizontalInset,
|
||||||
start = HomeContentHorizontalInset,
|
end = HomeContentHorizontalInset,
|
||||||
end = HomeContentHorizontalInset,
|
top = 28.dp,
|
||||||
top = 28.dp,
|
bottom = 18.dp,
|
||||||
bottom = 18.dp,
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,16 +117,6 @@ private fun MetadataHeroArtwork(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request == null) return
|
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) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -143,10 +131,7 @@ private fun MetadataHeroArtwork(
|
|||||||
alignment = Alignment.CenterEnd,
|
alignment = Alignment.CenterEnd,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.cinematicBackdropPullBack(artwork),
|
||||||
scaleX = artworkScale.value
|
|
||||||
scaleY = artworkScale.value
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize().background(
|
Modifier.fillMaxSize().background(
|
||||||
@@ -173,6 +158,4 @@ private fun MetadataHeroArtwork(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val MetadataHeroArtworkCrossfadeMs = 180
|
private const val MetadataHeroArtworkCrossfadeMs = 180
|
||||||
private const val MetadataHeroArtworkInitialScale = 1.05f
|
|
||||||
private const val MetadataHeroArtworkZoomOutMs = 10_000
|
|
||||||
private const val MetadataHeroArtworkWidthFraction = 0.68f
|
private const val MetadataHeroArtworkWidthFraction = 0.68f
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ annotation class TvPreview
|
|||||||
@Composable
|
@Composable
|
||||||
fun PreviewSurface(
|
fun PreviewSurface(
|
||||||
alignment: Alignment = Alignment.Center,
|
alignment: Alignment = Alignment.Center,
|
||||||
|
fontFamilyName: String = "system",
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
MembyTheme {
|
MembyTheme(fontFamilyName = fontFamilyName) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.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.MembyAccentBright
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccentMuted
|
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.MembyHairline
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyOutline
|
import com.ponzischeme89.memby.ui.theme.MembyOutline
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
|
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyQuietText
|
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
|
* One answer in a television settings panel. The panel owns its invariant chrome while
|
||||||
@@ -204,7 +202,7 @@ fun tvSettingsOptionView(
|
|||||||
|
|
||||||
fun render(focused: Boolean) {
|
fun render(focused: Boolean) {
|
||||||
val fill = when {
|
val fill = when {
|
||||||
focused -> MembyControlSurface.toArgb()
|
focused -> PlayerMenuFocusedSurface
|
||||||
prominent -> MembyAccentMuted.toArgb()
|
prominent -> MembyAccentMuted.toArgb()
|
||||||
selected -> MembyAccentMuted.toArgb()
|
selected -> MembyAccentMuted.toArgb()
|
||||||
else -> android.graphics.Color.TRANSPARENT
|
else -> android.graphics.Color.TRANSPARENT
|
||||||
@@ -243,7 +241,7 @@ fun applyTvSettingsPanelSurface(view: View) {
|
|||||||
val context = view.context
|
val context = view.context
|
||||||
view.background = roundedBackground(
|
view.background = roundedBackground(
|
||||||
context,
|
context,
|
||||||
MembySurfaceRaised.toArgb(),
|
PlayerMenuBackground,
|
||||||
MembyOutline.toArgb(),
|
MembyOutline.toArgb(),
|
||||||
1,
|
1,
|
||||||
MembyPanelCorner.value.toInt(),
|
MembyPanelCorner.value.toInt(),
|
||||||
@@ -259,10 +257,10 @@ private data class TvSettingsPanel(
|
|||||||
private fun tvSettingsPanel(context: Context, title: String, description: String): TvSettingsPanel {
|
private fun tvSettingsPanel(context: Context, title: String, description: String): TvSettingsPanel {
|
||||||
val root = LinearLayout(context).apply {
|
val root = LinearLayout(context).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
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(
|
background = roundedBackground(
|
||||||
context,
|
context,
|
||||||
MembySurfaceRaised.toArgb(),
|
PlayerMenuBackground,
|
||||||
MembyOutline.toArgb(),
|
MembyOutline.toArgb(),
|
||||||
1,
|
1,
|
||||||
MembyPanelCorner.value.toInt(),
|
MembyPanelCorner.value.toInt(),
|
||||||
@@ -272,7 +270,7 @@ private fun tvSettingsPanel(context: Context, title: String, description: String
|
|||||||
root.addView(TextView(context).apply {
|
root.addView(TextView(context).apply {
|
||||||
text = title
|
text = title
|
||||||
setTextColor(MembyOnSurface.toArgb())
|
setTextColor(MembyOnSurface.toArgb())
|
||||||
textSize = 24f
|
textSize = 22f
|
||||||
typeface = Typeface.create("sans-serif", Typeface.BOLD)
|
typeface = Typeface.create("sans-serif", Typeface.BOLD)
|
||||||
})
|
})
|
||||||
if (description.isNotBlank()) {
|
if (description.isNotBlank()) {
|
||||||
@@ -304,7 +302,7 @@ private fun tvSettingsDialog(context: Context, onDismiss: () -> Unit): Dialog =
|
|||||||
window?.apply {
|
window?.apply {
|
||||||
setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
|
setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
|
||||||
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
attributes = attributes.apply { dimAmount = 0.72f }
|
attributes = attributes.apply { dimAmount = 0.80f }
|
||||||
setGravity(Gravity.CENTER)
|
setGravity(Gravity.CENTER)
|
||||||
}
|
}
|
||||||
setOnDismissListener { onDismiss() }
|
setOnDismissListener { onDismiss() }
|
||||||
@@ -320,7 +318,7 @@ private fun finishTvSettingsDialog(
|
|||||||
val context = root.context
|
val context = root.context
|
||||||
val frame = FrameLayout(context).apply {
|
val frame = FrameLayout(context).apply {
|
||||||
setPadding(context.dp(8), context.dp(8), context.dp(8), context.dp(8))
|
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)
|
dialog.setContentView(frame)
|
||||||
val scroll = options.parent as ScrollView
|
val scroll = options.parent as ScrollView
|
||||||
@@ -354,7 +352,7 @@ private fun tvSettingsAction(context: Context, label: String, primary: Boolean,
|
|||||||
context,
|
context,
|
||||||
when {
|
when {
|
||||||
focused && primary -> MembyAccent.toArgb()
|
focused && primary -> MembyAccent.toArgb()
|
||||||
focused -> MembyControlSurface.toArgb()
|
focused -> PlayerMenuFocusedSurface
|
||||||
primary -> MembyAccentMuted.toArgb()
|
primary -> MembyAccentMuted.toArgb()
|
||||||
else -> android.graphics.Color.TRANSPARENT
|
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 fun Context.dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
private val PlayerMenuBackground = android.graphics.Color.BLACK
|
||||||
|
private val PlayerMenuFocusedSurface = 0xFF151719.toInt()
|
||||||
|
|||||||
+12
-9
@@ -9,6 +9,7 @@ import com.ponzischeme89.memby.data.model.BaseItem
|
|||||||
import com.ponzischeme89.memby.ui.ResumableMediaCard
|
import com.ponzischeme89.memby.ui.ResumableMediaCard
|
||||||
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
|
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
|
||||||
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
|
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
|
||||||
|
import com.ponzischeme89.memby.ui.theme.MembyTrialTypography
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContinueWatchingCard(
|
fun ContinueWatchingCard(
|
||||||
@@ -46,13 +47,15 @@ fun ContinueWatchingCard(
|
|||||||
primaryUrl = repository.primaryUrl(item, 480),
|
primaryUrl = repository.primaryUrl(item, 480),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ResumableMediaCard(
|
MembyTrialTypography {
|
||||||
model = model,
|
ResumableMediaCard(
|
||||||
width = width,
|
model = model,
|
||||||
portraitArtwork = portraitArtwork,
|
width = width,
|
||||||
onFocused = onFocused,
|
portraitArtwork = portraitArtwork,
|
||||||
onClick = onClick,
|
onFocused = onFocused,
|
||||||
onLongClick = onLongClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
onLongClick = onLongClick,
|
||||||
)
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ private fun MetadataTimeRemaining(
|
|||||||
label,
|
label,
|
||||||
color = if (colour.equals("white", ignoreCase = true)) Color.White else EmbyGreen,
|
color = if (colour.equals("white", ignoreCase = true)) Color.White else EmbyGreen,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 18.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
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.MembyPanelCorner
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyQuietText
|
import com.ponzischeme89.memby.ui.theme.MembyQuietText
|
||||||
import com.ponzischeme89.memby.ui.theme.MembySurface
|
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.BundledRemoteConfig
|
||||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
|
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
|
||||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
|
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
|
||||||
@@ -197,6 +198,7 @@ fun TvNavigationRail(
|
|||||||
animationSpec = tween(160),
|
animationSpec = tween(160),
|
||||||
label = "navigation-rail-width",
|
label = "navigation-rail-width",
|
||||||
)
|
)
|
||||||
|
MembyTrialTypography {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(TvRailCollapsedWidth)
|
.width(TvRailCollapsedWidth)
|
||||||
@@ -362,6 +364,7 @@ fun TvNavigationRail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configuredNavigationLabel(
|
private fun configuredNavigationLabel(
|
||||||
|
|||||||
@@ -4553,7 +4553,7 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
title = "Playback options",
|
title = "Playback options",
|
||||||
description = "Fine-tune this video without leaving playback.",
|
description = "Fine-tune this video without leaving playback.",
|
||||||
options = options,
|
options = options,
|
||||||
onDismiss = { playerView?.showController() },
|
onDismiss = { restorePlayerControlFocus(androidx.media3.ui.R.id.exo_settings) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4579,7 +4579,7 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
showManualPassthroughPicker(current.codecs)
|
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(),
|
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() {
|
private fun hideSubtitleOverlay() {
|
||||||
subtitleOverlay?.visibility = View.GONE
|
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. */
|
/** A completed choice returns directly to the programme, without reopening transport. */
|
||||||
@@ -5354,14 +5354,33 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
subtitleAutoSelectionAttempted = true
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
private fun selectedTrackLabel(trackType: Int): String {
|
private fun selectedTrackLabel(trackType: Int): String {
|
||||||
val playback = player ?: return ""
|
val playback = player ?: return ""
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.os.Build
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
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.ui.player.PlayerActivity
|
||||||
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
|
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyTheme
|
import com.ponzischeme89.memby.ui.theme.MembyTheme
|
||||||
@@ -24,7 +27,8 @@ class ScreensaverActivity : ComponentActivity() {
|
|||||||
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
MembyTheme {
|
val remoteConfig by ServiceLocator.remoteConfig.activeFlow.collectAsStateWithLifecycle()
|
||||||
|
MembyTheme(fontFamilyName = remoteConfig.presentation.fontFamily) {
|
||||||
ScreensaverContent(
|
ScreensaverContent(
|
||||||
onPlay = { item ->
|
onPlay = { item ->
|
||||||
startActivity(
|
startActivity(
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.LocalTextStyle
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.ponzischeme89.memby.R
|
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.PreviewSurface
|
||||||
import com.ponzischeme89.memby.ui.TvPreview
|
import com.ponzischeme89.memby.ui.TvPreview
|
||||||
import com.ponzischeme89.memby.ui.WelcomeQuoteStyle
|
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.MembyAccent
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccentBright
|
import com.ponzischeme89.memby.ui.theme.MembyAccentBright
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
||||||
@@ -694,6 +695,7 @@ fun SettingsSheet(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MembyTrialTypography {
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
Box(
|
Box(
|
||||||
@@ -755,6 +757,7 @@ fun SettingsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SettingsPanelContent(
|
internal fun SettingsPanelContent(
|
||||||
@@ -786,6 +789,7 @@ internal fun SettingsPanelContent(
|
|||||||
contentScrollState.scrollTo(0)
|
contentScrollState.scrollTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MembyTrialTypography {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.then(if (overlay) Modifier.width(760.dp) else Modifier.fillMaxWidth())
|
.then(if (overlay) Modifier.width(760.dp) else Modifier.fillMaxWidth())
|
||||||
@@ -1109,6 +1113,8 @@ internal fun SettingsPanelContent(
|
|||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1269,7 +1275,7 @@ private fun DeviceRenameDialog(
|
|||||||
onValueChange = { if (it.length <= 80) name = it },
|
onValueChange = { if (it.length <= 80) name = it },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = !busy,
|
enabled = !busy,
|
||||||
textStyle = TextStyle(color = TextPrimary, fontSize = 18.sp),
|
textStyle = LocalTextStyle.current.copy(color = TextPrimary, fontSize = 18.sp),
|
||||||
cursorBrush = SolidColor(EmbyGreen),
|
cursorBrush = SolidColor(EmbyGreen),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
keyboardActions = KeyboardActions(onDone = { if (valid && !busy) onSave(name) }),
|
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
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.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.em
|
import androidx.compose.ui.unit.em
|
||||||
import androidx.tv.material3.LocalTextStyle
|
import androidx.tv.material3.LocalTextStyle
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.darkColorScheme
|
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
|
// 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
|
// 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
|
@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
|
// 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,
|
// to sans-serif-medium. The generic family lets Compose select real regular,
|
||||||
// medium, semibold and bold faces from each Text's FontWeight, restoring hierarchy
|
// 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,
|
letterSpacing = 0.006.em,
|
||||||
lineHeight = 1.22.em,
|
lineHeight = 1.22.em,
|
||||||
)
|
)
|
||||||
|
val trialFontFamily = when (appFontFamily(fontFamilyName)) {
|
||||||
|
AppFontFamily.SYSTEM -> FontFamily.SansSerif
|
||||||
|
AppFontFamily.INTER -> InterFontFamily
|
||||||
|
}
|
||||||
MaterialTheme(colorScheme = embyColors()) {
|
MaterialTheme(colorScheme = embyColors()) {
|
||||||
CompositionLocalProvider(LocalTextStyle provides appTextStyle) {
|
CompositionLocalProvider(
|
||||||
|
LocalTextStyle provides appTextStyle,
|
||||||
|
LocalTrialFontFamily provides trialFontFamily,
|
||||||
|
) {
|
||||||
content()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- A simple outlined cog that matches Memby's other player glyphs. Kept app-owned so a
|
<!-- A centred, even-odd cog that stays legible under the player button's tint. -->
|
||||||
Media3 update cannot silently replace the settings entry with platform-era artwork. -->
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/transparent"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M12,8.75a3.25,3.25 0,1 0,0 6.5a3.25,3.25 0,1 0,0 -6.5M19.1,13.35c0.05,-0.44 0.05,-0.88 0,-1.32l1.66,-1.29l-1.85,-3.2l-1.94,0.78a7.7,7.7 0,0 0,-1.15 -0.67L15.53,5.6h-3.7l-0.29,2.05c-0.4,0.18 -0.79,0.4 -1.15,0.67L8.45,7.54l-1.85,3.2l1.66,1.29a6.1,6.1 0,0 0,0 1.32L6.6,14.64l1.85,3.2l1.94,-0.78c0.36,0.27 0.75,0.49 1.15,0.67l0.29,2.05h3.7l0.29,-2.05c0.4,-0.18 0.79,-0.4 1.15,-0.67l1.94,0.78l1.85,-3.2z"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#FFFFFFFF"
|
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" />
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="1.7" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
the only lit thing on it. -->
|
the only lit thing on it. -->
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="#F7000000" />
|
<solid android:color="#FF000000" />
|
||||||
<stroke
|
<stroke
|
||||||
android:width="1dp"
|
android:width="1dp"
|
||||||
android:color="#1FFFFFFF" />
|
android:color="#1FFFFFFF" />
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,7 @@ class MembyRemoteConfigTest {
|
|||||||
assertEquals(0, bundled.configVersion)
|
assertEquals(0, bundled.configVersion)
|
||||||
assertEquals("Favourites", bundled.navigation.labels.favourites)
|
assertEquals("Favourites", bundled.navigation.labels.favourites)
|
||||||
assertEquals(184, bundled.navigation.expandedWidthDp)
|
assertEquals(184, bundled.navigation.expandedWidthDp)
|
||||||
|
assertEquals("system", bundled.presentation.fontFamily)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -31,6 +32,17 @@ class MembyRemoteConfigTest {
|
|||||||
assertEquals(true, document.continueWatching.enabled)
|
assertEquals(true, document.continueWatching.enabled)
|
||||||
assertEquals(50, document.search.maxResults)
|
assertEquals(50, document.search.maxResults)
|
||||||
assertEquals("automatic", document.ui.artworkStyle)
|
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
|
@Test
|
||||||
|
|||||||
@@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -279,7 +279,7 @@ class DetailPageScreenshotTest {
|
|||||||
|
|
||||||
private fun capturePane(name: String, pane: @Composable () -> Unit) {
|
private fun capturePane(name: String, pane: @Composable () -> Unit) {
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) {
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -293,14 +293,14 @@ class DetailPageScreenshotTest {
|
|||||||
|
|
||||||
private fun capture(name: String, content: @Composable () -> Unit) {
|
private fun capture(name: String, content: @Composable () -> Unit) {
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) { content() }
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { content() }
|
||||||
}
|
}
|
||||||
compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png")
|
compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun captureTab(name: String, tab: String, content: @Composable () -> Unit) {
|
private fun captureTab(name: String, tab: String, content: @Composable () -> Unit) {
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) { content() }
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") { content() }
|
||||||
}
|
}
|
||||||
compose.onNodeWithText(tab).performClick()
|
compose.onNodeWithText(tab).performClick()
|
||||||
compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png")
|
compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png")
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) {
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = artwork,
|
bitmap = artwork,
|
||||||
@@ -262,7 +262,7 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) {
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = artwork,
|
bitmap = artwork,
|
||||||
@@ -293,7 +293,7 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
val railFocus = FocusRequester()
|
val railFocus = FocusRequester()
|
||||||
|
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
PreviewSurface(alignment = Alignment.TopStart) {
|
PreviewSurface(alignment = Alignment.TopStart, fontFamilyName = "inter") {
|
||||||
Row(Modifier.fillMaxSize()) {
|
Row(Modifier.fillMaxSize()) {
|
||||||
TvNavigationRail(
|
TvNavigationRail(
|
||||||
selected = BrowseDestination.HOME,
|
selected = BrowseDestination.HOME,
|
||||||
|
|||||||
+5
-2
@@ -179,7 +179,7 @@ class SettingsSheetScreenshotTest {
|
|||||||
fun record(title: String, enabled: Boolean) {
|
fun record(title: String, enabled: Boolean) {
|
||||||
if (enabled) onToggled(title)
|
if (enabled) onToggled(title)
|
||||||
}
|
}
|
||||||
PreviewSurface {
|
PreviewSurface(fontFamilyName = "inter") {
|
||||||
SettingsPanelContent(
|
SettingsPanelContent(
|
||||||
state = SettingsPanelState(
|
state = SettingsPanelState(
|
||||||
showLogo = showLogo,
|
showLogo = showLogo,
|
||||||
@@ -237,7 +237,10 @@ class SettingsSheetScreenshotTest {
|
|||||||
) {
|
) {
|
||||||
val firstFocus = remember { FocusRequester() }
|
val firstFocus = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { firstFocus.requestFocus() }
|
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(
|
SettingsPanelContent(
|
||||||
state = SettingsPanelState(
|
state = SettingsPanelState(
|
||||||
showLogo = true,
|
showLogo = true,
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.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.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: "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: "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: "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},
|
{Key: "trailers.enabled", Name: "Trailers", Description: "Offer trailers where available.", Type: "boolean", Scopes: []string{"global", "device", "experimental"}, Default: true},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"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) {
|
func TestClientCapabilitiesAreNormalizedAndBounded(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
|
req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
|
||||||
req.Header.Set("X-Memby-Capabilities", " Sonarr_Preroll_V1,server_features_v1,sonarr_preroll_v1,"+
|
req.Header.Set("X-Memby-Capabilities", " Sonarr_Preroll_V1,server_features_v1,sonarr_preroll_v1,"+
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ func applyGlobalConfiguration(document *config.RemoteConfig, policy store.Featur
|
|||||||
if v, ok := value.(string); ok {
|
if v, ok := value.(string); ok {
|
||||||
document.ContinueWatching.ProgressColour = v
|
document.ContinueWatching.ProgressColour = v
|
||||||
}
|
}
|
||||||
|
case "presentation.fontFamily":
|
||||||
|
if v, ok := value.(string); ok && (v == "system" || v == "inter") {
|
||||||
|
document.Presentation.FontFamily = v
|
||||||
|
}
|
||||||
case "home.heroRefreshSeconds":
|
case "home.heroRefreshSeconds":
|
||||||
if v, ok := value.(float64); ok {
|
if v, ok := value.(float64); ok {
|
||||||
document.Home.HeroRefreshSeconds = int(v)
|
document.Home.HeroRefreshSeconds = int(v)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ponzischeme89/memby/server/internal/config"
|
"github.com/ponzischeme89/memby/server/internal/config"
|
||||||
|
"github.com/ponzischeme89/memby/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRemoteConfigSupportsETagRevalidationWithoutAuthentication(t *testing.T) {
|
func TestRemoteConfigSupportsETagRevalidationWithoutAuthentication(t *testing.T) {
|
||||||
@@ -31,3 +33,15 @@ func TestRemoteConfigSupportsETagRevalidationWithoutAuthentication(t *testing.T)
|
|||||||
t.Fatalf("revalidation status = %d", second.Code)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ type RemoteConfigFeatures struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RemoteConfigPresentation struct {
|
type RemoteConfigPresentation struct {
|
||||||
NavigationRailExpandedWidthDp int `json:"navigationRailExpandedWidthDp"`
|
NavigationRailExpandedWidthDp int `json:"navigationRailExpandedWidthDp"`
|
||||||
NavigationContentShiftDp int `json:"navigationContentShiftDp"`
|
NavigationContentShiftDp int `json:"navigationContentShiftDp"`
|
||||||
|
FontFamily string `json:"fontFamily"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The remainder of the document is deliberately declarative. A new server-side row or
|
// The remainder of the document is deliberately declarative. A new server-side row or
|
||||||
@@ -160,6 +161,7 @@ func DefaultRemoteConfig() RemoteConfig {
|
|||||||
Presentation: RemoteConfigPresentation{
|
Presentation: RemoteConfigPresentation{
|
||||||
NavigationRailExpandedWidthDp: 184,
|
NavigationRailExpandedWidthDp: 184,
|
||||||
NavigationContentShiftDp: 112,
|
NavigationContentShiftDp: 112,
|
||||||
|
FontFamily: "system",
|
||||||
},
|
},
|
||||||
Home: RemoteHomeConfig{
|
Home: RemoteHomeConfig{
|
||||||
Sections: []string{"continue", "for-you", "favorites", "latest-movies"},
|
Sections: []string{"continue", "for-you", "favorites", "latest-movies"},
|
||||||
@@ -262,6 +264,9 @@ func validateRemoteConfig(document RemoteConfig) error {
|
|||||||
if shift < 80 || shift > 160 || shift >= width {
|
if shift < 80 || shift > 160 || shift >= width {
|
||||||
return fmt.Errorf("navigationContentShiftDp must be between 80 and 160 and less than the rail 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 {
|
if err := validateRemoteConfigSections(document); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ func TestRemoteConfigDefaultsAreComplete(t *testing.T) {
|
|||||||
if !document.ContinueWatching.IncludeNextUp || len(document.Home.Sections) == 0 {
|
if !document.ContinueWatching.IncludeNextUp || len(document.Home.Sections) == 0 {
|
||||||
t.Fatalf("central defaults are incomplete: %+v", document)
|
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) {
|
func TestRemoteConfigDefaultsMissingNewSectionsForOlderDocuments(t *testing.T) {
|
||||||
@@ -47,6 +50,16 @@ func TestRemoteConfigRejectsMalformedAndUnsafeDocuments(t *testing.T) {
|
|||||||
t.Fatal("unknown fields were accepted")
|
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 = DefaultRemoteConfig()
|
||||||
document.MinimumAppVersion = "0.3.0"
|
document.MinimumAppVersion = "0.3.0"
|
||||||
document.MaximumAppVersion = "0.2.54"
|
document.MaximumAppVersion = "0.2.54"
|
||||||
|
|||||||
Reference in New Issue
Block a user