mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 19:12:54 +00:00
meta hero layout changes
This commit is contained in:
parent
3a54098907
commit
b8f7ebdc4b
3 changed files with 417 additions and 359 deletions
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
|
|
@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
|
|
@ -46,6 +48,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
|
|
@ -306,303 +309,313 @@ fun MetaDetailsScreen(
|
|||
label = "detail_floating_header_progress",
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
DetailHero(
|
||||
meta = meta,
|
||||
scrollOffset = scrollState.value,
|
||||
onHeightChanged = { heroHeightPx = it },
|
||||
)
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
val isTablet = maxWidth >= 720.dp
|
||||
val contentHorizontalPadding = if (isTablet) 32.dp else 18.dp
|
||||
val contentMaxWidth = detailTabletContentMaxWidth(maxWidth, isTablet)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
DetailActionButtons(
|
||||
playLabel = playButtonLabel,
|
||||
saveLabel = if (isSaved) "Saved" else "Save",
|
||||
isSaved = isSaved,
|
||||
onPlayClick = {
|
||||
when {
|
||||
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
seriesAction.videoId,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
seriesAction.seasonNumber,
|
||||
seriesAction.episodeNumber,
|
||||
seriesAction.episodeTitle,
|
||||
seriesAction.episodeThumbnail,
|
||||
seriesPauseDescription,
|
||||
seriesAction.resumePositionMs,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
meta.id,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
meta.description,
|
||||
movieProgress?.lastPositionMs,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSaveClick = toggleSaved,
|
||||
)
|
||||
|
||||
DetailMetaInfo(meta = meta)
|
||||
|
||||
if (hasEpisodes && hasProductionSection) {
|
||||
DetailProductionSection(meta = meta)
|
||||
}
|
||||
|
||||
DetailCastSection(cast = meta.cast)
|
||||
|
||||
if (hasTrailersSection) {
|
||||
DetailTrailersSection(
|
||||
trailers = meta.trailers,
|
||||
onTrailerClick = resolveTrailer,
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasProductionSection) {
|
||||
DetailProductionSection(meta = meta)
|
||||
}
|
||||
|
||||
DetailSeriesContent(
|
||||
DetailHero(
|
||||
meta = meta,
|
||||
progressByVideoId = watchProgressUiState.byVideoId,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onEpisodeClick = { video ->
|
||||
val season = video.season
|
||||
val episode = video.episode
|
||||
val playbackVideoId = buildPlaybackVideoId(
|
||||
parentMetaId = meta.id,
|
||||
seasonNumber = season,
|
||||
episodeNumber = episode,
|
||||
fallbackVideoId = video.id,
|
||||
)
|
||||
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
|
||||
?.takeUnless { it.isCompleted }
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
playbackVideoId,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
season,
|
||||
episode,
|
||||
video.title,
|
||||
video.thumbnail,
|
||||
video.overview,
|
||||
savedProgress?.lastPositionMs,
|
||||
)
|
||||
},
|
||||
onEpisodeLongPress = { video ->
|
||||
selectedEpisodeForActions = video
|
||||
},
|
||||
isTablet = isTablet,
|
||||
contentMaxWidth = contentMaxWidth,
|
||||
scrollOffset = scrollState.value,
|
||||
onHeightChanged = { heroHeightPx = it },
|
||||
)
|
||||
|
||||
if (hasEpisodes && hasAdditionalInfoSection) {
|
||||
DetailAdditionalInfoSection(meta = meta)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = contentHorizontalPadding)
|
||||
.widthIn(max = if (isTablet) contentMaxWidth else Dp.Unspecified),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
DetailActionButtons(
|
||||
playLabel = playButtonLabel,
|
||||
saveLabel = if (isSaved) "Saved" else "Save",
|
||||
isSaved = isSaved,
|
||||
isTablet = isTablet,
|
||||
onPlayClick = {
|
||||
when {
|
||||
(meta.type == "series" || hasEpisodes) && seriesAction != null -> {
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
seriesAction.videoId,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
seriesAction.seasonNumber,
|
||||
seriesAction.episodeNumber,
|
||||
seriesAction.episodeTitle,
|
||||
seriesAction.episodeThumbnail,
|
||||
seriesPauseDescription,
|
||||
seriesAction.resumePositionMs,
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasAdditionalInfoSection) {
|
||||
DetailAdditionalInfoSection(meta = meta)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasCollectionSection) {
|
||||
DetailPosterRailSection(
|
||||
title = meta.collectionName.orEmpty(),
|
||||
items = meta.collectionItems,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onPosterClick = onOpenMeta,
|
||||
else -> {
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
meta.id,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
meta.description,
|
||||
movieProgress?.lastPositionMs,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSaveClick = toggleSaved,
|
||||
)
|
||||
}
|
||||
|
||||
if (hasMoreLikeThisSection) {
|
||||
DetailPosterRailSection(
|
||||
title = "More Like This",
|
||||
items = meta.moreLikeThis,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onPosterClick = onOpenMeta,
|
||||
)
|
||||
}
|
||||
DetailMetaInfo(meta = meta)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
|
||||
}
|
||||
}
|
||||
|
||||
if (headerProgress <= 0.05f) {
|
||||
NuvioBackButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 8.dp,
|
||||
),
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f),
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
|
||||
DetailFloatingHeader(
|
||||
meta = meta,
|
||||
isSaved = isSaved,
|
||||
progress = headerProgress,
|
||||
onBack = onBack,
|
||||
onToggleSaved = toggleSaved,
|
||||
)
|
||||
|
||||
selectedEpisodeForActions?.let { selectedEpisode ->
|
||||
val isSelectedEpisodeWatched = remember(meta, selectedEpisode, watchedUiState.watchedKeys) {
|
||||
WatchingState.isEpisodeWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episode = selectedEpisode,
|
||||
)
|
||||
}
|
||||
val previousEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
|
||||
meta.previousReleasedEpisodesBefore(
|
||||
target = selectedEpisode,
|
||||
todayIsoDate = todayIsoDate,
|
||||
)
|
||||
}
|
||||
val seasonEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
|
||||
meta.releasedEpisodesForSeason(
|
||||
seasonNumber = selectedEpisode.season,
|
||||
todayIsoDate = todayIsoDate,
|
||||
)
|
||||
}
|
||||
val arePreviousEpisodesWatched = remember(previousEpisodes, watchedUiState.watchedKeys) {
|
||||
WatchingState.areEpisodesWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episodes = previousEpisodes,
|
||||
)
|
||||
}
|
||||
val isSeasonWatched = remember(seasonEpisodes, watchedUiState.watchedKeys) {
|
||||
WatchingState.areEpisodesWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episodes = seasonEpisodes,
|
||||
)
|
||||
}
|
||||
EpisodeWatchedActionSheet(
|
||||
episode = selectedEpisode,
|
||||
seasonLabel = selectedEpisode.season?.let { "Season $it" } ?: "Specials",
|
||||
isEpisodeWatched = isSelectedEpisodeWatched,
|
||||
canMarkPreviousEpisodes = previousEpisodes.isNotEmpty(),
|
||||
arePreviousEpisodesWatched = arePreviousEpisodesWatched,
|
||||
isSeasonWatched = isSeasonWatched,
|
||||
onDismiss = { selectedEpisodeForActions = null },
|
||||
onToggleWatched = {
|
||||
WatchingActions.toggleEpisodeWatched(
|
||||
meta = meta,
|
||||
episode = selectedEpisode,
|
||||
isCurrentlyWatched = isSelectedEpisodeWatched,
|
||||
)
|
||||
},
|
||||
onTogglePreviousWatched = {
|
||||
WatchingActions.togglePreviousEpisodesWatched(
|
||||
meta = meta,
|
||||
episodes = previousEpisodes,
|
||||
areCurrentlyWatched = arePreviousEpisodesWatched,
|
||||
)
|
||||
},
|
||||
onToggleSeasonWatched = {
|
||||
WatchingActions.toggleSeasonWatched(
|
||||
meta = meta,
|
||||
episodes = seasonEpisodes,
|
||||
areCurrentlyWatched = isSeasonWatched,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
TrailerPlayerPopup(
|
||||
visible = selectedTrailer != null,
|
||||
trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(),
|
||||
trailerType = selectedTrailer?.type.orEmpty(),
|
||||
contentTitle = meta.name,
|
||||
playbackSource = trailerPlaybackSource,
|
||||
isLoading = trailerLoading,
|
||||
errorMessage = trailerErrorMessage,
|
||||
onDismiss = {
|
||||
trailerRequestToken += 1
|
||||
trailerLoading = false
|
||||
trailerPlaybackSource = null
|
||||
trailerErrorMessage = null
|
||||
selectedTrailer = null
|
||||
},
|
||||
onRetry = selectedTrailer?.let { trailer ->
|
||||
{ resolveTrailer(trailer) }
|
||||
},
|
||||
)
|
||||
|
||||
TraktListPickerDialog(
|
||||
visible = showLibraryListPicker,
|
||||
title = meta.name,
|
||||
tabs = pickerTabs,
|
||||
membership = pickerMembership,
|
||||
isPending = pickerPending,
|
||||
errorMessage = pickerError,
|
||||
onToggle = { listKey ->
|
||||
pickerMembership = pickerMembership.toMutableMap().apply {
|
||||
this[listKey] = !(this[listKey] == true)
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
if (!pickerPending) {
|
||||
showLibraryListPicker = false
|
||||
}
|
||||
},
|
||||
onSave = {
|
||||
detailsScope.launch {
|
||||
pickerPending = true
|
||||
pickerError = null
|
||||
runCatching {
|
||||
LibraryRepository.applyMembershipChanges(
|
||||
item = meta.toLibraryItem(savedAtEpochMs = 0L),
|
||||
desiredMembership = pickerMembership,
|
||||
)
|
||||
}.onSuccess {
|
||||
showLibraryListPicker = false
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: "Failed to update Trakt lists"
|
||||
if (hasEpisodes && hasProductionSection) {
|
||||
DetailProductionSection(meta = meta)
|
||||
}
|
||||
pickerPending = false
|
||||
|
||||
DetailCastSection(cast = meta.cast)
|
||||
|
||||
if (hasTrailersSection) {
|
||||
DetailTrailersSection(
|
||||
trailers = meta.trailers,
|
||||
onTrailerClick = resolveTrailer,
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasProductionSection) {
|
||||
DetailProductionSection(meta = meta)
|
||||
}
|
||||
|
||||
DetailSeriesContent(
|
||||
meta = meta,
|
||||
progressByVideoId = watchProgressUiState.byVideoId,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onEpisodeClick = { video ->
|
||||
val season = video.season
|
||||
val episode = video.episode
|
||||
val playbackVideoId = buildPlaybackVideoId(
|
||||
parentMetaId = meta.id,
|
||||
seasonNumber = season,
|
||||
episodeNumber = episode,
|
||||
fallbackVideoId = video.id,
|
||||
)
|
||||
val savedProgress = watchProgressUiState.byVideoId[playbackVideoId]
|
||||
?.takeUnless { it.isCompleted }
|
||||
onPlay?.invoke(
|
||||
meta.type,
|
||||
playbackVideoId,
|
||||
meta.id,
|
||||
meta.type,
|
||||
meta.name,
|
||||
meta.logo,
|
||||
meta.poster,
|
||||
meta.background,
|
||||
season,
|
||||
episode,
|
||||
video.title,
|
||||
video.thumbnail,
|
||||
video.overview,
|
||||
savedProgress?.lastPositionMs,
|
||||
)
|
||||
},
|
||||
onEpisodeLongPress = { video ->
|
||||
selectedEpisodeForActions = video
|
||||
},
|
||||
)
|
||||
|
||||
if (hasEpisodes && hasAdditionalInfoSection) {
|
||||
DetailAdditionalInfoSection(meta = meta)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasAdditionalInfoSection) {
|
||||
DetailAdditionalInfoSection(meta = meta)
|
||||
}
|
||||
|
||||
if (!hasEpisodes && hasCollectionSection) {
|
||||
DetailPosterRailSection(
|
||||
title = meta.collectionName.orEmpty(),
|
||||
items = meta.collectionItems,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onPosterClick = onOpenMeta,
|
||||
)
|
||||
}
|
||||
|
||||
if (hasMoreLikeThisSection) {
|
||||
DetailPosterRailSection(
|
||||
title = "More Like This",
|
||||
items = meta.moreLikeThis,
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
onPosterClick = onOpenMeta,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
|
||||
}
|
||||
Unit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (headerProgress <= 0.05f) {
|
||||
NuvioBackButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 8.dp,
|
||||
),
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f),
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
|
||||
DetailFloatingHeader(
|
||||
meta = meta,
|
||||
isSaved = isSaved,
|
||||
progress = headerProgress,
|
||||
onBack = onBack,
|
||||
onToggleSaved = toggleSaved,
|
||||
)
|
||||
|
||||
selectedEpisodeForActions?.let { selectedEpisode ->
|
||||
val isSelectedEpisodeWatched = remember(meta, selectedEpisode, watchedUiState.watchedKeys) {
|
||||
WatchingState.isEpisodeWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episode = selectedEpisode,
|
||||
)
|
||||
}
|
||||
val previousEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
|
||||
meta.previousReleasedEpisodesBefore(
|
||||
target = selectedEpisode,
|
||||
todayIsoDate = todayIsoDate,
|
||||
)
|
||||
}
|
||||
val seasonEpisodes = remember(meta, selectedEpisode, todayIsoDate) {
|
||||
meta.releasedEpisodesForSeason(
|
||||
seasonNumber = selectedEpisode.season,
|
||||
todayIsoDate = todayIsoDate,
|
||||
)
|
||||
}
|
||||
val arePreviousEpisodesWatched = remember(previousEpisodes, watchedUiState.watchedKeys) {
|
||||
WatchingState.areEpisodesWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episodes = previousEpisodes,
|
||||
)
|
||||
}
|
||||
val isSeasonWatched = remember(seasonEpisodes, watchedUiState.watchedKeys) {
|
||||
WatchingState.areEpisodesWatched(
|
||||
watchedKeys = watchedUiState.watchedKeys,
|
||||
metaType = meta.type,
|
||||
metaId = meta.id,
|
||||
episodes = seasonEpisodes,
|
||||
)
|
||||
}
|
||||
EpisodeWatchedActionSheet(
|
||||
episode = selectedEpisode,
|
||||
seasonLabel = selectedEpisode.season?.let { "Season $it" } ?: "Specials",
|
||||
isEpisodeWatched = isSelectedEpisodeWatched,
|
||||
canMarkPreviousEpisodes = previousEpisodes.isNotEmpty(),
|
||||
arePreviousEpisodesWatched = arePreviousEpisodesWatched,
|
||||
isSeasonWatched = isSeasonWatched,
|
||||
onDismiss = { selectedEpisodeForActions = null },
|
||||
onToggleWatched = {
|
||||
WatchingActions.toggleEpisodeWatched(
|
||||
meta = meta,
|
||||
episode = selectedEpisode,
|
||||
isCurrentlyWatched = isSelectedEpisodeWatched,
|
||||
)
|
||||
},
|
||||
onTogglePreviousWatched = {
|
||||
WatchingActions.togglePreviousEpisodesWatched(
|
||||
meta = meta,
|
||||
episodes = previousEpisodes,
|
||||
areCurrentlyWatched = arePreviousEpisodesWatched,
|
||||
)
|
||||
},
|
||||
onToggleSeasonWatched = {
|
||||
WatchingActions.toggleSeasonWatched(
|
||||
meta = meta,
|
||||
episodes = seasonEpisodes,
|
||||
areCurrentlyWatched = isSeasonWatched,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
TrailerPlayerPopup(
|
||||
visible = selectedTrailer != null,
|
||||
trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(),
|
||||
trailerType = selectedTrailer?.type.orEmpty(),
|
||||
contentTitle = meta.name,
|
||||
playbackSource = trailerPlaybackSource,
|
||||
isLoading = trailerLoading,
|
||||
errorMessage = trailerErrorMessage,
|
||||
onDismiss = {
|
||||
trailerRequestToken += 1
|
||||
trailerLoading = false
|
||||
trailerPlaybackSource = null
|
||||
trailerErrorMessage = null
|
||||
selectedTrailer = null
|
||||
},
|
||||
onRetry = selectedTrailer?.let { trailer ->
|
||||
{ resolveTrailer(trailer) }
|
||||
},
|
||||
)
|
||||
|
||||
TraktListPickerDialog(
|
||||
visible = showLibraryListPicker,
|
||||
title = meta.name,
|
||||
tabs = pickerTabs,
|
||||
membership = pickerMembership,
|
||||
isPending = pickerPending,
|
||||
errorMessage = pickerError,
|
||||
onToggle = { listKey ->
|
||||
pickerMembership = pickerMembership.toMutableMap().apply {
|
||||
this[listKey] = !(this[listKey] == true)
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
if (!pickerPending) {
|
||||
showLibraryListPicker = false
|
||||
}
|
||||
},
|
||||
onSave = {
|
||||
detailsScope.launch {
|
||||
pickerPending = true
|
||||
pickerError = null
|
||||
runCatching {
|
||||
LibraryRepository.applyMembershipChanges(
|
||||
item = meta.toLibraryItem(savedAtEpochMs = 0L),
|
||||
desiredMembership = pickerMembership,
|
||||
)
|
||||
}.onSuccess {
|
||||
showLibraryListPicker = false
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: "Failed to update Trakt lists"
|
||||
}
|
||||
pickerPending = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -621,6 +634,13 @@ fun MetaDetailsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
private fun detailTabletContentMaxWidth(maxWidth: Dp, isTablet: Boolean): Dp =
|
||||
if (!isTablet) {
|
||||
maxWidth
|
||||
} else {
|
||||
(maxWidth * 0.6f).coerceIn(520.dp, 680.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun TraktListPickerDialog(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -30,6 +31,7 @@ fun DetailActionButtons(
|
|||
playLabel: String = "Play",
|
||||
saveLabel: String = "Save",
|
||||
isSaved: Boolean = false,
|
||||
isTablet: Boolean = false,
|
||||
onPlayClick: () -> Unit = {},
|
||||
onSaveClick: () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -38,12 +40,22 @@ fun DetailActionButtons(
|
|||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = if (isTablet) {
|
||||
Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally)
|
||||
} else {
|
||||
Arrangement.spacedBy(12.dp)
|
||||
},
|
||||
) {
|
||||
Button(
|
||||
onClick = onPlayClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (isTablet) {
|
||||
Modifier.width(220.dp)
|
||||
} else {
|
||||
Modifier.weight(1f)
|
||||
}
|
||||
)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(40.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
|
|
@ -68,7 +80,13 @@ fun DetailActionButtons(
|
|||
OutlinedButton(
|
||||
onClick = onSaveClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (isTablet) {
|
||||
Modifier.width(220.dp)
|
||||
} else {
|
||||
Modifier.weight(1f)
|
||||
}
|
||||
)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(40.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package com.nuvio.app.features.details.components
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -27,97 +29,115 @@ import com.nuvio.app.features.details.MetaDetails
|
|||
@Composable
|
||||
fun DetailHero(
|
||||
meta: MetaDetails,
|
||||
isTablet: Boolean = false,
|
||||
scrollOffset: Int = 0,
|
||||
contentMaxWidth: Dp = 560.dp,
|
||||
onHeightChanged: (Int) -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.75f)
|
||||
.onSizeChanged { onHeightChanged(it.height) }
|
||||
.graphicsLayer {
|
||||
clip = true
|
||||
},
|
||||
BoxWithConstraints(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
val heroHeight = detailHeroHeight(maxWidth, isTablet)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
.fillMaxWidth()
|
||||
.height(heroHeight)
|
||||
.onSizeChanged { onHeightChanged(it.height) }
|
||||
.graphicsLayer {
|
||||
clip = true
|
||||
},
|
||||
) {
|
||||
val imageUrl = meta.background ?: meta.poster
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = meta.name,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
translationY = scrollOffset * 0.5f
|
||||
scaleX = 1.08f
|
||||
scaleY = 1.08f
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
|
||||
MaterialTheme.colorScheme.background,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
if (meta.logo != null) {
|
||||
val imageUrl = meta.background ?: meta.poster
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = meta.logo,
|
||||
contentDescription = "${meta.name} logo",
|
||||
model = imageUrl,
|
||||
contentDescription = meta.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.6f)
|
||||
.height(80.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
translationY = scrollOffset * 0.5f
|
||||
scaleX = 1.08f
|
||||
scaleY = 1.08f
|
||||
},
|
||||
alignment = if (isTablet) Alignment.TopCenter else Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = meta.name,
|
||||
style = MaterialTheme.typography.displayLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
)
|
||||
}
|
||||
|
||||
if (meta.genres.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = meta.genres.take(3).joinToString(" \u2022 "),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
|
||||
MaterialTheme.colorScheme.background,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = if (isTablet) 32.dp else 18.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (meta.logo != null) {
|
||||
AsyncImage(
|
||||
model = meta.logo,
|
||||
contentDescription = "${meta.name} logo",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(if (isTablet) 0.56f else 0.6f)
|
||||
.widthIn(max = contentMaxWidth)
|
||||
.height(if (isTablet) 72.dp else 80.dp),
|
||||
alignment = Alignment.Center,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = meta.name,
|
||||
style = if (isTablet) MaterialTheme.typography.displaySmall else MaterialTheme.typography.displayLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
if (meta.genres.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = meta.genres.take(3).joinToString(" \u2022 "),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun detailHeroHeight(maxWidth: Dp, isTablet: Boolean): Dp =
|
||||
if (!isTablet) {
|
||||
(maxWidth * 1.33f).coerceIn(420.dp, 760.dp)
|
||||
} else {
|
||||
(maxWidth * 0.42f).coerceIn(300.dp, 420.dp)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue