Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ data class MediaCardActions(
val onSetWatched: ((watched: Boolean) -> Unit)? = null,
val onToggleFavorite: ((favorite: Boolean) -> Unit)? = null,
val onToggleWatchlist: ((inWatchlist: Boolean) -> Unit)? = null,
val onRemoveFromContinueWatching: (() -> Unit)? = null,
/** Removes the card from its Home row; [removeFromHomeLabel] names the list it leaves. */
val onRemoveFromHome: (() -> Unit)? = null,
val removeFromHomeLabel: String = "Remove from Continue Watching",
) {
val isEmpty: Boolean
get() = onSetWatched == null &&
onToggleFavorite == null &&
onToggleWatchlist == null &&
onRemoveFromContinueWatching == null
onRemoveFromHome == null
}

/**
Expand Down Expand Up @@ -82,9 +84,9 @@ fun MediaCardContextMenu(
},
)
}
actions.onRemoveFromContinueWatching?.let { remove ->
actions.onRemoveFromHome?.let { remove ->
MenuRow(
text = "Remove from Continue Watching",
text = actions.removeFromHomeLabel,
icon = Icons.Default.Close,
onClick = {
remove()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
Expand All @@ -57,6 +60,7 @@ import org.prairieserver.prairie.common.cards.LocalEpisodeSpoilerPrefs
import org.prairieserver.prairie.common.ui.components.SpoilerImage
import org.prairieserver.prairie.common.ui.components.DefaultArtworkKind
import org.prairieserver.prairie.common.ui.components.ThumbhashImage
import org.prairieserver.prairie.model.catalog.EpisodeCardCaption
import org.prairieserver.prairie.model.catalog.EpisodeListItem
import org.prairieserver.prairie.model.catalog.hasMachineTranslatedOverview
import org.prairieserver.prairie.model.settings.EpisodeSpoilers
Expand All @@ -75,8 +79,6 @@ fun EpisodeList(
onEpisodeWatchedChange: ((String, Boolean) -> Unit)? = null,
highlightContentId: String? = null,
selectsCenteredEpisode: Boolean = false,
/** Expanded tablet/fold cards include their full editorial caption. */
showsEpisodeDetails: Boolean = false,
/** Expanded rails select on tap and never force the card into centre. */
tapToFocusEpisode: Boolean = false,
modifier: Modifier = Modifier,
Expand All @@ -89,6 +91,7 @@ fun EpisodeList(
val currentHighlightId by rememberUpdatedState(highlightContentId)
val currentOnSelect by rememberUpdatedState(onEpisodeDetailClick)
var wasScrolling by remember { mutableStateOf(false) }
val railHasCurrentEpisode = episodes.any { it.contentId == highlightContentId }

LaunchedEffect(episodes.map(EpisodeListItem::contentId), highlightContentId) {
val selectedIndex = episodes.indexOfFirst { it.contentId == highlightContentId }
Expand Down Expand Up @@ -147,8 +150,8 @@ fun EpisodeList(
{ watched -> change(episode.contentId, watched) }
},
isCurrent = episode.contentId == highlightContentId,
reservesNowViewing = railHasCurrentEpisode,
cardWidth = cardWidth,
showsEpisodeDetails = showsEpisodeDetails,
)
}
}
Expand All @@ -171,8 +174,9 @@ private fun EpisodeRailCard(
onSelect: () -> Unit,
onWatchedChange: ((Boolean) -> Unit)?,
isCurrent: Boolean,
/** The rail marks a current episode, so every card keeps room for the tag. */
reservesNowViewing: Boolean,
cardWidth: Dp,
showsEpisodeDetails: Boolean,
) {
var menuExpanded by remember { mutableStateOf(false) }
val isWatched = episode.userData?.played == true
Expand Down Expand Up @@ -272,74 +276,74 @@ private fun EpisodeRailCard(
}
}

