From 2434a04f9acdc5f38014b9333b02439fa814f3d6 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Mon, 18 Aug 2025 19:33:45 +0300 Subject: [PATCH 01/16] feat: Implement custom Snackbar component --- .../com/london/app/navigation/NavHostGraph.kt | 12 +- .../snackbar/LocalSnackbarController.kt | 7 ++ .../snackbar/ScaffoldWithSnackbar.kt | 113 ++++++++++++++++++ .../snackbar/SnackbarController.kt | 11 ++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt create mode 100644 designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt create mode 100644 designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt 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 1bf227ae1..dd7ad6458 100644 --- a/app/src/main/java/com/london/app/navigation/NavHostGraph.kt +++ b/app/src/main/java/com/london/app/navigation/NavHostGraph.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -19,6 +18,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.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 @@ -33,7 +35,6 @@ fun NavHostGraph() { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentScreen = when { navBackStackEntry.hasRoute(Home) -> Home navBackStackEntry.hasRoute(Search) -> Search @@ -50,8 +51,8 @@ fun NavHostGraph() { navBackStackEntry.hasRoute(Lists()) || navBackStackEntry.hasRoute(Account) - Scaffold( - backgroundColor = NovixTheme.colors.surface, + ScaffoldWithSnackbar( + containerColor = NovixTheme.colors.surface, bottomBar = { AnimatedVisibility( visible = showBottomNav, @@ -67,6 +68,9 @@ fun NavHostGraph() { } ) } + }, + snackbar = { data: SnackbarData -> + CustomSnackbarUI(data = data) } ) { innerPadding -> CompositionLocalProvider(LocalNavController provides navController) { 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..4edd87e0b --- /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..4a3d0fbc2 --- /dev/null +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt @@ -0,0 +1,113 @@ +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.SnackbarHost +import androidx.compose.material3.SnackbarHostState +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) { + job?.cancel() + job = coroutineScope.launch { + snackbarData.value = SnackbarData(message, icon, snackbarType) + delay(3000L) + 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, // Slot for your custom snackbar UI + 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() + ) + ) { + snackbar(currentSnackbarData!!) + } + } + } + } +} \ No newline at end of file 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..d579b0e4e --- /dev/null +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt @@ -0,0 +1,11 @@ +package com.london.designsystem.snackbar + +interface SnackbarController { + fun showMessage(message: String, icon: Int?, snackbarType: SnackbarType) +} + +enum class SnackbarType { + Success, + Error; +} + From ce31e1d887009b2b3e5d4450ed041890458cc82b Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 19:13:02 +0300 Subject: [PATCH 02/16] feat: Implement global snackbar and refactor movie list caching - Implement a global snackbar using `ScaffoldWithSnackbar` and `SnackbarController` to display messages across the app. - Refactor movie list caching logic in `CustomMovieListRepositoryImpl` for improved efficiency and error handling. - Clear `SyncMetadataDao` cache on logout in `AuthenticationRepositoryImpl`. - Update `BookmarkSheet` to use the new global snackbar. - Adjust UI padding in `OnboardingScreen` for better system bar handling. - Modify "no lists available" string for clarity. - Ensure `MovieListSyncWorker` is scheduled in `MainActivity`. --- .../main/java/com/london/app/MainActivity.kt | 8 +- .../java/com/london/app/NovixApplication.kt | 3 +- .../com/london/app/navigation/NavHostGraph.kt | 4 +- .../app/navigation/graph/mainNavGraph.kt | 3 +- .../dao/customLists/SyncMetadataDao.kt | 5 +- .../CustomMovieListLocalDataSourceImpl.kt | 1 + .../list/CustomMovieListsApiService.kt | 2 +- .../AuthenticationRepositoryImpl.kt | 5 +- .../list/CustomMovieListRepositoryImpl.kt | 192 +++++++++--------- .../snackbar/ScaffoldWithSnackbar.kt | 18 +- .../snackbar/SnackbarController.kt | 8 +- .../feature/list/viewitems/viewItemsScreen.kt | 2 - .../welcome/onboarding/onboardingScreen.kt | 10 +- .../shared/bookmarkSheet/BookmarkSheet.kt | 29 +-- .../bookmarkSheet/BookmarkSheetViewModel.kt | 6 +- .../src/main/res/values-ar/strings.xml | 5 +- presentation/src/main/res/values/strings.xml | 4 +- 17 files changed, 167 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/com/london/app/MainActivity.kt b/app/src/main/java/com/london/app/MainActivity.kt index 13570c083..ff7c3e015 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.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..7e30623a6 100644 --- a/app/src/main/java/com/london/app/NovixApplication.kt +++ b/app/src/main/java/com/london/app/NovixApplication.kt @@ -27,7 +27,6 @@ class NovixApplication : Application(), Configuration.Provider { timberConfig() WorkManager.initialize(this, workManagerConfiguration) - MovieListSyncWorker.schedulePeriodicSync(WorkManager.getInstance(applicationContext)) } 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 b73e3c153..ac4210c17 100644 --- a/app/src/main/java/com/london/app/navigation/NavHostGraph.kt +++ b/app/src/main/java/com/london/app/navigation/NavHostGraph.kt @@ -67,9 +67,7 @@ fun NavHostGraph() { ) } }, - snackbar = { data: SnackbarData -> - CustomSnackbarUI(data = data) - } + 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 d15473b32..56cc3d21e 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 @@ -117,7 +117,8 @@ fun NavGraphBuilder.mainNavGraph( ) } - composable { ReviewsScreen(onNavigateBack = ::navigateUp) + composable { + ReviewsScreen(onNavigateBack = ::navigateUp) } } } 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 8996eaad1..ce1b2b498 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 @@ -133,6 +133,7 @@ class CustomMovieListLocalDataSourceImpl @Inject constructor( override suspend fun clearAllCache() { membershipDao.clearAll() movieListDao.clearAll() + syncMetadataDao.clearAll() } companion object { diff --git a/data/src/main/java/com/london/data/remote/service/list/CustomMovieListsApiService.kt b/data/src/main/java/com/london/data/remote/service/list/CustomMovieListsApiService.kt index f29e8f005..aa2133846 100644 --- a/data/src/main/java/com/london/data/remote/service/list/CustomMovieListsApiService.kt +++ b/data/src/main/java/com/london/data/remote/service/list/CustomMovieListsApiService.kt @@ -49,7 +49,7 @@ interface CustomMovieListsApiService { @Body movieDeletionBody: ListMovieBody ): Response - @GET("3/account/{account_id}/lists") + @GET("3/account/0/lists") suspend fun getAllUserLists( @Query("session_id") sessionId: String?, @Query("page") page: Int 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 ce8731801..0fab0f09a 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 @@ -66,7 +66,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun deleteMovieList(id: Int): Boolean { - return try { + return runCatching { val success = remoteDataSource.delete( listId = id, sessionId = authenticationPreferences.getSessionId() @@ -75,12 +75,10 @@ class CustomMovieListRepositoryImpl @Inject constructor( 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 { @@ -93,7 +91,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun createMovieList(name: String): Boolean { - return try { + return runCatching { val result = remoteDataSource.create( name = name, sessionId = authenticationPreferences.getSessionId(), @@ -117,10 +115,10 @@ class CustomMovieListRepositoryImpl @Inject constructor( } else { false } - } catch (e: Exception) { - crashReporter.logException(e) - false - } + }.onFailure { + crashReporter.logException(it) + throw it + }.isSuccess } override suspend fun getMovieListName(listId: Int): String { @@ -144,44 +142,31 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun getMovieLists(pageNumber: Int): PagedFetchResponse { - return if (localDataSource.shouldRefreshCache()) { - val response = remoteDataSource.getAllMovieLists( - page = pageNumber, - sessionId = authenticationPreferences.getSessionId() - ).getOrThrow() + if (localDataSource.shouldRefreshCache()) { + refreshMovieListCache() + } - val localLists = response.items.map { it.toLocal() } - localDataSource.cacheMovieListsMetadata(localLists) + 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 { + return runCatching { val result = remoteDataSource.addMovieToList( listId = listId, movieId = movieId, @@ -201,10 +186,10 @@ class CustomMovieListRepositoryImpl @Inject constructor( } else { false } - } catch (e: Exception) { - crashReporter.logException(e) - false - } + }.onFailure { + crashReporter.logException(it) + throw it + }.isSuccess } override suspend fun getMovieListDetails( @@ -224,7 +209,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun removeMovieFromList(listId: Int, movieId: Int): Boolean { - return try { + return runCatching { val result = remoteDataSource.removeMovieFromList( listId = listId, movieId = movieId, @@ -244,67 +229,82 @@ class CustomMovieListRepositoryImpl @Inject constructor( } else { false } - } catch (e: Exception) { - crashReporter.logException(e) - false - } + }.onFailure { + crashReporter.logException(it) + throw it + }.isSuccess } 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) + runCatching { + val memberships = assembleMembershipsFromRemote() + val lists = assembleListsFromRemote() localDataSource.cacheMovieListsMetadata(lists) localDataSource.cacheMovieListMemberships(memberships) localDataSource.markCacheRefreshed(true) - - } catch (e: Exception) { + }.onFailure { localDataSource.markCacheRefreshed(false) - crashReporter.logException(e) - throw e + crashReporter.logException(it) + throw it } } } + private suspend fun assembleListsFromRemote(): MutableList { + val lists = mutableListOf() + var page = 1 + do { + val response = remoteDataSource.getAllMovieLists( + page = page, + sessionId = authenticationPreferences.getSessionId() + ).getOrThrow() + val pageLists = response.items.map { it.toLocal() } + lists.addAll(pageLists) + page++ + } while (page <= response.totalPages) + return lists + } + + private suspend fun assembleMembershipsFromRemote(): MutableList { + val memberships = mutableListOf() + var page = 1 + do { + val listsResponse = remoteDataSource.getAllMovieLists( + page = page, + sessionId = authenticationPreferences.getSessionId() + ).getOrThrow() + + for (list in listsResponse.items) { + if (list.id == null) continue + + var moviePage = 1 + var membershipCount = 0 + do { + val moviesResponse = remoteDataSource.getDetails( + listId = list.id, + page = moviePage + ).getOrThrow() + val items = moviesResponse.items ?: emptyList() + for (movie in items) { + if (movie.id == null) continue + memberships.add( + MovieListMembershipLocal( + movieId = movie.id, + listId = list.id + ) + ) + membershipCount++ + } + moviePage++ + } while (moviePage <= MAX_PAGES && items.isNotEmpty()) + } + page++ + } while (page <= listsResponse.totalPages) + return memberships + } + private suspend fun refreshMovieListCacheIfNecessary(forceRefresh: Boolean) { if (forceRefresh || localDataSource.shouldRefreshCache()) { refreshMovieListCache() diff --git a/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt index 4a3d0fbc2..4ddd72666 100644 --- a/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt @@ -12,8 +12,6 @@ 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.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -45,11 +43,17 @@ private class SnackbarManager( val snackbarData = mutableStateOf(null) private var job: Job? = null - override fun showMessage(message: String, icon: Int?, snackbarType: SnackbarType) { + 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 } } @@ -75,7 +79,7 @@ fun ScaffoldWithSnackbar( floatingActionButton: @Composable () -> Unit = {}, containerColor: Color = MaterialTheme.colorScheme.background, contentColor: Color = contentColorFor(containerColor), - snackbar: @Composable (data: SnackbarData) -> Unit, // Slot for your custom snackbar UI + snackbar: @Composable (data: SnackbarData) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val coroutineScope = rememberCoroutineScope() @@ -105,9 +109,11 @@ fun ScaffoldWithSnackbar( animationSpec = tween() ) ) { - snackbar(currentSnackbarData!!) + currentSnackbarData?.let { + snackbar(it) + } } } } } -} \ No newline at end of file +} diff --git a/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt index d579b0e4e..7d9970def 100644 --- a/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt @@ -1,11 +1,15 @@ package com.london.designsystem.snackbar interface SnackbarController { - fun showMessage(message: String, icon: Int?, snackbarType: SnackbarType) + fun showMessage( + message: String, + icon: Int?, + snackbarType: SnackbarType, + onComplete: () -> Unit + ) } enum class SnackbarType { Success, Error; } - 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 ba6a2089d..6e84d5510 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 @@ -94,8 +94,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/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/bookmarkSheet/BookmarkSheet.kt b/presentation/src/main/java/com/london/presentation/shared/bookmarkSheet/BookmarkSheet.kt index 56f7f8256..1c4a153dc 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..691a59086 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) } }, @@ -67,7 +67,9 @@ class BookmarkSheetViewModel @Inject constructor( ) } }, - onCompleted = { updateState { copy(isAddingToList = false) } } + onCompleted = { + updateState { copy(isAddingToList = false) } + } ) } 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 + From 26d9f7aaad59cdb94902035cbc9fa6f94e694f50 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 20:12:05 +0300 Subject: [PATCH 03/16] refactor: improve readability and error handling in CustomMovieListRepositoryImpl --- .../list/CustomMovieListRepositoryImpl.kt | 287 ++++++++---------- 1 file changed, 133 insertions(+), 154 deletions(-) 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 0fab0f09a..dd7cdd335 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,9 +7,10 @@ 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.orZero import com.london.domain.AppPreferencesService import com.london.domain.entity.movie.Movie @@ -32,16 +33,8 @@ 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 { @@ -49,16 +42,8 @@ class CustomMovieListRepositoryImpl @Inject constructor( } 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> { @@ -66,15 +51,19 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun deleteMovieList(id: Int): Boolean { + val success = deleteListRemotely(id) + if (success) { + localDataSource.removeMovieListCache(id) + } + return success + } + + private suspend fun deleteListRemotely(id: Int): Boolean { return runCatching { - val success = remoteDataSource.delete( + remoteDataSource.delete( listId = id, sessionId = authenticationPreferences.getSessionId() ).isSuccess - - if (success) { - localDataSource.removeMovieListCache(id) - } }.onFailure { crashReporter.logException(it) throw it @@ -91,61 +80,43 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun createMovieList(name: String): Boolean { - return runCatching { - 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 { listId -> - localDataSource.addMovieListCache( - MovieListLocal( - id = response.id, - name = name, - description = "", - itemCount = 0 - ) - ) - } - } - true - } else { - false - } - }.onFailure { - crashReporter.logException(it) - throw it - }.isSuccess + 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 { - if (localDataSource.shouldRefreshCache()) { - refreshMovieListCache() - } + refreshMovieListCacheIfNecessary(forceRefresh = false) + return buildPagedMovieListResponse(pageNumber) + } + private suspend fun buildPagedMovieListResponse(pageNumber: Int): PagedFetchResponse { val allLists = localDataSource.getAllUserLists() val itemsPerPage = 20 val startIndex = (pageNumber - 1) * itemsPerPage @@ -166,30 +137,27 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun addMovieToList(listId: Int, movieId: Int): Boolean { - return runCatching { - 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) - ) - } + private suspend fun updateLocalCacheAfterAddingMovie(listId: Int, movieId: Int) { + localDataSource.addMovieToListCache(movieId, listId) - true - } else { - false - } - }.onFailure { - crashReporter.logException(it) - throw it - }.isSuccess + localDataSource.getMovieList(listId)?.let { list -> + localDataSource.addMovieListCache( + list.copy(itemCount = list.itemCount + 1) + ) + } } override suspend fun getMovieListDetails( @@ -200,6 +168,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( listId = listId, page = pageNumber ).getOrThrow() + return PagedFetchResponse( currentPage = pageNumber, items = response.items.orEmpty().map { it.toEntity() }, @@ -209,40 +178,34 @@ class CustomMovieListRepositoryImpl @Inject constructor( } override suspend fun removeMovieFromList(listId: Int, movieId: Int): Boolean { - return runCatching { - 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)) - ) - } + private suspend fun updateLocalCacheAfterRemovingMovie(listId: Int, movieId: Int) { + localDataSource.removeMovieFromListCache(movieId, listId) - true - } else { - false - } - }.onFailure { - crashReporter.logException(it) - throw it - }.isSuccess + localDataSource.getMovieList(listId)?.let { list -> + localDataSource.addMovieListCache( + list.copy(itemCount = maxOf(0, list.itemCount - 1)) + ) + } } override suspend fun refreshMovieListCache() { syncMutex.withLock { runCatching { - val memberships = assembleMembershipsFromRemote() - val lists = assembleListsFromRemote() - - localDataSource.cacheMovieListsMetadata(lists) - localDataSource.cacheMovieListMemberships(memberships) + val (lists, memberships) = fetchAllListsAndMemberships() + cacheListsAndMemberships(lists, memberships) localDataSource.markCacheRefreshed(true) }.onFailure { localDataSource.markCacheRefreshed(false) @@ -252,59 +215,76 @@ class CustomMovieListRepositoryImpl @Inject constructor( } } - private suspend fun assembleListsFromRemote(): MutableList { + 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() - val pageLists = response.items.map { it.toLocal() } - lists.addAll(pageLists) + + lists.addAll(response.items.map { it.toLocal() }) + memberships.addAll(fetchMembershipsForListsPage(response.items)) + page++ } while (page <= response.totalPages) - return lists + + return Pair(lists, memberships) } - private suspend fun assembleMembershipsFromRemote(): MutableList { + private suspend fun fetchMembershipsForListsPage( + listsPage: List + ): List { val memberships = mutableListOf() - var page = 1 + + for (list in listsPage) { + if (list.id == null) continue + memberships.addAll(fetchAllMembershipsForList(list.id)) + } + + return memberships + } + + private suspend fun fetchAllMembershipsForList(listId: Int): List { + val memberships = mutableListOf() + var moviePage = 1 + do { - val listsResponse = remoteDataSource.getAllMovieLists( - page = page, - sessionId = authenticationPreferences.getSessionId() + val listDetailsResponse = remoteDataSource.getDetails( + listId = listId, + page = moviePage ).getOrThrow() - for (list in listsResponse.items) { - if (list.id == null) continue - - var moviePage = 1 - var membershipCount = 0 - do { - val moviesResponse = remoteDataSource.getDetails( - listId = list.id, - page = moviePage - ).getOrThrow() - val items = moviesResponse.items ?: emptyList() - for (movie in items) { - if (movie.id == null) continue - memberships.add( - MovieListMembershipLocal( - movieId = movie.id, - listId = list.id - ) + val items = listDetailsResponse.items.orEmpty() + + for (movie in items) { + if (movie.id != null) { + memberships.add( + MovieListMembershipLocal( + movieId = movie.id, + listId = listId ) - membershipCount++ - } - moviePage++ - } while (moviePage <= MAX_PAGES && items.isNotEmpty()) + ) + } } - page++ - } while (page <= listsResponse.totalPages) + + moviePage++ + } while (moviePage <= MAX_PAGES && items.isNotEmpty()) + return memberships } + private suspend fun cacheListsAndMemberships( + lists: List, + memberships: List + ) { + localDataSource.cacheMovieListsMetadata(lists) + localDataSource.cacheMovieListMemberships(memberships) + } + private suspend fun refreshMovieListCacheIfNecessary(forceRefresh: Boolean) { if (forceRefresh || localDataSource.shouldRefreshCache()) { refreshMovieListCache() @@ -315,4 +295,3 @@ class CustomMovieListRepositoryImpl @Inject constructor( const val MAX_PAGES = 10 } } - From 4585fba1ba598e02b5b30da169effd239492cb11 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 20:37:40 +0300 Subject: [PATCH 04/16] feat: implement bookmark bottom sheet for movies --- .../feature/search/SearchContract.kt | 2 ++ .../feature/search/SearchScreen.kt | 27 ++++++++++++++----- .../feature/search/SearchUiState.kt | 4 ++- .../feature/search/SearchViewModel.kt | 20 +++++++++++++- .../shared/container/MediaLazyVerticalGrid.kt | 6 ++--- 5 files changed, 47 insertions(+), 12 deletions(-) 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 2135221c7..77cce356d 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 @@ -54,6 +54,7 @@ import com.london.designsystem.component.TopBar import com.london.designsystem.theme.NovixTheme import com.london.designsystem.theme.ThemePreviews import com.london.domain.entity.recent.MediaType +import com.london.domain.entity.recent.MediaType.Companion.isMovie import com.london.domain.entity.recent.RecentSearch import com.london.domain.entity.recent.RecentViewed import com.london.presentation.R @@ -61,6 +62,7 @@ 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 @@ -167,6 +169,12 @@ private fun SearchMainContent( ) SearchBody(state = state, contract = contract) + + BookmarkBottomSheet( + onSheetDismiss = contract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) } } } @@ -252,7 +260,8 @@ private fun MovieSearchContent(state: SearchUiState, contract: SearchContract) { contract.onMovieGenreClick(it.genres) } contract.onMovieClick(id) - } + }, + hasSaveIcon = true ) } @@ -313,7 +322,8 @@ private fun MediaSearchContent( pagingItems: LazyPagingItems, contract: SearchContract, onNavigateToMovie: (Int) -> Unit = {}, - onNavigateToTvShow: (Int) -> Unit = {} + onNavigateToTvShow: (Int) -> Unit = {}, + hasSaveIcon: Boolean = false ) { EmptyContent( pagingItems, @@ -335,8 +345,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 @@ -504,7 +514,8 @@ fun RecentSectionContent( recentViewed = state.recentViewed, onClearAll = contract::clearRecentViewed, onNavigateToTvShowDetails = onNavigateToTvShowDetails, - onNavigateToMovieDetails = onNavigateToMovieDetails + onNavigateToMovieDetails = onNavigateToMovieDetails, + onManageBookmarkClicked = contract::onManageBookmarkClicked ) } } @@ -528,6 +539,7 @@ private fun RecentViewedSection( onClearAll: () -> Unit, onNavigateToTvShowDetails: (Int) -> Unit, onNavigateToMovieDetails: (Int) -> Unit, + onManageBookmarkClicked: (Int) -> Unit ) { SectionHeader( text = stringResource(R.string.recent_viewed), @@ -549,7 +561,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) @@ -683,4 +696,4 @@ private fun Preview() { onNavigateToTvShowDetails = {}, onNavigateToMovieDetails = {} ) -} \ No newline at end of file +} 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/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) ) From 0900e6aa8fa4c4efff4d3cec504f1c8ccc7f0949 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 21:53:04 +0300 Subject: [PATCH 05/16] feat: integrate BookmarkBottomSheet into TrendingMovies and ContinueWatching screens --- .../ContinueWatchingContract.kt | 2 ++ .../continuewatching/ContinueWatchingScreen.kt | 14 +++++++++++--- .../ContinueWatchingUiState.kt | 4 +++- .../ContinueWatchingViewModel.kt | 18 ++++++++++++++++++ .../trending/movie/TrendingMoviesContract.kt | 6 +++++- .../trending/movie/TrendingMoviesScreen.kt | 12 ++++++++++-- .../trending/movie/TrendingMoviesUiState.kt | 4 +++- .../trending/movie/TrendingMoviesViewModel.kt | 18 ++++++++++++++++++ .../presentation/shared/MediaLazyPagingGrid.kt | 5 +++-- 9 files changed, 73 insertions(+), 10 deletions(-) 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/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/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 +} From cdfb99f1300383dd26597d44d08af780ef368b06 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 22:16:44 +0300 Subject: [PATCH 06/16] feat: Implement bookmark functionality in TopRated screen --- .../app/navigation/graph/mainNavGraph.kt | 4 +-- .../feature/home/toprated/TopRatedContract.kt | 5 +++- .../feature/home/toprated/TopRatedScreen.kt | 30 +++++++++++-------- .../feature/home/toprated/TopRatedUiState.kt | 4 ++- .../home/toprated/TopRatedViewModel.kt | 20 ++++++++++++- 5 files changed, 46 insertions(+), 17 deletions(-) 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 56cc3d21e..8413c000a 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 @@ -141,8 +141,8 @@ fun NavGraphBuilder.homeNavGraph(navController: NavHostController) = composable { TopRatedScreen( onNavigateBack = ::navigateUp, - onNaviagteToMovieDetalis = ::navigateToMovieDetails, - onNaviagteToTvShowDetalis = ::navigateToTvShowDetails + onNavigateToMovieDetails = ::navigateToMovieDetails, + onNavigateToTvShowDetails = ::navigateToTvShowDetails ) } } 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 7f760efd3..ba91b2509 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,6 +33,7 @@ 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 @@ -44,8 +45,8 @@ import com.london.presentation.utils.isLoading 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 +54,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,7 +65,7 @@ fun TopRatedScreen( BuildScreen( isLoading = topRatedTvShowFlow.isLoading() && topRatedMovieFlow.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, ) { @@ -139,9 +140,8 @@ private fun Content( HomeCard( imageUrl = movieItem.posterUrl, isSaved = false, - onSaveClick = { - // TODO - }, + hasSaveIcon = true, + onSaveClick = { topRatedContract.onManageBookmarkClicked(movieItem.id) }, modifier = Modifier.clickable { topRatedContract.onMovieClick(movieItem.id) } @@ -154,10 +154,9 @@ private fun Content( tvSeries?.let { seriesItem -> HomeCard( imageUrl = seriesItem.posterUrl, + hasSaveIcon = false, isSaved = false, - onSaveClick = { - // TODO - }, + onSaveClick = {}, modifier = Modifier.clickable { topRatedContract.onTvShowClick(seriesItem.id) } @@ -165,6 +164,13 @@ private fun Content( } } } + + BookmarkBottomSheet( + onSheetDismiss = topRatedContract::onBookmarkSheetDismiss, + isSheetVisible = state.isBookmarkSheetVisible, + bookmarkedMovieId = state.bookmarkedMovieId + ) + } } @@ -214,4 +220,4 @@ private fun TvShowRow( onClick = { onGenreClick(genre) }) } } -} \ No newline at end of file +} 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 +} From 0da91b6a2cd69273a5395e5dccc6ed7b30f29c30 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 22:35:32 +0300 Subject: [PATCH 07/16] feat: implement bookmark bottom sheet functionality --- .../feature/details/movie/MovieDetailsContract.kt | 5 +++-- .../feature/details/movie/MovieDetailsScreen.kt | 9 ++++++++- .../feature/details/movie/MovieDetailsUiState.kt | 3 ++- .../feature/details/movie/MovieDetailsViewModel.kt | 7 ++++--- 4 files changed, 17 insertions(+), 7 deletions(-) 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 cba58f948..ad1c77642 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() +} 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 4343a8ed1..70d8ef95c 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 @@ -160,7 +161,7 @@ private fun Content( TopBar( onBackClick = movieDetailsContract::onBackClick, modifier = Modifier.detailsTopBar(backgroundAlpha), - onClickOption1 = { /*todo on click on save*/ }, + onClickOption1 = movieDetailsContract::onManageBookmarkClicked, option1Icon = R.drawable.icon_remove, ) @@ -185,6 +186,12 @@ private fun Content( ) BottomSheetsHandler(uiState, movieDetailsContract) + + BookmarkBottomSheet( + onSheetDismiss = movieDetailsContract::onBookmarkSheetDismiss, + isSheetVisible = uiState.isBookmarkSheetVisible, + bookmarkedMovieId = uiState.movieId + ) } uiState.isSuccessfullyRated?.let { isSuccessful -> 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..454f58c0f 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,8 @@ data class MovieDetailsUiState( val similarMovies: List = listOf(), val isRateBottomSheetVisible: Boolean = false, val isGuestUserBottomSheetVisible: Boolean = false, -){ + val isBookmarkSheetVisible: Boolean = false, +) { 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 a5f3df787..622742a21 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,9 +203,10 @@ class MovieDetailsViewModel @Inject constructor( emitEffect(MovieDetailsEffect.BackNavigation) } - override fun onSavedClick() { - // TODO: implement saving logic - } + override fun onManageBookmarkClicked() = updateState { copy(isBookmarkSheetVisible = true) } + + override fun onBookmarkSheetDismiss() = updateState { copy(isBookmarkSheetVisible = false) } + override fun onExpandClick() { updateState { copy(expanded = !expanded) } From 93525bf7159d4d3227b3a37c3cc2ad95d23f1041 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 22:46:49 +0300 Subject: [PATCH 08/16] feat: integrate bookmark bottom sheet into movie category screen --- .../category/movie/MovieCategoryContract.kt | 3 ++- .../category/movie/MovieCategoryScreen.kt | 14 ++++++++++++-- .../category/movie/MovieCategoryUiState.kt | 4 +++- .../category/movie/MovieCategoryViewModel.kt | 17 ++++++++++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) 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..b948bcbe7 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,8 +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( onStart = { onInitializeMoviesStarted(genre = genreUi) }, From 4f53f66c4821872908c4afbc05b0163ffc5f9f7b Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 23:32:15 +0300 Subject: [PATCH 09/16] refactor: pass CustomMovieListLocalDataSource to AuthenticationRepositoryImpl --- .../repository/AuthenticationRepositoryImplTest.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 +} From 2b8fee533348158a7971617ccacdf5e2c185cc74 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Tue, 19 Aug 2025 23:42:53 +0300 Subject: [PATCH 10/16] feat: clear all cache before caching movie lists and memberships --- .../london/data/repository/list/CustomMovieListRepositoryImpl.kt | 1 + 1 file changed, 1 insertion(+) 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 c71dcd1ce..1ce858e73 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 @@ -281,6 +281,7 @@ class CustomMovieListRepositoryImpl @Inject constructor( lists: List, memberships: List ) { + localDataSource.clearAllCache() localDataSource.cacheMovieListsMetadata(lists) localDataSource.cacheMovieListMemberships(memberships) } From 11619efe6b43860967d588248ac3b103fdf526fb Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 00:22:55 +0300 Subject: [PATCH 11/16] refactor: remove save tv show functionality --- .../actor/info/toptvshowspicks/TopTvShowsPicksContract.kt | 1 - .../actor/info/toptvshowspicks/TopTvShowsPicksScreen.kt | 3 +-- .../actor/info/toptvshowspicks/TopTvShowsPicksViewModel.kt | 4 ---- .../feature/details/tvshow/info/TvShowDetailScreen.kt | 1 - .../main/java/com/london/presentation/shared/MediaLazyGrid.kt | 2 ++ 5 files changed, 3 insertions(+), 8 deletions(-) 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/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/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) } ) } From 2be2964b6f6567de99116354c85af7b99a63666b Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 00:39:45 +0300 Subject: [PATCH 12/16] test: update CustomMovieListRepositoryImplTest to use assertThrows and refine test names --- .../list/CustomMovieListRepositoryImplTest.kt | 200 ++++++++++-------- 1 file changed, 113 insertions(+), 87 deletions(-) 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", From 26919eb2bd30fd623627d4fec6c11121dd87b7f4 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 00:51:35 +0300 Subject: [PATCH 13/16] feat: Implement bookmark functionality in TopMoviesPicks --- .../category/movie/MovieCategoryViewModel.kt | 1 + .../topmoviespicks/TopMoviesPicksContract.kt | 3 ++- .../topmoviespicks/TopMoviesPicksScreen.kt | 10 ++++++++- .../topmoviespicks/TopMoviesPicksUiState.kt | 2 ++ .../topmoviespicks/TopMoviesPicksViewModel.kt | 22 +++++++++++++++---- 5 files changed, 32 insertions(+), 6 deletions(-) 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 b948bcbe7..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 @@ -53,6 +53,7 @@ class MovieCategoryViewModel @Inject constructor( ) } } + private fun initializeMovies(genreUi: MovieGenreUi) { tryToExecute( onStart = { onInitializeMoviesStarted(genre = genreUi) }, 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) }, From 1c37b276ba562e141c7009f13f74e59dede9c8de Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 01:18:05 +0300 Subject: [PATCH 14/16] feat: pass movieId to onManageBookmarkClicked --- .../details/movie/MovieDetailsContract.kt | 2 +- .../details/movie/MovieDetailsScreen.kt | 13 ++++++------- .../details/movie/MovieDetailsUiState.kt | 1 + .../details/movie/MovieDetailsViewModel.kt | 19 ++++++++++++++++--- 4 files changed, 24 insertions(+), 11 deletions(-) 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 232f69c2b..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 @@ -15,5 +15,5 @@ interface MovieDetailsContract { fun onGenreClick(genre: MovieGenreUi) fun onReviewsClick(movieId: Int, mediaType: MediaType) fun onBookmarkSheetDismiss() - fun onManageBookmarkClicked() + 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 fe884416c..9ff2c804d 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 @@ -161,7 +161,7 @@ private fun Content( TopBar( onBackClick = movieDetailsContract::onBackClick, modifier = Modifier.detailsTopBar(backgroundAlpha), - onClickOption1 = movieDetailsContract::onManageBookmarkClicked, + onClickOption1 = { movieDetailsContract.onManageBookmarkClicked(uiState.movieId) }, option1Icon = R.drawable.icon_remove, ) @@ -190,7 +190,7 @@ private fun Content( BookmarkBottomSheet( onSheetDismiss = movieDetailsContract::onBookmarkSheetDismiss, isSheetVisible = uiState.isBookmarkSheetVisible, - bookmarkedMovieId = uiState.movieId + bookmarkedMovieId = uiState.bookmarkedMovieId ) } @@ -351,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 454f58c0f..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 @@ -28,6 +28,7 @@ data class MovieDetailsUiState( 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 1e9e807ad..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,10 +203,23 @@ class MovieDetailsViewModel @Inject constructor( emitEffect(MovieDetailsEffect.BackNavigation) } - override fun onManageBookmarkClicked() = updateState { copy(isBookmarkSheetVisible = true) } - - override fun onBookmarkSheetDismiss() = updateState { copy(isBookmarkSheetVisible = false) } + override fun onManageBookmarkClicked(movieId: Int) { + updateState { + copy( + isBookmarkSheetVisible = true, + bookmarkedMovieId = movieId + ) + } + } + override fun onBookmarkSheetDismiss() { + updateState { + copy( + isBookmarkSheetVisible = false, + bookmarkedMovieId = 0 + ) + } + } override fun onExpandClick() { updateState { copy(expanded = !expanded) } From e3543e46c94b8079fb8e6f43894cc95684bd9280 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 01:34:35 +0300 Subject: [PATCH 15/16] feat: Add bookmark functionality to actor details screen --- .../details/actor/ActorDetailsContract.kt | 2 + .../details/actor/ActorDetailsScreen.kt | 41 +++++++++++-------- .../details/actor/ActorDetailsUiState.kt | 2 + .../details/actor/ActorDetailsViewModel.kt | 18 ++++++++ 4 files changed, 47 insertions(+), 16 deletions(-) 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() From 35b8e7c337f18222b10603aed9cbf62ae8135da7 Mon Sep 17 00:00:00 2001 From: Yusuf Nasser Date: Wed, 20 Aug 2025 02:26:01 +0300 Subject: [PATCH 16/16] Refactor: Standardize SnackBar and contract naming --- .../java/com/london/app/NovixApplication.kt | 2 +- .../com/london/app/navigation/NavHostGraph.kt | 10 ++--- .../CustomMovieListLocalDataSourceImpl.kt | 5 +-- .../list/CustomMovieListRepositoryImpl.kt | 43 ++++++++----------- .../london/designsystem/component/Scaffold.kt | 4 +- .../snackbar/LocalSnackbarController.kt | 2 +- ...ithSnackbar.kt => ScaffoldWithSnackBar.kt} | 32 +++++++------- ...barController.kt => SnackBarController.kt} | 6 +-- .../details/movie/MovieDetailsScreen.kt | 16 +++---- .../feature/home/toprated/TopRatedScreen.kt | 23 +++++----- .../shared/bookmarkSheet/BookmarkSheet.kt | 12 +++--- .../bookmarkSheet/BookmarkSheetViewModel.kt | 4 +- 12 files changed, 72 insertions(+), 87 deletions(-) rename designSystem/src/main/java/com/london/designsystem/snackbar/{ScaffoldWithSnackbar.kt => ScaffoldWithSnackBar.kt} (83%) rename designSystem/src/main/java/com/london/designsystem/snackbar/{SnackbarController.kt => SnackBarController.kt} (65%) diff --git a/app/src/main/java/com/london/app/NovixApplication.kt b/app/src/main/java/com/london/app/NovixApplication.kt index 7e30623a6..a0f792579 100644 --- a/app/src/main/java/com/london/app/NovixApplication.kt +++ b/app/src/main/java/com/london/app/NovixApplication.kt @@ -26,7 +26,7 @@ class NovixApplication : Application(), Configuration.Provider { super.onCreate() timberConfig() - WorkManager.initialize(this, workManagerConfiguration) + WorkManager.initialize(context = this, configuration = workManagerConfiguration) } private fun timberConfig() { 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 90be497f3..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,9 +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.snackbar.CustomSnackbarUI -import com.london.designsystem.snackbar.ScaffoldWithSnackbar -import com.london.designsystem.snackbar.SnackbarData +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 @@ -34,7 +34,7 @@ fun NavHostGraph() { val navBackStackEntry by navController.currentBackStackEntryAsState() val showBottomNav = navBackStackEntry.hasRoute(Home, Search, Categories, Lists(), Account) - ScaffoldWithSnackbar( + ScaffoldWithSnackBar( containerColor = NovixTheme.colors.surface, bottomBar = { AnimatedVisibility( @@ -51,7 +51,7 @@ fun NavHostGraph() { ) } }, - snackbar = { data: SnackbarData -> CustomSnackbarUI(data = data) } + snackBar = { data: SnackBarData -> CustomSnackBarUI(data = data) } ) { innerPadding -> CompositionLocalProvider(LocalNavController provides navController) { NavHost( 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 84aa74414..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 ) @@ -108,6 +108,5 @@ class CustomMovieListLocalDataSourceImpl @Inject constructor( 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/repository/list/CustomMovieListRepositoryImpl.kt b/data/src/main/java/com/london/data/repository/list/CustomMovieListRepositoryImpl.kt index 1ce858e73..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 @@ -11,6 +11,7 @@ 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.isTrue import com.london.data.utils.orZero import com.london.domain.entity.movie.Movie import com.london.domain.entity.movie.MovieList @@ -37,18 +38,17 @@ class CustomMovieListRepositoryImpl @Inject constructor( return localDataSource.isMovieListed(movieId) } - override fun isMovieListedFlow(movieId: Int): Flow { - return localDataSource.isMovieListedFlow(movieId) - } + override fun isMovieListedFlow(movieId: Int): Flow = localDataSource.isMovieListedFlow(movieId) + override suspend fun getMovieListIds(movieId: Int, forceRefresh: Boolean): List { refreshMovieListCacheIfNecessary(forceRefresh) return localDataSource.getMovieListIds(movieId) } - override fun getMovieListIdsFlow(movieId: Int): Flow> { - return localDataSource.getMovieListIdsFlow(movieId) - } + override fun getMovieListIdsFlow(movieId: Int): Flow> = + localDataSource.getMovieListIdsFlow(movieId) + override suspend fun deleteMovieList(id: Int): Boolean { val success = deleteListRemotely(id) @@ -75,9 +75,8 @@ class CustomMovieListRepositoryImpl @Inject constructor( return localDataSource.getAllListedMovieIds() } - override fun getAllListedMovieIdsFlow(): Flow> { - return localDataSource.getAllListedMovieIdsFlow() - } + override fun getAllListedMovieIdsFlow(): Flow> = localDataSource.getAllListedMovieIdsFlow() + override suspend fun createMovieList(name: String): Boolean { val response = createListRemotely(name) @@ -142,13 +141,13 @@ class CustomMovieListRepositoryImpl @Inject constructor( return true } - private suspend fun addMovieRemotely(listId: Int, movieId: Int) { + private suspend fun addMovieRemotely(listId: Int, movieId: Int) = remoteDataSource.addMovieToList( listId = listId, movieId = movieId, sessionId = authenticationPreferences.getSessionId() ).getOrThrow() - } + private suspend fun updateLocalCacheAfterAddingMovie(listId: Int, movieId: Int) { localDataSource.addMovieToListCache(movieId, listId) @@ -183,13 +182,13 @@ class CustomMovieListRepositoryImpl @Inject constructor( return true } - private suspend fun removeMovieRemotely(listId: Int, movieId: Int) { + private suspend fun removeMovieRemotely(listId: Int, movieId: Int) = remoteDataSource.removeMovieFromList( listId = listId, movieId = movieId, sessionId = authenticationPreferences.getSessionId() ).getOrThrow() - } + private suspend fun updateLocalCacheAfterRemovingMovie(listId: Int, movieId: Int) { localDataSource.removeMovieFromListCache(movieId, listId) @@ -240,9 +239,8 @@ class CustomMovieListRepositoryImpl @Inject constructor( ): List { val memberships = mutableListOf() - for (list in listsPage) { - if (list.id == null) continue - memberships.addAll(fetchAllMembershipsForList(list.id)) + listsPage.forEach { list -> + list.id?.let { listId -> memberships.addAll(fetchAllMembershipsForList(listId)) } } return memberships @@ -260,14 +258,9 @@ class CustomMovieListRepositoryImpl @Inject constructor( val items = listDetailsResponse.items.orEmpty() - for (movie in items) { - if (movie.id != null) { - memberships.add( - MovieListMembershipLocal( - movieId = movie.id, - listId = listId - ) - ) + items.forEach { movie -> + movie.id?.let { movieId -> + memberships.add(MovieListMembershipLocal(movieId = movieId, listId = listId)) } } @@ -293,6 +286,6 @@ class CustomMovieListRepositoryImpl @Inject constructor( } private companion object { - const val MAX_PAGES = 10 + const val MAX_PAGES = 100 } } 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 index 4edd87e0b..29bf79e6e 100644 --- a/designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/LocalSnackbarController.kt @@ -2,6 +2,6 @@ package com.london.designsystem.snackbar import androidx.compose.runtime.staticCompositionLocalOf -val LocalSnackbarController = 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 similarity index 83% rename from designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt rename to designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackBar.kt index 4ddd72666..03854515c 100644 --- a/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackbar.kt +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/ScaffoldWithSnackBar.kt @@ -31,36 +31,36 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -data class SnackbarData( +data class SnackBarData( val message: String, @DrawableRes val icon: Int?, - val snackbarType: SnackbarType, + val snackBarType: SnackBarType, ) -private class SnackbarManager( +private class SnackBarManager( private val coroutineScope: CoroutineScope -) : SnackbarController { - val snackbarData = mutableStateOf(null) +) : SnackBarController { + val snackBarData = mutableStateOf(null) private var job: Job? = null override fun showMessage( message: String, icon: Int?, - snackbarType: SnackbarType, + snackBarType: SnackBarType, onComplete: () -> Unit ) { job?.cancel() job = coroutineScope.launch { - snackbarData.value = SnackbarData(message, icon, snackbarType) + snackBarData.value = SnackBarData(message, icon, snackBarType) delay(3000L) onComplete() - snackbarData.value = null + snackBarData.value = null } } } @Composable -fun CustomSnackbarUI(data: SnackbarData) { +fun CustomSnackBarUI(data: SnackBarData) { SnackBar( modifier = Modifier .fillMaxWidth() @@ -72,21 +72,21 @@ fun CustomSnackbarUI(data: SnackbarData) { } @Composable -fun ScaffoldWithSnackbar( +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, + snackBar: @Composable (data: SnackBarData) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val coroutineScope = rememberCoroutineScope() - val snackbarManager = remember(coroutineScope) { SnackbarManager(coroutineScope) } - val currentSnackbarData by snackbarManager.snackbarData + val snackBarManager = remember(coroutineScope) { SnackBarManager(coroutineScope) } + val currentSnackbarData by snackBarManager.snackBarData - CompositionLocalProvider(LocalSnackbarController provides snackbarManager) { + CompositionLocalProvider(LocalSnackbarController provides snackBarManager) { Scaffold( modifier = modifier, topBar = topBar, @@ -109,9 +109,7 @@ fun ScaffoldWithSnackbar( animationSpec = tween() ) ) { - currentSnackbarData?.let { - snackbar(it) - } + 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 similarity index 65% rename from designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt rename to designSystem/src/main/java/com/london/designsystem/snackbar/SnackBarController.kt index 7d9970def..e10026db5 100644 --- a/designSystem/src/main/java/com/london/designsystem/snackbar/SnackbarController.kt +++ b/designSystem/src/main/java/com/london/designsystem/snackbar/SnackBarController.kt @@ -1,15 +1,15 @@ package com.london.designsystem.snackbar -interface SnackbarController { +interface SnackBarController { fun showMessage( message: String, icon: Int?, - snackbarType: SnackbarType, + snackBarType: SnackBarType, onComplete: () -> Unit ) } -enum class SnackbarType { +enum class SnackBarType { Success, Error; } 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 9ff2c804d..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 @@ -118,7 +118,7 @@ fun MovieDetailsScreen( ) { Content( uiState = state, - movieDetailsContract = viewModel + contract = viewModel ) } } @@ -126,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() } @@ -159,15 +159,15 @@ private fun Content( ) { TopBar( - onBackClick = movieDetailsContract::onBackClick, + onBackClick = contract::onBackClick, modifier = Modifier.detailsTopBar(backgroundAlpha), - onClickOption1 = { movieDetailsContract.onManageBookmarkClicked(uiState.movieId) }, + onClickOption1 = { contract.onManageBookmarkClicked(uiState.movieId) }, option1Icon = R.drawable.icon_remove, ) HomeLazyVerticalGrid( uiState = uiState, - movieDetailsContract = movieDetailsContract, + movieDetailsContract = contract, lazyState = lazyState, footerHeight = footerHeight, screenWidthDp = screenWidthDp @@ -181,14 +181,14 @@ 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 = movieDetailsContract::onBookmarkSheetDismiss, + onSheetDismiss = contract::onBookmarkSheetDismiss, isSheetVisible = uiState.isBookmarkSheetVisible, bookmarkedMovieId = uiState.bookmarkedMovieId ) 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 97b71117d..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 @@ -39,7 +39,6 @@ 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( @@ -71,7 +70,7 @@ fun TopRatedScreen( ) { Content( state = state, - topRatedContract = viewModel + contract = viewModel ) } } @@ -79,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() } @@ -95,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( @@ -104,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 ) @@ -141,9 +140,9 @@ private fun Content( imageUrl = movieItem.posterUrl, isSaved = false, hasSaveIcon = true, - onSaveClick = { topRatedContract.onManageBookmarkClicked(movieItem.id) }, + onSaveClick = { contract.onManageBookmarkClicked(movieItem.id) }, modifier = Modifier.clickable { - topRatedContract.onMovieClick(movieItem.id) + contract.onMovieClick(movieItem.id) } ) } @@ -157,16 +156,14 @@ private fun Content( hasSaveIcon = false, isSaved = false, onSaveClick = {}, - modifier = Modifier.clickable { - topRatedContract.onTvShowClick(seriesItem.id) - } + modifier = Modifier.clickable { contract.onTvShowClick(seriesItem.id) } ) } } } BookmarkBottomSheet( - onSheetDismiss = topRatedContract::onBookmarkSheetDismiss, + onSheetDismiss = contract::onBookmarkSheetDismiss, isSheetVisible = state.isBookmarkSheetVisible, bookmarkedMovieId = state.bookmarkedMovieId ) 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 1c4a153dc..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 @@ -40,7 +40,7 @@ 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.snackbar.SnackBarType import com.london.designsystem.theme.NovixTheme import com.london.designsystem.utils.painter import com.london.designsystem.utils.string @@ -159,22 +159,22 @@ private fun BookmarkBottomSheetContent( } } - val snackbarController = LocalSnackbarController.current + val snackBarController = LocalSnackbarController.current if (state.isSuccessSnackbarVisible) { - snackbarController.showMessage( + snackBarController.showMessage( message = R.string.item_added_success.string, icon = com.london.designsystem.R.drawable.ic_success, - snackbarType = SnackbarType.Success, + snackBarType = SnackBarType.Success, onComplete = contract::onSnackbarShown ) } if (state.isErrorSnackbarVisible) { - snackbarController.showMessage( + snackBarController.showMessage( message = R.string.item_added_fail.string, icon = com.london.designsystem.R.drawable.ic_failed, - snackbarType = SnackbarType.Error, + snackBarType = SnackBarType.Error, onComplete = contract::onSnackbarShown ) } 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 691a59086..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 @@ -67,9 +67,7 @@ class BookmarkSheetViewModel @Inject constructor( ) } }, - onCompleted = { - updateState { copy(isAddingToList = false) } - } + onCompleted = { updateState { copy(isAddingToList = false) } } ) }