refactor media screens and consolidate mock data

- Move `EpisodeCard` and `MovieCard` logic directly into `EpisodeScreen` and `MovieScreen` as internal components.
- Delete `SeriesCard.kt` and merge its content into `SeriesScreen.kt`.
- Create `ContentMockData.kt` to centralize mock data for movies, series, and episodes, replacing local mock objects.
- Add Compose previews for `EpisodeScreen`, `MovieScreen`, and `SeriesScreen`.
- Clean up unused imports and streamline layout structures in content components.
This commit is contained in:
2026-01-21 21:13:39 +01:00
parent d2f5f8547a
commit decb8cd0cb
8 changed files with 544 additions and 448 deletions

View File

@@ -0,0 +1,162 @@
package hu.bbara.purefin.app.content
import hu.bbara.purefin.app.content.episode.EpisodeUiModel
import hu.bbara.purefin.app.content.movie.MovieUiModel
import hu.bbara.purefin.app.content.series.SeriesCastMemberUiModel
import hu.bbara.purefin.app.content.series.SeriesEpisodeUiModel
import hu.bbara.purefin.app.content.series.SeriesSeasonUiModel
import hu.bbara.purefin.app.content.series.SeriesUiModel
import org.jellyfin.sdk.model.UUID
import hu.bbara.purefin.app.content.episode.CastMember as EpisodeCastMember
import hu.bbara.purefin.app.content.movie.CastMember as MovieCastMember
object ContentMockData {
fun series(): SeriesUiModel {
val heroUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuD3hBjDpw00tDCQsK5xNcnJra301k1T4LksWVZzHieH9KHQItEQkVzhwevJvf8RkaQKdVKvObzRlfDDqa3_PNwLUlUQc1LpDih8p94VTGobEV62qi7QrmNyQm_o55KRMNWiTG3zLLpblGqo3uUNQcYmPFqfNML95dClXQ4lQNl85-zgerPPAbGPr23dswbIYCigyTAaXgrmdV_nbNQ5LdDB0Wh5cMHtP0uxz6k3ARjNom6clhphGIUF9e6YSvKuwuiZ-1lMYFg8C_4"
val episode1 = SeriesEpisodeUiModel(
id = "1",
title = "E1: The Beginning",
description = "The crew assembles for the first time as the anomaly begins to expand rapidly near Saturn's rings.",
duration = "58m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
)
val episode2 = SeriesEpisodeUiModel(
id = "2",
title = "E2: Event Horizon",
description = "Dr. Cole discovers a frequency embedded in the rift's radiation that suggests intelligent design.",
duration = "54m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
)
val episode3 = SeriesEpisodeUiModel(
id = "3",
title = "E3: Singularity",
description = "Tension rises as the ship approaches the event horizon, and the AI begins to behave erratically.",
duration = "1h 02m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U"
)
return SeriesUiModel(
title = "Interstellar Horizon: The Series",
year = "2024",
rating = "TV-MA",
seasons = "3 Seasons",
format = "4K HDR",
synopsis = "When a mysterious cosmic rift appears near Saturn, a team of seasoned astronauts and theoretical physicists must embark on a high-stakes voyage across dimensions. They seek to unlock the secrets of time-dilated anomalies that threaten the very fabric of human existence on Earth.",
heroImageUrl = heroUrl,
seasonTabs = listOf(
SeriesSeasonUiModel(
name = "Season 1",
isSelected = true,
episodes = listOf(episode1, episode2, episode3)
),
SeriesSeasonUiModel(
name = "Season 2",
isSelected = false,
episodes = listOf(episode1, episode2, episode3)
),
SeriesSeasonUiModel(
name = "Season 3",
isSelected = false,
episodes = listOf(episode1, episode2, episode3)
)
),
cast = listOf(
SeriesCastMemberUiModel(
name = "Marcus Thorne",
role = "Cmdr. Vance",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
),
SeriesCastMemberUiModel(
name = "Elena Rossi",
role = "Dr. Sarah Cole",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
),
SeriesCastMemberUiModel(
name = "Julian Chen",
role = "Tech Officer Lin",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U"
),
SeriesCastMemberUiModel(
name = "Sarah Jenkins",
role = "Mission Pilot",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBN6_72VggBdNx7ITLvEvIA6OSre5iJI6kQiUVMpKAlYgd8TpT-Jx6DzZwGsGACLnAXOUuzT2R7mx9A9DNZcqi5BF_jSaEdeYpfcBvJttmVPAwiCiq1_PI2BwoZZH_Ccmq2AHV5lQqcYaA2rPkf4e7YLLLgpmVbGjKhncTotQtxiZvmLNzCbLUdlEb7XLgHKfjS6FU6djV9ocOo9bxZ_YtrQj-mMFvYGzCxeFYC8OF0kIV2NN3kQYH8x1X-rYMqu2-d7klJfQdhKHw"
),
SeriesCastMemberUiModel(
name = "David Wu",
role = "The AI (Voice)",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuCnNkjaBc2hU2zJ5hAF8iZZ_ZZvMlU79o4JtPNCP2MEfttpF0fe_BHWsMMl6h3S37FJ1dTLk8AQuvRQ_ggy1u-71xlQWULB76rT8pdZiRE7TkInQ8gwpigs84KNWbTRxVUI7Nia9RPyJeFE7egZqnT46TQWUeN8llWF9EDQ6mpfVLH0vHhKUlko39iDgMnBIequYntugSFgWJQc1jH-AxZ4OpJr_-uZGkwtQ_CVYNV69u9y107gk5BwaUFwPeipe8Bn9I655kyHIuQ"
),
SeriesCastMemberUiModel(
name = "Alex Reed",
role = "Engineer",
imageUrl = null
)
)
)
}
fun movie(): MovieUiModel {
val castMembers = listOf(
MovieCastMember(
name = "Elena Rossi",
role = "Dr. Sarah Cole",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
),
MovieCastMember(
name = "Marcus Thorne",
role = "Cmdr. Vance",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
),
MovieCastMember(
name = "Julian Chen",
role = "Tech Officer Lin",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U"
)
)
return MovieUiModel(
id = UUID.randomUUID(),
title = "Interstellar Horizon",
year = "2024",
rating = "PG-13",
runtime = "2h 14m",
format = "4K HDR",
synopsis = "A deep-space rescue crew is dispatched to intercept a derelict vessel drifting back from a temporal rift. As timelines fracture around them, they must decide which reality is worth saving.",
heroImageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuD3hBjDpw00tDCQsK5xNcnJra301k1T4LksWVZzHieH9KHQItEQkVzhwevJvf8RkaQKdVKvObzRlfDDqa3_PNwLUlUQc1LpDih8p94VTGobEV62qi7QrmNyQm_o55KRMNWiTG3zLLpblGqo3uUNQcYmPFqfNML95dClXQ4lQNl85-zgerPPAbGPr23dswbIYCigyTAaXgrmdV_nbNQ5LdDB0Wh5cMHtP0uxz6k3ARjNom6clhphGIUF9e6YSvKuwuiZ-1lMYFg8C_4",
audioTrack = "English (Dolby Atmos)",
subtitles = "English, Spanish, French",
cast = castMembers
)
}
fun episode(): EpisodeUiModel {
val castMembers = listOf(
EpisodeCastMember(
name = "Elena Rossi",
role = "Dr. Sarah Cole",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
),
EpisodeCastMember(
name = "Marcus Thorne",
role = "Cmdr. Vance",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
),
EpisodeCastMember(
name = "David Wu",
role = "The AI (Voice)",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuCnNkjaBc2hU2zJ5hAF8iZZ_ZZvMlU79o4JtPNCP2MEfttpF0fe_BHWsMMl6h3S37FJ1dTLk8AQuvRQ_ggy1u-71xlQWULB76rT8pdZiRE7TkInQ8gwpigs84KNWbTRxVUI7Nia9RPyJeFE7egZqnT46TQWUeN8llWF9EDQ6mpfVLH0vHhKUlko39iDgMnBIequYntugSFgWJQc1jH-AxZ4OpJr_-uZGkwtQ_CVYNV69u9y107gk5BwaUFwPeipe8Bn9I655kyHIuQ"
)
)
return EpisodeUiModel(
id = UUID.randomUUID(),
title = "S1E1 · Event Horizon",
releaseDate = "Oct 12, 2024",
rating = "TV-MA",
runtime = "58m",
format = "4K HDR",
synopsis = "As the anomaly near Saturn destabilizes, the crew boards the research vessel Helios to intercept what might be a distress signal from another timeline.",
heroImageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0",
audioTrack = "English (Dolby Atmos)",
subtitles = "English, Spanish, German",
cast = castMembers
)
}
}

View File

@@ -1,34 +1,20 @@
package hu.bbara.purefin.app.content.episode package hu.bbara.purefin.app.content.episode
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement 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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -36,13 +22,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaActionButtons import hu.bbara.purefin.common.ui.MediaActionButtons
import hu.bbara.purefin.common.ui.MediaCastMember import hu.bbara.purefin.common.ui.MediaCastMember
import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.MediaGhostIconButton import hu.bbara.purefin.common.ui.MediaGhostIconButton
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaPlaybackSettings import hu.bbara.purefin.common.ui.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity
@Composable @Composable
internal fun EpisodeTopBar( internal fun EpisodeTopBar(
@@ -140,99 +123,6 @@ internal fun EpisodeDetails(
} }
} }
@Composable
fun EpisodeCard(
episode: EpisodeUiModel,
backGroundColor: Color,
modifier: Modifier = Modifier,
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
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)
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = episode.heroImageUrl,
height = 300.dp,
backgroundColor = backGroundColor,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
EpisodeDetails(
episode = episode,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
MediaHero(
imageUrl = episode.heroImageUrl,
backgroundColor = backGroundColor,
height = 400.dp,
modifier = Modifier.fillMaxWidth()
)
EpisodeDetails(
episode = episode,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
)
}
}
EpisodeTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = colors.primary,
onContainerColor = colors.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
}
private fun CastMember.toMediaCastMember() = MediaCastMember( private fun CastMember.toMediaCastMember() = MediaCastMember(
name = name, name = name,
role = role, role = role,

View File

@@ -1,13 +1,37 @@
package hu.bbara.purefin.app.content.episode package hu.bbara.purefin.app.content.episode
import android.content.Intent
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.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.content.ContentMockData
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.navigation.ItemDto import hu.bbara.purefin.navigation.ItemDto
import hu.bbara.purefin.player.PlayerActivity
@Composable @Composable
fun EpisodeScreen( fun EpisodeScreen(
@@ -22,13 +46,115 @@ fun EpisodeScreen(
val episode = viewModel.episode.collectAsState() val episode = viewModel.episode.collectAsState()
if (episode.value != null) { if (episode.value == null) {
EpisodeCard(
episode = episode.value!!,
modifier = modifier,
backGroundColor = MaterialTheme.colorScheme.background
)
} else {
PurefinWaitingScreen() PurefinWaitingScreen()
return
}
EpisodeScreenInternal(
episode = episode.value!!,
modifier = modifier,
backGroundColor = MaterialTheme.colorScheme.background
)
}
@Composable
private fun EpisodeScreenInternal(
episode: EpisodeUiModel,
backGroundColor: Color,
modifier: Modifier = Modifier,
) {
val colors = rememberEpisodeColors().toMediaDetailColors()
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)
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = episode.heroImageUrl,
height = 300.dp,
backgroundColor = backGroundColor,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
EpisodeDetails(
episode = episode,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
MediaHero(
imageUrl = episode.heroImageUrl,
backgroundColor = backGroundColor,
height = 400.dp,
modifier = Modifier.fillMaxWidth()
)
EpisodeDetails(
episode = episode,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
)
}
}
EpisodeTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = colors.primary,
onContainerColor = colors.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
} }
} }
@Preview
@Composable
fun EpisodeScreenPreview() {
EpisodeScreenInternal(
episode = ContentMockData.episode(),
backGroundColor = MaterialTheme.colorScheme.background
)
}

