move full actor credits list to separate screen

This commit is contained in:
Owen LeJeune
2023-06-26 21:45:26 -04:00
parent ef11a88a90
commit ca878e3577
18 changed files with 538 additions and 122 deletions

View File

@@ -0,0 +1,48 @@
package com.owenlejeune.tvtime.api
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class DateTypeAdapter: TypeAdapter<Date>() {
companion object {
private val acceptedDateFormats: List<String> = listOf(
"yyyy-MM-dd",
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
)
}
override fun read(jrIn: JsonReader): Date? {
if (jrIn.peek() == JsonToken.NULL) {
jrIn.nextNull()
throw Exception("JSON must not be null")
}
val dateFields = jrIn.nextString()
if (dateFields.isEmpty()) {
return null
}
for (dateFormat in acceptedDateFormats) {
try {
val formatter = SimpleDateFormat(dateFormat, Locale.getDefault())
return formatter.parse(dateFields) ?: throw Exception("Parsed date cannot be null")
} catch (e: Exception) {
continue
}
}
throw Exception("No accepted date format to match date string \"$dateFields\"")
}
override fun write(jrOut: JsonWriter, value: Date?) {
value?.let {
jrOut.value(it.toString())
} ?: run {
jrOut.nullValue()
}
}
}

View File

@@ -0,0 +1,25 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCast
import com.owenlejeune.tvtime.utils.types.MediaViewType
class DetailCastDeserializer: BaseDeserializer<DetailCast>() {
override fun processJson(obj: JsonObject): DetailCast {
if (obj.has(MediaViewType.JSON_KEY)) {
val typeStr = obj.get(MediaViewType.JSON_KEY).asString
return when (gson.fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> gson.fromJson(obj.toString(), MovieCast::class.java)
MediaViewType.TV -> gson.fromJson(obj.toString(), TvCast::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
}
throw JsonParseException("JSON object has no property ${MediaViewType.JSON_KEY}")
}
}

View File

@@ -0,0 +1,25 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrew
import com.owenlejeune.tvtime.utils.types.MediaViewType
class DetailCrewDeserializer: BaseDeserializer<DetailCrew>() {
override fun processJson(obj: JsonObject): DetailCrew {
if (obj.has(MediaViewType.JSON_KEY)) {
val typeStr = obj.get(MediaViewType.JSON_KEY).asString
return when (gson.fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> gson.fromJson(obj.toString(), MovieCrew::class.java)
MediaViewType.TV -> gson.fromJson(obj.toString(), TvCrew::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
}
throw JsonParseException("JSON object has no property ${MediaViewType.JSON_KEY}")
}
}

View File

@@ -0,0 +1,23 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.utils.types.MediaViewType
import java.util.Date
abstract class CastCrew(
@SerializedName("id") val id: Int,
@SerializedName("backdrop_path") val backdropPath: String?,
@SerializedName("genre_ids") val genreIds: List<Int>,
@SerializedName("original_language") val originalLanguage: String,
@SerializedName("original_title", alternate = ["original_name"]) val originalTitle: String,
@SerializedName("overview") val overview: String,
@SerializedName("popularity") val popularity: Float,
@SerializedName("poster_path") val posterPath: String?,
@SerializedName("release_date", alternate = ["first_air_date"]) val releaseDate: Date?,
@SerializedName("title", alternate = ["name"]) val title: String,
@SerializedName("vote_average") val voteAverage: Float,
@SerializedName("vote_count") val voteCount: Int,
@SerializedName("credit_id") val creditId: String,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("media_type") val mediaType: MediaViewType
)

View File

@@ -2,16 +2,66 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.utils.types.MediaViewType
import java.util.Date
class DetailCast(
@SerializedName("id") val id: Int,
@SerializedName("episode_count") val episodeCount: Int,
@SerializedName("overview") val overview: String,
@SerializedName("name", alternate = ["title"]) val name: String,
@SerializedName("media_type") val mediaType: MediaViewType,
@SerializedName("poster_path") val posterPath: String?,
@SerializedName("first_air_date") val firstAirDate: String,
@SerializedName("character") val character: String,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("release_date") val releaseDate: String
)
abstract class DetailCast(
id: Int,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
isAdult: Boolean,
mediaType: MediaViewType,
@SerializedName("character") val character: String
): CastCrew(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity,
posterPath, releaseDate, title, voteAverage, voteCount, creditId, isAdult, mediaType)
class MovieCast(
id: Int,
isAdult: Boolean,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
character: String,
@SerializedName("video") val isVideo: Boolean,
@SerializedName("order") val order: Int
): DetailCast(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity,
posterPath, releaseDate, title, voteAverage, voteCount, creditId, isAdult, MediaViewType.MOVIE, character)
class TvCast(
id: Int,
isAdult: Boolean,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
character: String,
@SerializedName("origin_country") val originCountry: List<String>,
@SerializedName("episode_count") val episodeCount: Int
): DetailCast(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity,
posterPath, releaseDate, title, voteAverage, voteCount, creditId, isAdult, MediaViewType.TV, character)

View File

@@ -2,18 +2,68 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.utils.types.MediaViewType
import java.util.Date
class DetailCrew(
@SerializedName("id") val id: Int,
abstract class DetailCrew(
id: Int,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
isAdult: Boolean,
mediaType: MediaViewType,
@SerializedName("department") val department: String,
@SerializedName("episode_count") val episodeCount: Int,
@SerializedName("job") val job: String,
@SerializedName("overview") val overview: String,
@SerializedName("name") val name: String?,
@SerializedName("media_type") val mediaType: MediaViewType,
@SerializedName("first_air_date") val firstAirDate: String,
@SerializedName("poster_path") val posterPath: String,
@SerializedName("title") val title: String?,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("release_date") val releaseDate: String
)
): CastCrew(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity,
posterPath, releaseDate, title, voteAverage, voteCount, creditId, isAdult, mediaType)
class MovieCrew(
id: Int,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
isAdult: Boolean,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
department: String,
job: String,
@SerializedName("video") val isVideo: Boolean
): DetailCrew(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity, posterPath,
releaseDate, title, voteAverage, voteCount, creditId, isAdult, MediaViewType.MOVIE, department, job)
class TvCrew(
id: Int,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
isAdult: Boolean,
originalTitle: String,
overview: String,
popularity: Float,
posterPath: String?,
releaseDate: Date?,
title: String,
voteAverage: Float,
voteCount: Int,
creditId: String,
department: String,
job: String,
@SerializedName("original_country") val originalCountry: List<String>,
@SerializedName("episode_count") val episodeCount: Int
): DetailCrew(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity, posterPath,
releaseDate, title, voteAverage, voteCount, creditId, isAdult, MediaViewType.TV, department, job)

View File

@@ -2,6 +2,7 @@ package com.owenlejeune.tvtime.di.modules
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.google.gson.TypeAdapter
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.*
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
@@ -13,9 +14,13 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.AccountStatesDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCastDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCrewDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.KnownForDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
@@ -29,6 +34,7 @@ import com.owenlejeune.tvtime.ui.viewmodel.SettingsViewModel
import com.owenlejeune.tvtime.utils.ResourceUtils
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import java.util.Date
val networkModule = module {
single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() }
@@ -58,18 +64,21 @@ val networkModule = module {
single { AuthenticationV4Service() }
single { ListV4Service() }
single<Map<Class<*>, JsonDeserializer<*>>> {
single<Map<Class<*>, Any>> {
mapOf(
ListItem::class.java to ListItemDeserializer(),
KnownFor::class.java to KnownForDeserializer(),
SortableSearchResult::class.java to SortableSearchResultDeserializer(),
AccountStates::class.java to AccountStatesDeserializer()
AccountStates::class.java to AccountStatesDeserializer(),
DetailCast::class.java to DetailCastDeserializer(),
DetailCrew::class.java to DetailCrewDeserializer(),
Date::class.java to DateTypeAdapter()
)
}
single {
GsonBuilder().apply {
get<Map<Class<*>, JsonDeserializer<*>>>().forEach { des ->
get<Map<Class<*>, Any>>().forEach { des ->
registerTypeAdapter(des.key, des.value)
}
}.create()

View File

@@ -0,0 +1,10 @@
package com.owenlejeune.tvtime.extensions
import java.util.Calendar
import java.util.Date
fun Date.getCalendarYear(): Int {
return Calendar.getInstance().apply {
time = this@getCalendarYear
}.get(Calendar.YEAR)
}

View File

@@ -6,4 +6,16 @@ fun <T> List<T>.lastOr(provider: () -> T): T {
} else {
provider()
}
}
fun <T> List<T>.bringToFront(predicate: (T) -> Boolean): List<T> {
val orig = toMutableList()
val frontItems = emptyList<T>().toMutableList()
forEach { i ->
if (predicate(i)) {
frontItems.add(i)
orig.remove(i)
}
}
return frontItems.plus(orig)
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -172,23 +173,43 @@ fun TwoLineImageTextCard(
elevation = 0.dp,
overrideShowTitle = false
)
MinLinesText(
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp),
minLines = 2,
text = title,
minLines = 2,
maxLines = 2,
color = titleTextColor,
style = MaterialTheme.typography.bodyMedium
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis
)
subtitle?.let {
MinLinesText(
modifier = Modifier.fillMaxWidth(),
minLines = 2,
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = subtitleTextColor
)
}
Text(
modifier = Modifier.fillMaxWidth(),
minLines = 2,
maxLines = 2,
text = subtitle ?: "",
style = MaterialTheme.typography.bodySmall,
color = subtitleTextColor,
overflow = TextOverflow.Ellipsis
)
// MinLinesText(
// modifier = Modifier
// .fillMaxWidth()
// .padding(top = 5.dp),
// minLines = 2,
// text = title,
// color = titleTextColor,
// style = MaterialTheme.typography.bodyMedium
// )
// subtitle?.let {
// MinLinesText(
// modifier = Modifier.fillMaxWidth(),
// minLines = 2,
// text = subtitle,
// style = MaterialTheme.typography.bodySmall,
// color = subtitleTextColor
// )
// }
}
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -51,7 +52,8 @@ fun DetailHeader(
backdropContentDescription: String? = null,
posterContentDescription: String? = null,
rating: Float? = null,
pagerState: PagerState? = null
pagerState: PagerState? = null,
elevation: Dp = 20.dp
) {
ConstraintLayout(modifier = modifier
.fillMaxWidth()
@@ -90,7 +92,7 @@ fun DetailHeader(
},
url = posterUrl,
title = posterContentDescription,
elevation = 20.dp,
elevation = elevation,
overrideShowTitle = false,
enabled = false
)
@@ -208,29 +210,6 @@ fun BackdropGallery(
}
}
@Composable
fun RatingView(
progress: Float,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(CircleShape)
.size(60.dp)
.background(color = MaterialTheme.colorScheme.surfaceVariant)
) {
RatingRing(
modifier = Modifier.padding(5.dp),
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
progress = progress,
textSize = 14.sp,
ringColor = MaterialTheme.colorScheme.primary,
ringStrokeWidth = 4.dp,
size = 50.dp
)
}
}
@Composable
fun ExternalIdsArea(
externalIds: ExternalIds,

View File

@@ -24,7 +24,6 @@ import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.utils.types.MediaViewType
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MediaResultCard(
appNavController: NavController,
@@ -34,13 +33,14 @@ fun MediaResultCard(
posterPath: Any?,
title: String,
additionalDetails: List<String>,
modifier: Modifier = Modifier,
rating: Float? = null
) {
Card(
shape = RoundedCornerShape(10.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
modifier = Modifier
.background(color = MaterialTheme.colorScheme.surface)
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
modifier = modifier.then(Modifier
.fillMaxWidth()
.clickable(
onClick = {
@@ -49,6 +49,7 @@ fun MediaResultCard(
)
}
)
)
) {
Box(
modifier = Modifier.height(112.dp)

View File

@@ -451,8 +451,8 @@ fun RatingRing(
progress: Float = 0f,
size: Dp = 60.dp,
ringStrokeWidth: Dp = 4.dp,
ringColor: Color = MaterialTheme.colorScheme.primary,
trackColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
ringColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
trackColor: Color = MaterialTheme.colorScheme.tertiary,
textColor: Color = Color.White,
textSize: TextUnit = 14.sp
) {
@@ -478,6 +478,29 @@ fun RatingRing(
}
}
@Composable
fun RatingView(
progress: Float,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(CircleShape)
.size(60.dp)
.background(color = MaterialTheme.colorScheme.secondary)
) {
RatingRing(
modifier = Modifier.padding(5.dp),
textColor = MaterialTheme.colorScheme.onSecondary,
progress = progress,
textSize = 14.sp,
ringStrokeWidth = 4.dp,
size = 50.dp
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RoundedTextField(

View File

@@ -22,6 +22,7 @@ import com.owenlejeune.tvtime.ui.screens.AboutScreen
import com.owenlejeune.tvtime.ui.screens.AccountScreen
import com.owenlejeune.tvtime.ui.screens.HomeScreen
import com.owenlejeune.tvtime.ui.screens.KeywordResultsScreen
import com.owenlejeune.tvtime.ui.screens.KnownForScreen
import com.owenlejeune.tvtime.ui.screens.ListDetailScreen
import com.owenlejeune.tvtime.ui.screens.MediaDetailScreen
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
@@ -176,6 +177,16 @@ fun AppNavigationHost(
appNavController = appNavController
)
}
composable(
route = AppNavItem.KnownForView.route.plus("/{${NavConstants.ID_KEY}}"),
arguments = listOf(
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
)
) { navBackStackEntry ->
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
KnownForScreen(appNavController = appNavController, id = id)
}
}
}
@@ -203,5 +214,8 @@ sealed class AppNavItem(val route: String) {
object KeywordsView: AppNavItem("keywords_route") {
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
}
object KnownForView: AppNavItem("known_for_route") {
fun withArgs(id: Int) = route.plus("/$id")
}
}

View File

@@ -0,0 +1,143 @@
package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCast
import com.owenlejeune.tvtime.extensions.bringToFront
import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KnownForScreen(
appNavController: NavController,
id: Int
) {
val mainViewModel = viewModel<MainViewModel>()
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
val peopleMap = remember { mainViewModel.peopleMap }
val person = peopleMap[id]
val topAppBarScrollState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults
.topAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.primary
),
title = { Text(text = person?.name ?: "") },
navigationIcon = {
IconButton(onClick = { appNavController.popBackStack() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_description_back_button),
tint = MaterialTheme.colorScheme.primary
)
}
}
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
val castCreditsMap = remember { mainViewModel.peopleCastMap }
val crewCreditsMap = remember { mainViewModel.peopleCrewMap }
val castCredits = castCreditsMap[id]?.sortedByDescending { it.releaseDate }?.bringToFront { it.releaseDate == null } ?: emptyList()
val crewCredits = crewCreditsMap[id]?.sortedByDescending { it.releaseDate }?.bringToFront { it.releaseDate == null } ?: emptyList()
var actorSelected by remember { mutableStateOf(true) }
val items = if (actorSelected) castCredits else crewCredits
LazyColumn(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Row(
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SelectableTextChip(
selected = actorSelected,
onSelected = { actorSelected = true },
text = stringResource(id = R.string.actor_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
)
SelectableTextChip(
selected = !actorSelected,
onSelected = { actorSelected = false },
text = stringResource(id = R.string.production_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
)
}
}
items(items) { item ->
val additionalDetails = emptyList<String>().toMutableList()
when (item) {
is MovieCast -> additionalDetails.add(stringResource(id = R.string.cast_character_template, item.character))
is TvCast -> additionalDetails.add(stringResource(id = R.string.cast_tv_character_template, item.character, item.episodeCount))
is DetailCrew -> additionalDetails.add(stringResource(id = R.string.crew_template, item.job))
}
val releaseYear = item.releaseDate?.getCalendarYear() ?: ""
MediaResultCard(
appNavController = appNavController,
mediaViewType = item.mediaType,
id = item.id,
backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath),
posterPath = TmdbUtils.getFullPosterPath(item.posterPath),
title = "${item.title}$releaseYear",
additionalDetails = additionalDetails,
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -30,6 +31,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi
@@ -45,6 +47,9 @@ import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import java.lang.Integer.min
private const val TAG = "PeopleDetailScreen"
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable
@@ -102,11 +107,10 @@ fun PersonDetailScreen(
) {
DetailHeader(
posterUrl = TmdbUtils.getFullPersonImagePath(person?.profilePath),
posterContentDescription = person?.profilePath
posterContentDescription = person?.profilePath,
elevation = 0.dp
)
BiographyCard(person = person)
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap }
val externalIds = externalIdsMap[personId]
externalIds?.let {
@@ -116,8 +120,10 @@ fun PersonDetailScreen(
)
}
BiographyCard(person = person)
val creditsMap = remember { mainViewModel.peopleCastMap }
val credits = creditsMap[personId]
val credits = creditsMap[personId] ?: emptyList()
ContentCard(
title = stringResource(R.string.known_for_label)
@@ -129,11 +135,11 @@ fun PersonDetailScreen(
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
items(credits?.size ?: 0) { i ->
val content = credits!![i]
items(min(credits.size, 15)) { i ->
val content = credits[i]
TwoLineImageTextCard(
title = content.name,
title = content.title,
titleTextColor = MaterialTheme.colorScheme.primary,
subtitle = content.character,
modifier = Modifier
@@ -148,54 +154,17 @@ fun PersonDetailScreen(
)
}
}
}
val crewMap = remember { mainViewModel.peopleCrewMap }
val crewCredits = crewMap[personId]
val departments = crewCredits?.map { it.department }?.toSet() ?: emptySet()
if (departments.isNotEmpty()) {
ContentCard(title = stringResource(R.string.also_known_for_label)) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
departments.forEach { department ->
Text(text = department, color = MaterialTheme.colorScheme.primary)
LazyRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
val jobsInDepartment = crewCredits!!.filter { it.department == department }
items(jobsInDepartment.size) { i ->
val content = jobsInDepartment[i]
val title = if (content.mediaType == MediaViewType.MOVIE) {
content.title ?: ""
} else {
content.name ?: ""
}
TwoLineImageTextCard(
title = title,
subtitle = content.job,
modifier = Modifier
.width(124.dp)
.wrapContentHeight(),
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
onItemClicked = {
appNavController.navigate(
AppNavItem.DetailView.withArgs(content.mediaType, content.id)
)
}
)
}
}
Text(
text = stringResource(id = R.string.expand_see_all),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
.clickable {
appNavController.navigate(AppNavItem.KnownForView.withArgs(personId))
}
}
}
)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.utils
import android.nfc.FormatException
import androidx.compose.ui.text.intl.Locale
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
@@ -16,6 +17,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvContentRatings
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
import java.text.SimpleDateFormat
import java.util.Date
object TmdbUtils {
@@ -251,4 +253,9 @@ object TmdbUtils {
return origFormat.parse(inDate)?.let { outFormat.format(it) }
}
fun toDate(releaseDate: String): Date {
val format = SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
return format.parse(releaseDate) ?: throw FormatException("Expected date format \"yyyy-MM-dd\", got $releaseDate")
}
}

View File

@@ -35,6 +35,7 @@
<string name="expandable_see_more">See more</string>
<string name="expandable_see_less">See less</string>
<string name="expand_see_all">See all</string>
<string name="search_placeholder">Search %1$s</string>
@@ -230,4 +231,10 @@
<string name="streaming_label">Streaming</string>
<string name="rent_label">Rent</string>
<string name="buy_label">Buy</string>
<string name="actor_label">Actor</string>
<string name="production_label">Production</string>
<string name="cast_character_template">as %1$s</string>
<string name="cast_tv_character_template">as %1$s (%2$d eps.)</string>
<string name="crew_template">… %1$s</string>
</resources>