Column(
modifier = if (showsEpisodeDetails) Modifier.height(132.dp) else Modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
// The iOS card's caption: NOW VIEWING, "S01E02 · Title", air date and
// runtime, then up to three lines of description. A LazyRow is as tall
// as its tallest visible card, so every card keeps the same lines, empty
// where there's nothing to show, and the rail doesn't change height as it
// scrolls. Accessibility font sizes lift the line limits, as on iOS.
val accessibilityText = LocalDensity.current.fontScale > 1.3f
val reservesLines = !accessibilityText
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (isCurrent || reservesNowViewing) {
Text(
text = "EPISODE ${episode.episodeNumber}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = Color.White.copy(alpha = 0.55f),
text = "NOW VIEWING",
fontSize = 9.sp,
fontWeight = FontWeight.Black,
letterSpacing = 0.8.sp,
color = Color.Black,
modifier = Modifier
.then(if (isCurrent) Modifier else Modifier.alpha(0f).clearAndSetSemantics {})
.clip(RoundedCornerShape(50))
.background(Color.White)
.padding(horizontal = 5.dp, vertical = 2.dp),
)
if (showsEpisodeDetails && visibleOverview != null &&
hasMachineTranslatedOverview(episode.machineTranslatedFields)
) {
DetailMachineTranslatedLabel(compact = true, iconSize = 11.dp)
}
if (isCurrent) {
Text(
text = "NOW VIEWING",
fontSize = 9.sp,
fontWeight = FontWeight.Black,
letterSpacing = 0.8.sp,
color = Color.Black,
modifier = Modifier
.clip(RoundedCornerShape(50))
.background(Color.White)
.padding(
horizontal = 5.dp,
vertical = if (showsEpisodeDetails) 1.dp else 2.dp,
),
)
}
}
Text(
text = episode.title?.trim()?.takeIf { it.isNotEmpty() }
?: "Episode ${episode.episodeNumber}",
text = EpisodeCardCaption.title(episode),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
color = DetailPrimaryText,
maxLines = 1,
maxLines = if (accessibilityText) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
)
if (showsEpisodeDetails) {
episodeMetadataLine(episode)?.let { metadata ->
Text(
text = metadata,
style = MaterialTheme.typography.labelSmall,
color = DetailSecondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
// Spoiler protection omits the text; the fixed-height column
// keeps the card the same size.
if (visibleOverview != null) {
Text(
text = visibleOverview,
style = MaterialTheme.typography.bodySmall,
color = DetailSecondaryText,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
val metadata = EpisodeCardCaption.metadata(
airDate = formatCardDate(episode.airDate) ?: episode.airDate,
runtimeMinutes = episode.runtime,
)
val showsTranslationLabel = visibleOverview != null &&
hasMachineTranslatedOverview(episode.machineTranslatedFields)
if (metadata != null || showsTranslationLabel || reservesLines) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
if (metadata != null || reservesLines) {
Text(
text = metadata.orEmpty(),
style = MaterialTheme.typography.labelSmall,
color = DetailSecondaryText,
maxLines = if (accessibilityText) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false),
)
}
if (showsTranslationLabel) {
DetailMachineTranslatedLabel(compact = true, iconSize = 11.dp)
}
}
}
// Spoiler protection leaves the description out; the title and
// metadata still identify the episode.
if (visibleOverview != null || reservesLines) {
Text(
text = visibleOverview.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = DetailSecondaryText,
minLines = if (accessibilityText) 1 else 3,
maxLines = if (accessibilityText) Int.MAX_VALUE else 3,
overflow = TextOverflow.Ellipsis,
)
}
}
PrairieDropdownMenu(
expanded = menuExpanded,
Expand All @@ -363,17 +367,9 @@ internal fun episodeProgressFraction(positionSeconds: Double?, durationSeconds:
return (position / duration).toFloat().coerceIn(0f, 1f)
}

private fun episodeMetadataLine(episode: EpisodeListItem): String? = buildList {
val airDate = formatCardDate(episode.airDate)
?: episode.airDate?.takeIf { it.isNotBlank() }
if (airDate != null) add(airDate)
if (episode.runtime > 0) add("${episode.runtime}m")
}.takeIf { it.isNotEmpty() }?.joinToString(" · ")

/** Static loading footprint matching the artwork and compact identity row. */
/** Static loading footprint matching the artwork and the card caption. */
@Composable
internal fun EpisodeListSkeleton(
showsEpisodeDetails: Boolean = false,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxWidth()) {
Expand All @@ -396,13 +392,11 @@ internal fun EpisodeListSkeleton(
.clip(RoundedCornerShape(8.dp))
.background(Color.White.copy(alpha = 0.09f + index * 0.01f)),
)
EpisodeSkeletonLine(width = 82.dp, height = 9.dp)
if (showsEpisodeDetails) {
EpisodeSkeletonLine(width = 150.dp, height = 14.dp)
EpisodeSkeletonLine(width = 112.dp, height = 10.dp)
EpisodeSkeletonLine(width = 224.dp, height = 10.dp)
EpisodeSkeletonLine(width = 196.dp, height = 10.dp)
}
EpisodeSkeletonLine(width = 150.dp, height = 14.dp)
EpisodeSkeletonLine(width = 112.dp, height = 10.dp)
EpisodeSkeletonLine(width = 224.dp, height = 10.dp)
EpisodeSkeletonLine(width = 224.dp, height = 10.dp)
EpisodeSkeletonLine(width = 196.dp, height = 10.dp)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ internal fun SeasonEpisodePager(
showsSeasonSelector: Boolean = true,
selectsCenteredEpisode: Boolean = false,
allowsSeasonPaging: Boolean = true,
/** Tablet/fold rail cards show title, date/runtime, and overview. */
showsEpisodeDetails: Boolean = false,
/** Tablet/fold rail selection follows taps rather than centre position. */
tapToFocusEpisode: Boolean = false,
) {
Expand All @@ -67,7 +65,6 @@ internal fun SeasonEpisodePager(
onEpisodeWatchedChange = onEpisodeWatchedChange,
highlightContentId = highlightContentId,
selectsCenteredEpisode = selectsCenteredEpisode,
showsEpisodeDetails = showsEpisodeDetails,
tapToFocusEpisode = tapToFocusEpisode,
)
return
Expand Down Expand Up @@ -167,7 +164,6 @@ internal fun SeasonEpisodePager(
onEpisodeWatchedChange = onEpisodeWatchedChange,
highlightContentId = highlightContentId,
selectsCenteredEpisode = selectsCenteredEpisode,
showsEpisodeDetails = showsEpisodeDetails,
tapToFocusEpisode = tapToFocusEpisode,
modifier = Modifier
.onSizeChanged { pageHeightsPx[page] = it.height }
Expand All @@ -191,16 +187,12 @@ private fun SeasonEpisodePage(
onEpisodeWatchedChange: ((String, Boolean) -> Unit)?,
highlightContentId: String?,
selectsCenteredEpisode: Boolean,
showsEpisodeDetails: Boolean,
tapToFocusEpisode: Boolean,
modifier: Modifier = Modifier,
) {
when {
isLoading -> {
EpisodeListSkeleton(
showsEpisodeDetails = showsEpisodeDetails,
modifier = modifier,
)
EpisodeListSkeleton(modifier = modifier)
}
episodes.isEmpty() -> {
Text(
Expand All @@ -218,7 +210,6 @@ private fun SeasonEpisodePage(
onEpisodeWatchedChange = onEpisodeWatchedChange,
highlightContentId = highlightContentId,
selectsCenteredEpisode = selectsCenteredEpisode,
showsEpisodeDetails = showsEpisodeDetails,
tapToFocusEpisode = tapToFocusEpisode,
modifier = modifier,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ fun SeriesDetailContent(
showsSeasonSelector = false,
selectsCenteredEpisode = !showsEpisodeDetails,
allowsSeasonPaging = false,
showsEpisodeDetails = showsEpisodeDetails,
tapToFocusEpisode = showsEpisodeDetails,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,11 +457,7 @@ fun HomeScreen(
onSetWatched = viewModel::setWatched,
onToggleFavorite = viewModel::toggleFavorite,
onToggleWatchlist = viewModel::toggleWatchlist,
onDismissContinueWatching = { item ->
item.progressUpdatedAt?.let { ts ->
viewModel.dismissContinueWatching(item.contentId, ts)
}
},
onRemoveFromHome = viewModel::dismissFromHome,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.prairieserver.prairie.android.ui.components.MediaCardActions
import org.prairieserver.prairie.android.ui.components.MediaRow
import org.prairieserver.prairie.model.section.ResolvedSection
import org.prairieserver.prairie.model.section.SectionItem
import org.prairieserver.prairie.viewmodel.HomeDismissal

/**
* Renders a single home screen section as a horizontal row of media cards.
Expand All @@ -32,7 +33,8 @@ fun HomeSectionRow(
onSetWatched: ((String, Boolean) -> Unit)? = null,
onToggleFavorite: ((String, Boolean) -> Unit)? = null,
onToggleWatchlist: ((String, Boolean) -> Unit)? = null,
onDismissContinueWatching: ((SectionItem) -> Unit)? = null,
/** Removes a Continue Watching or Next Up card; see [HomeDismissal]. */
onRemoveFromHome: ((SectionItem) -> Unit)? = null,
onCenteredContinueWatchingItemChanged: ((SectionItem?) -> Unit)? = null,
modifier: Modifier = Modifier,
) {
Expand All @@ -41,6 +43,7 @@ fun HomeSectionRow(
val isContinueWatching = section.sectionType == "continue_watching" ||
section.sectionType == "in_progress"
val showProgress = isContinueWatching
val offersRemoval = onRemoveFromHome != null && section.sectionType in HomeDismissal.sectionTypes

val useBackdrop = showProgress || section.sectionType == "next_up"
val cardStyle = if (useBackdrop) CardStyle.Backdrop else CardStyle.Poster
Expand All @@ -57,13 +60,13 @@ fun HomeSectionRow(
icon = if (isContinueWatching) Icons.Filled.PlayCircle else null,
modifier = modifier,
cardActions = { item ->
val dismissal = if (offersRemoval) HomeDismissal.of(item) else null
MediaCardActions(
onSetWatched = onSetWatched?.let { cb -> { watched -> cb(item.contentId, watched) } },
onToggleFavorite = onToggleFavorite?.let { cb -> { fav -> cb(item.contentId, fav) } },
onToggleWatchlist = onToggleWatchlist?.let { cb -> { wl -> cb(item.contentId, wl) } },
onRemoveFromContinueWatching = if (isContinueWatching && onDismissContinueWatching != null && item.progressUpdatedAt != null) {
{ onDismissContinueWatching(item) }
} else null,
onRemoveFromHome = dismissal?.let { { onRemoveFromHome?.invoke(item) } },
removeFromHomeLabel = dismissal?.menuLabel.orEmpty(),
)
},
onCenteredItemChanged = if (isContinueWatching) onCenteredContinueWatchingItemChanged else null,
Expand Down
Loading
Loading