Skip to content

Commit

Permalink
Added favorite screen
Browse files Browse the repository at this point in the history
  • Loading branch information
diareuse committed Nov 19, 2023
1 parent b18e638 commit 89b32aa
Show file tree
Hide file tree
Showing 16 changed files with 399 additions and 12 deletions.
18 changes: 18 additions & 0 deletions app/src/main/java/movie/metropolis/app/di/FacadeModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ 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.favorite.FavoriteFacade
import movie.metropolis.app.presentation.favorite.FavoriteFacadeFromFeature
import movie.metropolis.app.presentation.favorite.FavoriteFacadeRating
import movie.metropolis.app.presentation.favorite.FavoriteFacadeReactive
import movie.metropolis.app.presentation.home.HomeFacade
import movie.metropolis.app.presentation.home.HomeFacadeFromFeature
import movie.metropolis.app.presentation.listing.ListingFacade
Expand Down Expand Up @@ -283,4 +287,18 @@ class FacadeModule {
}
}

@Provides
@Reusable
fun favorite(
favorite: FavoriteFeature,
detail: EventDetailFeature,
rating: MetadataProvider
): FavoriteFacade {
var out: FavoriteFacade
out = FavoriteFacadeFromFeature(favorite, detail)
out = FavoriteFacadeReactive(out)
out = FavoriteFacadeRating(out, rating, detail)
return out
}

}
4 changes: 2 additions & 2 deletions app/src/main/java/movie/metropolis/app/model/MovieView.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package movie.metropolis.app.model

import androidx.compose.runtime.*
import movie.core.model.MoviePreview
import movie.core.model.Movie

