diff --git a/app/src/main/java/com/london/app/MainActivity.kt b/app/src/main/java/com/london/app/MainActivity.kt index 9658cae6d..7b326ee3a 100644 --- a/app/src/main/java/com/london/app/MainActivity.kt +++ b/app/src/main/java/com/london/app/MainActivity.kt @@ -12,15 +12,16 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowInsetsControllerCompat import com.london.app.navigation.NavHostGraph import com.london.data.local.preference.readLanguageCode +import com.london.data.worker.MovieListSyncWorker import com.london.designsystem.theme.NovixTheme import com.london.domain.service.AppPreferencesService import com.london.presentation.localization.LocalizationManager @@ -39,6 +40,9 @@ class MainActivity : ComponentActivity() { @Inject lateinit var localizationManager: LocalizationManager + @Inject + lateinit var workManager: androidx.work.WorkManager + override fun attachBaseContext(newBase: Context) { val languageCode = readLanguageCode(newBase) val localeWrappedContext = newBase.wrapWithLocale(Locale.forLanguageTag(languageCode)) @@ -75,6 +79,8 @@ class MainActivity : ComponentActivity() { } } } + + MovieListSyncWorker.schedulePeriodicSync(workManager) } } diff --git a/app/src/main/java/com/london/app/NovixApplication.kt b/app/src/main/java/com/london/app/NovixApplication.kt index cc6c5cfc9..a0f792579 100644 --- a/app/src/main/java/com/london/app/NovixApplication.kt +++ b/app/src/main/java/com/london/app/NovixApplication.kt @@ -26,8 +26,7 @@ class NovixApplication : Application(), Configuration.Provider { super.onCreate() timberConfig() - WorkManager.initialize(this, workManagerConfiguration) - MovieListSyncWorker.schedulePeriodicSync(WorkManager.getInstance(applicationContext)) + WorkManager.initialize(context = this, configuration = workManagerConfiguration) } private fun timberConfig() { @@ -53,4 +52,4 @@ class NovixApplication : Application(), Configuration.Provider { } }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/london/app/navigation/NavHostGraph.kt b/app/src/main/java/com/london/app/navigation/NavHostGraph.kt index 3d2498d39..2ee91696d 100644 --- a/app/src/main/java/com/london/app/navigation/NavHostGraph.kt +++ b/app/src/main/java/com/london/app/navigation/NavHostGraph.kt @@ -17,7 +17,9 @@ import com.london.app.navigation.graph.mainNavGraph import com.london.app.navigation.graph.onboardingNavGraph import com.london.app.navigation.graph.splashNavGraph import com.london.designsystem.component.NavBar -import com.london.designsystem.component.Scaffold +import com.london.designsystem.snackbar.CustomSnackBarUI +import com.london.designsystem.snackbar.ScaffoldWithSnackBar +import com.london.designsystem.snackbar.SnackBarData import com.london.designsystem.theme.NovixTheme import com.london.presentation.navigation.LocalNavController import com.london.presentation.navigation.Screen.Account @@ -32,7 +34,7 @@ fun NavHostGraph() { val navBackStackEntry by navController.currentBackStackEntryAsState() val showBottomNav = navBackStackEntry.hasRoute(Home, Search, Categories, Lists(), Account) - Scaffold( + ScaffoldWithSnackBar( containerColor = NovixTheme.colors.surface, bottomBar = { AnimatedVisibility( @@ -48,7 +50,8 @@ fun NavHostGraph() { navBackStackEntry = navBackStackEntry ) } - } + }, + snackBar = { data: SnackBarData -> CustomSnackBarUI(data = data) } ) { innerPadding -> CompositionLocalProvider(LocalNavController provides navController) { NavHost( diff --git a/app/src/main/java/com/london/app/navigation/graph/mainNavGraph.kt b/app/src/main/java/com/london/app/navigation/graph/mainNavGraph.kt index 0fae16824..d15abc88a 100644 --- a/app/src/main/java/com/london/app/navigation/graph/mainNavGraph.kt +++ b/app/src/main/java/com/london/app/navigation/graph/mainNavGraph.kt @@ -116,7 +116,10 @@ fun NavGraphBuilder.mainNavGraph( onNavigateBack = ::navigateUp ) } - composable { ReviewsScreen(onNavigateBack = ::navigateUp) } + + composable { + ReviewsScreen(onNavigateBack = ::navigateUp) + } } } @@ -138,8 +141,8 @@ private fun NavGraphBuilder.homeNavGraph(navController: NavHostController) = composable { TopRatedScreen( onNavigateBack = ::navigateUp, - onNaviagteToMovieDetalis = ::navigateToMovieDetails, - onNaviagteToTvShowDetalis = ::navigateToTvShowDetails + onNavigateToMovieDetails = ::navigateToMovieDetails, + onNavigateToTvShowDetails = ::navigateToTvShowDetails ) } } diff --git a/data/src/main/java/com/london/data/local/database/dao/customLists/SyncMetadataDao.kt b/data/src/main/java/com/london/data/local/database/dao/customLists/SyncMetadataDao.kt index 8b000a55f..99e552add 100644 --- a/data/src/main/java/com/london/data/local/database/dao/customLists/SyncMetadataDao.kt +++ b/data/src/main/java/com/london/data/local/database/dao/customLists/SyncMetadataDao.kt @@ -15,7 +15,10 @@ interface SyncMetadataDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSyncMetadata(metadata: SyncMetadataLocal) + @Query("DELETE FROM sync_metadata") + suspend fun clearAll() + companion object { const val MOVIE_LISTS_SYNC_KEY = "movie_lists_sync" } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/london/data/local/source/customLists/CustomMovieListLocalDataSourceImpl.kt b/data/src/main/java/com/london/data/local/source/customLists/CustomMovieListLocalDataSourceImpl.kt index 96860693d..93b05b282 100644 --- a/data/src/main/java/com/london/data/local/source/customLists/CustomMovieListLocalDataSourceImpl.kt +++ b/data/src/main/java/com/london/data/local/source/customLists/CustomMovieListLocalDataSourceImpl.kt @@ -84,7 +84,7 @@ class CustomMovieListLocalDataSourceImpl @Inject constructor( movieListDao.updateItemCount(listId = listId, itemCount = itemCount) override suspend fun shouldRefreshCache(): Boolean { - val metadata = syncMetadataDao.getSyncMetadata(SYNC_KEY) + val metadata = syncMetadataDao.getSyncMetadata(SyncMetadataDao.MOVIE_LISTS_SYNC_KEY) return metadata == null || !metadata.isSuccess || (System.currentTimeMillis() - metadata.lastSyncTime) > CACHE_VALIDITY_MS @@ -93,7 +93,7 @@ class CustomMovieListLocalDataSourceImpl @Inject constructor( override suspend fun markCacheRefreshed(success: Boolean) { syncMetadataDao.insertSyncMetadata( SyncMetadataLocal( - syncKey = SYNC_KEY, + syncKey = SyncMetadataDao.MOVIE_LISTS_SYNC_KEY, lastSyncTime = System.currentTimeMillis(), isSuccess = success ) @@ -103,10 +103,10 @@ class CustomMovieListLocalDataSourceImpl @Inject constructor( override suspend fun clearAllCache() { membershipDao.clearAll() movieListDao.clearAll() + syncMetadataDao.clearAll() } private companion object { const val CACHE_VALIDITY_MS = 30 * 60 * 1000L // 30 minutes - const val SYNC_KEY = SyncMetadataDao.MOVIE_LISTS_SYNC_KEY } } diff --git a/data/src/main/java/com/london/data/remote/model/ApiConstants.kt b/data/src/main/java/com/london/data/remote/model/ApiConstants.kt index 336e22653..13d9e4385 100644 --- a/data/src/main/java/com/london/data/remote/model/ApiConstants.kt +++ b/data/src/main/java/com/london/data/remote/model/ApiConstants.kt @@ -57,6 +57,6 @@ object ApiConstants { const val GET_LIST_DETAILS_PATH = "list/{list_id}" const val ADD_MOVIE_TO_LIST_PATH = "list/{list_id}/add_item" const val REMOVE_MOVIE_FROM_LIST_PATH = "list/{list_id}/remove_item" - const val GET_ACCOUNT_LISTS_PATH = "account/{account_id}/lists" + const val GET_ACCOUNT_LISTS_PATH = "account/0/lists" } diff --git a/data/src/main/java/com/london/data/repository/authentication/AuthenticationRepositoryImpl.kt b/data/src/main/java/com/london/data/repository/authentication/AuthenticationRepositoryImpl.kt index 0a2e259ed..a2fd1e7bd 100644 --- a/data/src/main/java/com/london/data/repository/authentication/AuthenticationRepositoryImpl.kt +++ b/data/src/main/java/com/london/data/repository/authentication/AuthenticationRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.london.data.repository.authentication import com.london.data.local.preference.AuthenticationPreferences +import com.london.data.local.source.customLists.CustomMovieListLocalDataSource import com.london.data.mapper.account.toEntity import com.london.data.remote.model.authentication.RequestTokenResponse import com.london.data.remote.model.authentication.SessionResponse @@ -13,7 +14,8 @@ import javax.inject.Inject class AuthenticationRepositoryImpl @Inject constructor( private val authenticationRemoteDataSource: AuthenticationRemoteDataSource, private val accountRemoteDataSource: AccountRemoteDataSource, - private val authenticationPreferences: AuthenticationPreferences + private val authenticationPreferences: AuthenticationPreferences, + private val customMovieListLocalDataSource: CustomMovieListLocalDataSource ) : AuthenticationRepository { override suspend fun login(username: String, password: String): Boolean { @@ -51,6 +53,7 @@ class AuthenticationRepositoryImpl @Inject constructor( if (sessionId != null && !authenticationPreferences.isGuestMode()) { authenticationRemoteDataSource.deleteSession(sessionId).getOrThrow() } + customMovieListLocalDataSource.clearAllCache() authenticationPreferences.clearAuthentication() return true } diff --git a/data/src/main/java/com/london/data/repository/list/CustomMovieListRepositoryImpl.kt b/data/src/main/java/com/london/data/repository/list/CustomMovieListRepositoryImpl.kt index 40414b443..1ab96cf89 100644 --- a/data/src/main/java/com/london/data/repository/list/CustomMovieListRepositoryImpl.kt +++ b/data/src/main/java/com/london/data/repository/list/CustomMovieListRepositoryImpl.kt @@ -7,15 +7,17 @@ import com.london.data.local.source.customLists.CustomMovieListLocalDataSource import com.london.data.mapper.list.toEntity import com.london.data.mapper.list.toLocal import com.london.data.mapper.search.toEntity +import com.london.data.remote.model.list.CreateCustomListResponse +import com.london.data.remote.model.list.CustomMovieListResponse import com.london.data.remote.source.list.CustomMovieListsRemoteDataSource import com.london.data.utils.CrashReporter -import com.london.data.utils.fetchAndSync +import com.london.data.utils.isTrue import com.london.data.utils.orZero import com.london.domain.entity.movie.Movie import com.london.domain.entity.movie.MovieList import com.london.domain.entity.shared.PagedFetchResponse -import com.london.domain.service.AppPreferencesService import com.london.domain.repository.CustomMovieListRepository +import com.london.domain.service.AppPreferencesService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -32,53 +34,40 @@ class CustomMovieListRepositoryImpl @Inject constructor( private val syncMutex = Mutex() override suspend fun isMovieListed(movieId: Int, forceRefresh: Boolean): Boolean { - return fetchAndSync( - cacheBlock = if (!forceRefresh) { - { localDataSource.isMovieListed(movieId) } - } else null, - networkBlock = { - refreshMovieListCacheIfNecessary(forceRefresh = true) - localDataSource.isMovieListed(movieId) - }, - crashReporter = crashReporter - ) + refreshMovieListCacheIfNecessary(forceRefresh) + return localDataSource.isMovieListed(movieId) } - override fun isMovieListedFlow(movieId: Int): Flow = - localDataSource.isMovieListedFlow(movieId) + override fun isMovieListedFlow(movieId: Int): Flow = localDataSource.isMovieListedFlow(movieId) + override suspend fun getMovieListIds(movieId: Int, forceRefresh: Boolean): List { - return fetchAndSync( - cacheBlock = if (!forceRefresh) { - { localDataSource.getMovieListIds(movieId) } - } else null, - networkBlock = { - refreshMovieListCacheIfNecessary(forceRefresh = true) - localDataSource.getMovieListIds(movieId) - }, - crashReporter = crashReporter - ) + refreshMovieListCacheIfNecessary(forceRefresh) + return localDataSource.getMovieListIds(movieId) } override fun getMovieListIdsFlow(movieId: Int): Flow> = localDataSource.getMovieListIdsFlow(movieId) + override suspend fun deleteMovieList(id: Int): Boolean { - return try { - val success = remoteDataSource.delete( + val success = deleteListRemotely(id) + if (success) { + localDataSource.removeMovieListCache(id) + } + return success + } + + private suspend fun deleteListRemotely(id: Int): Boolean { + return runCatching { + remoteDataSource.delete( listId = id, sessionId = authenticationPreferences.getSessionId() ).isSuccess - - if (success) { - localDataSource.removeMovieListCache(id) - } - - success - } catch (e: Exception) { - crashReporter.logException(e) - false - } + }.onFailure { + crashReporter.logException(it) + throw it + }.isSuccess } override suspend fun getAllListedMovieIds(): List { @@ -86,116 +75,87 @@ class CustomMovieListRepositoryImpl @Inject constructor( return localDataSource.getAllListedMovieIds() } -// override fun getAllListedMovieIdsFlow(): Flow> = -// localDataSource.getAllListedMovieIdsFlow() + override fun getAllListedMovieIdsFlow(): Flow> = localDataSource.getAllListedMovieIdsFlow() + override suspend fun createMovieList(name: String): Boolean { - return try { - val result = remoteDataSource.create( - name = name, - sessionId = authenticationPreferences.getSessionId(), - languageCode = preferencesService.appLanguage.value.code - ) + val response = createListRemotely(name) + cacheNewlyCreatedList(response, name) + return true + } - if (result.isSuccess) { - result.getOrNull()?.let { response -> - response.id?.let { - localDataSource.addMovieListCache( - MovieListLocal( - id = response.id, - name = name, - description = "", - itemCount = 0 - ) - ) - } - } - true - } else { - false - } - } catch (e: Exception) { - crashReporter.logException(e) - false + private suspend fun createListRemotely(name: String): CreateCustomListResponse { + return remoteDataSource.create( + name = name, + sessionId = authenticationPreferences.getSessionId(), + languageCode = preferencesService.appLanguage.value.code + ).getOrThrow() + } + + private suspend fun cacheNewlyCreatedList(response: CreateCustomListResponse, name: String) { + response.id?.let { listId -> + localDataSource.addMovieListCache( + MovieListLocal( + id = listId, + name = name, + description = "", + itemCount = 0 + ) + ) } } override suspend fun getMovieListName(listId: Int): String { - return fetchAndSync( - cacheBlock = { localDataSource.getMovieList(listId)?.name }, - networkBlock = { - remoteDataSource.getDetails(listId = listId, page = 1).getOrThrow().name.orEmpty() - }, - syncBlock = { name -> - localDataSource.getMovieList(listId)?.let { existing -> - localDataSource.addMovieListCache(existing.copy(name = name)) - } - }, - crashReporter = crashReporter - ) + refreshMovieListCacheIfNecessary(forceRefresh = false) + return localDataSource.getMovieList(listId)?.name.orEmpty() } override suspend fun getMovieLists(pageNumber: Int): PagedFetchResponse { - return if (localDataSource.shouldRefreshCache()) { - val response = remoteDataSource.getAllMovieLists( - page = pageNumber, - sessionId = authenticationPreferences.getSessionId() - ).getOrThrow() + refreshMovieListCacheIfNecessary(forceRefresh = false) + return buildPagedMovieListResponse(pageNumber) + } - val localLists = response.items.map { it.toLocal() } - localDataSource.cacheMovieListsMetadata(localLists) + private suspend fun buildPagedMovieListResponse(pageNumber: Int): PagedFetchResponse { + val allLists = localDataSource.getAllUserLists() + val itemsPerPage = 20 + val startIndex = (pageNumber - 1) * itemsPerPage + val endIndex = minOf(startIndex + itemsPerPage, allLists.size) - PagedFetchResponse( - currentPage = response.currentPage, - items = response.items.map { it.toEntity() }, - totalPages = response.totalPages, - totalItems = response.totalItems - ) + val items = if (startIndex < allLists.size) { + allLists.subList(startIndex, endIndex).map { it.toEntity() } } else { - val allLists = localDataSource.getAllUserLists() - val itemsPerPage = 20 - val startIndex = (pageNumber - 1) * itemsPerPage - val endIndex = minOf(startIndex + itemsPerPage, allLists.size) - - val items = if (startIndex < allLists.size) { - allLists.subList(startIndex, endIndex).map { it.toEntity() } - } else { - emptyList() - } - - PagedFetchResponse( - currentPage = pageNumber, - items = items, - totalPages = (allLists.size + itemsPerPage - 1) / itemsPerPage, - totalItems = allLists.size - ) + emptyList() } + + return PagedFetchResponse( + currentPage = pageNumber, + items = items, + totalPages = (allLists.size + itemsPerPage - 1) / itemsPerPage, + totalItems = allLists.size + ) } override suspend fun addMovieToList(listId: Int, movieId: Int): Boolean { - return try { - val result = remoteDataSource.addMovieToList( - listId = listId, - movieId = movieId, - sessionId = authenticationPreferences.getSessionId() - ) + addMovieRemotely(listId, movieId) + updateLocalCacheAfterAddingMovie(listId, movieId) + return true + } - if (result.isSuccess) { - localDataSource.addMovieToListCache(movieId, listId) + private suspend fun addMovieRemotely(listId: Int, movieId: Int) = + remoteDataSource.addMovieToList( + listId = listId, + movieId = movieId, + sessionId = authenticationPreferences.getSessionId() + ).getOrThrow() - localDataSource.getMovieList(listId)?.let { list -> - localDataSource.addMovieListCache( - list.copy(itemCount = list.itemCount + 1) - ) - } - true - } else { - false - } - } catch (e: Exception) { - crashReporter.logException(e) - false + private suspend fun updateLocalCacheAfterAddingMovie(listId: Int, movieId: Int) { + localDataSource.addMovieToListCache(movieId, listId) + + localDataSource.getMovieList(listId)?.let { list -> + localDataSource.addMovieListCache( + list.copy(itemCount = list.itemCount + 1) + ) } } @@ -207,6 +167,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( listId = listId, page = pageNumber ).getOrThrow() + return PagedFetchResponse( currentPage = pageNumber, items = response.items.orEmpty().map { it.toEntity() }, @@ -216,93 +177,115 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun removeMovieFromList(listId: Int, movieId: Int): Boolean { - return try { - val result = remoteDataSource.removeMovieFromList( - listId = listId, - movieId = movieId, - sessionId = authenticationPreferences.getSessionId() - ) + removeMovieRemotely(listId, movieId) + updateLocalCacheAfterRemovingMovie(listId, movieId) + return true + } - if (result.isSuccess) { - localDataSource.removeMovieFromListCache(movieId, listId) + private suspend fun removeMovieRemotely(listId: Int, movieId: Int) = + remoteDataSource.removeMovieFromList( + listId = listId, + movieId = movieId, + sessionId = authenticationPreferences.getSessionId() + ).getOrThrow() - localDataSource.getMovieList(listId)?.let { list -> - localDataSource.addMovieListCache( - list.copy(itemCount = maxOf(0, list.itemCount - 1)) - ) - } - true - } else { - false - } - } catch (e: Exception) { - crashReporter.logException(e) - false + private suspend fun updateLocalCacheAfterRemovingMovie(listId: Int, movieId: Int) { + localDataSource.removeMovieFromListCache(movieId, listId) + + localDataSource.getMovieList(listId)?.let { list -> + localDataSource.addMovieListCache( + list.copy(itemCount = maxOf(0, list.itemCount - 1)) + ) } } override suspend fun refreshMovieListCache() { syncMutex.withLock { - try { - val memberships = mutableListOf() - val lists = mutableListOf() - - var page = 1 - do { - val listsResponse = remoteDataSource.getAllMovieLists( - page = page, - sessionId = authenticationPreferences.getSessionId() - ).getOrThrow() - - lists.addAll(listsResponse.items.map { it.toLocal() }) - - for (list in listsResponse.items) { - if (list.id == null) continue - - var moviePage = 1 - do { - val moviesResponse = remoteDataSource.getDetails( - listId = list.id, - page = moviePage - ).getOrThrow() - - moviesResponse.items?.forEach { movie -> - if (movie.id == null) return@forEach - - memberships.add( - MovieListMembershipLocal( - movieId = movie.id, - listId = list.id - ) - ) - } - - moviePage++ - } while (moviePage <= MAX_PAGES && moviesResponse.items?.isNotEmpty() == true) - } - - page++ - } while (page <= listsResponse.totalPages) - - localDataSource.cacheMovieListsMetadata(lists) - localDataSource.cacheMovieListMemberships(memberships) + runCatching { + val (lists, memberships) = fetchAllListsAndMemberships() + cacheListsAndMemberships(lists, memberships) localDataSource.markCacheRefreshed(true) - - } catch (e: Exception) { + }.onFailure { localDataSource.markCacheRefreshed(false) - crashReporter.logException(e) - throw e + crashReporter.logException(it) + throw it } } } + private suspend fun fetchAllListsAndMemberships(): Pair, List> { + val lists = mutableListOf() + val memberships = mutableListOf() + + var page = 1 + do { + val response = remoteDataSource.getAllMovieLists( + page = page, + sessionId = authenticationPreferences.getSessionId() + ).getOrThrow() + + lists.addAll(response.items.map { it.toLocal() }) + memberships.addAll(fetchMembershipsForListsPage(response.items)) + + page++ + } while (page <= response.totalPages) + + return Pair(lists, memberships) + } + + private suspend fun fetchMembershipsForListsPage( + listsPage: List + ): List { + val memberships = mutableListOf() + + listsPage.forEach { list -> + list.id?.let { listId -> memberships.addAll(fetchAllMembershipsForList(listId)) } + } + + return memberships + } + + private suspend fun fetchAllMembershipsForList(listId: Int): List { + val memberships = mutableListOf() + var moviePage = 1 + + do { + val listDetailsResponse = remoteDataSource.getDetails( + listId = listId, + page = moviePage + ).getOrThrow() + + val items = listDetailsResponse.items.orEmpty() + + items.forEach { movie -> + movie.id?.let { movieId -> + memberships.add(MovieListMembershipLocal(movieId = movieId, listId = listId)) + } + } + + moviePage++ + } while (moviePage <= MAX_PAGES && items.isNotEmpty()) + + return memberships + } + + private suspend fun cacheListsAndMemberships( + lists: List, + memberships: List + ) { + localDataSource.clearAllCache() + localDataSource.cacheMovieListsMetadata(lists) + localDataSource.cacheMovieListMemberships(memberships) + } + private suspend fun refreshMovieListCacheIfNecessary(forceRefresh: Boolean) { - if (forceRefresh || localDataSource.shouldRefreshCache()) refreshMovieListCache() + if (forceRefresh || localDataSource.shouldRefreshCache()) { + refreshMovieListCache() + } } private companion object { - const val MAX_PAGES = 10 + const val MAX_PAGES = 100 } } - diff --git a/data/src/test/java/com/london/data/repository/AuthenticationRepositoryImplTest.kt b/data/src/test/java/com/london/data/repository/AuthenticationRepositoryImplTest.kt index d8fe3d0b5..db158de01 100644 --- a/data/src/test/java/com/london/data/repository/AuthenticationRepositoryImplTest.kt +++ b/data/src/test/java/com/london/data/repository/AuthenticationRepositoryImplTest.kt @@ -1,6 +1,7 @@ package com.london.data.repository import com.london.data.local.preference.AuthenticationPreferences +import com.london.data.local.source.customLists.CustomMovieListLocalDataSource import com.london.data.remote.model.account.AccountInfoResponse import com.london.data.remote.model.authentication.DeleteSessionResponse import com.london.data.remote.model.authentication.GuestSessionResponse @@ -27,14 +28,16 @@ class AuthenticationRepositoryImplTest { private val authRemoteDataSource: AuthenticationRemoteDataSource = mockk() private val accountRemoteDataSource: AccountRemoteDataSource = mockk() private val authenticationPreferences: AuthenticationPreferences = mockk(relaxed = true) + private val customMovieListLocalDataSource: CustomMovieListLocalDataSource = mockk(relaxed = true) @Before fun setUp() { repository = AuthenticationRepositoryImpl( - authRemoteDataSource, - accountRemoteDataSource, - authenticationPreferences + authenticationRemoteDataSource = authRemoteDataSource, + accountRemoteDataSource = accountRemoteDataSource, + authenticationPreferences = authenticationPreferences, + customMovieListLocalDataSource = customMovieListLocalDataSource ) } @@ -309,4 +312,4 @@ class AuthenticationRepositoryImplTest { private const val EXPIRES_AT = "123" private const val ACCOUNT_ID = 12345 } -} \ No newline at end of file +} diff --git a/data/src/test/java/com/london/data/repository/list/CustomMovieListRepositoryImplTest.kt b/data/src/test/java/com/london/data/repository/list/CustomMovieListRepositoryImplTest.kt index 5a7c0d5df..8d05c7ed4 100644 --- a/data/src/test/java/com/london/data/repository/list/CustomMovieListRepositoryImplTest.kt +++ b/data/src/test/java/com/london/data/repository/list/CustomMovieListRepositoryImplTest.kt @@ -5,6 +5,7 @@ import com.london.data.local.model.customLists.MovieListLocal import com.london.data.local.preference.AuthenticationPreferences import com.london.data.local.source.customLists.CustomMovieListLocalDataSource import com.london.data.remote.exception.ResponseException +import com.london.data.remote.model.list.CreateCustomListResponse import com.london.data.remote.model.list.ListDetailsResponse import com.london.data.remote.model.search.SearchMovieRemote import com.london.data.remote.source.list.CustomMovieListsRemoteDataSource @@ -51,7 +52,7 @@ class CustomMovieListRepositoryImplTest { } @Test - fun `deleteMovieList should return true and clear cache on remote success`() = runTest { + fun `deleteMovieList returns true and removes cache on success`() = runTest { // Given coEvery { remoteDataSource.delete(any(), any()) } returns Result.success(mockk()) val listId = 1 @@ -61,51 +62,63 @@ class CustomMovieListRepositoryImplTest { // Then assertThat(result).isTrue() - coVerify { remoteDataSource.delete(listId, "session_123") } coVerify { localDataSource.removeMovieListCache(listId) } } @Test - fun `deleteMovieList should return false and not touch cache on remote failure`() = runTest { + fun `deleteMovieList throws and does not remove cache on failure`() = runTest { // Given - coEvery { remoteDataSource.delete(any(), any()) } returns Result.failure(Exception("error")) + coEvery { remoteDataSource.delete(any(), any()) } throws Exception("error") val listId = 1 - // When - val result = repository.deleteMovieList(listId) + // When & Then + assertThrows { + repository.deleteMovieList(listId) + } - // Then - assertThat(result).isFalse() coVerify(exactly = 0) { localDataSource.removeMovieListCache(any()) } } @Test - fun `createMovieList should return false and not touch cache on remote failure`() = runTest { + fun `createMovieList returns true and adds cache on success`() = runTest { // Given - coEvery { - remoteDataSource.create( - any(), - any(), - any() - ) - } returns Result.failure(Exception("error")) + val name = "My List" + coEvery { remoteDataSource.create(any(), any(), any()) } returns Result.success( + CreateCustomListResponse(id = 1) + ) // When - val result = repository.createMovieList("My List") + val result = repository.createMovieList(name) // Then - assertThat(result).isFalse() - coVerify(exactly = 0) { localDataSource.markCacheRefreshed(any()) } + assertThat(result).isTrue() + coVerify { localDataSource.addMovieListCache(any()) } } @Test - fun `addMovieToList should return true and update cache on remote success`() = runTest { + fun `createMovieList throws and does not add cache on failure`() = runTest { + // Given + coEvery { remoteDataSource.create(any(), any(), any()) } returns Result.failure(Exception("error")) + + // When & Then + assertThrows { + repository.createMovieList("My List") + } + coVerify(exactly = 0) { localDataSource.addMovieListCache(any()) } + } + + @Test + fun `addMovieToList returns true and updates cache on success`() = runTest { // Given - coEvery { remoteDataSource.addMovieToList(any(), any(), any()) } returns Result.success( - mockk() - ) val listId = 1 val movieId = 100 + coEvery { remoteDataSource.addMovieToList(any(), any(), any()) } returns Result.success(mockk()) + coEvery { localDataSource.getMovieList(listId) } returns MovieListLocal( + id = listId, + name = "", + description = "", + itemCount = 0 + ) // When val result = repository.addMovieToList(listId, movieId) @@ -116,22 +129,46 @@ class CustomMovieListRepositoryImplTest { } @Test - fun `addMovieToList should return false and not touch cache on remote failure`() = runTest { + fun `addMovieToList throws and does not update cache on failure`() = runTest { // Given - coEvery { remoteDataSource.addMovieToList(any(), any(), any()) } returns Result.failure( - Exception("error") + coEvery { + remoteDataSource.addMovieToList( + any(), + any(), + any() + ) + } returns Result.failure(Exception("error")) + + // When & Then + assertThrows { + repository.addMovieToList(1, 100) + } + coVerify(exactly = 0) { localDataSource.addMovieToListCache(any(), any()) } + } + + @Test + fun `removeMovieFromList returns true and updates cache on success`() = runTest { + // Given + val listId = 1 + val movieId = 100 + coEvery { remoteDataSource.removeMovieFromList(any(), any(), any()) } returns Result.success(mockk()) + coEvery { localDataSource.getMovieList(listId) } returns MovieListLocal( + id = listId, + name = "", + description = "", + itemCount = 1 ) // When - val result = repository.addMovieToList(1, 100) + val result = repository.removeMovieFromList(listId, movieId) // Then - assertThat(result).isFalse() - coVerify(exactly = 0) { localDataSource.addMovieToListCache(any(), any()) } + assertThat(result).isTrue() + coVerify { localDataSource.removeMovieFromListCache(movieId, listId) } } @Test - fun `removeMovieFromList should return true and update cache on remote success`() = runTest { + fun `removeMovieFromList throws and does not update cache on failure`() = runTest { // Given coEvery { remoteDataSource.removeMovieFromList( @@ -139,57 +176,65 @@ class CustomMovieListRepositoryImplTest { any(), any() ) - } returns Result.success(mockk()) - val listId = 1 - val movieId = 100 - - // When - val result = repository.removeMovieFromList(listId, movieId) + } returns Result.failure(Exception("error")) - // Then - assertThat(result).isTrue() - coVerify { localDataSource.removeMovieFromListCache(movieId, listId) } + // When & Then + assertThrows { + repository.removeMovieFromList(1, 100) + } + coVerify(exactly = 0) { localDataSource.removeMovieFromListCache(any(), any()) } } @Test - fun `removeMovieFromList should return false and not touch cache on remote failure`() = - runTest { - // Given - coEvery { - remoteDataSource.removeMovieFromList( - any(), - any(), - any() + fun `getMovieListDetails returns paged response on success`() = runTest { + // Given + val listId = 1 + val page = 1 + val response = ListDetailsResponse( + itemCount = 1, + items = listOf( + SearchMovieRemote( + genreIds = emptyList(), + id = 100, + posterPath = "", + releaseDate = "2021-01-01", + voteAverage = 7.5, + name = "Movie" ) - } returns Result.failure(Exception("error")) + ), + id = "1", + name = "My List" + ) + coEvery { remoteDataSource.getDetails(any(), any()) } returns Result.success(response) - // When - val result = repository.removeMovieFromList(1, 100) + // When + val result = repository.getMovieListDetails(listId, page) - // Then - assertThat(result).isFalse() - coVerify(exactly = 0) { localDataSource.removeMovieFromListCache(any(), any()) } - } + // Then + assertThat(result.totalItems).isEqualTo(1) + } @Test - fun `getMovieListDetails should throw exception and not touch cache on remote failure`() = - runTest { - // Given - coEvery { remoteDataSource.getDetails(any(), any()) } returns Result.failure( - ResponseException(code = 1, message = "error") + fun `getMovieListDetails throws on failure`() = runTest { + // Given + coEvery { + remoteDataSource.getDetails( + any(), + any() ) + } returns Result.failure(ResponseException(code = 1, message = "error")) - // When & Then - assertThrows { - repository.getMovieListDetails(1, 1) - } - coVerify(exactly = 0) { localDataSource.replaceMembershipsForList(any(), any()) } + // When & Then + assertThrows { + repository.getMovieListDetails(1, 1) } + } @Test - fun `getMovieListName should return name from local cache if available`() = runTest { + fun `getMovieListName returns name from local if available`() = runTest { // Given val listId = 1 + coEvery { localDataSource.shouldRefreshCache() } returns false coEvery { localDataSource.getMovieList(listId) } returns MovieListLocalMock // When @@ -197,42 +242,23 @@ class CustomMovieListRepositoryImplTest { // Then assertThat(result).isEqualTo(MovieListLocalMock.name) - coVerify(exactly = 0) { remoteDataSource.getDetails(any(), any()) } } @Test - fun `getMovieListName should fetch from remote when not in local cache`() = runTest { + fun `getMovieListName returns empty if not available locally`() = runTest { // Given val listId = 1 + coEvery { localDataSource.shouldRefreshCache() } returns false coEvery { localDataSource.getMovieList(listId) } returns null - coEvery { remoteDataSource.getDetails(any(), any()) } returns Result.success( - MovieListDetailsMock - ) // When val result = repository.getMovieListName(listId) // Then - assertThat(result).isEqualTo(MovieListDetailsMock.name) - coVerify { remoteDataSource.getDetails(listId, any()) } + assertThat(result).isEmpty() } private companion object { - val MovieListDetailsMock = ListDetailsResponse( - itemCount = 1, - items = listOf( - SearchMovieRemote( - genreIds = emptyList(), - id = 100, - posterPath = "", - releaseDate = "2021-01-01", - voteAverage = 7.5, - name = "" - ) - ), - id = "1", - name = "My Detailed List", - ) val MovieListLocalMock = MovieListLocal( id = 1, name = "Local My List", diff --git a/designSystem/src/main/java/com/london/designsystem/component/Scaffold.kt b/designSystem/src/main/java/com/london/designsystem/component/Scaffold.kt index 52fc3c43b..370f7cebf 100644 --- a/designSystem/src/main/java/com/london/designsystem/component/Scaffold.kt +++ b/designSystem/src/main/java/com/london/designsystem/component/Scaffold.kt @@ -14,7 +14,7 @@ fun Scaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, - snackbarHost: @Composable () -> Unit = {}, + snackBarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, containerColor: Color, contentColor: Color = contentColorFor(containerColor), @@ -25,7 +25,7 @@ fun Scaffold( modifier = modifier, topBar = topBar, bottomBar = bottomBar, - snackbarHost = snackbarHost, + snackbarHost = snackBarHost, floatingActionButton = floatingActionButton, floatingActionButtonPosition = FabPosition.End, containerColor = containerColor, diff --git a/designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt b/designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt new file mode 100644 index 000000000..29bf79e6e --- /dev/null +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt @@ -0,0 +1,7 @@ +package com.london.designsystem.snackbar + +import androidx.compose.runtime.staticCompositionLocalOf + +val LocalSnackbarController = staticCompositionLocalOf { + error("No SnackbarController provided.") +} diff --git a/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackBar.kt b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackBar.kt new file mode 100644 index 000000000..03854515c --- /dev/null +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackBar.kt @@ -0,0 +1,117 @@ +package com.london.designsystem.snackbar + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.london.designsystem.R +import com.london.designsystem.component.Scaffold +import com.london.designsystem.component.SnackBar +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +data class SnackBarData( + val message: String, + @DrawableRes val icon: Int?, + val snackBarType: SnackBarType, +) + +private class SnackBarManager( + private val coroutineScope: CoroutineScope +) : SnackBarController { + val snackBarData = mutableStateOf(null) + private var job: Job? = null + + override fun showMessage( + message: String, + icon: Int?, + snackBarType: SnackBarType, + onComplete: () -> Unit + ) { + job?.cancel() + job = coroutineScope.launch { + snackBarData.value = SnackBarData(message, icon, snackBarType) + delay(3000L) + onComplete() + snackBarData.value = null + } + } +} + +@Composable +fun CustomSnackBarUI(data: SnackBarData) { + SnackBar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .offset(y = 56.dp), + title = data.message, + icon = painterResource(data.icon ?: R.drawable.ic_failed) + ) +} + +@Composable +fun ScaffoldWithSnackBar( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = contentColorFor(containerColor), + snackBar: @Composable (data: SnackBarData) -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + val coroutineScope = rememberCoroutineScope() + val snackBarManager = remember(coroutineScope) { SnackBarManager(coroutineScope) } + val currentSnackbarData by snackBarManager.snackBarData + + CompositionLocalProvider(LocalSnackbarController provides snackBarManager) { + Scaffold( + modifier = modifier, + topBar = topBar, + bottomBar = bottomBar, + floatingActionButton = floatingActionButton, + containerColor = containerColor, + contentColor = contentColor + ) { innerPadding -> + Box(modifier = Modifier.fillMaxSize()) { + content(innerPadding) + + AnimatedVisibility( + visible = currentSnackbarData != null, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween() + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween() + ) + ) { + currentSnackbarData?.let { snackBar(it) } + } + } + } + } +} diff --git a/designSystem/src/main/java/com/london/designsystem/snackbar/SnackBarController.kt b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackBarController.kt new file mode 100644 index 000000000..e10026db5 --- /dev/null +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackBarController.kt @@ -0,0 +1,15 @@ +package com.london.designsystem.snackbar + +interface SnackBarController { + fun showMessage( + message: String, + icon: Int?, + snackBarType: SnackBarType, + onComplete: () -> Unit + ) +} + +enum class SnackBarType { + Success, + Error; +} diff --git a/domain/src/main/java/com/london/domain/repository/CustomMovieListRepository.kt b/domain/src/main/java/com/london/domain/repository/CustomMovieListRepository.kt index d17439faa..dbd96eb16 100644 --- a/domain/src/main/java/com/london/domain/repository/CustomMovieListRepository.kt +++ b/domain/src/main/java/com/london/domain/repository/CustomMovieListRepository.kt @@ -10,16 +10,16 @@ interface CustomMovieListRepository { suspend fun createMovieList(name: String): Boolean suspend fun deleteMovieList(id: Int): Boolean suspend fun getAllListedMovieIds(): List -// fun getAllListedMovieIdsFlow(): Flow> + fun getAllListedMovieIdsFlow(): Flow> suspend fun addMovieToList(listId: Int, movieId: Int): Boolean suspend fun removeMovieFromList(listId: Int, movieId: Int): Boolean suspend fun getMovieListName(listId: Int): String suspend fun getMovieLists(pageNumber: Int): PagedFetchResponse suspend fun getMovieListDetails(listId: Int, pageNumber: Int): PagedFetchResponse suspend fun isMovieListed(movieId: Int, forceRefresh: Boolean = false): Boolean - suspend fun getMovieListIds(movieId: Int, forceRefresh: Boolean = false): List - suspend fun refreshMovieListCache() fun isMovieListedFlow(movieId: Int): Flow + suspend fun getMovieListIds(movieId: Int, forceRefresh: Boolean = false): List fun getMovieListIdsFlow(movieId: Int): Flow> + suspend fun refreshMovieListCache() } diff --git a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryContract.kt b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryContract.kt index 3ed2170a8..2c0976dba 100644 --- a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryContract.kt @@ -3,6 +3,7 @@ package com.london.presentation.feature.category.movie interface MovieCategoryContract { fun onBack() - fun onSavedClick(movieId: Int) fun onMovieClick(movieId: Int) + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryScreen.kt b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryScreen.kt index 6c987d943..6107f227b 100644 --- a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryScreen.kt @@ -18,6 +18,7 @@ import com.london.designsystem.component.TopBar import com.london.designsystem.theme.ThemePreviews import com.london.presentation.R import com.london.presentation.shared.MediaLazyPagingGrid +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.utils.Listen import com.london.presentation.utils.isLoading @@ -85,9 +86,17 @@ private fun Content( .weight(1f) .fillMaxWidth() .padding(horizontal = 16.dp), - onSaveClick = { /* TODO: Implement save functionality */ }, + onSaveClick = { contract.onManageBookmarkClicked(it.id) }, isItemSaved = { false }, + hasSaveIcon = true ) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) + } } } @@ -98,8 +107,9 @@ private fun MoviesByCategoryContentPreview() { Content( state = MovieCategoryUiState(), contract = object : MovieCategoryContract { - override fun onSavedClick(movieId: Int) {} override fun onMovieClick(movieId: Int) {} + override fun onBookmarkSheetDismiss() {} + override fun onManageBookmarkClicked(movieId: Int) {} override fun onBack() {} }, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryUiState.kt b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryUiState.kt index 457bed237..cdbd1f62c 100644 --- a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryUiState.kt @@ -11,5 +11,7 @@ data class MovieCategoryUiState( val error: ErrorState? = null, val isLoading: Boolean = false, val genre: MovieGenreUi = MovieGenreUi.All, - val moviesFlow: Flow> = flow {} + val moviesFlow: Flow> = flow {}, + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryViewModel.kt index 8087d8dde..2082ed3ea 100644 --- a/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/category/movie/MovieCategoryViewModel.kt @@ -36,7 +36,23 @@ class MovieCategoryViewModel @Inject constructor( override fun onBack() = emitEffect(MovieCategoryEffect.BackNavigation) - override fun onSavedClick(movieId: Int) = Unit //toDo() save movie + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } private fun initializeMovies(genreUi: MovieGenreUi) { tryToExecute( diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsContract.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsContract.kt index 67e466ccc..a48098353 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsContract.kt @@ -8,4 +8,6 @@ interface ActorDetailsContract { fun onMovieScreenClick(movieId: Int) fun onTopTvShowPicksClick(actorId: Int) fun onTvShowScreenClick(tvShowId: Int) + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsScreen.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsScreen.kt index 040a0541f..9960201c9 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape @@ -56,6 +57,7 @@ import com.london.presentation.shared.CustomBackDropImagePager import com.london.presentation.shared.HomeCard import com.london.presentation.shared.ImageView import com.london.presentation.shared.TextWithIcon +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.utils.Listen import com.london.presentation.utils.detailsTopBar @@ -154,10 +156,8 @@ private fun Content( item { MoviesSection( movies = uiState.actorMovieDetails?.mediaItems, - onTopMoviePicksClick = { - actorDetailsContract.onTopMoviePicksClick(uiState.actorDetails.id) - }, - onMovieScreenClick = actorDetailsContract::onMovieScreenClick + contract = actorDetailsContract, + actorId = uiState.actorDetails.id ) } item { @@ -177,6 +177,12 @@ private fun Content( .detailsTopBar(backgroundAlpha) .zIndex(1f) ) + + BookmarkBottomSheet( + onSheetDismiss = actorDetailsContract::onBookmarkSheetDismiss, + isSheetVisible = uiState.isBookmarkSheetVisible, + bookmarkedMovieId = uiState.bookmarkedMovieId + ) } } @@ -240,8 +246,8 @@ private fun GallerySection( @Composable private fun MoviesSection( movies: List?, - onTopMoviePicksClick: () -> Unit, - onMovieScreenClick: (Int) -> Unit + contract: ActorDetailsContract, + actorId: Int ) { movies?.takeIf { it.isNotEmpty() }?.let { movieCast -> SectionHeader( @@ -251,11 +257,12 @@ private fun MoviesSection( modifier = Modifier .padding(top = 16.dp, bottom = 12.dp) .padding(horizontal = 16.dp), - onClick = onTopMoviePicksClick + onClick = { contract.onTopMoviePicksClick(actorId) } ) TopMoviesPicksList( - movie = movieCast, - onNavigateToMoviePicks = onMovieScreenClick + movies = movieCast, + onNavigateToMoviePicks = contract::onMovieScreenClick, + onManageBookmarkClicked = contract::onManageBookmarkClicked ) } } @@ -285,8 +292,9 @@ private fun TvShowsSection( @Composable fun TopMoviesPicksList( - movie: List, - onNavigateToMoviePicks: (Int) -> Unit + movies: List, + onNavigateToMoviePicks: (Int) -> Unit, + onManageBookmarkClicked: (Int) -> Unit ) { LazyHorizontalGrid( rows = GridCells.Adaptive(minSize = 128.dp), @@ -294,13 +302,13 @@ fun TopMoviesPicksList( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) ) { - items(movie.size) { index -> + items(movies) { movie -> HomeCard( - imageUrl = movie[index].posterUrl, + imageUrl = movie.posterUrl, isSaved = false, - onSaveClick = { /* TODO: Not yet implemented */ }, + onSaveClick = { onManageBookmarkClicked(movie.id) }, modifier = Modifier.clickable { - onNavigateToMoviePicks(movie[index].id) + onNavigateToMoviePicks(movie.id) } ) } @@ -322,7 +330,8 @@ fun TopTvShowsPicksList( HomeCard( imageUrl = tvShow[index].posterUrl, isSaved = false, - onSaveClick = { /* TODO: Not yet implemented */ }, + hasSaveIcon = false, + onSaveClick = {}, modifier = Modifier.clickable { onNavigateToTvShowPicks(tvShow[index].id) } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsUiState.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsUiState.kt index 024235eb8..a5efc5343 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsUiState.kt @@ -13,4 +13,6 @@ data class ActorDetailsUiState( val actorDetails: ActorDetails = ActorDetails(), val actorMovieDetails: ActorMediaDetails? = null, val actorTvShowDetails: ActorMediaDetails? = null, + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsViewModel.kt index b4e82ce83..73501abca 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/ActorDetailsViewModel.kt @@ -50,6 +50,24 @@ class ActorDetailsViewModel @Inject constructor( emitEffect(ActorEffect.TvShowScreenNavigation(tvShowId)) } + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private fun getActorInformation(){ getActorImage() getActorDetails() diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksContract.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksContract.kt index 6ab775229..caff6cc78 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksContract.kt @@ -3,6 +3,7 @@ package com.london.presentation.feature.details.actor.info.topmoviespicks interface TopMoviesPicksContract { fun onBackClick() fun onRetryClick() - fun onSaveMovieClick(movieId: Int) fun onMovieClick(movieId: Int) + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksScreen.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksScreen.kt index d865fabac..b636ce94a 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksScreen.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.london.presentation.R import com.london.presentation.shared.MediaLazyGrid import com.london.presentation.shared.base.ErrorState +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.utils.Listen @@ -52,7 +53,14 @@ private fun Content( onBack = contract::onBackClick, getImageUrl = { it.posterUrl }, onItemClick = { contract.onMovieClick(it.id) }, - onSavedClick = { contract.onSaveMovieClick(it.id) }, + onSavedClick = { contract.onManageBookmarkClicked(it.id) }, + hasSaveIcon = true + ) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId ) } } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksUiState.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksUiState.kt index 708323303..8dfa4f12f 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksUiState.kt @@ -7,4 +7,6 @@ data class TopMoviesPicksUiState( val isLoading: Boolean = false, val errorState: ErrorState? = null, val movieDetails: ActorMediaDetails = ActorMediaDetails(), + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksViewModel.kt index 497bb9b77..f50256b02 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/topmoviespicks/TopMoviesPicksViewModel.kt @@ -28,10 +28,6 @@ class TopMoviesPicksViewModel @Inject constructor( getActorMoviePicksData() } - override fun onSaveMovieClick(movieId: Int) { - // TODO: Handle save click - } - override fun onMovieClick(movieId: Int) { emitEffect(TopMoviesPicksEffect.MovieDetailsNavigation(movieId)) } @@ -40,6 +36,24 @@ class TopMoviesPicksViewModel @Inject constructor( emitEffect(TopMoviesPicksEffect.BackNavigation) } + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private fun getActorMoviePicksData() { tryToExecute( block = { getActorUseCase.getActorMoviePicksById(actorId) }, diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksContract.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksContract.kt index 9c8cb6bb2..4c5d26352 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksContract.kt @@ -4,5 +4,4 @@ interface TopTvShowsPicksContract { fun onBackClick() fun onRetryClick() fun onTvShowClick(tvShowId: Int) - fun onSaveTvShowClick(tvShowId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksScreen.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksScreen.kt index 30495cd49..bfab114a8 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksScreen.kt @@ -50,8 +50,7 @@ private fun Content( items = state.tvShowDetails.mediaItems, onBack = contract::onBackClick, getImageUrl = { it.posterUrl }, - onItemClick = { contract.onTvShowClick(it.id) }, - onSavedClick = { contract.onSaveTvShowClick(it.id) }, + onItemClick = { contract.onTvShowClick(it.id) } ) } } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksViewModel.kt index 7b9328f23..6ec8b8c8d 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/actor/info/toptvshowspicks/TopTvShowsPicksViewModel.kt @@ -28,10 +28,6 @@ class TopTvShowsPicksViewModel @Inject constructor( getActorTvShowsPicksData() } - override fun onSaveTvShowClick(tvShowId: Int) { - // TODO: Handle save tv show - } - override fun onBackClick() { emitEffect(TopTvShowsPicksEffect.BackNavigation) } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsContract.kt b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsContract.kt index fab1af999..c7a06cb94 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsContract.kt @@ -7,7 +7,6 @@ interface MovieDetailsContract { fun onRetryClick() fun onBackClick() fun onLoginClick() - fun onSavedClick() fun onExpandClick() fun onRateBottomSheetClick() fun onMovieClick(movieId: Int) @@ -15,4 +14,6 @@ interface MovieDetailsContract { fun onSelectRatingClick(rating: Int) fun onGenreClick(genre: MovieGenreUi) fun onReviewsClick(movieId: Int, mediaType: MediaType) -} \ No newline at end of file + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) +} diff --git a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsScreen.kt b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsScreen.kt index c2a0f71f9..408a1e7a7 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsScreen.kt @@ -69,6 +69,7 @@ import com.london.presentation.shared.FooterSection import com.london.presentation.shared.HomeCard import com.london.presentation.shared.SnackBarAnimation import com.london.presentation.shared.TextWithIcon +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.shared.genre.MovieGenreUi import com.london.presentation.utils.Listen @@ -117,7 +118,7 @@ fun MovieDetailsScreen( ) { Content( uiState = state, - movieDetailsContract = viewModel + contract = viewModel ) } } @@ -125,7 +126,7 @@ fun MovieDetailsScreen( @Composable private fun Content( uiState: MovieDetailsUiState, - movieDetailsContract: MovieDetailsContract + contract: MovieDetailsContract ) { val screenWidthDp = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.width.toDp() } @@ -158,15 +159,15 @@ private fun Content( ) { TopBar( - onBackClick = movieDetailsContract::onBackClick, + onBackClick = contract::onBackClick, modifier = Modifier.detailsTopBar(backgroundAlpha), - onClickOption1 = { /*todo on click on save*/ }, + onClickOption1 = { contract.onManageBookmarkClicked(uiState.movieId) }, option1Icon = R.drawable.icon_remove, ) HomeLazyVerticalGrid( uiState = uiState, - movieDetailsContract = movieDetailsContract, + movieDetailsContract = contract, lazyState = lazyState, footerHeight = footerHeight, screenWidthDp = screenWidthDp @@ -180,11 +181,17 @@ private fun Content( } .align(Alignment.BottomCenter), onVideoClick = { uriHandler.openUrl(uiState.movieVideo) }, - onRateClick = movieDetailsContract::onRateBottomSheetClick, + onRateClick = contract::onRateBottomSheetClick, isRateEnabled = !uiState.isRated && (uiState.movieRating.isBlank() || uiState.movieRating.isNotZeroRate()) ) - BottomSheetsHandler(uiState, movieDetailsContract) + BottomSheetsHandler(uiState, contract) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = uiState.isBookmarkSheetVisible, + bookmarkedMovieId = uiState.bookmarkedMovieId + ) } uiState.isSuccessfullyRated?.let { isSuccessful -> @@ -344,11 +351,10 @@ private fun HomeLazyVerticalGrid( HomeCard( imageUrl = movie.posterUrl, isSaved = false, - onSaveClick = {}, - modifier = Modifier - .clickable { - movieDetailsContract.onMovieClick(movie.id) - } + onSaveClick = { movieDetailsContract.onManageBookmarkClicked(movie.id) }, + modifier = Modifier.clickable { + movieDetailsContract.onMovieClick(movie.id) + } ) } } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsUiState.kt b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsUiState.kt index fd76306db..c3e17dafa 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsUiState.kt @@ -27,7 +27,9 @@ data class MovieDetailsUiState( val similarMovies: List = listOf(), val isRateBottomSheetVisible: Boolean = false, val isGuestUserBottomSheetVisible: Boolean = false, -){ + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, +) { val hasTrailer: Boolean get() = movieVideo.isNotEmpty() } diff --git a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsViewModel.kt index 18ed9d814..e7128db81 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/movie/MovieDetailsViewModel.kt @@ -203,8 +203,22 @@ class MovieDetailsViewModel @Inject constructor( emitEffect(MovieDetailsEffect.BackNavigation) } - override fun onSavedClick() { - // TODO: implement saving logic + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } } override fun onExpandClick() { diff --git a/presentation/src/main/java/com/london/presentation/feature/details/tvshow/info/TvShowDetailScreen.kt b/presentation/src/main/java/com/london/presentation/feature/details/tvshow/info/TvShowDetailScreen.kt index 2e0d4cace..d3d7d427a 100644 --- a/presentation/src/main/java/com/london/presentation/feature/details/tvshow/info/TvShowDetailScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/details/tvshow/info/TvShowDetailScreen.kt @@ -164,7 +164,6 @@ private fun Content( TopBar( onBackClick = tvShowDetailsContract::onBackClicked, modifier = Modifier.detailsTopBar(backgroundAlpha), - onClickOption1 = { /*todo on click on save*/ }, option1Icon = R.drawable.icon_remove, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingContract.kt b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingContract.kt index 6dcd210c3..35d2c4529 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingContract.kt @@ -12,4 +12,6 @@ interface ContinueWatchingContract { fun onMovieGenreClick(genre: MovieGenreUi) fun onTvShowGenreClick(genre: TvShowGenreUi) fun onMediaCategoryTabClick(selectedMediaCategory: MediaCategory) + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingScreen.kt b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingScreen.kt index 9878b98e5..f2a5b8bba 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingScreen.kt @@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.london.designsystem.theme.NovixTheme +import com.london.domain.entity.movie.Movie import com.london.presentation.R import com.london.presentation.shared.DefaultAppTopBar import com.london.presentation.shared.MediaCategory import com.london.presentation.shared.base.ErrorState +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.shared.container.MediaGridConfig import com.london.presentation.shared.container.MediaLazyGridWithTabs @@ -76,7 +78,7 @@ private fun Content( onMovieGenreClick = contract::onMovieGenreClick, onTvShowGenreClick = contract::onTvShowGenreClick, config = MediaGridConfig( - showSaveIcon = true, + showSaveIcon = state.selectedMediaCategory == MediaCategory.Movies, isDarkMode = NovixTheme.isThemeDark, selectedMovieGenre = state.selectedMovieGenre, selectedTvShowGenre = state.selectedTvShowGenre, @@ -84,7 +86,7 @@ private fun Content( isTvShowSelected = MediaCategory.TvShows == state.selectedMediaCategory, onNavigateToMovie = contract::onNavigateToMovieClick, onNavigateToTvShow = contract::onNavigateToTvShowClick, - onSaveClick = { /* TODO: Implement save functionality */ }, + onSaveClick = { if (it is Movie) contract.onManageBookmarkClicked(it.id) }, isItemSaved = { false }, rate = null ), @@ -96,13 +98,19 @@ private fun Content( }, isLoading = state.isLoading ) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) } } @Composable private fun getCombinedItems(state: ContinueWatchingUiState): List { return state.movies.collectAsStateWithLifecycle(emptyList()).value + - state.tvSeries.collectAsStateWithLifecycle(emptyList()).value + state.tvSeries.collectAsStateWithLifecycle(emptyList()).value } private fun getSelectedTabIndex(state: ContinueWatchingUiState): Int { diff --git a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingUiState.kt b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingUiState.kt index c854337b0..4bd1abfee 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingUiState.kt @@ -16,5 +16,7 @@ data class ContinueWatchingUiState( val tvSeries: Flow> = emptyFlow(), val selectedMovieGenre: MovieGenreUi = MovieGenreUi.All, val selectedTvShowGenre: TvShowGenreUi = TvShowGenreUi.All, - val selectedMediaCategory: MediaCategory = MediaCategory.Movies + val selectedMediaCategory: MediaCategory = MediaCategory.Movies, + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingViewModel.kt index 90bd24cc1..6a1869904 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/continuewatching/ContinueWatchingViewModel.kt @@ -53,6 +53,24 @@ class ContinueWatchingViewModel @Inject constructor( override fun onRetryClick() = getRecentWatchedMedia() + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private fun setSelectedCategory(category: MediaCategory) = updateState { copy(selectedMediaCategory = category) } diff --git a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedContract.kt b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedContract.kt index 94fa35a46..09f6a3240 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedContract.kt @@ -12,4 +12,7 @@ interface TopRatedContract { fun movieGenre(genre: MovieGenreUi) fun tvShowGenre(genre: TvShowGenreUi) fun onMediaCategoryTabSelected(selectedMediaCategory: MediaCategory) -} \ No newline at end of file + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) + +} diff --git a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedScreen.kt b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedScreen.kt index d0454a642..57f41a7bf 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedScreen.kt @@ -33,19 +33,19 @@ import com.london.designsystem.theme.NovixTheme import com.london.designsystem.utils.string import com.london.presentation.shared.HomeCard import com.london.presentation.shared.MediaCategory +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.shared.genre.MovieGenreUi import com.london.presentation.shared.genre.TvShowGenreUi import com.london.presentation.utils.Listen import com.london.presentation.utils.gridColumns -import com.london.presentation.utils.isLoading @Composable fun TopRatedScreen( viewModel: TopRatedViewModel = hiltViewModel(), onNavigateBack: () -> Unit = {}, - onNaviagteToMovieDetalis: (Int) -> Unit = {}, - onNaviagteToTvShowDetalis: (Int) -> Unit = {} + onNavigateToMovieDetails: (Int) -> Unit = {}, + onNavigateToTvShowDetails: (Int) -> Unit = {} ) { val state by viewModel.state.collectAsStateWithLifecycle() val effect by viewModel.effect.collectAsState(null) @@ -53,8 +53,8 @@ fun TopRatedScreen( effect?.Listen { currentEffect -> when (currentEffect) { is TopRatedEffect.NavigateBack -> onNavigateBack() - is TopRatedEffect.NavigateToMovieDetails -> onNaviagteToMovieDetalis(currentEffect.id) - is TopRatedEffect.NavigateToTvShowDetails -> onNaviagteToTvShowDetalis(currentEffect.id) + is TopRatedEffect.NavigateToMovieDetails -> onNavigateToMovieDetails(currentEffect.id) + is TopRatedEffect.NavigateToTvShowDetails -> onNavigateToTvShowDetails(currentEffect.id) } } @@ -64,13 +64,13 @@ fun TopRatedScreen( BuildScreen( isLoading = state.isLoading, isError = topRatedMovieFlow.loadState.refresh is LoadState.Error - && topRatedTvShowFlow.loadState.refresh is LoadState.Error, + && topRatedTvShowFlow.loadState.refresh is LoadState.Error, onBack = viewModel::onBackClicked, onRetry = viewModel::onRetry, ) { Content( state = state, - topRatedContract = viewModel + contract = viewModel ) } } @@ -78,7 +78,7 @@ fun TopRatedScreen( @Composable private fun Content( state: TopRatedUiState, - topRatedContract: TopRatedContract, + contract: TopRatedContract, ) { val screenWidth = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.width.toDp() } @@ -94,7 +94,7 @@ private fun Content( .padding(horizontal = 16.dp) .padding(top = 12.dp), title = com.london.presentation.R.string.top_rated.string, - onBackClick = topRatedContract::onBackClicked + onBackClick = contract::onBackClicked ) TabLayout( @@ -103,18 +103,18 @@ private fun Content( MediaCategory.TvShows ), selectedTab = state.selectedMediaCategory, - onTabSelected = topRatedContract::onMediaCategoryTabSelected, + onTabSelected = contract::onMediaCategoryTabSelected, modifier = Modifier.background(NovixTheme.colors.surface) ) if (state.isMovieSelected) MovieGenreRow( - onGenreClick = topRatedContract::movieGenre, + onGenreClick = contract::movieGenre, state = state, screenWidth = screenWidth ) else TvShowRow( - onGenreClick = topRatedContract::tvShowGenre, + onGenreClick = contract::tvShowGenre, state = state, screenWidth = screenWidth ) @@ -139,11 +139,10 @@ private fun Content( HomeCard( imageUrl = movieItem.posterUrl, isSaved = false, - onSaveClick = { - // TODO - }, + hasSaveIcon = true, + onSaveClick = { contract.onManageBookmarkClicked(movieItem.id) }, modifier = Modifier.clickable { - topRatedContract.onMovieClick(movieItem.id) + contract.onMovieClick(movieItem.id) } ) } @@ -154,17 +153,21 @@ private fun Content( tvSeries?.let { seriesItem -> HomeCard( imageUrl = seriesItem.posterUrl, + hasSaveIcon = false, isSaved = false, - onSaveClick = { - // TODO - }, - modifier = Modifier.clickable { - topRatedContract.onTvShowClick(seriesItem.id) - } + onSaveClick = {}, + modifier = Modifier.clickable { contract.onTvShowClick(seriesItem.id) } ) } } } + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) + } } diff --git a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedUiState.kt b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedUiState.kt index 17a873624..16964c808 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedUiState.kt @@ -16,5 +16,7 @@ data class TopRatedUiState( val selectedTvShowGenre: TvShowGenreUi = TvShowGenreUi.All, val movies: Flow> = emptyFlow(), val tvSeries: Flow> = emptyFlow(), - val selectedMediaCategory: MediaCategory = MediaCategory.Movies + val selectedMediaCategory: MediaCategory = MediaCategory.Movies, + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedViewModel.kt index 901bfdc1d..f79510212 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/toprated/TopRatedViewModel.kt @@ -62,6 +62,24 @@ class TopRatedViewModel @Inject constructor( emitEffect(TopRatedEffect.NavigateToTvShowDetails(id)) } + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private fun initializeTopRated() { if (state.value.isMovieSelected) initializeTopMovies() else initializeTvShow() @@ -107,4 +125,4 @@ class TopRatedViewModel @Inject constructor( updateState { copy(isLoading = false) } }, checkSuccess = { true }) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesContract.kt b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesContract.kt index 3fd43ca32..99d046c32 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesContract.kt @@ -7,6 +7,8 @@ interface TrendingMoviesContract { fun onRetryClick() fun onMovieClick(id: Int) fun onGenreClick(genre: MovieGenreUi) + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } fun defaultTrendingMoviesContract() = object : TrendingMoviesContract { @@ -14,4 +16,6 @@ fun defaultTrendingMoviesContract() = object : TrendingMoviesContract { override fun onRetryClick() {} override fun onMovieClick(id: Int) {} override fun onGenreClick(genre: MovieGenreUi) {} -} \ No newline at end of file + override fun onBookmarkSheetDismiss() {} + override fun onManageBookmarkClicked(movieId: Int) {} +} diff --git a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesScreen.kt b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesScreen.kt index 4756bfc2a..eb9b3b87d 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesScreen.kt @@ -22,6 +22,7 @@ import com.london.designsystem.theme.NovixTheme import com.london.presentation.R import com.london.presentation.shared.GenresSection import com.london.presentation.shared.MediaLazyPagingGrid +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.utils.Listen @@ -85,6 +86,7 @@ private fun Content( modifier = Modifier.padding(bottom = 12.dp), getGenreName = { stringResource(it.stringResId) } ) + MediaLazyPagingGrid( pagingFlow = state.moviesFlow.collectAsLazyPagingItems(), onItemClick = { contract.onMovieClick(it.id) }, @@ -94,8 +96,14 @@ private fun Content( .weight(1f) .fillMaxWidth() .padding(horizontal = 16.dp), - onSaveClick = { /* TODO: Implement save functionality */ }, - isItemSaved = { false }, + onSaveClick = { contract.onManageBookmarkClicked(it.id) }, + hasSaveIcon = true + ) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId ) } } diff --git a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesUiState.kt b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesUiState.kt index dff436e15..a78eadcbf 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesUiState.kt @@ -14,5 +14,7 @@ data class TrendingMoviesUiState( val isLoading: Boolean = false, val errorState: ErrorState? = null, val moviesFlow: Flow> = emptyFlow(), - val movieGenres: List = MovieGenreUi.getList() + val movieGenres: List = MovieGenreUi.getList(), + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) diff --git a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesViewModel.kt index 1349759b1..b7364f55f 100644 --- a/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/home/trending/movie/TrendingMoviesViewModel.kt @@ -40,6 +40,24 @@ class TrendingMoviesViewModel @Inject constructor( override fun onRetryClick() = getTrendingMovies() + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private fun getTrendingMovies() { tryToCollect( block = ::createTrendingMoviesPagingFlow, diff --git a/presentation/src/main/java/com/london/presentation/feature/list/viewitems/viewItemsScreen.kt b/presentation/src/main/java/com/london/presentation/feature/list/viewitems/viewItemsScreen.kt index 55259a847..0a5bc4c15 100644 --- a/presentation/src/main/java/com/london/presentation/feature/list/viewitems/viewItemsScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/list/viewitems/viewItemsScreen.kt @@ -93,8 +93,6 @@ private fun Content( contract = contract, ) - - if (state.isSnackBarErrorVisible) { SnackBarAnimation( stringResource(R.string.deletion_failed), diff --git a/presentation/src/main/java/com/london/presentation/feature/search/SearchContract.kt b/presentation/src/main/java/com/london/presentation/feature/search/SearchContract.kt index e53b30fa4..a5b30eff7 100644 --- a/presentation/src/main/java/com/london/presentation/feature/search/SearchContract.kt +++ b/presentation/src/main/java/com/london/presentation/feature/search/SearchContract.kt @@ -26,5 +26,7 @@ interface SearchContract { fun performSearch(query: String, category: SearchCategory) fun incrementGenreInterest(genre: Genre, mediaType: String) fun updateRecentData() + fun onBookmarkSheetDismiss() + fun onManageBookmarkClicked(movieId: Int) } diff --git a/presentation/src/main/java/com/london/presentation/feature/search/SearchScreen.kt b/presentation/src/main/java/com/london/presentation/feature/search/SearchScreen.kt index b35687665..7afc998af 100644 --- a/presentation/src/main/java/com/london/presentation/feature/search/SearchScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/search/SearchScreen.kt @@ -56,11 +56,13 @@ import com.london.designsystem.theme.ThemePreviews import com.london.domain.entity.recent.RecentSearch import com.london.domain.entity.recent.RecentViewed import com.london.domain.entity.shared.MediaType +import com.london.domain.entity.shared.MediaType.Companion.isMovie import com.london.presentation.R import com.london.presentation.shared.ActorsLayout import com.london.presentation.shared.HomeCard import com.london.presentation.shared.TriangleBlurredShape import com.london.presentation.shared.base.ErrorState +import com.london.presentation.shared.bookmarkSheet.BookmarkBottomSheet import com.london.presentation.shared.buildscreen.BuildScreen import com.london.presentation.shared.buildscreen.NetworkErrorScreen import com.london.presentation.shared.container.MediaLazyVerticalGrid @@ -169,6 +171,12 @@ private fun SearchMainContent( ) SearchBody(state = state, contract = contract) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) } } } @@ -254,7 +262,8 @@ private fun MovieSearchContent(state: SearchUiState, contract: SearchContract) { contract.onMovieGenreClick(it.genres) } contract.onMovieClick(id) - } + }, + hasSaveIcon = true ) } @@ -315,7 +324,8 @@ private fun MediaSearchContent( pagingItems: LazyPagingItems, contract: SearchContract, onNavigateToMovie: (Int) -> Unit = {}, - onNavigateToTvShow: (Int) -> Unit = {} + onNavigateToTvShow: (Int) -> Unit = {}, + hasSaveIcon: Boolean = false ) { EmptyContent( pagingItems, @@ -337,8 +347,8 @@ private fun MediaSearchContent( content = { MediaLazyVerticalGrid( pagingItems = pagingItems, - hasSaveIcon = true, - onSaveClick = { /* Handle save click */ }, + hasSaveIcon = hasSaveIcon, + onSaveClick = contract::onManageBookmarkClicked, isItemSaved = { false }, onNavigateToMovie = onNavigateToMovie, onNavigateToTvShow = onNavigateToTvShow @@ -506,7 +516,8 @@ fun RecentSectionContent( recentViewed = state.recentViewed, onClearAll = contract::clearRecentViewed, onNavigateToTvShowDetails = onNavigateToTvShowDetails, - onNavigateToMovieDetails = onNavigateToMovieDetails + onNavigateToMovieDetails = onNavigateToMovieDetails, + onManageBookmarkClicked = contract::onManageBookmarkClicked ) } } @@ -530,6 +541,7 @@ private fun RecentViewedSection( onClearAll: () -> Unit, onNavigateToTvShowDetails: (Int) -> Unit, onNavigateToMovieDetails: (Int) -> Unit, + onManageBookmarkClicked: (Int) -> Unit ) { SectionHeader( text = stringResource(R.string.recent_viewed), @@ -551,7 +563,8 @@ private fun RecentViewedSection( HomeCard( imageUrl = item.imageUrl, isSaved = false, - onSaveClick = { }, + hasSaveIcon = item.type.isMovie(), + onSaveClick = { onManageBookmarkClicked(item.id) }, modifier = Modifier.clickable { when (item.type) { MediaType.Movie -> onNavigateToMovieDetails(item.id) diff --git a/presentation/src/main/java/com/london/presentation/feature/search/SearchUiState.kt b/presentation/src/main/java/com/london/presentation/feature/search/SearchUiState.kt index 90823684e..38379233f 100644 --- a/presentation/src/main/java/com/london/presentation/feature/search/SearchUiState.kt +++ b/presentation/src/main/java/com/london/presentation/feature/search/SearchUiState.kt @@ -28,6 +28,8 @@ data class SearchUiState( val recentSearches: List = emptyList(), val searchQuery: TextFieldValue = TextFieldValue(""), val selectedCategory: SearchCategory = SearchCategory.Movies, + val isBookmarkSheetVisible: Boolean = false, + val bookmarkedMovieId: Int = 0, ) data class MovieUi( @@ -35,4 +37,4 @@ data class MovieUi( val title: String, val isSaved: Boolean, val posterUrl: String, -) \ No newline at end of file +) diff --git a/presentation/src/main/java/com/london/presentation/feature/search/SearchViewModel.kt b/presentation/src/main/java/com/london/presentation/feature/search/SearchViewModel.kt index 5222d79e9..3b09f2ea5 100644 --- a/presentation/src/main/java/com/london/presentation/feature/search/SearchViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/feature/search/SearchViewModel.kt @@ -186,6 +186,24 @@ class SearchViewModel @Inject constructor( setupSearchDebouncing() } + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } + private suspend fun getRecentData(): Pair, List> { val recentViewed = manageRecentViewedUseCase.getRecentViewed().reversed() val recentSearches = manageRecentSearchUseCase.getRecentSearch() @@ -336,4 +354,4 @@ class SearchViewModel @Inject constructor( category = state.value.selectedCategory ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/london/presentation/feature/welcome/onboarding/onboardingScreen.kt b/presentation/src/main/java/com/london/presentation/feature/welcome/onboarding/onboardingScreen.kt index bd57bc204..ad9ddaabd 100644 --- a/presentation/src/main/java/com/london/presentation/feature/welcome/onboarding/onboardingScreen.kt +++ b/presentation/src/main/java/com/london/presentation/feature/welcome/onboarding/onboardingScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState @@ -72,7 +72,6 @@ fun OnboardingScreen( modifier = Modifier .fillMaxSize() .background(NovixTheme.colors.surface) - .padding(WindowInsets.systemBars.asPaddingValues()) ) { Content( pagerState = pagerState, @@ -120,7 +119,11 @@ private fun Content( viewModel: OnboardingViewModel, scope: CoroutineScope ) { - Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(WindowInsets.navigationBars.asPaddingValues()) + ) { HorizontalPager( state = pagerState, modifier = Modifier.weight(1f) @@ -191,6 +194,7 @@ private fun SkipButton( AnimatedVisibility(visible) { Text( modifier = Modifier + .padding(WindowInsets.statusBars.asPaddingValues()) .padding(16.dp) .clickable { onClick() }, text = stringResource(R.string.skip), diff --git a/presentation/src/main/java/com/london/presentation/shared/MediaLazyGrid.kt b/presentation/src/main/java/com/london/presentation/shared/MediaLazyGrid.kt index b090ee3eb..fbc274ea8 100644 --- a/presentation/src/main/java/com/london/presentation/shared/MediaLazyGrid.kt +++ b/presentation/src/main/java/com/london/presentation/shared/MediaLazyGrid.kt @@ -73,6 +73,7 @@ fun MediaLazyGrid( emptyTitle: String = "", emptyImage: Int? = null, myRatingList: Boolean = false, + hasSaveIcon: Boolean = false, rate: String = "5", onDeleteClick: () -> Unit = {} ) { @@ -125,6 +126,7 @@ fun MediaLazyGrid( onSaveClick = { onSavedClick(item) }, rate = rate, onDeleteClick = onDeleteClick, + hasSaveIcon = hasSaveIcon, modifier = Modifier.clickable { onItemClick(item) } ) } diff --git a/presentation/src/main/java/com/london/presentation/shared/MediaLazyPagingGrid.kt b/presentation/src/main/java/com/london/presentation/shared/MediaLazyPagingGrid.kt index 85e3bd140..99e05a8f9 100644 --- a/presentation/src/main/java/com/london/presentation/shared/MediaLazyPagingGrid.kt +++ b/presentation/src/main/java/com/london/presentation/shared/MediaLazyPagingGrid.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems -import com.london.designsystem.theme.NovixTheme import com.london.presentation.utils.gridColumns @Deprecated( @@ -59,6 +58,7 @@ fun MediaLazyPagingGrid( modifier: Modifier = Modifier, onSaveClick: (T) -> Unit = {}, isItemSaved: (T) -> Boolean = { false }, + hasSaveIcon: Boolean = false, ) { LazyVerticalGrid( state = rememberLazyGridState(), @@ -73,6 +73,7 @@ fun MediaLazyPagingGrid( if (item != null) { HomeCard( imageUrl = getImageUrl(item), + hasSaveIcon = hasSaveIcon, onSaveClick = { onSaveClick(item) }, isSaved = isItemSaved(item), imageDescription = getTitle(item), @@ -81,4 +82,4 @@ fun MediaLazyPagingGrid( } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheet.kt b/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheet.kt index 56f7f8256..9c32642c3 100644 --- a/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheet.kt +++ b/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheet.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -38,13 +39,14 @@ import com.london.designsystem.component.Text import com.london.designsystem.component.button.OutlineButton import com.london.designsystem.component.button.PrimaryButton import com.london.designsystem.component.rememberModalBottomSheetState +import com.london.designsystem.snackbar.LocalSnackbarController +import com.london.designsystem.snackbar.SnackBarType import com.london.designsystem.theme.NovixTheme import com.london.designsystem.utils.painter import com.london.designsystem.utils.string import com.london.presentation.R import com.london.presentation.navigation.LocalNavController import com.london.presentation.navigation.Screen -import com.london.presentation.shared.SnackBarAnimation import com.london.presentation.utils.Listen import com.london.presentation.utils.getThemeAwarePainter import kotlinx.coroutines.launch @@ -157,25 +159,25 @@ private fun BookmarkBottomSheetContent( } } - /** - * Currently, these snack-bars aren't showing as the sheet goes out of the - * composition before they start to show, to fix this, we need a snack-bar - * host to manage and show snack-bars across screens regardless of the parent lifecycle. - */ + val snackBarController = LocalSnackbarController.current if (state.isSuccessSnackbarVisible) { - SnackBarAnimation( + snackBarController.showMessage( message = R.string.item_added_success.string, - icon = com.london.designsystem.R.drawable.ic_success + icon = com.london.designsystem.R.drawable.ic_success, + snackBarType = SnackBarType.Success, + onComplete = contract::onSnackbarShown ) } if (state.isErrorSnackbarVisible) { - SnackBarAnimation( - message = R.string.item_added_fail.string + snackBarController.showMessage( + message = R.string.item_added_fail.string, + icon = com.london.designsystem.R.drawable.ic_failed, + snackBarType = SnackBarType.Error, + onComplete = contract::onSnackbarShown ) } - } @Composable @@ -365,7 +367,8 @@ private fun NoListsMessage() { Text( text = R.string.no_lists_available.string, style = NovixTheme.typography.body.small, - color = NovixTheme.colors.body + color = NovixTheme.colors.body, + textAlign = TextAlign.Center ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheetViewModel.kt b/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheetViewModel.kt index 0f0a896fe..d181a166c 100644 --- a/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheetViewModel.kt +++ b/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheetViewModel.kt @@ -39,7 +39,7 @@ class BookmarkSheetViewModel @Inject constructor( } override fun onAddToLists(bookmarkedId: Int) { - val listsToAdd = state.value.selectedLists.toList() // Capture current selection + val listsToAdd = state.value.selectedLists.toList() tryToExecute( onStart = { updateState { copy(isAddingToList = true) } }, diff --git a/presentation/src/main/java/com/london/presentation/shared/container/MediaLazyVerticalGrid.kt b/presentation/src/main/java/com/london/presentation/shared/container/MediaLazyVerticalGrid.kt index 38c04048a..7fe5562ce 100644 --- a/presentation/src/main/java/com/london/presentation/shared/container/MediaLazyVerticalGrid.kt +++ b/presentation/src/main/java/com/london/presentation/shared/container/MediaLazyVerticalGrid.kt @@ -82,7 +82,7 @@ fun MediaLazyVerticalGrid( name: (T) -> String = { it.getName() }, imageUrl: (T) -> String? = { it.getImageUrl() }, hasSaveIcon: Boolean = true, - onSaveClick: (T) -> Unit = {}, + onSaveClick: (Int) -> Unit = {}, isItemSaved: (T) -> Boolean = { false }, onDeleteClick: (T) -> Unit = {}, topBar: @Composable (() -> Unit)? = null, @@ -153,7 +153,7 @@ private fun RenderPagingGridItem( name: (T) -> String, isItemSaved: (T) -> Boolean, hasSaveIcon: Boolean, - onSaveClick: (T) -> Unit, + onSaveClick: (Int) -> Unit, onDeleteClick: (T) -> Unit, rate: (T) -> String?, onNavigateToMovie: (Int) -> Unit, @@ -172,7 +172,7 @@ private fun RenderPagingGridItem( imageDescription = name(item), isSaved = isItemSaved(item), hasSaveIcon = hasSaveIcon, - onSaveClick = { onSaveClick(item) }, + onSaveClick = { if (item is Movie) onSaveClick(item.id) }, onDeleteClick = { onDeleteClick(item) }, rate = rate(item) ) diff --git a/presentation/src/main/res/values-ar/strings.xml b/presentation/src/main/res/values-ar/strings.xml index 135bc2bae..52f8c9583 100644 --- a/presentation/src/main/res/values-ar/strings.xml +++ b/presentation/src/main/res/values-ar/strings.xml @@ -162,9 +162,10 @@ تم اضافه القائمه بنجاح حدث خطا ما لا يمكن اضافه القائمه يرجى تسجيل الدخول للتمكن من الإضافة إالي قائمة - لا يوجد قوائم متاحة! قم بإنشاء واحدة الآن. + لا يوجد قوائم متاحة لهذا الفيلم!\nقم بإنشاء واحدة جديدة الآن. تمت الإضافة إلى القائمة بنجاح حدث خطأ ما، فشلت إضافة العنصر التصنيفات فشل الحذف - \ No newline at end of file + أيقونة المسلسلات + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 520361259..6d45ee8eb 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -162,10 +162,10 @@ close My Lists Please login to save to list - No lists available! Create one now. + No lists available for this item!\nCreate a new one now. Added to list successfully Something went wrong, item addition failed Categories Tv icon Deletion failed - \ No newline at end of file +