mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
move full actor credits list to separate screen
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user