mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor(media): centralize detail scaffold content
Move duplicated media detail title, metadata, actions, synopsis, playback, and cast rendering behind MediaDetailScaffoldUiModel so screens only provide model data and optional trailing content.
This commit is contained in:
@@ -5,44 +5,137 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
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.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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
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.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
|
import hu.bbara.purefin.model.CastMember
|
||||||
|
import hu.bbara.purefin.ui.common.button.DownloadActionButton
|
||||||
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
|
data class MediaDetailScaffoldUiModel(
|
||||||
|
val imageUrl: String,
|
||||||
|
val title: String,
|
||||||
|
val imageContentDescription: String? = null,
|
||||||
|
val titleFontSize: TextUnit = 32.sp,
|
||||||
|
val titleLineHeight: TextUnit = 38.sp,
|
||||||
|
val subtitle: String? = null,
|
||||||
|
val subtitleFontSize: TextUnit = 14.sp,
|
||||||
|
val subtitleFontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
val metadataItems: List<String> = emptyList(),
|
||||||
|
val highlightedMetadataItem: String? = null,
|
||||||
|
val actions: MediaDetailActionsUiModel? = null,
|
||||||
|
val synopsis: MediaDetailSynopsisUiModel? = null,
|
||||||
|
val playbackSettings: MediaDetailPlaybackSettingsUiModel? = null,
|
||||||
|
val cast: MediaDetailCastUiModel? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaDetailActionsUiModel(
|
||||||
|
val primaryAction: MediaDetailPrimaryActionUiModel,
|
||||||
|
val secondaryActions: List<MediaDetailSecondaryActionUiModel> = emptyList(),
|
||||||
|
val dividerThickness: Dp? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaDetailPrimaryActionUiModel(
|
||||||
|
val text: String,
|
||||||
|
val progress: Float,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val testTag: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface MediaDetailSecondaryActionUiModel {
|
||||||
|
val testTag: String?
|
||||||
|
|
||||||
|
data class Download(
|
||||||
|
val downloadState: DownloadState,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
override val testTag: String? = null,
|
||||||
|
) : MediaDetailSecondaryActionUiModel
|
||||||
|
|
||||||
|
data class Icon(
|
||||||
|
val icon: ImageVector,
|
||||||
|
val onClick: () -> Unit = {},
|
||||||
|
override val testTag: String? = null,
|
||||||
|
) : MediaDetailSecondaryActionUiModel
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MediaDetailSynopsisUiModel(
|
||||||
|
val text: String,
|
||||||
|
val bodyColor: Color? = null,
|
||||||
|
val bodyFontSize: TextUnit = 15.sp,
|
||||||
|
val bodyLineHeight: TextUnit? = 22.sp,
|
||||||
|
val titleSpacing: Dp = 12.dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaDetailPlaybackSettingsUiModel(
|
||||||
|
val audioTrack: String,
|
||||||
|
val subtitles: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaDetailCastUiModel(
|
||||||
|
val members: List<CastMember>,
|
||||||
|
val title: String = "Cast",
|
||||||
|
val topSpacing: Dp = 0.dp,
|
||||||
|
val cardWidth: Dp = 96.dp,
|
||||||
|
val nameFontSize: TextUnit = 12.sp,
|
||||||
|
val roleFontSize: TextUnit = 10.sp,
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaDetailScaffold(
|
fun MediaDetailScaffold(
|
||||||
imageUrl: String,
|
uiModel: MediaDetailScaffoldUiModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageHeight: Dp = 320.dp,
|
imageHeight: Dp = 320.dp,
|
||||||
contentOverlap: Dp = 24.dp,
|
contentOverlap: Dp = 24.dp,
|
||||||
contentSpacing: Dp = 16.dp,
|
contentSpacing: Dp = 16.dp,
|
||||||
topBar: @Composable (scrollBehavior: TopAppBarScrollBehavior) -> Unit = {},
|
topBar: @Composable (scrollBehavior: TopAppBarScrollBehavior) -> Unit = {},
|
||||||
content: @Composable ColumnScope.(Modifier) -> Unit
|
content: @Composable ColumnScope.(Modifier) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||||
val topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(topBarScrollBehavior.nestedScrollConnection),
|
.nestedScroll(topBarScrollBehavior.nestedScrollConnection),
|
||||||
containerColor = scheme.background,
|
containerColor = scheme.background,
|
||||||
@@ -66,8 +159,8 @@ fun MediaDetailScaffold(
|
|||||||
.height(imageHeight)
|
.height(imageHeight)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = imageUrl,
|
model = uiModel.imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = uiModel.imageContentDescription,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
@@ -87,8 +180,187 @@ fun MediaDetailScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isHomeMediaTransitionActive) {
|
if (!isHomeMediaTransitionActive) {
|
||||||
content(Modifier.padding(horizontal = 16.dp))
|
val contentModifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
MediaDetailScaffoldContent(
|
||||||
|
uiModel = uiModel,
|
||||||
|
modifier = contentModifier
|
||||||
|
)
|
||||||
|
content(contentModifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.MediaDetailScaffoldContent(
|
||||||
|
uiModel: MediaDetailScaffoldUiModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = uiModel.title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = uiModel.titleFontSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = uiModel.titleLineHeight,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
uiModel.subtitle?.takeIf { it.isNotBlank() }?.let { subtitle ->
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = uiModel.subtitleFontSize,
|
||||||
|
fontWeight = uiModel.subtitleFontWeight,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiModel.metadataItems.any { it.isNotBlank() }) {
|
||||||
|
MediaMetadataFlowRow(
|
||||||
|
items = uiModel.metadataItems,
|
||||||
|
highlightedItem = uiModel.highlightedMetadataItem,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiModel.actions?.let { actions ->
|
||||||
|
MediaDetailActions(
|
||||||
|
uiModel = actions,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiModel.synopsis?.takeIf { it.text.isNotBlank() }?.let { synopsis ->
|
||||||
|
MediaSynopsis(
|
||||||
|
synopsis = synopsis.text,
|
||||||
|
bodyColor = synopsis.bodyColor ?: scheme.onSurfaceVariant,
|
||||||
|
bodyFontSize = synopsis.bodyFontSize,
|
||||||
|
bodyLineHeight = synopsis.bodyLineHeight,
|
||||||
|
titleSpacing = synopsis.titleSpacing,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiModel.playbackSettings?.let { playbackSettings ->
|
||||||
|
MediaPlaybackSettings(
|
||||||
|
backgroundColor = scheme.surface,
|
||||||
|
foregroundColor = scheme.onSurface,
|
||||||
|
audioTrack = playbackSettings.audioTrack,
|
||||||
|
subtitles = playbackSettings.subtitles,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uiModel.cast?.takeIf { it.members.isNotEmpty() }?.let { cast ->
|
||||||
|
if (cast.topSpacing > 0.dp) {
|
||||||
|
Spacer(modifier = Modifier.height(cast.topSpacing))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = cast.title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
Spacer(modifier = modifier.height(12.dp))
|
||||||
|
MediaDetailCastRow(
|
||||||
|
cast = cast,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaDetailCastRow(
|
||||||
|
cast: MediaDetailCastUiModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(cast.members) { member ->
|
||||||
|
Column(modifier = Modifier.width(cast.cardWidth)) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = member.imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(4f / 5f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(scheme.surfaceVariant),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
fallbackIcon = Icons.Outlined.Person
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = member.name,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = cast.nameFontSize,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = member.role,
|
||||||
|
color = mutedStrong,
|
||||||
|
fontSize = cast.roleFontSize,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MediaDetailActions(
|
||||||
|
uiModel: MediaDetailActionsUiModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier) {
|
||||||
|
MediaResumeButton(
|
||||||
|
text = uiModel.primaryAction.text,
|
||||||
|
progress = uiModel.primaryAction.progress,
|
||||||
|
onClick = uiModel.primaryAction.onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.sizeIn(maxWidth = 200.dp)
|
||||||
|
.optionalTestTag(uiModel.primaryAction.testTag)
|
||||||
|
)
|
||||||
|
uiModel.dividerThickness?.let { dividerThickness ->
|
||||||
|
VerticalDivider(
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
thickness = dividerThickness,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
} ?: Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
uiModel.secondaryActions.forEach { action ->
|
||||||
|
when (action) {
|
||||||
|
is MediaDetailSecondaryActionUiModel.Download -> {
|
||||||
|
DownloadActionButton(
|
||||||
|
downloadState = action.downloadState,
|
||||||
|
onClick = action.onClick,
|
||||||
|
modifier = Modifier.optionalTestTag(action.testTag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaDetailSecondaryActionUiModel.Icon -> {
|
||||||
|
MediaActionButton(
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
|
iconColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
icon = action.icon,
|
||||||
|
height = 48.dp,
|
||||||
|
onClick = action.onClick,
|
||||||
|
modifier = Modifier.optionalTestTag(action.testTag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.optionalTestTag(testTag: String?): Modifier =
|
||||||
|
if (testTag == null) this else then(Modifier.testTag(testTag))
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package hu.bbara.purefin.ui.screen.episode
|
package hu.bbara.purefin.ui.screen.episode
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import hu.bbara.purefin.core.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
@@ -21,10 +20,20 @@ import hu.bbara.purefin.core.navigation.Route
|
|||||||
import hu.bbara.purefin.model.CastMember
|
import hu.bbara.purefin.model.CastMember
|
||||||
import hu.bbara.purefin.model.Episode
|
import hu.bbara.purefin.model.Episode
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailActionsUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailCastUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailPlaybackSettingsUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailPrimaryActionUiModel
|
||||||
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffoldUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSecondaryActionUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSynopsisUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDownloadButtonTag
|
||||||
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodePlayButtonTag
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
@@ -88,8 +97,21 @@ private fun EpisodeScreenInternal(
|
|||||||
onDownloadClick: () -> Unit,
|
onDownloadClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val playAction = remember(episode.id) {
|
||||||
|
{
|
||||||
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
|
intent.putExtra("MEDIA_ID", episode.id.toString())
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaDetailScaffold(
|
MediaDetailScaffold(
|
||||||
imageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
uiModel = episode.toMediaDetailScaffoldUiModel(
|
||||||
|
onPlayClick = playAction,
|
||||||
|
downloadState = downloadState,
|
||||||
|
onDownloadClick = onDownloadClick
|
||||||
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
EpisodeTopBar(
|
EpisodeTopBar(
|
||||||
@@ -98,59 +120,46 @@ private fun EpisodeScreenInternal(
|
|||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { _modifier ->
|
|
||||||
EpisodeHeroContent(
|
|
||||||
episode = episode,
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
)
|
||||||
EpisodeDetails(
|
}
|
||||||
episode = episode,
|
|
||||||
|
private fun Episode.toMediaDetailScaffoldUiModel(
|
||||||
|
onPlayClick: () -> Unit,
|
||||||
|
downloadState: DownloadState,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel(
|
||||||
|
imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
|
title = title,
|
||||||
|
subtitle = "Episode $index",
|
||||||
|
metadataItems = listOf(releaseDate, rating, runtime, format),
|
||||||
|
highlightedMetadataItem = format,
|
||||||
|
actions = MediaDetailActionsUiModel(
|
||||||
|
primaryAction = MediaDetailPrimaryActionUiModel(
|
||||||
|
text = mediaPlayButtonText(progress, watched),
|
||||||
|
progress = mediaPlaybackProgress(progress),
|
||||||
|
onClick = onPlayClick,
|
||||||
|
testTag = EpisodePlayButtonTag
|
||||||
|
),
|
||||||
|
secondaryActions = listOf(
|
||||||
|
MediaDetailSecondaryActionUiModel.Download(
|
||||||
downloadState = downloadState,
|
downloadState = downloadState,
|
||||||
onDownloadClick = onDownloadClick,
|
onClick = onDownloadClick,
|
||||||
modifier = _modifier
|
testTag = EpisodeDownloadButtonTag
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
dividerThickness = 2.dp
|
||||||
|
),
|
||||||
|
synopsis = MediaDetailSynopsisUiModel(text = synopsis),
|
||||||
|
playbackSettings = MediaDetailPlaybackSettingsUiModel(
|
||||||
|
audioTrack = "ENG",
|
||||||
|
subtitles = "ENG"
|
||||||
|
),
|
||||||
|
cast = if (cast.isNotEmpty()) {
|
||||||
|
MediaDetailCastUiModel(members = cast)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EpisodeHeroContent(
|
|
||||||
episode: Episode,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = episode.title,
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
lineHeight = 38.sp,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Episode ${episode.index}",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
EpisodeMetaChips(
|
|
||||||
episode = episode,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EpisodeMetaChips(
|
|
||||||
episode: Episode,
|
|
||||||
modifier: Modifier
|
|
||||||
) {
|
|
||||||
MediaMetadataFlowRow(
|
|
||||||
items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format),
|
|
||||||
highlightedItem = episode.format,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package hu.bbara.purefin.ui.screen.episode.components
|
package hu.bbara.purefin.ui.screen.episode.components
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
@@ -17,27 +12,15 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.VerticalDivider
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
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.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
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.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.core.download.DownloadState
|
|
||||||
import hu.bbara.purefin.model.Episode
|
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
|
||||||
import hu.bbara.purefin.ui.common.button.DownloadActionButton
|
|
||||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
|
||||||
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
||||||
|
|
||||||
internal sealed interface EpisodeTopBarShortcut {
|
internal sealed interface EpisodeTopBarShortcut {
|
||||||
@@ -102,76 +85,6 @@ internal fun EpisodeTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun EpisodeDetails(
|
|
||||||
episode: Episode,
|
|
||||||
downloadState: DownloadState,
|
|
||||||
onDownloadClick: () -> Unit,
|
|
||||||
modifier: Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val playAction = remember(episode.id) {
|
|
||||||
{
|
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
|
||||||
intent.putExtra("MEDIA_ID", episode.id.toString())
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaSynopsis(
|
|
||||||
synopsis = episode.synopsis,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
Row(modifier = modifier) {
|
|
||||||
MediaResumeButton(
|
|
||||||
text = mediaPlayButtonText(episode.progress, episode.watched),
|
|
||||||
progress = mediaPlaybackProgress(episode.progress),
|
|
||||||
onClick = playAction,
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(maxWidth = 200.dp)
|
|
||||||
.testTag(EpisodePlayButtonTag)
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
thickness = 2.dp,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(48.dp)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
)
|
|
||||||
Row() {
|
|
||||||
DownloadActionButton(
|
|
||||||
downloadState = downloadState,
|
|
||||||
modifier = Modifier.testTag(EpisodeDownloadButtonTag),
|
|
||||||
onClick = onDownloadClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MediaPlaybackSettings(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
|
||||||
foregroundColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
//TODO fix it
|
|
||||||
audioTrack = "ENG",
|
|
||||||
subtitles = "ENG",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
if (episode.cast.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Cast",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
Spacer(modifier = modifier.height(12.dp))
|
|
||||||
//TODO use it
|
|
||||||
// MediaCastRow(
|
|
||||||
// cast = episode.cast
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal const val EpisodeSeriesButtonTag = "episode-series-button"
|
internal const val EpisodeSeriesButtonTag = "episode-series-button"
|
||||||
internal const val EpisodePlayButtonTag = "episode-play-button"
|
internal const val EpisodePlayButtonTag = "episode-play-button"
|
||||||
internal const val EpisodeDownloadButtonTag = "episode-download-button"
|
internal const val EpisodeDownloadButtonTag = "episode-download-button"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package hu.bbara.purefin.ui.screen.movie
|
package hu.bbara.purefin.ui.screen.movie
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import hu.bbara.purefin.core.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
@@ -18,10 +18,20 @@ import hu.bbara.purefin.core.image.ImageUrlBuilder
|
|||||||
import hu.bbara.purefin.core.navigation.MovieDto
|
import hu.bbara.purefin.core.navigation.MovieDto
|
||||||
import hu.bbara.purefin.model.CastMember
|
import hu.bbara.purefin.model.CastMember
|
||||||
import hu.bbara.purefin.model.Movie
|
import hu.bbara.purefin.model.Movie
|
||||||
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailActionsUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailCastUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailPlaybackSettingsUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailPrimaryActionUiModel
|
||||||
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffoldUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSecondaryActionUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSynopsisUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
import hu.bbara.purefin.ui.screen.movie.components.MovieDownloadButtonTag
|
||||||
|
import hu.bbara.purefin.ui.screen.movie.components.MoviePlayButtonTag
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
@@ -73,8 +83,21 @@ private fun MovieScreenInternal(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val playAction = remember(movie.id) {
|
||||||
|
{
|
||||||
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
|
intent.putExtra("MEDIA_ID", movie.id.toString())
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaDetailScaffold(
|
MediaDetailScaffold(
|
||||||
imageUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
|
uiModel = movie.toMediaDetailScaffoldUiModel(
|
||||||
|
onPlayClick = playAction,
|
||||||
|
downloadState = downloadState,
|
||||||
|
onDownloadClick = onDownloadClick
|
||||||
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
MovieTopBar(
|
MovieTopBar(
|
||||||
@@ -82,42 +105,48 @@ private fun MovieScreenInternal(
|
|||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { _modifier ->
|
|
||||||
MovieHeroContent(
|
|
||||||
movie = movie,
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
)
|
||||||
MovieDetails(
|
}
|
||||||
movie = movie,
|
|
||||||
|
private fun Movie.toMediaDetailScaffoldUiModel(
|
||||||
|
onPlayClick: () -> Unit,
|
||||||
|
downloadState: DownloadState,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
|
): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel(
|
||||||
|
imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
|
title = title,
|
||||||
|
metadataItems = listOf(year, rating, runtime, format),
|
||||||
|
highlightedMetadataItem = format,
|
||||||
|
actions = MediaDetailActionsUiModel(
|
||||||
|
primaryAction = MediaDetailPrimaryActionUiModel(
|
||||||
|
text = mediaPlayButtonText(progress, watched),
|
||||||
|
progress = mediaPlaybackProgress(progress),
|
||||||
|
onClick = onPlayClick,
|
||||||
|
testTag = MoviePlayButtonTag
|
||||||
|
),
|
||||||
|
secondaryActions = listOf(
|
||||||
|
MediaDetailSecondaryActionUiModel.Download(
|
||||||
downloadState = downloadState,
|
downloadState = downloadState,
|
||||||
onDownloadClick = onDownloadClick,
|
onClick = onDownloadClick,
|
||||||
modifier = _modifier
|
testTag = MovieDownloadButtonTag
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
dividerThickness = 4.dp
|
||||||
|
),
|
||||||
|
synopsis = MediaDetailSynopsisUiModel(text = synopsis),
|
||||||
|
playbackSettings = MediaDetailPlaybackSettingsUiModel(
|
||||||
|
audioTrack = "ENG",
|
||||||
|
subtitles = "ENG"
|
||||||
|
),
|
||||||
|
cast = if (cast.isNotEmpty()) {
|
||||||
|
MediaDetailCastUiModel(
|
||||||
|
members = cast,
|
||||||
|
topSpacing = 24.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MovieHeroContent(
|
|
||||||
movie: Movie,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = movie.title,
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
lineHeight = 38.sp,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
// Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
MediaMetadataFlowRow(
|
|
||||||
items = listOf(movie.year, movie.rating, movie.runtime, movie.format),
|
|
||||||
highlightedItem = movie.format,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,39 +1,13 @@
|
|||||||
package hu.bbara.purefin.ui.screen.movie.components
|
package hu.bbara.purefin.ui.screen.movie.components
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Cast
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.VerticalDivider
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import hu.bbara.purefin.core.download.DownloadState
|
|
||||||
import hu.bbara.purefin.model.Movie
|
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
|
||||||
import hu.bbara.purefin.ui.common.button.DownloadActionButton
|
|
||||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
|
||||||
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -62,76 +36,5 @@ internal fun MovieTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
internal fun MovieDetails(
|
|
||||||
movie: Movie,
|
|
||||||
downloadState: DownloadState,
|
|
||||||
onDownloadClick: () -> Unit,
|
|
||||||
modifier: Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val playAction = remember(movie.id) {
|
|
||||||
{
|
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
|
||||||
intent.putExtra("MEDIA_ID", movie.id.toString())
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaSynopsis(
|
|
||||||
synopsis = movie.synopsis,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
Row(modifier = modifier) {
|
|
||||||
MediaResumeButton(
|
|
||||||
text = mediaPlayButtonText(movie.progress, movie.watched),
|
|
||||||
progress = mediaPlaybackProgress(movie.progress),
|
|
||||||
onClick = playAction,
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(maxWidth = 200.dp)
|
|
||||||
.testTag(MoviePlayButtonTag)
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
thickness = 4.dp,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(48.dp)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
)
|
|
||||||
Row() {
|
|
||||||
DownloadActionButton(
|
|
||||||
downloadState = downloadState,
|
|
||||||
modifier = Modifier.testTag(MovieDownloadButtonTag),
|
|
||||||
onClick = onDownloadClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MediaPlaybackSettings(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
|
||||||
foregroundColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
audioTrack = "ENG",
|
|
||||||
subtitles = "ENG",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
if (movie.cast.isNotEmpty()) {
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Text(
|
|
||||||
text = "Cast",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
Spacer(modifier = modifier.height(12.dp))
|
|
||||||
//TODO fix
|
|
||||||
// MediaCastRow(
|
|
||||||
// cast = movie.cast,
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal const val MoviePlayButtonTag = "movie-play-button"
|
internal const val MoviePlayButtonTag = "movie-play-button"
|
||||||
internal const val MovieDownloadButtonTag = "movie-download-button"
|
internal const val MovieDownloadButtonTag = "movie-download-button"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package hu.bbara.purefin.ui.screen.series
|
package hu.bbara.purefin.ui.screen.series
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -12,7 +14,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -21,19 +24,31 @@ import hu.bbara.purefin.core.download.DownloadState
|
|||||||
import hu.bbara.purefin.core.feature.content.series.SeriesViewModel
|
import hu.bbara.purefin.core.feature.content.series.SeriesViewModel
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||||
import hu.bbara.purefin.model.Episode
|
import hu.bbara.purefin.model.Episode
|
||||||
import hu.bbara.purefin.model.Season
|
import hu.bbara.purefin.model.Season
|
||||||
import hu.bbara.purefin.model.Series
|
import hu.bbara.purefin.model.Series
|
||||||
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailActionsUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailCastUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailPrimaryActionUiModel
|
||||||
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffold
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaDetailScaffoldUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSecondaryActionUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.MediaDetailSynopsisUiModel
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate
|
||||||
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
import hu.bbara.purefin.ui.screen.series.components.DownloadOptionsBottomSheet
|
||||||
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
|
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeriesActionButtons
|
import hu.bbara.purefin.ui.screen.series.components.SeriesAddButtonTag
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadButtonTag
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadOption
|
import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadOption
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeriesMetaChips
|
import hu.bbara.purefin.ui.screen.series.components.SeriesPlayButtonTag
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
|
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -125,6 +140,9 @@ private fun SeriesScreenInternal(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigationManager = LocalNavigationManager.current
|
||||||
|
var showDownloadDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun getDefaultSeason(): Season? {
|
fun getDefaultSeason(): Season? {
|
||||||
return series.seasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: series.seasons.firstOrNull()
|
return series.seasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: series.seasons.firstOrNull()
|
||||||
@@ -135,6 +153,25 @@ private fun SeriesScreenInternal(
|
|||||||
series.seasons.firstOrNull { it.id == selectedSeasonId } ?: getDefaultSeason()
|
series.seasons.firstOrNull { it.id == selectedSeasonId } ?: getDefaultSeason()
|
||||||
val nextUpEpisode = selectedSeason?.episodes?.firstOrNull { !it.watched }
|
val nextUpEpisode = selectedSeason?.episodes?.firstOrNull { !it.watched }
|
||||||
?: selectedSeason?.episodes?.firstOrNull()
|
?: selectedSeason?.episodes?.firstOrNull()
|
||||||
|
val playAction = remember(nextUpEpisode, offline) {
|
||||||
|
nextUpEpisode?.let { episode ->
|
||||||
|
{
|
||||||
|
navigationManager.navigate(
|
||||||
|
Route.EpisodeRoute(
|
||||||
|
EpisodeDto(
|
||||||
|
id = episode.id,
|
||||||
|
seasonId = episode.seasonId,
|
||||||
|
seriesId = episode.seriesId,
|
||||||
|
offline = offline,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
|
intent.putExtra("MEDIA_ID", episode.id.toString())
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(series.id, selectedSeason?.id) {
|
LaunchedEffect(series.id, selectedSeason?.id) {
|
||||||
selectedSeason?.let { onLoadSeasonEpisodes(series.id, it.id) }
|
selectedSeason?.let { onLoadSeasonEpisodes(series.id, it.id) }
|
||||||
@@ -145,7 +182,13 @@ private fun SeriesScreenInternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MediaDetailScaffold(
|
MediaDetailScaffold(
|
||||||
imageUrl = ImageUrlBuilder.finishImageUrl(series.imageUrlPrefix, ArtworkKind.PRIMARY),
|
uiModel = series.toMediaDetailScaffoldUiModel(
|
||||||
|
selectedSeason = selectedSeason,
|
||||||
|
nextUpEpisode = nextUpEpisode,
|
||||||
|
onPlayClick = playAction ?: {},
|
||||||
|
onDownloadClick = { showDownloadDialog = true },
|
||||||
|
bodyColor = scheme.onSurface
|
||||||
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
SeriesTopBar(
|
SeriesTopBar(
|
||||||
@@ -154,32 +197,6 @@ private fun SeriesScreenInternal(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { _modifier ->
|
) { _modifier ->
|
||||||
SeriesHeroContent(
|
|
||||||
series = series,
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
|
||||||
if (selectedSeason != null) {
|
|
||||||
SeriesActionButtons(
|
|
||||||
nextUpEpisode = nextUpEpisode,
|
|
||||||
selectedSeason = selectedSeason,
|
|
||||||
seriesDownloadState = seriesDownloadState,
|
|
||||||
seasonDownloadState = seasonDownloadState,
|
|
||||||
isSmartDownloadEnabled = isSmartDownloadEnabled,
|
|
||||||
offline = offline,
|
|
||||||
onDownloadOptionSelected = { option ->
|
|
||||||
onDownloadOptionSelected(option, selectedSeason)
|
|
||||||
},
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MediaSynopsis(
|
|
||||||
synopsis = series.synopsis,
|
|
||||||
bodyColor = scheme.onSurface,
|
|
||||||
bodyFontSize = 13.sp,
|
|
||||||
bodyLineHeight = null,
|
|
||||||
titleSpacing = 8.dp,
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
|
||||||
if (selectedSeason != null) {
|
if (selectedSeason != null) {
|
||||||
SeasonTabs(
|
SeasonTabs(
|
||||||
seasons = series.seasons,
|
seasons = series.seasons,
|
||||||
@@ -199,40 +216,74 @@ private fun SeriesScreenInternal(
|
|||||||
modifier = _modifier
|
modifier = _modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (series.cast.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Cast",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = _modifier
|
|
||||||
)
|
|
||||||
Spacer(modifier = _modifier.height(12.dp))
|
|
||||||
CastRow(cast = series.cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showDownloadDialog && selectedSeason != null) {
|
||||||
|
DownloadOptionsBottomSheet(
|
||||||
|
selectedSeasonName = selectedSeason.name,
|
||||||
|
seriesDownloadState = seriesDownloadState,
|
||||||
|
seasonDownloadState = seasonDownloadState,
|
||||||
|
isSmartDownloadEnabled = isSmartDownloadEnabled,
|
||||||
|
onDownloadOptionSelected = {
|
||||||
|
showDownloadDialog = false
|
||||||
|
onDownloadOptionSelected(it, selectedSeason)
|
||||||
|
},
|
||||||
|
onDismiss = { showDownloadDialog = false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun Series.toMediaDetailScaffoldUiModel(
|
||||||
private fun SeriesHeroContent(
|
selectedSeason: Season?,
|
||||||
series: Series,
|
nextUpEpisode: Episode?,
|
||||||
modifier: Modifier = Modifier,
|
onPlayClick: () -> Unit,
|
||||||
) {
|
onDownloadClick: () -> Unit,
|
||||||
val scheme = MaterialTheme.colorScheme
|
bodyColor: Color,
|
||||||
|
): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel(
|
||||||
Text(
|
imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
text = series.name,
|
title = name,
|
||||||
color = scheme.onBackground,
|
titleFontSize = 30.sp,
|
||||||
fontSize = 30.sp,
|
titleLineHeight = 36.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
metadataItems = listOf(year, "$seasonCount Seasons"),
|
||||||
lineHeight = 36.sp,
|
actions = selectedSeason?.let {
|
||||||
modifier = modifier
|
MediaDetailActionsUiModel(
|
||||||
|
primaryAction = MediaDetailPrimaryActionUiModel(
|
||||||
|
text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched),
|
||||||
|
progress = mediaPlaybackProgress(nextUpEpisode?.progress),
|
||||||
|
onClick = onPlayClick,
|
||||||
|
testTag = SeriesPlayButtonTag
|
||||||
|
),
|
||||||
|
secondaryActions = listOf(
|
||||||
|
MediaDetailSecondaryActionUiModel.Icon(
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
testTag = SeriesAddButtonTag
|
||||||
|
),
|
||||||
|
MediaDetailSecondaryActionUiModel.Icon(
|
||||||
|
icon = Icons.Outlined.Download,
|
||||||
|
onClick = onDownloadClick,
|
||||||
|
testTag = SeriesDownloadButtonTag
|
||||||
)
|
)
|
||||||
SeriesMetaChips(
|
|
||||||
series = series,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
},
|
||||||
|
synopsis = MediaDetailSynopsisUiModel(
|
||||||
|
text = synopsis,
|
||||||
|
bodyColor = bodyColor,
|
||||||
|
bodyFontSize = 13.sp,
|
||||||
|
bodyLineHeight = null,
|
||||||
|
titleSpacing = 8.dp
|
||||||
|
),
|
||||||
|
cast = if (cast.isNotEmpty()) {
|
||||||
|
MediaDetailCastUiModel(
|
||||||
|
members = cast,
|
||||||
|
cardWidth = 84.dp,
|
||||||
|
nameFontSize = 11.sp,
|
||||||
|
roleFontSize = 10.sp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private fun SeriesDownloadOption.canPerform(
|
private fun SeriesDownloadOption.canPerform(
|
||||||
seriesDownloadState: DownloadState,
|
seriesDownloadState: DownloadState,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package hu.bbara.purefin.ui.screen.series.components
|
package hu.bbara.purefin.ui.screen.series.components
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
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.clickable
|
||||||
@@ -8,16 +7,13 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
import androidx.compose.material.icons.outlined.AutoAwesome
|
import androidx.compose.material.icons.outlined.AutoAwesome
|
||||||
@@ -45,7 +41,6 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
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
|
||||||
@@ -56,23 +51,13 @@ import hu.bbara.purefin.core.download.DownloadState
|
|||||||
import hu.bbara.purefin.core.feature.content.series.SeriesViewModel
|
import hu.bbara.purefin.core.feature.content.series.SeriesViewModel
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.navigation.EpisodeDto
|
|
||||||
import hu.bbara.purefin.core.navigation.Route
|
|
||||||
import hu.bbara.purefin.model.CastMember
|
import hu.bbara.purefin.model.CastMember
|
||||||
import hu.bbara.purefin.model.Episode
|
import hu.bbara.purefin.model.Episode
|
||||||
import hu.bbara.purefin.model.Season
|
import hu.bbara.purefin.model.Season
|
||||||
import hu.bbara.purefin.model.Series
|
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
|
||||||
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
|
||||||
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -100,96 +85,6 @@ internal fun SeriesTopBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun SeriesMetaChips(
|
|
||||||
series: Series,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
MediaMetadataFlowRow(
|
|
||||||
items = listOf(series.year, "${series.seasonCount} Seasons"),
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun SeriesActionButtons(
|
|
||||||
nextUpEpisode: Episode?,
|
|
||||||
selectedSeason: Season,
|
|
||||||
seriesDownloadState: DownloadState,
|
|
||||||
seasonDownloadState: DownloadState,
|
|
||||||
isSmartDownloadEnabled: Boolean,
|
|
||||||
offline: Boolean,
|
|
||||||
onDownloadOptionSelected: (SeriesDownloadOption) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val navigationManager = LocalNavigationManager.current
|
|
||||||
var showDownloadDialog by remember { mutableStateOf(false) }
|
|
||||||
val playAction = remember(nextUpEpisode, offline) {
|
|
||||||
nextUpEpisode?.let { episode ->
|
|
||||||
{
|
|
||||||
navigationManager.navigate(
|
|
||||||
Route.EpisodeRoute(
|
|
||||||
EpisodeDto(
|
|
||||||
id = episode.id,
|
|
||||||
seasonId = episode.seasonId,
|
|
||||||
seriesId = episode.seriesId,
|
|
||||||
offline = offline,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
|
||||||
intent.putExtra("MEDIA_ID", episode.id.toString())
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
MediaResumeButton(
|
|
||||||
text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched),
|
|
||||||
progress = mediaPlaybackProgress(nextUpEpisode?.progress),
|
|
||||||
onClick = playAction ?: {},
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(maxWidth = 200.dp)
|
|
||||||
.testTag(SeriesPlayButtonTag)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
MediaActionButton(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
|
||||||
iconColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
icon = Icons.Outlined.Add,
|
|
||||||
height = 48.dp,
|
|
||||||
modifier = Modifier.testTag(SeriesAddButtonTag)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
MediaActionButton(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
|
||||||
iconColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
icon = Icons.Outlined.Download,
|
|
||||||
height = 48.dp,
|
|
||||||
modifier = Modifier.testTag(SeriesDownloadButtonTag),
|
|
||||||
onClick = { showDownloadDialog = true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDownloadDialog) {
|
|
||||||
DownloadOptionsBottomSheet(
|
|
||||||
selectedSeasonName = selectedSeason.name,
|
|
||||||
seriesDownloadState = seriesDownloadState,
|
|
||||||
seasonDownloadState = seasonDownloadState,
|
|
||||||
isSmartDownloadEnabled = isSmartDownloadEnabled,
|
|
||||||
onDownloadOptionSelected = {
|
|
||||||
showDownloadDialog = false
|
|
||||||
onDownloadOptionSelected(it)
|
|
||||||
},
|
|
||||||
onDismiss = { showDownloadDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum class SeriesDownloadOption {
|
internal enum class SeriesDownloadOption {
|
||||||
SEASON,
|
SEASON,
|
||||||
SERIES,
|
SERIES,
|
||||||
@@ -199,7 +94,7 @@ internal enum class SeriesDownloadOption {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadOptionsBottomSheet(
|
internal fun DownloadOptionsBottomSheet(
|
||||||
selectedSeasonName: String,
|
selectedSeasonName: String,
|
||||||
seriesDownloadState: DownloadState,
|
seriesDownloadState: DownloadState,
|
||||||
seasonDownloadState: DownloadState,
|
seasonDownloadState: DownloadState,
|
||||||
|
|||||||
Reference in New Issue
Block a user