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
|
// 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.
|
// source tree. Local builds keep using the checked-in default.
|
||||||
val defaultVersionName = "0.2.94"
|
val defaultVersionName = "0.2.95"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.ponzischeme89.memby.ui
|
|||||||
|
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||||
import com.ponzischeme89.memby.ui.theme.mark
|
import com.ponzischeme89.memby.ui.theme.mark
|
||||||
import com.ponzischeme89.memby.data.decodeLineList
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ponzischeme89.memby.ServiceLocator
|
import com.ponzischeme89.memby.ServiceLocator
|
||||||
import com.ponzischeme89.memby.data.Settings
|
import com.ponzischeme89.memby.data.Settings
|
||||||
|
import com.ponzischeme89.memby.data.decodeLineList
|
||||||
import com.ponzischeme89.memby.data.friendlyEmbyError
|
import com.ponzischeme89.memby.data.friendlyEmbyError
|
||||||
import com.ponzischeme89.memby.data.analytics.PlaybackJourney
|
import com.ponzischeme89.memby.data.analytics.PlaybackJourney
|
||||||
import com.ponzischeme89.memby.data.analytics.playbackEntryPointFor
|
import com.ponzischeme89.memby.data.analytics.playbackEntryPointFor
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ponzischeme89.memby.ui.alerts
|
package com.ponzischeme89.memby.ui.alerts
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||||
import com.ponzischeme89.memby.ui.theme.mark
|
import com.ponzischeme89.memby.ui.theme.mark
|
||||||
import android.provider.Settings as AndroidSettings
|
import android.provider.Settings as AndroidSettings
|
||||||
@@ -117,7 +118,7 @@ fun MyAlertsPage(
|
|||||||
onDismiss: (UserNotification) -> Unit,
|
onDismiss: (UserNotification) -> Unit,
|
||||||
onDismissAll: (List<UserNotification>) -> Unit,
|
onDismissAll: (List<UserNotification>) -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var tab by remember { mutableStateOf(AlertsTab.INBOX) }
|
var tab by remember { mutableStateOf(AlertsTab.INBOX) }
|
||||||
var page by remember { mutableStateOf(0) }
|
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.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.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.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.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.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.BaseItem
|
||||||
import com.ponzischeme89.memby.data.model.MembyViewer
|
import com.ponzischeme89.memby.ui.ResumableMediaCard
|
||||||
import com.ponzischeme89.memby.ui.alerts.alertBadgeLabel
|
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
|
||||||
import com.ponzischeme89.memby.ui.detail.UHD_MIN_WIDTH
|
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContinueWatchingCard(
|
fun ContinueWatchingCard(
|
||||||
|
|||||||
@@ -138,9 +138,17 @@ fun LandscapeCard(
|
|||||||
else -> 4
|
else -> 4
|
||||||
}
|
}
|
||||||
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 164.dp, 360.dp)
|
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 164.dp, 360.dp)
|
||||||
|
val model = remember(item, showSecondaryMetadata, showWatchedEpisodeCount) {
|
||||||
|
item.toMediaCardUiModel(
|
||||||
|
showProgress = false,
|
||||||
|
showSecondaryMetadata,
|
||||||
|
showWatchedEpisodeCount,
|
||||||
|
showMediaTypeIcon = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
MediaCard(
|
MediaCard(
|
||||||
item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
model, item, width, 16f / 9f, preferPrimary = false, showProgress = false,
|
||||||
showSecondaryMetadata, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
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.MembyIcon
|
||||||
import com.ponzischeme89.memby.ui.theme.mark
|
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.background
|
||||||
import androidx.compose.foundation.border
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.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.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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.compose.ui.zIndex
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.ponzischeme89.memby.ServiceLocator
|
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.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.detail.formatRuntime
|
||||||
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
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.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.MembyMutedText
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
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.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.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
|
// 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,
|
// (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
|
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
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
@@ -164,9 +159,12 @@ fun PosterCard(
|
|||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 102.dp, 218.dp)
|
val width = responsiveRowCardWidth(availableWidth, cardsAcross, 102.dp, 218.dp)
|
||||||
|
val model = remember(item, showProgress, showSecondaryMetadata, showWatchedEpisodeCount) {
|
||||||
|
item.toMediaCardUiModel(showProgress, showSecondaryMetadata, showWatchedEpisodeCount, showMediaTypeIcon = false)
|
||||||
|
}
|
||||||
MediaCard(
|
MediaCard(
|
||||||
item, width, 2f / 3f, preferPrimary = true, showProgress = showProgress,
|
model, item, width, 2f / 3f, preferPrimary = true, showProgress,
|
||||||
showSecondaryMetadata, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
onFocused, onClick, onLongClick, modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,34 +186,33 @@ fun PosterGridCard(
|
|||||||
showWatchedEpisodeCount: Boolean = false,
|
showWatchedEpisodeCount: Boolean = false,
|
||||||
showMediaTypeIcon: Boolean = false,
|
showMediaTypeIcon: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val model = remember(item, showWatchedEpisodeCount, showMediaTypeIcon) {
|
||||||
|
item.toMediaCardUiModel(
|
||||||
|
showProgress = false,
|
||||||
|
showSecondaryMetadata = true,
|
||||||
|
showWatchedEpisodeCount,
|
||||||
|
showMediaTypeIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
MediaCard(
|
MediaCard(
|
||||||
item, width, 2f / 3f, preferPrimary = true, showProgress = false,
|
model, item, width, 2f / 3f, preferPrimary = true, showProgress = false,
|
||||||
showSecondaryMetadata = true, showWatchedEpisodeCount, onFocused, onClick, onLongClick, modifier,
|
onFocused, onClick, onLongClick, modifier,
|
||||||
showMediaTypeIcon = showMediaTypeIcon,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
@Composable
|
||||||
|
private fun rememberMediaCardArtwork(
|
||||||
internal fun MediaCard(
|
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
width: Dp,
|
widthPx: Int,
|
||||||
aspectRatio: Float,
|
|
||||||
preferPrimary: Boolean,
|
preferPrimary: Boolean,
|
||||||
showProgress: Boolean,
|
): Pair<String, ContentScale>? {
|
||||||
showSecondaryMetadata: Boolean,
|
|
||||||
showWatchedEpisodeCount: Boolean,
|
|
||||||
onFocused: () -> Unit,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
modifier: Modifier,
|
|
||||||
showMediaTypeIcon: Boolean = false,
|
|
||||||
) {
|
|
||||||
val repo = ServiceLocator.repository
|
val repo = ServiceLocator.repository
|
||||||
val density = LocalDensity.current
|
return remember(item.id, widthPx, preferPrimary) {
|
||||||
val widthPx = with(density) { width.roundToPx() }.coerceIn(180, 720)
|
|
||||||
val heightPx = (widthPx / aspectRatio).toInt().coerceAtLeast(1)
|
|
||||||
val image = remember(item.id, widthPx, preferPrimary) {
|
|
||||||
if (preferPrimary) {
|
if (preferPrimary) {
|
||||||
repo.primaryUrl(item, widthPx)?.let { it to ContentScale.Fit }
|
repo.primaryUrl(item, widthPx)?.let { it to ContentScale.Fit }
|
||||||
?: repo.backdropUrl(item, widthPx)?.let { it to ContentScale.Crop }
|
?: 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 }
|
?: 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 imageUrl = image?.first
|
||||||
val position = item.userData?.playbackPositionTicks ?: 0L
|
var failed by remember(model.id, imageUrl) { mutableStateOf(false) }
|
||||||
val runtime = item.runTimeTicks ?: 0L
|
var loading by remember(model.id, imageUrl) { mutableStateOf(imageUrl != null) }
|
||||||
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) }
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val imageRequest = remember(imageUrl, widthPx, heightPx, context) {
|
val imageRequest = remember(imageUrl, widthPx, heightPx, context) {
|
||||||
imageUrl?.let {
|
imageUrl?.let {
|
||||||
@@ -245,7 +258,7 @@ internal fun MediaCard(
|
|||||||
onFocused = onFocused,
|
onFocused = onFocused,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
contentDescription = cardDescription(item, progress, showWatchedEpisodeCount, showMediaTypeIcon),
|
contentDescription = model.contentDescription,
|
||||||
modifier = modifier.width(width),
|
modifier = modifier.width(width),
|
||||||
) { focused ->
|
) { focused ->
|
||||||
Column {
|
Column {
|
||||||
@@ -298,27 +311,27 @@ internal fun MediaCard(
|
|||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showProgress && progress > 0f) {
|
if (showProgress && model.progress > 0f) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(5.dp)
|
Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(5.dp)
|
||||||
.background(Color.Black.copy(alpha = 0.65f)),
|
.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(
|
Column(
|
||||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
if (item.userData?.played == true) {
|
if (model.played) {
|
||||||
MediaStatusIcon(
|
MediaStatusIcon(
|
||||||
icon = MembyIcon.CheckCircle.mark,
|
icon = MembyIcon.CheckCircle.mark,
|
||||||
description = "Watched",
|
description = "Watched",
|
||||||
tint = EmbyGreen,
|
tint = EmbyGreen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (item.isFavorite) {
|
if (model.favourite) {
|
||||||
MediaStatusIcon(
|
MediaStatusIcon(
|
||||||
icon = MembyIcon.Favourite.mark,
|
icon = MembyIcon.Favourite.mark,
|
||||||
description = "Favourite",
|
description = "Favourite",
|
||||||
@@ -327,10 +340,10 @@ internal fun MediaCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.isSchedule) {
|
if (model.isSchedule) {
|
||||||
scheduleStatusBadgeLabel(item.membyAvailability.orEmpty())?.let {
|
model.scheduleStatusLabel?.let {
|
||||||
ScheduleStatusBadge(
|
ScheduleStatusBadge(
|
||||||
status = item.membyAvailability.orEmpty(),
|
status = model.scheduleAvailability,
|
||||||
label = it,
|
label = it,
|
||||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
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
|
// 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
|
// this episode's availability, and the progress bar owns the bottom edge
|
||||||
// only on cards that can be resumed — which a schedule card cannot.
|
// only on cards that can be resumed — which a schedule card cannot.
|
||||||
lifecycleBadgeLabel(item)?.let {
|
model.lifecycleLabel?.let {
|
||||||
LifecycleBadge(
|
LifecycleBadge(
|
||||||
status = item.membyLifecycle.orEmpty(),
|
status = model.lifecycleStatus,
|
||||||
label = it,
|
label = it,
|
||||||
modifier = Modifier.align(Alignment.BottomStart).padding(8.dp),
|
modifier = Modifier.align(Alignment.BottomStart).padding(8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val airingLabel = airingBadgeLabel(item)
|
if (model.airingLabel != null) {
|
||||||
if (airingLabel != null) {
|
|
||||||
MediaBadge(
|
MediaBadge(
|
||||||
airingLabel,
|
model.airingLabel,
|
||||||
modifier = Modifier.align(Alignment.TopStart).padding(8.dp),
|
modifier = Modifier.align(Alignment.TopStart).padding(8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -358,7 +370,7 @@ internal fun MediaCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
item.name,
|
model.name,
|
||||||
color = if (focused) Color.White else MembyOnSurface,
|
color = if (focused) Color.White else MembyOnSurface,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
||||||
@@ -366,18 +378,17 @@ internal fun MediaCard(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
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:
|
// 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
|
// 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
|
// 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
|
// vertically centred and the mark is smaller than the line it sits on, so
|
||||||
// it cannot make the card taller than one without it.
|
// it cannot make the card taller than one without it.
|
||||||
val typeMark = if (showMediaTypeIcon) mediaTypeMark(item) else null
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
typeMark?.let { (icon, _) ->
|
model.typeMark?.let { (icon, _) ->
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
// The card's own description already names the type; a second
|
// The card's own description already names the type; a second
|
||||||
@@ -389,7 +400,7 @@ internal fun MediaCard(
|
|||||||
Spacer(Modifier.width(5.dp))
|
Spacer(Modifier.width(5.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
cardSubtitle(item, showProgress, position, showWatchedEpisodeCount),
|
model.subtitle,
|
||||||
color = QuietText,
|
color = QuietText,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import androidx.compose.ui.semantics.contentDescription
|
|||||||
import androidx.compose.ui.semantics.onLongClick
|
import androidx.compose.ui.semantics.onLongClick
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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 RailSurface: Color get() = MembySurface.copy(alpha = 0.95f)
|
||||||
private val MutedText: Color get() = MembyMutedText
|
private val MutedText: Color get() = MembyMutedText
|
||||||
private val QuietText: Color get() = MembyQuietText
|
private val QuietText: Color get() = MembyQuietText
|
||||||
internal val TvRailCollapsedWidth = 54.dp
|
internal val TvRailCollapsedWidth = 104.dp
|
||||||
internal val TvRailExpandedWidth = 184.dp
|
internal val TvRailExpandedWidth = 184.dp
|
||||||
internal val HomeContentHorizontalInset = 48.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
|
* The rail's order, which is this declaration order — see [navigationRailItems], the only
|
||||||
@@ -230,16 +233,19 @@ fun TvNavigationRail(
|
|||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
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()
|
.fillMaxHeight()
|
||||||
.zIndex(8f),
|
.zIndex(8f),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// The parent deliberately reports only the collapsed footprint to the
|
// The parent deliberately reports only the Home footprint to the Row.
|
||||||
// home Row. requiredWidth lets the focused surface draw outward without
|
// The unbounded width lets the collapsed and focused surfaces draw outward
|
||||||
// remeasuring gallery cards or clipping their labels to 54dp.
|
// without remeasuring gallery cards or clipping their labels to 104dp.
|
||||||
// Only width may escape the collapsed 54dp footprint. Unbinding height
|
// Only width may escape the collapsed 104dp footprint. Unbinding height
|
||||||
// here would make fillMaxHeight lose the screen constraint and stop the
|
// here would make fillMaxHeight lose the screen constraint and stop the
|
||||||
// rail surface at its final child (the version label).
|
// rail surface at its final child (the version label).
|
||||||
.wrapContentWidth(Alignment.Start, unbounded = true)
|
.wrapContentWidth(Alignment.Start, unbounded = true)
|
||||||
@@ -264,12 +270,16 @@ fun TvNavigationRail(
|
|||||||
}
|
}
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
.padding(horizontal = 5.dp, vertical = 15.dp),
|
.padding(horizontal = 5.dp, vertical = 15.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.height(42.dp).padding(horizontal = 6.dp),
|
modifier = Modifier.fillMaxWidth().height(42.dp).padding(horizontal = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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
|
// 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.
|
// 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,
|
fontSize = if (expanded) 10.sp else 8.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
maxLines = 1,
|
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(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.then(
|
||||||
|
if (expanded) Modifier.fillMaxWidth()
|
||||||
|
else Modifier.width(TvRailCollapsedItemWidth),
|
||||||
|
)
|
||||||
.height(44.dp)
|
.height(44.dp)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
focused = it.isFocused
|
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,
|
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) {
|
Box(Modifier.width(28.dp), contentAlignment = Alignment.Center) {
|
||||||
if (avatarInitials != null) {
|
if (avatarInitials != null) {
|
||||||
@@ -909,7 +929,7 @@ fun ExpandableNavigationItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Over the icon rather than after the label: the rail spends most of its life
|
// 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) {
|
if (badge != null) {
|
||||||
Text(
|
Text(
|
||||||
badge,
|
badge,
|
||||||
|
|||||||
@@ -2,39 +2,24 @@ package com.ponzischeme89.memby.ui
|
|||||||
|
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||||
import com.ponzischeme89.memby.ui.theme.mark
|
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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -42,80 +27,35 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
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.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.type
|
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.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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.Text
|
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.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.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.MembyOnSurface
|
||||||
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.components.media.ContinueWatchingCard
|
||||||
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.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.ponzischeme89.memby.ui
|
package com.ponzischeme89.memby.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class NavigationRailTest {
|
class NavigationRailTest {
|
||||||
|
@Test
|
||||||
|
fun `collapsed rail stays within the slim TV target`() {
|
||||||
|
assertTrue(TvRailCollapsedWidth in 96.dp..112.dp)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `user switcher is pinned directly above home`() {
|
fun `user switcher is pinned directly above home`() {
|
||||||
val items = navigationRailItems(calendarEnabled = false)
|
val items = navigationRailItems(calendarEnabled = false)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.takahirom.roborazzi.captureRoboImage
|
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.MembyCardCorner
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyPalette
|
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
|
* 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.
|
* 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,
|
* 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)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
@@ -113,8 +114,8 @@ class GenreRailScreenshotTest {
|
|||||||
val categories = genreCategoryTabs(ALL_MEDIA_ITEM_TYPE)
|
val categories = genreCategoryTabs(ALL_MEDIA_ITEM_TYPE)
|
||||||
compose.setContent {
|
compose.setContent {
|
||||||
Row(Modifier.fillMaxSize().background(MembySurface)) {
|
Row(Modifier.fillMaxSize().background(MembySurface)) {
|
||||||
// The launcher's own rail, at its real collapsed footprint.
|
// The launcher's own rail, at its real collapsed surface width.
|
||||||
Box(Modifier.width(54.dp).fillMaxSize().background(MembySurface))
|
Box(Modifier.width(TvRailCollapsedWidth).fillMaxSize().background(MembySurface))
|
||||||
GenreRail(
|
GenreRail(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
activeCategoryId = active,
|
activeCategoryId = active,
|
||||||
|
|||||||
Reference in New Issue
Block a user