@Immutable
interface MovieView {
Expand All @@ -20,5 +20,5 @@ interface MovieView {
val posterLarge: ImageView?
val video: VideoView?

fun getBase(): MoviePreview
fun getBase(): Movie
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package movie.metropolis.app.model.adapter

import movie.core.model.Media
import movie.core.model.Movie
import movie.core.model.MoviePreview
import movie.metropolis.app.model.ImageView
import movie.metropolis.app.model.MovieView
Expand Down Expand Up @@ -52,5 +53,5 @@ data class MovieViewFromFeature(
.firstOrNull()
?.let(::VideoViewFromFeature)

override fun getBase() = movie
override fun getBase(): Movie = movie
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package movie.metropolis.app.model.adapter

import movie.core.model.Media
import movie.core.model.Movie
import movie.core.model.MovieFavorite
import movie.metropolis.app.model.ImageView
import movie.metropolis.app.model.MovieView
import movie.metropolis.app.model.VideoView
import movie.metropolis.app.util.toStringComponents
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale

data class MovieViewFromMovie(
private val movie: MovieFavorite,
private val media: Iterable<Media>
) : MovieView {
private val yearFormat = SimpleDateFormat("yyyy", Locale.getDefault())
private val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM)
private val images
get() = media
.asSequence()
.filterIsInstance<Media.Image>()
.sortedByDescending { it.height * it.width }

override val id: String
get() = movie.movie.id
override val name: String
get() = movie.movie.name
override val releasedAt: String
get() = yearFormat.format(movie.movie.releasedAt)
override val duration: String
get() = movie.movie.duration.toStringComponents()
override val availableFrom: String
get() = dateFormat.format(movie.movie.screeningFrom)
override val directors: List<String>
get() = emptyList()
override val cast: List<String>
get() = emptyList()
override val countryOfOrigin: String
get() = ""
override val favorite: Boolean
get() = true
override val rating: String?
get() = null
override val poster: ImageView?
get() = images.middleOrNull()?.let { ImageViewFromFeature(it) }
override val posterLarge: ImageView?
get() = images.firstOrNull()?.let { ImageViewFromFeature(it) }
override val video: VideoView?
get() = media
.asSequence()
.filterIsInstance<Media.Video>()
.firstOrNull()
?.let(::VideoViewFromFeature)

override fun getBase(): Movie {
return movie.movie
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package movie.metropolis.app.model.adapter

import movie.core.model.MoviePreview
import movie.core.model.Movie
import movie.core.model.MovieReference
import movie.metropolis.app.model.ImageView
import movie.metropolis.app.model.MovieView
Expand Down Expand Up @@ -52,7 +52,7 @@ data class MovieViewFromReference(
}
}

override fun getBase(): MoviePreview {
throw NotImplementedError()
override fun getBase(): Movie {
return ref
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package movie.metropolis.app.presentation.favorite

import kotlinx.coroutines.flow.Flow
import movie.metropolis.app.model.MovieView

interface FavoriteFacade {

fun get(): Flow<List<MovieView>>
suspend fun remove(view: MovieView)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package movie.metropolis.app.presentation.favorite

import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import movie.core.EventDetailFeature
import movie.core.FavoriteFeature
import movie.core.model.MovieDetail
import movie.metropolis.app.model.MovieView
import movie.metropolis.app.model.adapter.MovieViewFromMovie

class FavoriteFacadeFromFeature(
private val favorite: FavoriteFeature,
private val detail: EventDetailFeature
) : FavoriteFacade {

private val cache = mutableMapOf<String, MovieDetail>()

override fun get() = channelFlow {
val input = favorite.getAll()
.getOrDefault(emptyList())
.map { MovieViewFromMovie(it, emptyList()) }
.toMutableList()
val mutex = Mutex()
for ((index, it) in input.withIndex()) launch {
val detail = cache.getOrPut(it.id) { detail.get(it.getBase()).getOrThrow() }
val out = mutex.withLock {
input[index] = it.copy(media = detail.media)
input.toList()
}
send(out)
}
}

override suspend fun remove(view: MovieView) {
favorite.toggle(view.getBase())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package movie.metropolis.app.presentation.favorite

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.core.EventDetailFeature
import movie.core.model.Movie
import movie.metropolis.app.model.MovieView
import movie.metropolis.app.model.adapter.MovieViewWithRating
import movie.rating.MetadataProvider
import movie.rating.MovieDescriptor
import movie.rating.MovieMetadata
import java.util.Calendar

class FavoriteFacadeRating(
private val origin: FavoriteFacade,
private val rating: MetadataProvider,
private val detail: EventDetailFeature
) : FavoriteFacade by origin {

private val cache = mutableMapOf<String, MovieMetadata>()

override fun get(): Flow<List<MovieView>> = origin.get().flatMapLatest { withRating(it) }

private fun withRating(items: List<MovieView>) = channelFlow {
if (cache.isEmpty()) send(items)
val output = items.toMutableList()
val lock = Mutex()
for ((index, movie) in output.withIndex()) launch {
val base = movie.getBase()
val rating = getRating(base) ?: return@launch
val out = lock.withLock {
output[index] = MovieViewWithRating(movie, rating)
output.toList()
}
send(out)
}
}

private suspend fun getRating(movie: Movie): MovieMetadata? = cache.getOrPut(movie.id) {
val descriptors = detail.get(movie).map {
val year = Calendar.getInstance().apply { time = it.releasedAt }[Calendar.YEAR]
arrayOf(
MovieDescriptor.Original(it.originalName, year),
MovieDescriptor.Local(it.name, year)
)
}.getOrNull() ?: return null
descriptors.fold(null as MovieMetadata?) { acc, it ->
acc ?: rating.get(it)
} ?: return@getRating null
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package movie.metropolis.app.presentation.favorite

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import movie.metropolis.app.model.MovieView

class FavoriteFacadeReactive(
private val origin: FavoriteFacade
) : FavoriteFacade {

private val trigger = Channel<Unit>()

override fun get() = trigger.receiveAsFlow()
.onStart { emit(Unit) }
.flatMapLatest { origin.get() }

override suspend fun remove(view: MovieView) {
origin.remove(view)
trigger.send(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import movie.core.EventDetailFeature
import movie.core.model.MoviePreview
import movie.core.model.Movie
import movie.metropolis.app.model.Genre
import movie.metropolis.app.model.MovieView
import movie.metropolis.app.model.adapter.MovieViewWithRating
Expand All @@ -27,7 +27,7 @@ class ListingFacadeActionWithRating(
override val groups = origin.groups.flatMapResult { withRating(it) }

private fun withRating(items: Map<Genre, List<MovieView>>) = channelFlow {
send(items)
if (cache.isEmpty()) send(items)
val output = items.mapValues { (_, it) -> it.toMutableList() }
val writeLock = Mutex()
val movies = items.asSequence()
Expand All @@ -48,7 +48,7 @@ class ListingFacadeActionWithRating(
}
}.map(Result.Companion::success)

private suspend fun getRating(movie: MoviePreview): MovieMetadata? = cache.getOrPut(movie.id) {
private suspend fun getRating(movie: Movie): MovieMetadata? = cache.getOrPut(movie.id) {
val descriptors = detail.get(movie).map {
val year = Calendar.getInstance().apply { time = it.releasedAt }[Calendar.YEAR]
arrayOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ fun Navigation(
booking(navController)
upcoming(navController)
editor(navController)
favorite(navController)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import movie.metropolis.app.R
import movie.metropolis.app.feature.location.rememberLocation
Expand All @@ -37,6 +38,7 @@ import movie.metropolis.app.screen.booking.BookingViewModel
import movie.metropolis.app.screen.booking.component.rememberMultiChildPagerState
import movie.metropolis.app.screen.cinema.CinemasScreen
import movie.metropolis.app.screen.cinema.CinemasViewModel
import movie.metropolis.app.screen.favorite.FavoriteViewModel
import movie.metropolis.app.screen.home.HomeScreen
import movie.metropolis.app.screen.home.HomeState
import movie.metropolis.app.screen.home.HomeViewModel
Expand Down Expand Up @@ -298,7 +300,7 @@ fun NavGraphBuilder.home(
contentPadding = padding,
onClickCard = { showCard = true },
onClickEdit = { navController.navigate(Route.UserEditor()) },
onClickFavorite = {},
onClickFavorite = { navController.navigate(Route.Favorite()) },
onClickSettings = { navController.navigate(Route.Settings()) }
)
}
Expand Down Expand Up @@ -526,4 +528,38 @@ fun NavGraphBuilder.booking(
)
}
}
}

fun NavGraphBuilder.favorite(
navController: NavHostController
) = composable(
route = Route.Favorite.route,
deepLinks = Route.Favorite.deepLinks
) {
val viewModel = hiltViewModel<FavoriteViewModel>()
val movies by viewModel.items.collectAsState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
topBar = {
CollapsingTopAppBar(
title = { Text(stringResource(R.string.favorite_movies)) },
scrollBehavior = scrollBehavior,
navigationIcon = {
IconButton(onClick = navController::navigateUp) {
Icon(painterResource(R.drawable.ic_back), null)
}
}
)
}
) { innerPadding ->
ListingScreen(
contentPadding = innerPadding,
state = rememberLazyStaggeredGridState(),
movies = movies,
promotions = persistentListOf(),
onClick = { navController.navigate(Route.Movie(it.id, true)) },
onFavoriteClick = viewModel::remove,
connection = scrollBehavior.nestedScrollConnection
)
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/movie/metropolis/app/screen/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,8 @@ sealed class Route(
operator fun invoke() = route
}

data object Favorite : Route("movies/favorites") {
operator fun invoke() = route
}

}

0 comments on commit 89b32aa

Please sign in to comment.