View File

@@ -1,34 +1,20 @@
package hu.bbara.purefin.app.content.movie package hu.bbara.purefin.app.content.movie
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement 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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -36,13 +22,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.MediaActionButtons import hu.bbara.purefin.common.ui.MediaActionButtons
import hu.bbara.purefin.common.ui.MediaCastMember import hu.bbara.purefin.common.ui.MediaCastMember
import hu.bbara.purefin.common.ui.MediaCastRow import hu.bbara.purefin.common.ui.MediaCastRow
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.MediaGhostIconButton import hu.bbara.purefin.common.ui.MediaGhostIconButton
import hu.bbara.purefin.common.ui.MediaMetaChip import hu.bbara.purefin.common.ui.MediaMetaChip
import hu.bbara.purefin.common.ui.MediaPlaybackSettings import hu.bbara.purefin.common.ui.MediaPlaybackSettings
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.common.ui.toMediaDetailColors import hu.bbara.purefin.common.ui.toMediaDetailColors
import hu.bbara.purefin.player.PlayerActivity
@Composable @Composable
internal fun MovieTopBar( internal fun MovieTopBar(
@@ -140,100 +123,6 @@ internal fun MovieDetails(
} }
} }
@Composable
fun MovieCard(
movie: MovieUiModel,
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)
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
height = 300.dp,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
MovieDetails(
movie = movie,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
MediaHero(
imageUrl = movie.heroImageUrl,
height = 400.dp,
backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxWidth()
)
MovieDetails(
movie = movie,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
)
}
}
MovieTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
}
private fun CastMember.toMediaCastMember() = MediaCastMember( private fun CastMember.toMediaCastMember() = MediaCastMember(
name = name, name = name,
role = role, role = role,

View File

@@ -1,18 +1,39 @@
package hu.bbara.purefin.app.content.movie package hu.bbara.purefin.app.content.movie
import android.content.Intent
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.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.content.ContentMockData
import hu.bbara.purefin.common.ui.MediaFloatingPlayButton
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.navigation.ItemDto import hu.bbara.purefin.navigation.ItemDto
import hu.bbara.purefin.player.PlayerActivity
@Composable @Composable
fun MovieScreen( fun MovieScreen(
movie: ItemDto, movie: ItemDto, viewModel: MovieScreenViewModel = hiltViewModel(), modifier: Modifier = Modifier
viewModel: MovieScreenViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) { ) {
LaunchedEffect(movie.id) { LaunchedEffect(movie.id) {
viewModel.selectMovie(movie.id) viewModel.selectMovie(movie.id)
@@ -21,11 +42,107 @@ fun MovieScreen(
val movieItem = viewModel.movie.collectAsState() val movieItem = viewModel.movie.collectAsState()
if (movieItem.value != null) { if (movieItem.value != null) {
MovieCard( MovieScreenInternal(
movie = movieItem.value!!, movie = movieItem.value!!, modifier = modifier
modifier = modifier
) )
} else { } else {
PurefinWaitingScreen() PurefinWaitingScreen()
} }
} }
@Composable
private fun MovieScreenInternal(
movie: MovieUiModel,
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)
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
val isWide = maxWidth >= 900.dp
val contentPadding = if (isWide) 32.dp else 20.dp
Box(modifier = Modifier.fillMaxSize()) {
if (isWide) {
Row(modifier = Modifier.fillMaxSize()) {
MediaHero(
imageUrl = movie.heroImageUrl,
backgroundColor = MaterialTheme.colorScheme.background,
height = 300.dp,
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
)
MovieDetails(
movie = movie,
modifier = Modifier
.weight(0.5f)
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.padding(
start = contentPadding,
end = contentPadding,
top = 96.dp,
bottom = 32.dp
)
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
MediaHero(
imageUrl = movie.heroImageUrl,
height = 400.dp,
backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxWidth()
)
MovieDetails(
movie = movie,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
.offset(y = (-48).dp)
.padding(bottom = 96.dp)
)
}
}
MovieTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
)
if (!isWide) {
MediaFloatingPlayButton(
containerColor = MaterialTheme.colorScheme.primary,
onContainerColor = MaterialTheme.colorScheme.onPrimary,
onClick = playAction,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
)
}
}
}
}
@Preview
@Composable
fun MovieScreenPreview() {
MovieScreenInternal(movie = ContentMockData.movie())
}

View File

@@ -1,128 +0,0 @@
package hu.bbara.purefin.app.content.series
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SeriesCard(
series: SeriesUiModel,
modifier: Modifier = Modifier,
) {
val colors = rememberSeriesColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val heroHeight = maxHeight * 0.4f
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SeriesHero(
imageUrl = series.heroImageUrl,
height = heroHeight
)
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = (-96).dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
Text(
text = series.title,
color = colors.textPrimary,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
)
Spacer(modifier = Modifier.height(16.dp))
SeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp))
SeriesActionButtons()
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = series.synopsis,
color = colors.textMutedStrong,
fontSize = 13.sp,
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Episodes",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(28.dp))
SeasonTabs(seasons = series.seasonTabs)
Spacer(modifier = Modifier.height(16.dp))
}
EpisodeCarousel(
episodes = series.seasonTabs.firstOrNull { it.isSelected }?.episodes
?: series.seasonTabs.firstOrNull()?.episodes
?: emptyList()
)
Spacer(modifier = Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 0.dp, bottom = 0.dp)
) {
Text(
text = "Cast",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(modifier = Modifier.height(12.dp))
CastRow(cast = series.cast)
}
}
}
SeriesTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.TopCenter)
)
}
}
@Preview
@Composable
fun SeriesCardPreview() {
SeriesCard(series = SeriesMockData.series())
}

