diff --git a/app/src/main/java/movie/metropolis/app/di/FacadeModule.kt b/app/src/main/java/movie/metropolis/app/di/FacadeModule.kt index 2f0ef910..0efda8bf 100644 --- a/app/src/main/java/movie/metropolis/app/di/FacadeModule.kt +++ b/app/src/main/java/movie/metropolis/app/di/FacadeModule.kt @@ -40,6 +40,7 @@ import movie.metropolis.app.presentation.detail.MovieFacadeFromFeature import movie.metropolis.app.presentation.detail.MovieFacadeRating import movie.metropolis.app.presentation.detail.MovieFacadeReactive import movie.metropolis.app.presentation.detail.MovieFacadeRecover +import movie.metropolis.app.presentation.detail.MovieFacadeWithActors import movie.metropolis.app.presentation.home.HomeFacade import movie.metropolis.app.presentation.home.HomeFacadeFromFeature import movie.metropolis.app.presentation.listing.ListingFacade @@ -73,6 +74,7 @@ import movie.metropolis.app.presentation.ticket.TicketFacade import movie.metropolis.app.presentation.ticket.TicketFacadeCinemaFromFeature import movie.metropolis.app.presentation.ticket.TicketFacadeFilter import movie.metropolis.app.presentation.ticket.TicketFacadeMovieFromFeature +import movie.rating.ActorProvider import movie.rating.MetadataProvider @Module @@ -121,11 +123,13 @@ class FacadeModule { showings: EventShowingsFeature.Factory, detail: EventDetailFeature, favorite: FavoriteFeature, - rating: MetadataProvider + rating: MetadataProvider, + actors: ActorProvider ): MovieFacade.Factory = MovieFacade.Factory { var facade: MovieFacade facade = MovieFacadeFromFeature(it, showings, detail, favorite) facade = MovieFacadeRating(facade, rating) + facade = MovieFacadeWithActors(facade, actors) facade = MovieFacadeReactive(facade) facade = MovieFacadeRecover(facade) facade = MovieFacadeFilterable(facade) diff --git a/app/src/main/java/movie/metropolis/app/model/MovieDetailView.kt b/app/src/main/java/movie/metropolis/app/model/MovieDetailView.kt index a5e9c471..fafcde73 100644 --- a/app/src/main/java/movie/metropolis/app/model/MovieDetailView.kt +++ b/app/src/main/java/movie/metropolis/app/model/MovieDetailView.kt @@ -11,8 +11,8 @@ interface MovieDetailView { val releasedAt: String val duration: String val countryOfOrigin: String - val cast: List - val directors: List + val cast: List + val directors: List val description: String val availableFrom: String val poster: ImageView? diff --git a/app/src/main/java/movie/metropolis/app/model/PersonView.kt b/app/src/main/java/movie/metropolis/app/model/PersonView.kt new file mode 100644 index 00000000..84d68014 --- /dev/null +++ b/app/src/main/java/movie/metropolis/app/model/PersonView.kt @@ -0,0 +1,11 @@ +package movie.metropolis.app.model + +import androidx.compose.runtime.* + +@Immutable +interface PersonView { + val name: String + val popularity: Int + val image: String + val starredInMovies: Int +} \ No newline at end of file diff --git a/app/src/main/java/movie/metropolis/app/model/adapter/MovieDetailViewFromFeature.kt b/app/src/main/java/movie/metropolis/app/model/adapter/MovieDetailViewFromFeature.kt index 82236e53..fe175d39 100644 --- a/app/src/main/java/movie/metropolis/app/model/adapter/MovieDetailViewFromFeature.kt +++ b/app/src/main/java/movie/metropolis/app/model/adapter/MovieDetailViewFromFeature.kt @@ -4,6 +4,7 @@ import movie.core.model.Media import movie.core.model.MovieDetail import movie.metropolis.app.model.ImageView import movie.metropolis.app.model.MovieDetailView +import movie.metropolis.app.model.PersonView import movie.metropolis.app.model.VideoView import movie.metropolis.app.util.toStringComponents import java.text.DateFormat @@ -29,10 +30,10 @@ data class MovieDetailViewFromFeature( get() = movie.duration.toStringComponents() override val countryOfOrigin: String get() = movie.countryOfOrigin.orEmpty() - override val cast: List - get() = movie.cast.toList() - override val directors: List - get() = movie.directors.toList() + override val cast: List + get() = movie.cast.map(::PersonViewFromName) + override val directors: List + get() = movie.directors.map(::PersonViewFromName) override val description: String get() = movie.description override val availableFrom: String @@ -51,4 +52,12 @@ data class MovieDetailViewFromFeature( return movie } + data class PersonViewFromName( + override val name: String + ) : PersonView { + override val popularity: Int = -1 + override val image: String = "" + override val starredInMovies: Int = -1 + } + } \ No newline at end of file diff --git a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacade.kt b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacade.kt index 312aafa1..56dd24ba 100644 --- a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacade.kt +++ b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacade.kt @@ -8,7 +8,7 @@ import java.util.Date interface MovieFacade { - val movie: Flow> + val movie: Flow val favorite: Flow val availability: Flow diff --git a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeFromFeature.kt b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeFromFeature.kt index 19c6083b..3ed8b05f 100644 --- a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeFromFeature.kt +++ b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeFromFeature.kt @@ -1,6 +1,7 @@ package movie.metropolis.app.presentation.detail import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -25,18 +26,14 @@ class MovieFacadeFromFeature( private val model = MovieFromId(id) private val detailFlow = flow { - emit(detail.get(model)) + emit(detail.get(model).getOrThrow()) } - override val movie: Flow> = detailFlow.map { - it.map(::MovieDetailViewFromFeature) - } + override val movie: Flow = detailFlow.map(::MovieDetailViewFromFeature) override val favorite = flow { emit(favorites.isFavorite(model).getOrDefault(false)) } - override val availability = detailFlow.map { result -> - result.fold({ it.screeningFrom }, { Date() }).coerceAtLeast(Date()) - } + override val availability = detailFlow.map { it.screeningFrom }.catch { emit(Date()) } override fun showings( date: Date, @@ -54,7 +51,7 @@ class MovieFacadeFromFeature( } override suspend fun toggleFavorite() { - val detail = detailFlow.first().getOrNull() ?: return + val detail = detailFlow.first() val preview = MoviePreviewFromDetail(detail) favorites.toggle(preview) } diff --git a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRating.kt b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRating.kt index 211a67ee..0b938ac5 100644 --- a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRating.kt +++ b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRating.kt @@ -1,11 +1,10 @@ package movie.metropolis.app.presentation.detail import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map import movie.metropolis.app.model.MovieDetailView import movie.metropolis.app.model.adapter.MovieDetailViewWithRating -import movie.metropolis.app.util.flatMapResult import movie.rating.MetadataProvider import movie.rating.MovieDescriptor import movie.rating.MovieMetadata @@ -16,19 +15,22 @@ class MovieFacadeRating( private val rating: MetadataProvider ) : MovieFacade by origin { - override val movie: Flow> = origin.movie.flatMapResult { + private var metadata: MovieMetadata? = null + + override val movie: Flow = origin.movie.flatMapLatest { flow { - emit(it) + if (metadata == null) emit(it) val year = Calendar.getInstance().apply { time = it.base().releasedAt }[Calendar.YEAR] val descriptors = arrayOf( MovieDescriptor.Original(it.nameOriginal, year), MovieDescriptor.Local(it.name, year) ) - val rating = descriptors.fold(null as MovieMetadata?) { acc, it -> + val rating = metadata ?: descriptors.fold(null as MovieMetadata?) { acc, it -> acc ?: rating.get(it) } + metadata = rating emit(MovieDetailViewWithRating(it, rating)) - }.map(Result.Companion::success) + } } } \ No newline at end of file diff --git a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRecover.kt b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRecover.kt index 46ba9bde..f3ab19cb 100644 --- a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRecover.kt +++ b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeRecover.kt @@ -4,15 +4,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import movie.log.logSevere import movie.metropolis.app.model.CinemaBookingView -import movie.metropolis.app.model.MovieDetailView import java.util.Date class MovieFacadeRecover( private val origin: MovieFacade ) : MovieFacade by origin { - override val movie: Flow> = origin.movie - .catch { emit(Result.failure(it)) } override val favorite: Flow = origin.favorite .catch { emit(false) } override val availability: Flow = origin.availability diff --git a/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeWithActors.kt b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeWithActors.kt new file mode 100644 index 00000000..1dd1d275 --- /dev/null +++ b/app/src/main/java/movie/metropolis/app/presentation/detail/MovieFacadeWithActors.kt @@ -0,0 +1,81 @@ +package movie.metropolis.app.presentation.detail + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import movie.metropolis.app.model.MovieDetailView +import movie.metropolis.app.model.PersonView +import movie.rating.Actor +import movie.rating.ActorProvider + +class MovieFacadeWithActors( + private val origin: MovieFacade, + provider: ActorProvider +) : MovieFacade by origin { + + private val actors = ActorProviderCaching(provider) + + override val movie: Flow = origin.movie.flatMapLatest { + channelFlow { + val it = MovieDetailReplacing(it) + send(it) + val directors = it.directors.toMutableList() + val cast = it.cast.toMutableList() + val directorsMutex = Mutex() + val castMutex = Mutex() + for ((index, d) in directors.withIndex()) launch { + val actor = actors.runCatching { get(d.name) }.getOrNull() ?: return@launch + val out = directorsMutex.withLock { + directors[index] = PersonViewFromActor(actor) + directors.toList() + } + send(it.copy(directors = out)) + } + for ((index, c) in cast.withIndex()) launch { + val actor = actors.runCatching { get(c.name) }.getOrNull() ?: return@launch + val out = castMutex.withLock { + cast[index] = PersonViewFromActor(actor) + cast.toList() + } + send(it.copy(cast = out)) + } + } + } + + class ActorProviderCaching( + private val origin: ActorProvider + ) : ActorProvider { + private val cache = mutableMapOf() + private val mutex = Mutex() + override suspend fun get(query: String): Actor { + return cache[query] ?: mutex.withLock { cache[query] } ?: origin.get(query).also { + mutex.withLock { + cache[query] = it + } + } + } + } + + data class MovieDetailReplacing( + private val origin: MovieDetailView, + override val directors: List = origin.directors, + override val cast: List = origin.cast + ) : MovieDetailView by origin + + data class PersonViewFromActor( + private val actor: Actor + ) : PersonView { + override val name: String + get() = actor.name + override val popularity: Int + get() = actor.popularity + override val image: String + get() = actor.image + override val starredInMovies: Int + get() = actor.movies.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/movie/metropolis/app/screen/detail/MovieScreen.kt b/app/src/main/java/movie/metropolis/app/screen/detail/MovieScreen.kt index f6383c37..a1387ae8 100644 --- a/app/src/main/java/movie/metropolis/app/screen/detail/MovieScreen.kt +++ b/app/src/main/java/movie/metropolis/app/screen/detail/MovieScreen.kt @@ -34,6 +34,7 @@ import movie.metropolis.app.model.CinemaView import movie.metropolis.app.model.Filter import movie.metropolis.app.model.ImageView import movie.metropolis.app.model.MovieDetailView +import movie.metropolis.app.model.PersonView import movie.metropolis.app.model.VideoView import movie.metropolis.app.presentation.Loadable import movie.metropolis.app.presentation.map @@ -359,8 +360,8 @@ class MovieDetailViewProvider : CollectionPreviewParameterProvider = listOf("Foo Bar", "Bar Foo-Foo"), - override val directors: List = listOf("Foofoo Barbar"), + override val cast: List = listOf(Person("Foo Bar"), Person("Bar Foo-Foo")), + override val directors: List = listOf(Person("Foofoo Barbar")), override val description: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam vel finibus augue. Praesent porta, nibh rhoncus ultrices tempus, metus lacus facilisis lorem, id venenatis nisl mi non massa. Vestibulum eu ipsum leo. Mauris et sagittis tortor. Fusce dictum cursus quam in ornare. Curabitur posuere ligula sem, et tincidunt lorem commodo vitae. Fusce mollis elementum dignissim. Fusce suscipit massa maximus metus gravida, vitae posuere sem semper. Nullam auctor venenatis elementum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus nibh sem, volutpat nec egestas convallis, ultricies quis massa. Duis quis placerat neque, eu bibendum arcu. ", override val availableFrom: String = "23. 4. 2022", override val poster: ImageView? = null, @@ -370,6 +371,13 @@ class MovieDetailViewProvider : CollectionPreviewParameterProvider 0) it.popularity.toString() else "n/a") }, + movieCount = { Text(if (it.starredInMovies > 0) it.starredInMovies.toString() else "n/a") }, + color = state.palette.color ) } } @@ -162,14 +161,13 @@ fun MovieScreen( flingBehavior = rememberSnapFlingBehavior(state) ) { items(movie.cast) { - val state = - rememberPaletteImageState(url = "https://image.tmdb.org/t/p/w500/80DH2zWgZiXHehH7TLe6HKDldyl.jpg") + val state = rememberPaletteImageState(url = it.image) ActorRow( - color = state.palette.color, image = { Image(state) }, - name = { Text(it) }, - popularity = { Text("n/a") }, - movieCount = { Text("n/a") } + name = { Text(it.name) }, + popularity = { Text(if (it.popularity > 0) it.popularity.toString() else "n/a") }, + movieCount = { Text(if (it.starredInMovies > 0) it.starredInMovies.toString() else "n/a") }, + color = state.palette.color ) } } diff --git a/app/src/main/java/movie/metropolis/app/screen2/movie/MovieViewModel.kt b/app/src/main/java/movie/metropolis/app/screen2/movie/MovieViewModel.kt index 81163964..37d25f98 100644 --- a/app/src/main/java/movie/metropolis/app/screen2/movie/MovieViewModel.kt +++ b/app/src/main/java/movie/metropolis/app/screen2/movie/MovieViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.mapNotNull import movie.core.UserDataFeature import movie.metropolis.app.presentation.detail.MovieFacade import movie.metropolis.app.screen.Route @@ -39,7 +38,7 @@ class MovieViewModel private constructor( args.upcoming ) - val movie = facade.movie.mapNotNull { it.getOrNull() } + val movie = facade.movie .retainStateIn(viewModelScope, null) } \ No newline at end of file diff --git a/app/src/main/java/movie/metropolis/app/screen2/movie/component/ActorRow.kt b/app/src/main/java/movie/metropolis/app/screen2/movie/component/ActorRow.kt index 25da70a3..dcf9b086 100644 --- a/app/src/main/java/movie/metropolis/app/screen2/movie/component/ActorRow.kt +++ b/app/src/main/java/movie/metropolis/app/screen2/movie/component/ActorRow.kt @@ -33,7 +33,6 @@ fun ActorRow( movieCount: @Composable () -> Unit, modifier: Modifier = Modifier, color: Color = Theme.color.container.background, - contentColor: Color = Theme.color.content.background, imageOffset: PaddingValues = PaddingValues(16.dp), imageSize: DpSize = DpSize(64.dp, 64.dp), imagePadding: Dp = 8.dp, @@ -123,7 +122,7 @@ fun ActorRow( Row( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Text("Known for") + Text("No. of movies") ProvideTextStyle(LocalTextStyle.current.copy(fontWeight = FontWeight.Bold)) { CompositionLocalProvider(LocalContentColor provides colorVariant.copy(1f)) { movieCount() @@ -203,7 +202,7 @@ private fun ActorRowPreview() = PreviewLayout { }, name = { Text("Brie Larson") }, popularity = { Text("139") }, - movieCount = { Text("4 movies") }, + movieCount = { Text("4") }, color = Color.Magenta ) } \ No newline at end of file