mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-14 07:42:43 -05:00
refact detail screen composables
This commit is contained in:
@@ -9,5 +9,6 @@ abstract class DetailedItem(
|
||||
@Transient open val overview: String?,
|
||||
@Transient open val productionCompanies: List<ProductionCompany>,
|
||||
@Transient open val status: String,
|
||||
@Transient open val tagline: String?
|
||||
@Transient open val tagline: String?,
|
||||
@Transient open val voteAverage: Float
|
||||
): TmdbItem(id, title, posterPath)
|
||||
@@ -12,9 +12,10 @@ class DetailedMovie(
|
||||
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
|
||||
@SerializedName("status") override val status: String,
|
||||
@SerializedName("tagline") override val tagline: String?,
|
||||
@SerializedName("vote_average") override val voteAverage: Float,
|
||||
@SerializedName("adult") val isAdult: Boolean,
|
||||
@SerializedName("budget") val budget: Int,
|
||||
@SerializedName("release_date") val releaseDate: String,
|
||||
@SerializedName("revenue") val revenue: Int,
|
||||
@SerializedName("runtime") val runtime: Int?
|
||||
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline)
|
||||
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)
|
||||
|
||||
@@ -12,6 +12,7 @@ class DetailedTv(
|
||||
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
|
||||
@SerializedName("status") override val status: String,
|
||||
@SerializedName("tagline") override val tagline: String?,
|
||||
@SerializedName("vote_average") override val voteAverage: Float,
|
||||
@SerializedName("created_by") val createdBy: List<Person>,
|
||||
@SerializedName("first_air_date") val firstAirDate: String,
|
||||
@SerializedName("in_production") val inProduction: Boolean,
|
||||
@@ -19,4 +20,4 @@ class DetailedTv(
|
||||
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
|
||||
@SerializedName("number_of_seasons") val numberOfSeasons: Int,
|
||||
@SerializedName("seasons") val seasons: List<Season>
|
||||
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline)
|
||||
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)
|
||||
@@ -10,10 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -23,10 +20,20 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@@ -154,4 +161,49 @@ fun SearchFab() {
|
||||
@Composable
|
||||
fun SearchFabPreview() {
|
||||
SearchFab()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MinLinesText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
minLines: Int = 1,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
val lineHeight = style.fontSize*4/3
|
||||
|
||||
Text(
|
||||
modifier = modifier
|
||||
.sizeIn(
|
||||
minHeight = with(LocalDensity.current) {
|
||||
(lineHeight * minLines).toDp()
|
||||
}
|
||||
),
|
||||
text = text,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
overflow = overflow,
|
||||
softWrap = softWrap,
|
||||
maxLines = maxLines,
|
||||
onTextLayout = onTextLayout,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
@@ -14,18 +14,13 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.rememberImagePainter
|
||||
@@ -35,11 +30,10 @@ import com.owenlejeune.tvtime.api.tmdb.DetailService
|
||||
import com.owenlejeune.tvtime.api.tmdb.MoviesService
|
||||
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
|
||||
import com.owenlejeune.tvtime.api.tmdb.TvService
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import com.owenlejeune.tvtime.extensions.dpToPx
|
||||
import com.owenlejeune.tvtime.ui.components.BackdropImage
|
||||
import com.owenlejeune.tvtime.ui.components.MinLinesText
|
||||
import com.owenlejeune.tvtime.ui.components.PosterItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -65,13 +59,6 @@ fun DetailView(
|
||||
}
|
||||
}
|
||||
|
||||
val images = remember { mutableStateOf<ImageCollection?>(null) }
|
||||
itemId?.let {
|
||||
if (images.value == null) {
|
||||
fetchImages(itemId, service, images)
|
||||
}
|
||||
}
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
ConstraintLayout(
|
||||
@@ -81,23 +68,15 @@ fun DetailView(
|
||||
.verticalScroll(state = scrollState)
|
||||
) {
|
||||
val (
|
||||
backButton,
|
||||
backdropImage,
|
||||
posterImage,
|
||||
titleText,
|
||||
contentColumn
|
||||
backButton, backdropImage, posterImage, titleText, contentColumn
|
||||
) = createRefs()
|
||||
|
||||
BackdropImage(
|
||||
modifier = Modifier
|
||||
.constrainAs(backdropImage) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.height(280.dp),
|
||||
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
|
||||
// collection = images.value
|
||||
Backdrop(
|
||||
modifier = Modifier.constrainAs(backdropImage) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
mediaItem = mediaItem
|
||||
)
|
||||
|
||||
PosterItem(
|
||||
@@ -110,151 +89,204 @@ fun DetailView(
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
text = mediaItem.value?.title ?: "",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.constrainAs(titleText) {
|
||||
bottom.linkTo(posterImage.bottom)
|
||||
start.linkTo(posterImage.end, margin = 8.dp)
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
}
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
.fillMaxWidth(.6f),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
softWrap = true
|
||||
TitleText(
|
||||
modifier = Modifier.constrainAs(titleText) {
|
||||
bottom.linkTo(posterImage.bottom)
|
||||
start.linkTo(posterImage.end, margin = 8.dp)
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
},
|
||||
mediaItem = mediaItem
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { appNavController.popBackStack() },
|
||||
modifier = Modifier
|
||||
.constrainAs(backButton) {
|
||||
top.linkTo(parent.top)//, 8.dp)
|
||||
start.linkTo(parent.start, 12.dp)
|
||||
bottom.linkTo(posterImage.top)
|
||||
}
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Black,
|
||||
Color.Transparent
|
||||
)
|
||||
BackButton(
|
||||
modifier = Modifier.constrainAs(backButton) {
|
||||
top.linkTo(parent.top)//, 8.dp)
|
||||
start.linkTo(parent.start, 12.dp)
|
||||
bottom.linkTo(posterImage.top)
|
||||
},
|
||||
appNavController = appNavController
|
||||
)
|
||||
|
||||
ContentColumn(
|
||||
modifier = Modifier.constrainAs(contentColumn) {
|
||||
top.linkTo(backdropImage.bottom, margin = 8.dp)
|
||||
},
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
service = service
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Backdrop(modifier: Modifier, mediaItem: MutableState<DetailedItem?>) {
|
||||
// val images = remember { mutableStateOf<ImageCollection?>(null) }
|
||||
// itemId?.let {
|
||||
// if (images.value == null) {
|
||||
// fetchImages(itemId, service, images)
|
||||
// }
|
||||
// }
|
||||
BackdropImage(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(280.dp),
|
||||
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
|
||||
// collection = images.value
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleText(modifier: Modifier, mediaItem: MutableState<DetailedItem?>) {
|
||||
Text(
|
||||
text = mediaItem.value?.title ?: "",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
.fillMaxWidth(.6f),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
softWrap = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackButton(modifier: Modifier, appNavController: NavController) {
|
||||
IconButton(
|
||||
onClick = { appNavController.popBackStack() },
|
||||
modifier = modifier
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Black,
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
.wrapContentSize()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
.wrapContentSize()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val castAndCrew = remember { mutableStateOf<CastAndCrew?>(null) }
|
||||
itemId?.let {
|
||||
if (castAndCrew.value == null) {
|
||||
fetchCastAndCrew(itemId, service, castAndCrew)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun ContentColumn(modifier: Modifier,
|
||||
itemId: Int?,
|
||||
mediaItem: MutableState<DetailedItem?>,
|
||||
service: DetailService
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
OverviewCard(mediaItem = mediaItem)
|
||||
|
||||
Column(
|
||||
CastCard(itemId = itemId, service = service)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverviewCard(mediaItem: MutableState<DetailedItem?>) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(bottom = 12.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
elevation = 8.dp
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
.constrainAs(contentColumn) {
|
||||
top.linkTo(backdropImage.bottom, margin = 8.dp)
|
||||
}
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(bottom = 12.dp),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
elevation = 8.dp
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
text = mediaItem.value?.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
text = mediaItem.value?.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
elevation = 8.dp
|
||||
) {
|
||||
LazyRow(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
items(castAndCrew.value?.cast?.size ?: 0) { i ->
|
||||
val castMember = castAndCrew.value!!.cast[i]
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(124.dp)
|
||||
.wrapContentHeight()
|
||||
.padding(end = 12.dp)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(width = 120.dp, height = 180.dp),
|
||||
painter = rememberImagePainter(
|
||||
data = TmdbUtils.getFullPersonImagePath(castMember),
|
||||
builder = {
|
||||
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
|
||||
placeholder(R.drawable.placeholder)
|
||||
}
|
||||
),
|
||||
contentDescription = ""
|
||||
)
|
||||
val nameLineHeight = MaterialTheme.typography.bodyMedium.fontSize*4/3
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp)
|
||||
.sizeIn(
|
||||
minHeight = with(LocalDensity.current) {
|
||||
(nameLineHeight * 2).toDp()
|
||||
}
|
||||
),
|
||||
text = castMember.name,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
lineHeight = nameLineHeight
|
||||
)
|
||||
val characterLineHeight = MaterialTheme.typography.bodySmall.fontSize*4/3
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(
|
||||
minHeight = with(LocalDensity.current) {
|
||||
(characterLineHeight * 2).toDp()
|
||||
}
|
||||
),
|
||||
text = castMember.character,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
lineHeight = characterLineHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun CastCard(itemId: Int?, service: DetailService) {
|
||||
val castAndCrew = remember { mutableStateOf<CastAndCrew?>(null) }
|
||||
itemId?.let {
|
||||
if (castAndCrew.value == null) {
|
||||
fetchCastAndCrew(itemId, service, castAndCrew)
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
elevation = 8.dp
|
||||
) {
|
||||
LazyRow(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
items(castAndCrew.value?.cast?.size ?: 0) { i ->
|
||||
val castMember = castAndCrew.value!!.cast[i]
|
||||
|
||||
CastCrewCard(person = castMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CastCrewCard(person: Person) {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(124.dp)
|
||||
.wrapContentHeight()
|
||||
.padding(end = 12.dp)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(width = 120.dp, height = 180.dp),
|
||||
painter = rememberImagePainter(
|
||||
data = TmdbUtils.getFullPersonImagePath(person),
|
||||
builder = {
|
||||
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
|
||||
placeholder(R.drawable.placeholder)
|
||||
}
|
||||
),
|
||||
contentDescription = ""
|
||||
)
|
||||
MinLinesText(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp),
|
||||
minLines = 2,
|
||||
text = person.name,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
MinLinesText(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
minLines = 2,
|
||||
text = when (person) {
|
||||
is CastMember -> person.character
|
||||
is CrewMember -> person.job
|
||||
else -> ""
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.getById(id)
|
||||
|
||||
Reference in New Issue
Block a user