View File

@@ -31,88 +31,3 @@ data class SeriesUiModel(
val seasonTabs: List<SeriesSeasonUiModel>, val seasonTabs: List<SeriesSeasonUiModel>,
val cast: List<SeriesCastMemberUiModel> val cast: List<SeriesCastMemberUiModel>
) )
internal object SeriesMockData {
fun series(): SeriesUiModel {
val heroUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuD3hBjDpw00tDCQsK5xNcnJra301k1T4LksWVZzHieH9KHQItEQkVzhwevJvf8RkaQKdVKvObzRlfDDqa3_PNwLUlUQc1LpDih8p94VTGobEV62qi7QrmNyQm_o55KRMNWiTG3zLLpblGqo3uUNQcYmPFqfNML95dClXQ4lQNl85-zgerPPAbGPr23dswbIYCigyTAaXgrmdV_nbNQ5LdDB0Wh5cMHtP0uxz6k3ARjNom6clhphGIUF9e6YSvKuwuiZ-1lMYFg8C_4"
val episode1 = SeriesEpisodeUiModel(
id = "1",
title = "E1: The Beginning",
description = "The crew assembles for the first time as the anomaly begins to expand rapidly near Saturn's rings.",
duration = "58m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
)
val episode2 = SeriesEpisodeUiModel(
id = "2",
title = "E2: Event Horizon",
description = "Dr. Cole discovers a frequency embedded in the rift's radiation that suggests intelligent design.",
duration = "54m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
)
val episode3 = SeriesEpisodeUiModel(
id = "3",
title = "E3: Singularity",
description = "Tension rises as the ship approaches the event horizon, and the AI begins to behave erratically.",
duration = "1h 02m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U"
)
return SeriesUiModel(
title = "Interstellar Horizon: The Series",
year = "2024",
rating = "TV-MA",
seasons = "3 Seasons",
format = "4K HDR",
synopsis = "When a mysterious cosmic rift appears near Saturn, a team of seasoned astronauts and theoretical physicists must embark on a high-stakes voyage across dimensions. They seek to unlock the secrets of time-dilated anomalies that threaten the very fabric of human existence on Earth.",
heroImageUrl = heroUrl,
seasonTabs = listOf(
SeriesSeasonUiModel(
name = "Season 1",
isSelected = true,
episodes = listOf(episode1, episode2, episode3)
),
SeriesSeasonUiModel(
name = "Season 2",
isSelected = false,
episodes = listOf(episode1, episode2, episode3)
),
SeriesSeasonUiModel(
name = "Season 3",
isSelected = false,
episodes = listOf(episode1, episode2, episode3)
)
),
cast = listOf(
SeriesCastMemberUiModel(
name = "Marcus Thorne",
role = "Cmdr. Vance",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0"
),
SeriesCastMemberUiModel(
name = "Elena Rossi",
role = "Dr. Sarah Cole",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc"
),
SeriesCastMemberUiModel(
name = "Julian Chen",
role = "Tech Officer Lin",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U"
),
SeriesCastMemberUiModel(
name = "Sarah Jenkins",
role = "Mission Pilot",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBN6_72VggBdNx7ITLvEvIA6OSre5iJI6kQiUVMpKAlYgd8TpT-Jx6DzZwGsGACLnAXOUuzT2R7mx9A9DNZcqi5BF_jSaEdeYpfcBvJttmVPAwiCiq1_PI2BwoZZH_Ccmq2AHV5lQqcYaA2rPkf4e7YLLLgpmVbGjKhncTotQtxiZvmLNzCbLUdlEb7XLgHKfjS6FU6djV9ocOo9bxZ_YtrQj-mMFvYGzCxeFYC8OF0kIV2NN3kQYH8x1X-rYMqu2-d7klJfQdhKHw"
),
SeriesCastMemberUiModel(
name = "David Wu",
role = "The AI (Voice)",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuCnNkjaBc2hU2zJ5hAF8iZZ_ZZvMlU79o4JtPNCP2MEfttpF0fe_BHWsMMl6h3S37FJ1dTLk8AQuvRQ_ggy1u-71xlQWULB76rT8pdZiRE7TkInQ8gwpigs84KNWbTRxVUI7Nia9RPyJeFE7egZqnT46TQWUeN8llWF9EDQ6mpfVLH0vHhKUlko39iDgMnBIequYntugSFgWJQc1jH-AxZ4OpJr_-uZGkwtQ_CVYNV69u9y107gk5BwaUFwPeipe8Bn9I655kyHIuQ"
),
SeriesCastMemberUiModel(
name = "Alex Reed",
role = "Engineer",
imageUrl = null
)
)
)
}
}

