mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-12 06:42:45 -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.google.gson.annotations.SerializedName
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class DetailCast(
|
abstract class DetailCast(
|
||||||
@SerializedName("id") val id: Int,
|
id: Int,
|
||||||
@SerializedName("episode_count") val episodeCount: Int,
|
backdropPath: String?,
|
||||||
@SerializedName("overview") val overview: String,
|
genreIds: List<Int>,
|
||||||
@SerializedName("name", alternate = ["title"]) val name: String,
|
originalLanguage: String,
|
||||||
@SerializedName("media_type") val mediaType: MediaViewType,
|
originalTitle: String,
|
||||||
@SerializedName("poster_path") val posterPath: String?,
|
overview: String,
|
||||||
@SerializedName("first_air_date") val firstAirDate: String,
|
popularity: Float,
|
||||||
@SerializedName("character") val character: String,
|
posterPath: String?,
|
||||||
@SerializedName("adult") val isAdult: Boolean,
|
releaseDate: Date?,
|
||||||
@SerializedName("release_date") val releaseDate: String
|
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.google.gson.annotations.SerializedName
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class DetailCrew(
|
abstract class DetailCrew(
|
||||||
@SerializedName("id") val id: Int,
|
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("department") val department: String,
|
||||||
@SerializedName("episode_count") val episodeCount: Int,
|
|
||||||
@SerializedName("job") val job: String,
|
@SerializedName("job") val job: String,
|
||||||
@SerializedName("overview") val overview: String,
|
): CastCrew(id, backdropPath, genreIds, originalLanguage, originalTitle, overview, popularity,
|
||||||
@SerializedName("name") val name: String?,
|
posterPath, releaseDate, title, voteAverage, voteCount, creditId, isAdult, mediaType)
|
||||||
@SerializedName("media_type") val mediaType: MediaViewType,
|
|
||||||
@SerializedName("first_air_date") val firstAirDate: String,
|
class MovieCrew(
|
||||||
@SerializedName("poster_path") val posterPath: String,
|
id: Int,
|
||||||
@SerializedName("title") val title: String?,
|
backdropPath: String?,
|
||||||
@SerializedName("adult") val isAdult: Boolean,
|
genreIds: List<Int>,
|
||||||
@SerializedName("release_date") val releaseDate: String
|
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.GsonBuilder
|
||||||
import com.google.gson.JsonDeserializer
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
import com.owenlejeune.tvtime.BuildConfig
|
import com.owenlejeune.tvtime.BuildConfig
|
||||||
import com.owenlejeune.tvtime.api.*
|
import com.owenlejeune.tvtime.api.*
|
||||||
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
|
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.SearchService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
|
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.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.KnownForDeserializer
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer
|
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.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.KnownFor
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
|
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 com.owenlejeune.tvtime.utils.ResourceUtils
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
val networkModule = module {
|
val networkModule = module {
|
||||||
single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() }
|
single { if (BuildConfig.DEBUG) DebugHttpClient() else ProdHttpClient() }
|
||||||
@@ -58,18 +64,21 @@ val networkModule = module {
|
|||||||
single { AuthenticationV4Service() }
|
single { AuthenticationV4Service() }
|
||||||
single { ListV4Service() }
|
single { ListV4Service() }
|
||||||
|
|
||||||
single<Map<Class<*>, JsonDeserializer<*>>> {
|
single<Map<Class<*>, Any>> {
|
||||||
mapOf(
|
mapOf(
|
||||||
ListItem::class.java to ListItemDeserializer(),
|
ListItem::class.java to ListItemDeserializer(),
|
||||||
KnownFor::class.java to KnownForDeserializer(),
|
KnownFor::class.java to KnownForDeserializer(),
|
||||||
SortableSearchResult::class.java to SortableSearchResultDeserializer(),
|
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 {
|
single {
|
||||||
GsonBuilder().apply {
|
GsonBuilder().apply {
|
||||||
get<Map<Class<*>, JsonDeserializer<*>>>().forEach { des ->
|
get<Map<Class<*>, Any>>().forEach { des ->
|
||||||
registerTypeAdapter(des.key, des.value)
|
registerTypeAdapter(des.key, des.value)
|
||||||
}
|
}
|
||||||
}.create()
|
}.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)
|
||||||
|
}
|
||||||
@@ -7,3 +7,15 @@ fun <T> List<T>.lastOr(provider: () -> T): T {
|
|||||||
provider()
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -172,23 +173,43 @@ fun TwoLineImageTextCard(
|
|||||||
elevation = 0.dp,
|
elevation = 0.dp,
|
||||||
overrideShowTitle = false
|
overrideShowTitle = false
|
||||||
)
|
)
|
||||||
MinLinesText(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 5.dp),
|
.padding(top = 5.dp),
|
||||||
minLines = 2,
|
|
||||||
text = title,
|
text = title,
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 2,
|
||||||
color = titleTextColor,
|
color = titleTextColor,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
subtitle?.let {
|
Text(
|
||||||
MinLinesText(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
minLines = 2,
|
minLines = 2,
|
||||||
text = subtitle,
|
maxLines = 2,
|
||||||
|
text = subtitle ?: "",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = subtitleTextColor
|
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.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -51,7 +52,8 @@ fun DetailHeader(
|
|||||||
backdropContentDescription: String? = null,
|
backdropContentDescription: String? = null,
|
||||||
posterContentDescription: String? = null,
|
posterContentDescription: String? = null,
|
||||||
rating: Float? = null,
|
rating: Float? = null,
|
||||||
pagerState: PagerState? = null
|
pagerState: PagerState? = null,
|
||||||
|
elevation: Dp = 20.dp
|
||||||
) {
|
) {
|
||||||
ConstraintLayout(modifier = modifier
|
ConstraintLayout(modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -90,7 +92,7 @@ fun DetailHeader(
|
|||||||
},
|
},
|
||||||
url = posterUrl,
|
url = posterUrl,
|
||||||
title = posterContentDescription,
|
title = posterContentDescription,
|
||||||
elevation = 20.dp,
|
elevation = elevation,
|
||||||
overrideShowTitle = false,
|
overrideShowTitle = false,
|
||||||
enabled = 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
|
@Composable
|
||||||
fun ExternalIdsArea(
|
fun ExternalIdsArea(
|
||||||
externalIds: ExternalIds,
|
externalIds: ExternalIds,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import com.owenlejeune.tvtime.R
|
|||||||
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaResultCard(
|
fun MediaResultCard(
|
||||||
appNavController: NavController,
|
appNavController: NavController,
|
||||||
@@ -34,13 +33,14 @@ fun MediaResultCard(
|
|||||||
posterPath: Any?,
|
posterPath: Any?,
|
||||||
title: String,
|
title: String,
|
||||||
additionalDetails: List<String>,
|
additionalDetails: List<String>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
rating: Float? = null
|
rating: Float? = null
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
||||||
modifier = Modifier
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
.background(color = MaterialTheme.colorScheme.surface)
|
modifier = modifier.then(Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -49,6 +49,7 @@ fun MediaResultCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.height(112.dp)
|
modifier = Modifier.height(112.dp)
|
||||||
|
|||||||
@@ -451,8 +451,8 @@ fun RatingRing(
|
|||||||
progress: Float = 0f,
|
progress: Float = 0f,
|
||||||
size: Dp = 60.dp,
|
size: Dp = 60.dp,
|
||||||
ringStrokeWidth: Dp = 4.dp,
|
ringStrokeWidth: Dp = 4.dp,
|
||||||
ringColor: Color = MaterialTheme.colorScheme.primary,
|
ringColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
trackColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
|
trackColor: Color = MaterialTheme.colorScheme.tertiary,
|
||||||
textColor: Color = Color.White,
|
textColor: Color = Color.White,
|
||||||
textSize: TextUnit = 14.sp
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoundedTextField(
|
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.AccountScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.HomeScreen
|
import com.owenlejeune.tvtime.ui.screens.HomeScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.KeywordResultsScreen
|
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.ListDetailScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaDetailScreen
|
import com.owenlejeune.tvtime.ui.screens.MediaDetailScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
|
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
|
||||||
@@ -176,6 +177,16 @@ fun AppNavigationHost(
|
|||||||
appNavController = appNavController
|
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") {
|
object KeywordsView: AppNavItem("keywords_route") {
|
||||||
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
|
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
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -30,6 +31,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
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.ui.viewmodel.MainViewModel
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
|
import java.lang.Integer.min
|
||||||
|
|
||||||
|
private const val TAG = "PeopleDetailScreen"
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -102,11 +107,10 @@ fun PersonDetailScreen(
|
|||||||
) {
|
) {
|
||||||
DetailHeader(
|
DetailHeader(
|
||||||
posterUrl = TmdbUtils.getFullPersonImagePath(person?.profilePath),
|
posterUrl = TmdbUtils.getFullPersonImagePath(person?.profilePath),
|
||||||
posterContentDescription = person?.profilePath
|
posterContentDescription = person?.profilePath,
|
||||||
|
elevation = 0.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
BiographyCard(person = person)
|
|
||||||
|
|
||||||
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap }
|
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap }
|
||||||
val externalIds = externalIdsMap[personId]
|
val externalIds = externalIdsMap[personId]
|
||||||
externalIds?.let {
|
externalIds?.let {
|
||||||
@@ -116,8 +120,10 @@ fun PersonDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BiographyCard(person = person)
|
||||||
|
|
||||||
val creditsMap = remember { mainViewModel.peopleCastMap }
|
val creditsMap = remember { mainViewModel.peopleCastMap }
|
||||||
val credits = creditsMap[personId]
|
val credits = creditsMap[personId] ?: emptyList()
|
||||||
|
|
||||||
ContentCard(
|
ContentCard(
|
||||||
title = stringResource(R.string.known_for_label)
|
title = stringResource(R.string.known_for_label)
|
||||||
@@ -129,11 +135,11 @@ fun PersonDetailScreen(
|
|||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(credits?.size ?: 0) { i ->
|
items(min(credits.size, 15)) { i ->
|
||||||
val content = credits!![i]
|
val content = credits[i]
|
||||||
|
|
||||||
TwoLineImageTextCard(
|
TwoLineImageTextCard(
|
||||||
title = content.name,
|
title = content.title,
|
||||||
titleTextColor = MaterialTheme.colorScheme.primary,
|
titleTextColor = MaterialTheme.colorScheme.primary,
|
||||||
subtitle = content.character,
|
subtitle = content.character,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -148,58 +154,21 @@ fun PersonDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val crewMap = remember { mainViewModel.peopleCrewMap }
|
Text(
|
||||||
val crewCredits = crewMap[personId]
|
text = stringResource(id = R.string.expand_see_all),
|
||||||
val departments = crewCredits?.map { it.department }?.toSet() ?: emptySet()
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
if (departments.isNotEmpty()) {
|
fontSize = 12.sp,
|
||||||
ContentCard(title = stringResource(R.string.also_known_for_label)) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(start = 16.dp, bottom = 16.dp)
|
||||||
.wrapContentHeight()
|
.clickable {
|
||||||
.padding(12.dp),
|
appNavController.navigate(AppNavItem.KnownForView.withArgs(personId))
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.owenlejeune.tvtime.utils
|
package com.owenlejeune.tvtime.utils
|
||||||
|
|
||||||
|
import android.nfc.FormatException
|
||||||
import androidx.compose.ui.text.intl.Locale
|
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.AccountDetails
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
|
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.api.tmdb.api.v3.model.Video
|
||||||
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object TmdbUtils {
|
object TmdbUtils {
|
||||||
|
|
||||||
@@ -251,4 +253,9 @@ object TmdbUtils {
|
|||||||
return origFormat.parse(inDate)?.let { outFormat.format(it) }
|
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_more">See more</string>
|
||||||
<string name="expandable_see_less">See less</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>
|
<string name="search_placeholder">Search %1$s</string>
|
||||||
|
|
||||||
@@ -230,4 +231,10 @@
|
|||||||
<string name="streaming_label">Streaming</string>
|
<string name="streaming_label">Streaming</string>
|
||||||
<string name="rent_label">Rent</string>
|
<string name="rent_label">Rent</string>
|
||||||
<string name="buy_label">Buy</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user