0.2.95 - Refactor
This commit is contained in:
@@ -64,7 +64,7 @@ val projectNoticeText =
|
||||
|
||||
// A release workflow can derive the app version from its Git tag without editing the
|
||||
// source tree. Local builds keep using the checked-in default.
|
||||
val defaultVersionName = "0.2.94"
|
||||
val defaultVersionName = "0.2.95"
|
||||
val membyVersionName: String =
|
||||
(project.findProperty("memby.versionName") as String?)
|
||||
?.trim()
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.ponzischeme89.memby.ui
|
||||
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import com.ponzischeme89.memby.data.decodeLineList
|
||||
import android.os.Build
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.foundation.background
|
||||
|
||||
@@ -68,6 +68,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ponzischeme89.memby.ServiceLocator
|
||||
import com.ponzischeme89.memby.data.Settings
|
||||
import com.ponzischeme89.memby.data.decodeLineList
|
||||
import com.ponzischeme89.memby.data.friendlyEmbyError
|
||||
import com.ponzischeme89.memby.data.analytics.PlaybackJourney
|
||||
import com.ponzischeme89.memby.data.analytics.playbackEntryPointFor
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ponzischeme89.memby.ui.alerts
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import android.provider.Settings as AndroidSettings
|
||||
@@ -117,7 +118,7 @@ fun MyAlertsPage(
|
||||
onDismiss: (UserNotification) -> Unit,
|
||||
onDismissAll: (List<UserNotification>) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier,
|
||||
) {
|
||||
var tab by remember { mutableStateOf(AlertsTab.INBOX) }
|
||||
var page by remember { mutableStateOf(0) }
|
||||
|
||||
+4
-113
@@ -1,124 +1,15 @@
|
||||
package com.ponzischeme89.memby.ui
|
||||
package com.ponzischeme89.memby.ui.components.media
|
||||
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.Text
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ponzischeme89.memby.ServiceLocator
|
||||
import com.ponzischeme89.memby.R
|
||||
import com.ponzischeme89.memby.BuildConfig
|
||||
import com.ponzischeme89.memby.data.EmbyProfile
|
||||
import com.ponzischeme89.memby.data.model.BaseItem
|
||||
import com.ponzischeme89.memby.data.model.MembyViewer
|
||||
import com.ponzischeme89.memby.ui.alerts.alertBadgeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.UHD_MIN_WIDTH
|
||||
import com.ponzischeme89.memby.ui.detail.channelLabel
|
||||
import com.ponzischeme89.memby.ui.detail.dynamicRangeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.formatRuntime
|
||||
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
||||
import com.ponzischeme89.memby.ui.theme.MembyCardCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyChipCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurfaceRaised
|
||||
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||
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.ValueSeparator
|
||||
import com.ponzischeme89.memby.data.remoteconfig.BundledRemoteConfig
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import com.ponzischeme89.memby.ui.ResumableMediaCard
|
||||
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
|
||||
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
|
||||
|
||||
@Composable
|
||||
fun ContinueWatchingCard(
|
||||
|
||||
@@ -138,9 +138,17 @@ fun LandscapeCard(
|
||||
else -> 4
|
||||
}
|
||||
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 164.dp, 360.dp)
|
||||
val model = remember(item, showSecondaryMetadata, showWatchedEpisodeCount) {
|
||||
item.toMediaCardUiModel(
|
||||
showProgress = false,
|
||||
showSecondaryMetadata,
|
||||
showWatchedEpisodeCount,
|
||||
showMediaTypeIcon = false,
|
||||
)
|
||||
}
|
||||
MediaCard(
|
||||
item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
||||
showSecondaryMetadata, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
||||
model, item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
||||
onFocused, onClick, onLongClick, modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,123 +2,54 @@ package com.ponzischeme89.memby.ui
|
||||
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.Text
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ponzischeme89.memby.ServiceLocator
|
||||
import com.ponzischeme89.memby.R
|
||||
import com.ponzischeme89.memby.BuildConfig
|
||||
import com.ponzischeme89.memby.data.EmbyProfile
|
||||
import com.ponzischeme89.memby.data.model.BaseItem
|
||||
import com.ponzischeme89.memby.data.model.MembyViewer
|
||||
import com.ponzischeme89.memby.ui.alerts.alertBadgeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.UHD_MIN_WIDTH
|
||||
import com.ponzischeme89.memby.ui.detail.channelLabel
|
||||
import com.ponzischeme89.memby.ui.detail.dynamicRangeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.formatRuntime
|
||||
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
||||
import com.ponzischeme89.memby.ui.theme.MembyCardCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyChipCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurfaceRaised
|
||||
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||
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.ValueSeparator
|
||||
import com.ponzischeme89.memby.data.remoteconfig.BundledRemoteConfig
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// The launcher's names for the shared tokens. Both surfaces read from one palette now
|
||||
// (ui/theme/DesignTokens.kt): a detail page opens from a row and the two sit side by side,
|
||||
@@ -145,6 +76,70 @@ private val QuietText: Color get() = MembyQuietText
|
||||
*/
|
||||
private val MediaTypeMarkSize = 13.dp
|
||||
|
||||
/**
|
||||
* Everything [MediaCard] needs to render a poster or landscape row card, with no
|
||||
* [BaseItem] field reads of its own. Mirrors the split [ResumableMediaCardModel] already
|
||||
* makes for [com.ponzischeme89.memby.ui.components.media.ContinueWatchingCard]: the row-level card (this file's [PosterCard],
|
||||
* [PosterGridCard] and [LandscapeCard] in the sibling file) still takes a [BaseItem] — a home
|
||||
* row is a `List<BaseItem>` — and only the innermost renderer is decoupled from the wire
|
||||
* model. The artwork URL is resolved by the caller rather than carried on the model: sizing
|
||||
* it needs the card's width and the local density, which are known one level up and would
|
||||
* otherwise have to be threaded back onto every field of a data class just to get here.
|
||||
*/
|
||||
internal data class MediaCardUiModel(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val subtitle: String,
|
||||
val showSecondaryMetadata: Boolean,
|
||||
val contentDescription: String,
|
||||
val played: Boolean,
|
||||
val favourite: Boolean,
|
||||
val progress: Float,
|
||||
val isSchedule: Boolean,
|
||||
val scheduleAvailability: String,
|
||||
val scheduleStatusLabel: String?,
|
||||
val lifecycleStatus: String,
|
||||
val lifecycleLabel: String?,
|
||||
val airingLabel: String?,
|
||||
val typeMark: Pair<ImageVector, String>?,
|
||||
)
|
||||
|
||||
/**
|
||||
* @param showProgress whether a "Resume at …" subtitle should replace the default one when
|
||||
* the item has a saved position — a row-level choice, not a fact about the item.
|
||||
*/
|
||||
internal fun BaseItem.toMediaCardUiModel(
|
||||
showProgress: Boolean,
|
||||
showSecondaryMetadata: Boolean,
|
||||
showWatchedEpisodeCount: Boolean,
|
||||
showMediaTypeIcon: Boolean,
|
||||
): MediaCardUiModel {
|
||||
val position = userData?.playbackPositionTicks ?: 0L
|
||||
val runtime = runTimeTicks ?: 0L
|
||||
val progress = if (runtime > 0L) (position.toFloat() / runtime).coerceIn(0f, 1f) else 0f
|
||||
return MediaCardUiModel(
|
||||
id = id,
|
||||
name = name,
|
||||
subtitle = if (showSecondaryMetadata) {
|
||||
cardSubtitle(this, showProgress, position, showWatchedEpisodeCount)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
showSecondaryMetadata = showSecondaryMetadata,
|
||||
contentDescription = cardDescription(this, progress, showWatchedEpisodeCount, showMediaTypeIcon),
|
||||
played = userData?.played == true,
|
||||
favourite = isFavorite,
|
||||
progress = progress,
|
||||
isSchedule = isSchedule,
|
||||
scheduleAvailability = membyAvailability.orEmpty(),
|
||||
scheduleStatusLabel = if (isSchedule) scheduleStatusBadgeLabel(membyAvailability.orEmpty()) else null,
|
||||
lifecycleStatus = membyLifecycle.orEmpty(),
|
||||
lifecycleLabel = if (isSchedule) lifecycleBadgeLabel(this) else null,
|
||||
airingLabel = airingBadgeLabel(this),
|
||||
typeMark = if (showMediaTypeIcon) mediaTypeMark(this) else null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PosterCard(
|
||||
item: BaseItem,
|
||||
@@ -164,9 +159,12 @@ fun PosterCard(
|
||||
else -> 7
|
||||
}
|
||||
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 102.dp, 218.dp)
|
||||
val model = remember(item, showProgress, showSecondaryMetadata, showWatchedEpisodeCount) {
|
||||
item.toMediaCardUiModel(showProgress, showSecondaryMetadata, showWatchedEpisodeCount, showMediaTypeIcon = false)
|
||||
}
|
||||
MediaCard(
|
||||
item, width, 2f / 3f, preferPrimary = true, showProgress = showProgress,
|
||||
showSecondaryMetadata, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
||||
model, item, width, 2f / 3f, preferPrimary = true, showProgress,
|
||||
onFocused, onClick, onLongClick, modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,34 +186,33 @@ fun PosterGridCard(
|
||||
showWatchedEpisodeCount: Boolean = false,
|
||||
showMediaTypeIcon: Boolean = false,
|
||||
) {
|
||||
val model = remember(item, showWatchedEpisodeCount, showMediaTypeIcon) {
|
||||
item.toMediaCardUiModel(
|
||||
showProgress = false,
|
||||
showSecondaryMetadata = true,
|
||||
showWatchedEpisodeCount,
|
||||
showMediaTypeIcon,
|
||||
)
|
||||
}
|
||||
MediaCard(
|
||||
item, width, 2f / 3f, preferPrimary = true, showProgress = false,
|
||||
showSecondaryMetadata = true, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
||||
showMediaTypeIcon = showMediaTypeIcon,
|
||||
model, item, width, 2f / 3f, preferPrimary = true, showProgress = false,
|
||||
onFocused, onClick, onLongClick, modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the artwork URL and scale for [item] at [widthPx], preferring the primary
|
||||
* (poster) image or the backdrop depending on [preferPrimary] and falling back to whichever
|
||||
* of the two the item actually has.
|
||||
*/
|
||||
@Composable
|
||||
|
||||
internal fun MediaCard(
|
||||
private fun rememberMediaCardArtwork(
|
||||
item: BaseItem,
|
||||
width: Dp,
|
||||
aspectRatio: Float,
|
||||
widthPx: Int,
|
||||
preferPrimary: Boolean,
|
||||
showProgress: Boolean,
|
||||
showSecondaryMetadata: Boolean,
|
||||
showWatchedEpisodeCount: Boolean,
|
||||
onFocused: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier,
|
||||
showMediaTypeIcon: Boolean = false,
|
||||
) {
|
||||
): Pair<String, ContentScale>? {
|
||||
val repo = ServiceLocator.repository
|
||||
val density = LocalDensity.current
|
||||
val widthPx = with(density) { width.roundToPx() }.coerceIn(180, 720)
|
||||
val heightPx = (widthPx / aspectRatio).toInt().coerceAtLeast(1)
|
||||
val image = remember(item.id, widthPx, preferPrimary) {
|
||||
return remember(item.id, widthPx, preferPrimary) {
|
||||
if (preferPrimary) {
|
||||
repo.primaryUrl(item, widthPx)?.let { it to ContentScale.Fit }
|
||||
?: repo.backdropUrl(item, widthPx)?.let { it to ContentScale.Crop }
|
||||
@@ -224,12 +221,28 @@ internal fun MediaCard(
|
||||
?: repo.primaryUrl(item, widthPx)?.let { it to ContentScale.Fit }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MediaCard(
|
||||
model: MediaCardUiModel,
|
||||
item: BaseItem,
|
||||
width: Dp,
|
||||
aspectRatio: Float,
|
||||
preferPrimary: Boolean,
|
||||
showProgress: Boolean,
|
||||
onFocused: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val widthPx = with(density) { width.roundToPx() }.coerceIn(180, 720)
|
||||
val heightPx = (widthPx / aspectRatio).toInt().coerceAtLeast(1)
|
||||
val image = rememberMediaCardArtwork(item, widthPx, preferPrimary)
|
||||
val imageUrl = image?.first
|
||||
val position = item.userData?.playbackPositionTicks ?: 0L
|
||||
val runtime = item.runTimeTicks ?: 0L
|
||||
val progress = if (runtime > 0L) (position.toFloat() / runtime).coerceIn(0f, 1f) else 0f
|
||||
var failed by remember(item.id, imageUrl) { mutableStateOf(false) }
|
||||
var loading by remember(item.id, imageUrl) { mutableStateOf(imageUrl != null) }
|
||||
var failed by remember(model.id, imageUrl) { mutableStateOf(false) }
|
||||
var loading by remember(model.id, imageUrl) { mutableStateOf(imageUrl != null) }
|
||||
val context = LocalContext.current
|
||||
val imageRequest = remember(imageUrl, widthPx, heightPx, context) {
|
||||
imageUrl?.let {
|
||||
@@ -245,7 +258,7 @@ internal fun MediaCard(
|
||||
onFocused = onFocused,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
contentDescription = cardDescription(item, progress, showWatchedEpisodeCount, showMediaTypeIcon),
|
||||
contentDescription = model.contentDescription,
|
||||
modifier = modifier.width(width),
|
||||
) { focused ->
|
||||
Column {
|
||||
@@ -298,27 +311,27 @@ internal fun MediaCard(
|
||||
modifier = Modifier.size(30.dp),
|
||||
)
|
||||
}
|
||||
if (showProgress && progress > 0f) {
|
||||
if (showProgress && model.progress > 0f) {
|
||||
Box(
|
||||
Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(5.dp)
|
||||
.background(Color.Black.copy(alpha = 0.65f)),
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth(progress).height(5.dp).background(EmbyGreen))
|
||||
Box(Modifier.fillMaxWidth(model.progress).height(5.dp).background(EmbyGreen))
|
||||
}
|
||||
}
|
||||
if (item.userData?.played == true || item.isFavorite) {
|
||||
if (model.played || model.favourite) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (item.userData?.played == true) {
|
||||
if (model.played) {
|
||||
MediaStatusIcon(
|
||||
icon = MembyIcon.CheckCircle.mark,
|
||||
description = "Watched",
|
||||
tint = EmbyGreen,
|
||||
)
|
||||
}
|
||||
if (item.isFavorite) {
|
||||
if (model.favourite) {
|
||||
MediaStatusIcon(
|
||||
icon = MembyIcon.Favourite.mark,
|
||||
description = "Favourite",
|
||||
@@ -327,10 +340,10 @@ internal fun MediaCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.isSchedule) {
|
||||
scheduleStatusBadgeLabel(item.membyAvailability.orEmpty())?.let {
|
||||
if (model.isSchedule) {
|
||||
model.scheduleStatusLabel?.let {
|
||||
ScheduleStatusBadge(
|
||||
status = item.membyAvailability.orEmpty(),
|
||||
status = model.scheduleAvailability,
|
||||
label = it,
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
||||
)
|
||||
@@ -338,18 +351,17 @@ internal fun MediaCard(
|
||||
// Bottom-left, because the two top corners already carry the air day and
|
||||
// this episode's availability, and the progress bar owns the bottom edge
|
||||
// only on cards that can be resumed — which a schedule card cannot.
|
||||
lifecycleBadgeLabel(item)?.let {
|
||||
model.lifecycleLabel?.let {
|
||||
LifecycleBadge(
|
||||
status = item.membyLifecycle.orEmpty(),
|
||||
status = model.lifecycleStatus,
|
||||
label = it,
|
||||
modifier = Modifier.align(Alignment.BottomStart).padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
val airingLabel = airingBadgeLabel(item)
|
||||
if (airingLabel != null) {
|
||||
if (model.airingLabel != null) {
|
||||
MediaBadge(
|
||||
airingLabel,
|
||||
model.airingLabel,
|
||||
modifier = Modifier.align(Alignment.TopStart).padding(8.dp),
|
||||
)
|
||||
}
|
||||
@@ -358,7 +370,7 @@ internal fun MediaCard(
|
||||
}
|
||||
}
|
||||
Text(
|
||||
item.name,
|
||||
model.name,
|
||||
color = if (focused) Color.White else MembyOnSurface,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
||||
@@ -366,18 +378,17 @@ internal fun MediaCard(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
||||
)
|
||||
if (showSecondaryMetadata) {
|
||||
if (model.showSecondaryMetadata) {
|
||||
// The media type sits *in* the metadata line rather than over the poster:
|
||||
// it is informational, like the year and the runtime beside it, and the
|
||||
// artwork is the one thing on the card worth looking at. The row is
|
||||
// vertically centred and the mark is smaller than the line it sits on, so
|
||||
// it cannot make the card taller than one without it.
|
||||
val typeMark = if (showMediaTypeIcon) mediaTypeMark(item) else null
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
||||
) {
|
||||
typeMark?.let { (icon, _) ->
|
||||
model.typeMark?.let { (icon, _) ->
|
||||
Icon(
|
||||
icon,
|
||||
// The card's own description already names the type; a second
|
||||
@@ -389,7 +400,7 @@ internal fun MediaCard(
|
||||
Spacer(Modifier.width(5.dp))
|
||||
}
|
||||
Text(
|
||||
cardSubtitle(item, showProgress, position, showWatchedEpisodeCount),
|
||||
model.subtitle,
|
||||
color = QuietText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
|
||||
@@ -78,6 +78,7 @@ import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -134,9 +135,11 @@ private val EmbyGreen: Color get() = MembyAccent
|
||||
private val RailSurface: Color get() = MembySurface.copy(alpha = 0.95f)
|
||||
private val MutedText: Color get() = MembyMutedText
|
||||
private val QuietText: Color get() = MembyQuietText
|
||||
internal val TvRailCollapsedWidth = 54.dp
|
||||
internal val TvRailCollapsedWidth = 104.dp
|
||||
internal val TvRailExpandedWidth = 184.dp
|
||||
internal val HomeContentHorizontalInset = 48.dp
|
||||
private val TvRailCollapsedItemWidth = 72.dp
|
||||
private val TvRailHomeFootprint = 54.dp
|
||||
|
||||
/**
|
||||
* The rail's order, which is this declaration order — see [navigationRailItems], the only
|
||||
@@ -230,16 +233,19 @@ fun TvNavigationRail(
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(TvRailCollapsedWidth)
|
||||
// Home content begins after this slim footprint, then applies its own 48dp
|
||||
// reading inset. Together they meet the 104dp rail edge without preserving
|
||||
// a second, invisible gutter from the old sidebar layout.
|
||||
.width(TvRailHomeFootprint)
|
||||
.fillMaxHeight()
|
||||
.zIndex(8f),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
// The parent deliberately reports only the collapsed footprint to the
|
||||
// home Row. requiredWidth lets the focused surface draw outward without
|
||||
// remeasuring gallery cards or clipping their labels to 54dp.
|
||||
// Only width may escape the collapsed 54dp footprint. Unbinding height
|
||||
// The parent deliberately reports only the Home footprint to the Row.
|
||||
// The unbounded width lets the collapsed and focused surfaces draw outward
|
||||
// without remeasuring gallery cards or clipping their labels to 104dp.
|
||||
// Only width may escape the collapsed 104dp footprint. Unbinding height
|
||||
// here would make fillMaxHeight lose the screen constraint and stop the
|
||||
// rail surface at its final child (the version label).
|
||||
.wrapContentWidth(Alignment.Start, unbounded = true)
|
||||
@@ -264,12 +270,16 @@ fun TvNavigationRail(
|
||||
}
|
||||
.focusGroup()
|
||||
.padding(horizontal = 5.dp, vertical = 15.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(42.dp).padding(horizontal = 6.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(42.dp).padding(horizontal = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = if (expanded) {
|
||||
Arrangement.spacedBy(12.dp)
|
||||
} else {
|
||||
Arrangement.Center
|
||||
},
|
||||
) {
|
||||
// Memby's own icon rather than Emby's mark: the rail is Memby's chrome, and
|
||||
// the one place the app names itself should not be somebody else's logo.
|
||||
@@ -382,7 +392,10 @@ fun TvNavigationRail(
|
||||
fontSize = if (expanded) 10.sp else 8.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(horizontal = if (expanded) 8.dp else 4.dp),
|
||||
textAlign = if (expanded) TextAlign.Start else TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = if (expanded) 8.dp else 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -856,7 +869,10 @@ fun ExpandableNavigationItem(
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (expanded) Modifier.fillMaxWidth()
|
||||
else Modifier.width(TvRailCollapsedItemWidth),
|
||||
)
|
||||
.height(44.dp)
|
||||
.onFocusChanged {
|
||||
focused = it.isFocused
|
||||
@@ -874,9 +890,13 @@ fun ExpandableNavigationItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp),
|
||||
.padding(horizontal = if (expanded) 8.dp else 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = if (expanded) {
|
||||
Arrangement.spacedBy(12.dp)
|
||||
} else {
|
||||
Arrangement.Center
|
||||
},
|
||||
) {
|
||||
Box(Modifier.width(28.dp), contentAlignment = Alignment.Center) {
|
||||
if (avatarInitials != null) {
|
||||
@@ -909,7 +929,7 @@ fun ExpandableNavigationItem(
|
||||
)
|
||||
}
|
||||
// Over the icon rather than after the label: the rail spends most of its life
|
||||
// collapsed to 54dp, where there is no label to sit beside.
|
||||
// collapsed, where there is no label to sit beside.
|
||||
if (badge != null) {
|
||||
Text(
|
||||
badge,
|
||||
|
||||
@@ -2,39 +2,24 @@ package com.ponzischeme89.memby.ui
|
||||
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -42,80 +27,35 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.Text
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ponzischeme89.memby.ServiceLocator
|
||||
import com.ponzischeme89.memby.R
|
||||
import com.ponzischeme89.memby.BuildConfig
|
||||
import com.ponzischeme89.memby.data.EmbyProfile
|
||||
import com.ponzischeme89.memby.data.model.BaseItem
|
||||
import com.ponzischeme89.memby.data.model.MembyViewer
|
||||
import com.ponzischeme89.memby.ui.alerts.alertBadgeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.UHD_MIN_WIDTH
|
||||
import com.ponzischeme89.memby.ui.detail.channelLabel
|
||||
import com.ponzischeme89.memby.ui.detail.dynamicRangeLabel
|
||||
import com.ponzischeme89.memby.ui.detail.formatRuntime
|
||||
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
|
||||
import com.ponzischeme89.memby.ui.theme.MembyCardCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyChipCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurfaceRaised
|
||||
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||
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.ValueSeparator
|
||||
import com.ponzischeme89.memby.data.remoteconfig.BundledRemoteConfig
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
|
||||
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
|
||||
import java.util.Locale
|
||||
import com.ponzischeme89.memby.ui.components.media.ContinueWatchingCard
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.ponzischeme89.memby.ui
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NavigationRailTest {
|
||||
@Test
|
||||
fun `collapsed rail stays within the slim TV target`() {
|
||||
assertTrue(TvRailCollapsedWidth in 96.dp..112.dp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `user switcher is pinned directly above home`() {
|
||||
val items = navigationRailItems(calendarEnabled = false)
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.ponzischeme89.memby.ui.TvRailCollapsedWidth
|
||||
import com.ponzischeme89.memby.ui.theme.MembyCardCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyPalette
|
||||
@@ -55,9 +56,9 @@ import org.robolectric.annotation.GraphicsMode
|
||||
* metres, and getting it wrong in either direction (invisible, or a second black column
|
||||
* that makes the screen look like two applications side by side) looks fine in code.
|
||||
*
|
||||
* The launcher rail is a stand-in of its real 54dp collapsed width rather than the real
|
||||
* The launcher rail is a stand-in of its real collapsed width rather than the real
|
||||
* component, which would want a service locator; what is being judged is the boundary,
|
||||
* and 54dp of near-black is exactly what sits there.
|
||||
* and the launcher's near-black strip is exactly what sits there.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@@ -113,8 +114,8 @@ class GenreRailScreenshotTest {
|
||||
val categories = genreCategoryTabs(ALL_MEDIA_ITEM_TYPE)
|
||||
compose.setContent {
|
||||
Row(Modifier.fillMaxSize().background(MembySurface)) {
|
||||
// The launcher's own rail, at its real collapsed footprint.
|
||||
Box(Modifier.width(54.dp).fillMaxSize().background(MembySurface))
|
||||
// The launcher's own rail, at its real collapsed surface width.
|
||||
Box(Modifier.width(TvRailCollapsedWidth).fillMaxSize().background(MembySurface))
|
||||
GenreRail(
|
||||
categories = categories,
|
||||
activeCategoryId = active,
|
||||
|
||||
Reference in New Issue
Block a user