View File

@@ -1,10 +1,28 @@
package hu.bbara.purefin.app.content.series package hu.bbara.purefin.app.content.series
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.app.content.ContentMockData
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.navigation.ItemDto import hu.bbara.purefin.navigation.ItemDto
@@ -21,7 +39,7 @@ fun SeriesScreen(
val series = viewModel.series.collectAsState() val series = viewModel.series.collectAsState()
if (series.value != null) { if (series.value != null) {
SeriesCard( SeriesScreenInternal(
series = series.value!!, series = series.value!!,
modifier = modifier modifier = modifier
) )
@@ -29,3 +47,110 @@ fun SeriesScreen(
PurefinWaitingScreen() PurefinWaitingScreen()
} }
} }
@Composable
private fun SeriesScreenInternal(
series: SeriesUiModel,
modifier: Modifier = Modifier,
) {
val colors = rememberSeriesColors()
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(colors.background)
) {
val heroHeight = maxHeight * 0.4f
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SeriesHero(
imageUrl = series.heroImageUrl,
height = heroHeight
)
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = (-96).dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
Text(
text = series.title,
color = colors.textPrimary,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 36.sp
)
Spacer(modifier = Modifier.height(16.dp))
SeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp))
SeriesActionButtons()
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Synopsis",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = series.synopsis,
color = colors.textMutedStrong,
fontSize = 13.sp,
)
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "Episodes",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(28.dp))
SeasonTabs(seasons = series.seasonTabs)
Spacer(modifier = Modifier.height(16.dp))
}
EpisodeCarousel(
episodes = series.seasonTabs.firstOrNull { it.isSelected }?.episodes
?: series.seasonTabs.firstOrNull()?.episodes
?: emptyList()
)
Spacer(modifier = Modifier.height(32.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 0.dp, bottom = 0.dp)
) {
Text(
text = "Cast",
color = colors.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(modifier = Modifier.height(12.dp))
CastRow(cast = series.cast)
}
}
}
SeriesTopBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.TopCenter)
)
}
}
@Preview
@Composable
fun SeriesScreenPreview() {
SeriesScreenInternal(series = ContentMockData.series())
}