From a82dfd8a882cbea583998ff0cd1b351b2c2bb99b Mon Sep 17 00:00:00 2001 From: Louis FARIN Date: Sun, 3 Nov 2024 00:20:16 +0100 Subject: [PATCH] Bump dependencies --- app/build.gradle.kts | 1 - .../somovie/app/ui/main/MainActivity.kt | 16 - build-logic/src/main/kotlin/AppConfig.kt | 4 +- detekt/config.yml | 6 +- feature/home/account/build.gradle.kts | 1 - .../feature/home/account/AccountScreen.kt | 2 +- .../home/container/HomeBottomSheetItem.kt | 8 +- .../feature/home/discover/DiscoverScreen.kt | 42 +- .../home/discover/DiscoverViewModel.kt | 2 +- .../home/watchlist/WatchListScreenTest.kt | 38 +- .../feature/home/watchlist/WatchListScreen.kt | 46 +- feature/login/build.gradle.kts | 1 - .../somovie/feature/login/LogInLayout.kt | 12 +- .../somovie/feature/login/LogInManager.kt | 8 +- .../somovie/feature/login/LogInWebView.kt | 24 +- feature/moviedetails/build.gradle.kts | 1 - .../moviedetails/MovieDetailsHeader.kt | 13 +- .../moviedetails/MovieDetailsScreen.kt | 11 +- .../poster/MovieDetailsDraggablePoster.kt | 7 +- .../poster/MovieDetailsPosterFullScreen.kt | 10 +- .../MovieDetailsPosterStateController.kt | 8 +- gradle/libs.versions.toml | 40 +- .../feature/login/FakeLogInManager.kt | 4 +- .../somovie/ui/common/CompositionLocal.kt | 5 - .../somovie/ui/common/extension/Modifier.kt | 21 +- .../somovie/ui/common/modifier/Shake.kt | 36 +- .../louisfn/somovie/ui/component/Button.kt | 2 +- .../com/louisfn/somovie/ui/component/Retry.kt | 6 +- .../louisfn/somovie/ui/component/WebView.kt | 726 ++++++++++++++++++ .../ui/component/WormPagerIndicator.kt | 88 +++ .../ui/component/swipe/SwipeContainer.kt | 24 +- .../ui/component/swipe/SwipeController.kt | 26 +- .../ui/component/swipe/SwipeableItem.kt | 9 +- 33 files changed, 1021 insertions(+), 227 deletions(-) create mode 100644 ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WebView.kt create mode 100644 ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WormPagerIndicator.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3efb693..18745f7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,6 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.process) implementation(libs.retrofit.core) - implementation(libs.accompanist.systemuicontroller) implementation(libs.timber) debugImplementation(libs.leakcanary) diff --git a/app/src/main/kotlin/com/louisfn/somovie/app/ui/main/MainActivity.kt b/app/src/main/kotlin/com/louisfn/somovie/app/ui/main/MainActivity.kt index 0a83e1e..ed25bce 100644 --- a/app/src/main/kotlin/com/louisfn/somovie/app/ui/main/MainActivity.kt +++ b/app/src/main/kotlin/com/louisfn/somovie/app/ui/main/MainActivity.kt @@ -3,43 +3,27 @@ package com.louisfn.somovie.app.ui.main import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color -import androidx.core.view.WindowCompat -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.louisfn.somovie.ui.common.LocalAppRouter -import com.louisfn.somovie.ui.common.LocalMoshi import com.louisfn.somovie.ui.common.base.BaseActivity import com.louisfn.somovie.ui.common.navigation.AppRouter import com.louisfn.somovie.ui.theme.AppTheme -import com.squareup.moshi.Moshi import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint internal class MainActivity : BaseActivity() { - @Inject - internal lateinit var moshi: Moshi - @Inject internal lateinit var appRouter: AppRouter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) setContent { CompositionLocalProvider( - LocalMoshi provides moshi, LocalAppRouter provides appRouter, ) { AppTheme { - val systemUiController = rememberSystemUiController() - SideEffect { - systemUiController.setSystemBarsColor(Color.Transparent) - } - MainScreen() } } diff --git a/build-logic/src/main/kotlin/AppConfig.kt b/build-logic/src/main/kotlin/AppConfig.kt index 15104ac..ab0ca87 100644 --- a/build-logic/src/main/kotlin/AppConfig.kt +++ b/build-logic/src/main/kotlin/AppConfig.kt @@ -2,8 +2,8 @@ object AppConfig { const val MIN_SDK_VERSION = 26 - const val COMPILE_SDK_VERSION = 34 - const val TARGET_SDK_VERSION = 34 + const val COMPILE_SDK_VERSION = 35 + const val TARGET_SDK_VERSION = 35 const val VERSION_CODE = 1 const val VERSION_NAME = "0.1.0" diff --git a/detekt/config.yml b/detekt/config.yml index da22f0c..cd61227 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -558,6 +558,8 @@ style: - '10' - '60' - '24' + - '50' + - '100' - '3600' ignoreHashCodeFunction: true ignorePropertyDeclaration: true @@ -709,7 +711,7 @@ Compose: ContentEmitterReturningValues: active: true ModifierComposable: - active: true + active: false ModifierMissing: active: true ModifierReused: @@ -729,7 +731,7 @@ Compose: RememberMissing: active: true UnstableCollections: - active: true + active: false ViewModelForwarding: active: true ViewModelInjection: diff --git a/feature/home/account/build.gradle.kts b/feature/home/account/build.gradle.kts index b5bc431..27a28a1 100644 --- a/feature/home/account/build.gradle.kts +++ b/feature/home/account/build.gradle.kts @@ -10,5 +10,4 @@ dependencies { implementation(project(":feature:home:common")) implementation(project(":feature:login")) implementation(libs.androidx.lifecycle.process) - implementation(libs.accompanist.webview) } diff --git a/feature/home/account/src/main/kotlin/com/louisfn/somovie/feature/home/account/AccountScreen.kt b/feature/home/account/src/main/kotlin/com/louisfn/somovie/feature/home/account/AccountScreen.kt index 4194d0a..cbf23f8 100644 --- a/feature/home/account/src/main/kotlin/com/louisfn/somovie/feature/home/account/AccountScreen.kt +++ b/feature/home/account/src/main/kotlin/com/louisfn/somovie/feature/home/account/AccountScreen.kt @@ -65,8 +65,8 @@ private fun AccountScreen( private fun AccountContent( state: AccountUiState, logInManager: LogInManager, - modifier: Modifier = Modifier, onLogOutButtonClick: () -> Unit, + modifier: Modifier = Modifier, ) { Box( modifier = modifier diff --git a/feature/home/container/src/main/kotlin/com/louisfn/somovie/feature/home/container/HomeBottomSheetItem.kt b/feature/home/container/src/main/kotlin/com/louisfn/somovie/feature/home/container/HomeBottomSheetItem.kt index 2a909bf..f76990d 100644 --- a/feature/home/container/src/main/kotlin/com/louisfn/somovie/feature/home/container/HomeBottomSheetItem.kt +++ b/feature/home/container/src/main/kotlin/com/louisfn/somovie/feature/home/container/HomeBottomSheetItem.kt @@ -23,10 +23,10 @@ sealed class HomeBottomSheetItem( @StringRes val titleId: Int, val icon: ImageVector, ) { - object Explore : HomeBottomSheetItem(ExploreNavigation, R.string.home_explore, Icons.Default.Movie) - object WatchList : HomeBottomSheetItem(WatchlistDestination, R.string.home_watchlist, Icons.AutoMirrored.Filled.List) - object Discover : HomeBottomSheetItem(DiscoverNavigation, R.string.home_discover, Icons.Default.Swipe) - object Account : HomeBottomSheetItem(AccountNavigation, R.string.home_account, Icons.Default.Settings) + data object Explore : HomeBottomSheetItem(ExploreNavigation, R.string.home_explore, Icons.Default.Movie) + data object WatchList : HomeBottomSheetItem(WatchlistDestination, R.string.home_watchlist, Icons.AutoMirrored.Filled.List) + data object Discover : HomeBottomSheetItem(DiscoverNavigation, R.string.home_discover, Icons.Default.Swipe) + data object Account : HomeBottomSheetItem(AccountNavigation, R.string.home_account, Icons.Default.Settings) } val HomeBottomSheetItems = listOf(Explore, WatchList, Discover, Account) diff --git a/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverScreen.kt b/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverScreen.kt index fa48716..b696edc 100644 --- a/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverScreen.kt +++ b/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverScreen.kt @@ -61,20 +61,20 @@ internal fun DiscoverScreen( DiscoverScreen( uiState = uiState, - onSwiped = viewModel::onMovieSwiped, - onDisappeared = viewModel::onMovieDisappeared, + onSwipe = viewModel::onMovieSwipe, + onDisappear = viewModel::onMovieDisappeared, retry = { viewModel.retry() }, - onLogInSnackbarActionClicked = { showAccount() }, + onLogInSnackbarActionClick = { showAccount() }, ) } @Composable private fun DiscoverScreen( uiState: DiscoverUiState, - onSwiped: (MovieItem, SwipeDirection) -> Unit, - onDisappeared: (MovieItem) -> Unit, + onSwipe: (MovieItem, SwipeDirection) -> Unit, + onDisappear: (MovieItem) -> Unit, retry: () -> Unit, - onLogInSnackbarActionClicked: () -> Unit, + onLogInSnackbarActionClick: () -> Unit, ) { Box( modifier = Modifier.fillMaxSize(), @@ -83,9 +83,9 @@ private fun DiscoverScreen( is DiscoverUiState.Discover -> DiscoverContent( items = uiState.items, logInSnackbarState = uiState.logInSnackbarState, - onSwiped = onSwiped, - onDisappeared = onDisappeared, - onLogInSnackbarActionClicked = onLogInSnackbarActionClicked, + onSwipe = onSwipe, + onDisappear = onDisappear, + onLogInSnackbarActionClick = onLogInSnackbarActionClick, ) is DiscoverUiState.Retry -> Retry( modifier = Modifier.align(Alignment.Center), @@ -103,20 +103,20 @@ private fun DiscoverScreen( private fun BoxScope.DiscoverContent( items: ImmutableList, logInSnackbarState: LogInSnackbarState, - onSwiped: (MovieItem, SwipeDirection) -> Unit, - onDisappeared: (MovieItem) -> Unit, - onLogInSnackbarActionClicked: () -> Unit, + onSwipe: (MovieItem, SwipeDirection) -> Unit, + onDisappear: (MovieItem) -> Unit, + onLogInSnackbarActionClick: () -> Unit, ) { DiscoverSwipeContainer( items = items, - onSwiped = onSwiped, - onDisappeared = onDisappeared, + onSwipe = onSwipe, + onDisappear = onDisappear, ) if (logInSnackbarState != LogInSnackbarState.HIDDEN) { DefaultSnackbar( message = stringResource(id = commonR.string.discover_log_in_description), actionLabel = stringResource(id = commonR.string.discover_log_in_action), - onActionClick = onLogInSnackbarActionClicked, + onActionClick = onLogInSnackbarActionClick, modifier = Modifier .shake(logInSnackbarState == LogInSnackbarState.SHAKING) .align(Alignment.BottomCenter), @@ -127,8 +127,8 @@ private fun BoxScope.DiscoverContent( @Composable private fun BoxScope.DiscoverSwipeContainer( items: ImmutableList, - onSwiped: (MovieItem, SwipeDirection) -> Unit, - onDisappeared: (MovieItem) -> Unit, + onSwipe: (MovieItem, SwipeDirection) -> Unit, + onDisappear: (MovieItem) -> Unit, ) { var draggingState by remember { mutableStateOf(null) } @@ -139,12 +139,12 @@ private fun BoxScope.DiscoverSwipeContainer( onDragging = { _, direction, ratio -> draggingState = DraggingState(direction, ratio) }, - onCanceled = { draggingState = null }, - onSwiped = { item, direction -> + onCancel = { draggingState = null }, + onSwipe = { item, direction -> draggingState = null - onSwiped(item, direction) + onSwipe(item, direction) }, - onDisappeared = { item, _ -> onDisappeared(item) }, + onDisappear = { item, _ -> onDisappear(item) }, ) { item -> DiscoverMovieItem(item) } diff --git a/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverViewModel.kt b/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverViewModel.kt index 30ebf3d..f9beb86 100644 --- a/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverViewModel.kt +++ b/feature/home/discover/src/main/kotlin/com/louisfn/somovie/feature/home/discover/DiscoverViewModel.kt @@ -101,7 +101,7 @@ internal class DiscoverViewModel @Inject constructor( } @AnyThread - fun onMovieSwiped(movieItem: MovieItem, direction: SwipeDirection) { + fun onMovieSwipe(movieItem: MovieItem, direction: SwipeDirection) { viewModelScope.launch(defaultDispatcher) { if (direction.shouldAddMovieToWatchlist()) { if (!isLoggedIn.first()) { diff --git a/feature/home/watchlist/src/androidTest/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreenTest.kt b/feature/home/watchlist/src/androidTest/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreenTest.kt index 32be0d2..82a06c7 100644 --- a/feature/home/watchlist/src/androidTest/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreenTest.kt +++ b/feature/home/watchlist/src/androidTest/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreenTest.kt @@ -60,7 +60,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -90,7 +90,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -127,7 +127,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -162,7 +162,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -201,7 +201,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -245,7 +245,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -284,7 +284,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -325,7 +325,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -362,7 +362,7 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, + onMovieSwipe = {}, ) } } @@ -395,7 +395,7 @@ class WatchListScreenTest { movie = movie, isHidden = false, onClick = {}, - onSwiped = { isSwiped = true }, + onSwipe = { isSwiped = true }, ) } } @@ -426,7 +426,7 @@ class WatchListScreenTest { movie = movie, isHidden = false, onClick = {}, - onSwiped = { isSwiped = true }, + onSwipe = { isSwiped = true }, ) } } @@ -445,7 +445,7 @@ class WatchListScreenTest { //region Undo dismissed Snackbar @Test - fun shouldInvokedOnSnackbarActionPerformed_whenSnackbarActionClicked() { + fun shouldInvokedonSnackbarActionPerform_whenSnackbarActionClicked() { // Given val movieId = 1L val action = flowOf(ShowUndoSwipeToDismissSnackbar(movieId)) @@ -467,9 +467,9 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, - onSnackbarActionPerformed = { isSnackbarActionPerformed = true }, - onSnackbarDismissed = {}, + onMovieSwipe = {}, + onSnackbarActionPerform = { isSnackbarActionPerformed = true }, + onSnackbarDismiss = {}, ) } } @@ -483,7 +483,7 @@ class WatchListScreenTest { } @Test - fun shouldInvokeOnSnackbarDismissed_whenSnackbarActionClicked() { + fun shouldInvokeonSnackbarDismiss_whenSnackbarActionClicked() { // Given val movieId = 1L val action = flowOf(ShowUndoSwipeToDismissSnackbar(movieId)) @@ -505,9 +505,9 @@ class WatchListScreenTest { scaffoldState = rememberScaffoldState(), logInManager = FakeLogInManager(), onMovieClick = {}, - onMovieSwiped = {}, - onSnackbarActionPerformed = {}, - onSnackbarDismissed = { isSnackbarDismissed = true }, + onMovieSwipe = {}, + onSnackbarActionPerform = {}, + onSnackbarDismiss = { isSnackbarDismissed = true }, ) } } diff --git a/feature/home/watchlist/src/main/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreen.kt b/feature/home/watchlist/src/main/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreen.kt index 678f461..42fd920 100644 --- a/feature/home/watchlist/src/main/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreen.kt +++ b/feature/home/watchlist/src/main/kotlin/com/louisfn/somovie/feature/home/watchlist/WatchListScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -108,9 +109,9 @@ internal fun WatchlistScreen( scaffoldState = scaffoldState, logInManager = logInViewModel, onMovieClick = showDetails, - onMovieSwiped = viewModel::onSwipeToDismiss, - onSnackbarActionPerformed = viewModel::onUndoSwipeToDismissSnackbarActionPerformed, - onSnackbarDismissed = viewModel::onUndoSwipeToDismissSnackbarDismissed, + onMovieSwipe = viewModel::onSwipeToDismiss, + onSnackbarActionPerform = viewModel::onUndoSwipeToDismissSnackbarActionPerformed, + onSnackbarDismiss = viewModel::onUndoSwipeToDismissSnackbarDismissed, ) } @@ -122,11 +123,14 @@ internal fun WatchlistScreen( scaffoldState: ScaffoldState, logInManager: LogInManager, onMovieClick: (Movie) -> Unit, - onMovieSwiped: (Movie) -> Unit, - onSnackbarActionPerformed: (movieId: Long) -> Unit, - onSnackbarDismissed: (movieId: Long) -> Unit, + onMovieSwipe: (Movie) -> Unit, + onSnackbarActionPerform: (movieId: Long) -> Unit, + onSnackbarDismiss: (movieId: Long) -> Unit, ) { val resources = LocalContext.current.resources + val updatedSnackbarActionPerform by rememberUpdatedState(onSnackbarActionPerform) + val updatedSnackbarDismiss by rememberUpdatedState(onSnackbarDismiss) + LaunchedEffect(Unit) { action .collect { action -> @@ -134,8 +138,8 @@ internal fun WatchlistScreen( is ShowUndoSwipeToDismissSnackbar -> scaffoldState.snackbarHostState.showCancelSwipeToDismissSnackbar( resources = resources, - onSnackbarActionPerformed = { onSnackbarActionPerformed(action.movieId) }, - onSnackbarDismissed = { onSnackbarDismissed(action.movieId) }, + onSnackbarActionPerform = { updatedSnackbarActionPerform(action.movieId) }, + onSnackbarDismiss = { updatedSnackbarDismiss(action.movieId) }, ) } } @@ -147,7 +151,7 @@ internal fun WatchlistScreen( scaffoldState = scaffoldState, logInManager = logInManager, onMovieClick = onMovieClick, - onMovieSwiped = onMovieSwiped, + onMovieSwipe = onMovieSwipe, ) } @@ -158,7 +162,7 @@ internal fun WatchlistScreen( scaffoldState: ScaffoldState, logInManager: LogInManager, onMovieClick: (Movie) -> Unit, - onMovieSwiped: (Movie) -> Unit, + onMovieSwipe: (Movie) -> Unit, ) { Scaffold( modifier = Modifier.statusBarsPadding(), @@ -176,7 +180,7 @@ internal fun WatchlistScreen( pagingItems = pagingItems, contentPadding = it, onMovieClick = onMovieClick, - onMovieSwiped = onMovieSwiped, + onMovieSwipe = onMovieSwipe, ) is WatchlistUiState.AccountDisconnected -> @@ -217,7 +221,7 @@ private fun WatchlistContent( pagingItems: LazyPagingItems, contentPadding: PaddingValues, onMovieClick: (Movie) -> Unit, - onMovieSwiped: (Movie) -> Unit, + onMovieSwipe: (Movie) -> Unit, ) { Box( modifier = Modifier @@ -240,7 +244,7 @@ private fun WatchlistContent( loadNextPageState = uiState.loadNextPageState, contentPadding = contentPadding, onMovieClick = onMovieClick, - onMovieSwiped = onMovieSwiped, + onMovieSwipe = onMovieSwipe, ) } } @@ -252,7 +256,7 @@ private fun WatchlistLazyColumn( loadNextPageState: LoadNextPageState, contentPadding: PaddingValues, onMovieClick: (Movie) -> Unit, - onMovieSwiped: (Movie) -> Unit, + onMovieSwipe: (Movie) -> Unit, ) { // https://issuetracker.google.com/issues/177245496#comment23 if (pagingItems.itemCount == 0) return @@ -277,7 +281,7 @@ private fun WatchlistLazyColumn( movie = movieItem.movie, isHidden = movieItem.isHidden, onClick = { onMovieClick(movieItem.movie) }, - onSwiped = { onMovieSwiped(movieItem.movie) }, + onSwipe = { onMovieSwipe(movieItem.movie) }, ) if (!movieItem.isHidden) { Divider(color = MaterialTheme.colors.onBackground) @@ -352,12 +356,12 @@ fun WatchlistMovieItem( movie: Movie, isHidden: Boolean, onClick: () -> Unit, - onSwiped: () -> Unit, + onSwipe: () -> Unit, ) { val dismissState = rememberDismissState( confirmStateChange = { if (it == DismissValue.DismissedToEnd) { - onSwiped() + onSwipe() } true }, @@ -469,15 +473,15 @@ private fun BottomLoader(modifier: Modifier = Modifier) { private suspend fun SnackbarHostState.showCancelSwipeToDismissSnackbar( resources: Resources, - onSnackbarActionPerformed: () -> Unit, - onSnackbarDismissed: () -> Unit, + onSnackbarActionPerform: () -> Unit, + onSnackbarDismiss: () -> Unit, ) { val result = showSnackbar( message = resources.getString(R.string.watchlist_remove_from_watchlist_confirm_message), actionLabel = resources.getString(R.string.watchlist_remove_from_watchlist_action), ) when (result) { - SnackbarResult.ActionPerformed -> onSnackbarActionPerformed() - SnackbarResult.Dismissed -> onSnackbarDismissed() + SnackbarResult.ActionPerformed -> onSnackbarActionPerform() + SnackbarResult.Dismissed -> onSnackbarDismiss() } } diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index eb1d65d..19f3b64 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -7,5 +7,4 @@ android { } dependencies { - implementation(libs.accompanist.webview) } diff --git a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInLayout.kt b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInLayout.kt index 85b6b2d..fa248b0 100644 --- a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInLayout.kt +++ b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInLayout.kt @@ -31,8 +31,8 @@ fun LogInLayout( modifier = modifier .semantics { testTag = LogInTestTag.LogInLayout }, onLogInButtonClick = logInManager::start, - onLogInApproved = logInManager::onApproved, - onLogInDenied = logInManager::onDenied, + onLogInApprove = logInManager::onApprove, + onLogInDeny = logInManager::onDeny, buttonDecorator = content, ) } @@ -42,8 +42,8 @@ private fun LogInLayout( uiState: LogInState, modifier: Modifier = Modifier, onLogInButtonClick: () -> Unit = {}, - onLogInApproved: () -> Unit = {}, - onLogInDenied: () -> Unit = {}, + onLogInApprove: () -> Unit = {}, + onLogInDeny: () -> Unit = {}, buttonDecorator: @Composable BoxScope.(button: @Composable (modifier: Modifier) -> Unit) -> Unit, ) { Box(modifier = modifier) { @@ -63,8 +63,8 @@ private fun LogInLayout( LogInWebView( uri = uiState.uri, modifier = Modifier.fillMaxSize(), - onApproved = onLogInApproved, - onDenied = onLogInDenied, + onApprove = onLogInApprove, + onDeny = onLogInDeny, ) else -> Unit } diff --git a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInManager.kt b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInManager.kt index 4dc9938..34a236e 100644 --- a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInManager.kt +++ b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInManager.kt @@ -22,10 +22,10 @@ interface LogInManager { fun start() @AnyThread - fun onApproved() + fun onApprove() @AnyThread - fun onDenied() + fun onDeny() } class DefaultLogInManager @Inject constructor( @@ -61,13 +61,13 @@ class DefaultLogInManager @Inject constructor( } } - override fun onApproved() { + override fun onApprove() { scope.launch { logIn() } } - override fun onDenied() { + override fun onDeny() { scope.launch { _state.value = LogInState.Idle } diff --git a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInWebView.kt b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInWebView.kt index c00629c..48aaa34 100644 --- a/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInWebView.kt +++ b/feature/login/src/main/kotlin/com/louisfn/somovie/feature/login/LogInWebView.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import com.google.accompanist.web.WebView -import com.google.accompanist.web.rememberWebViewState +import com.louisfn.somovie.ui.component.WebView +import com.louisfn.somovie.ui.component.rememberWebViewState @Composable fun LogInWebView( @@ -20,8 +20,8 @@ fun LogInWebView( LogInWebView( uri = uri, modifier = modifier, - onApproved = { logInManager.onApproved() }, - onDenied = { logInManager.onDenied() }, + onApprove = { logInManager.onApprove() }, + onDeny = { logInManager.onDeny() }, ) } @@ -30,31 +30,31 @@ fun LogInWebView( fun LogInWebView( uri: Uri, modifier: Modifier = Modifier, - onApproved: () -> Unit = {}, - onDenied: () -> Unit = {}, + onApprove: () -> Unit = {}, + onDeny: () -> Unit = {}, ) { val state = rememberWebViewState(uri.toString()) - val currentOnApproved by rememberUpdatedState(onApproved) - val currentOnDenied by rememberUpdatedState(onDenied) + val currentonApprove by rememberUpdatedState(onApprove) + val currentonDeny by rememberUpdatedState(onDeny) val currentUrl = state.lastLoadedUrl LaunchedEffect(key1 = currentUrl) { currentUrl?.run { when { - contains(LogInConfig.APPROVE_PATH) -> currentOnApproved() - contains(LogInConfig.DENY_PATH) -> currentOnDenied() + contains(LogInConfig.APPROVE_PATH) -> currentonApprove() + contains(LogInConfig.DENY_PATH) -> currentonDeny() else -> {} } } } - BackHandler(true) { currentOnDenied() } + BackHandler(true) { currentonDeny() } WebView( modifier = modifier, state = state, - onCreated = { + onCreate = { it.settings.javaScriptEnabled = true }, ) diff --git a/feature/moviedetails/build.gradle.kts b/feature/moviedetails/build.gradle.kts index d039fc6..a9185ef 100644 --- a/feature/moviedetails/build.gradle.kts +++ b/feature/moviedetails/build.gradle.kts @@ -7,5 +7,4 @@ android { } dependencies { - implementation(libs.accompanist.pager.indicators) } diff --git a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsHeader.kt b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsHeader.kt index 74660b4..b1cd415 100644 --- a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsHeader.kt +++ b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsHeader.kt @@ -39,12 +39,12 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import coil.compose.rememberAsyncImagePainter -import com.google.accompanist.pager.HorizontalPagerIndicator import com.louisfn.somovie.domain.model.BackdropPath import com.louisfn.somovie.ui.common.LocalAppRouter import com.louisfn.somovie.ui.common.model.ImmutableList import com.louisfn.somovie.ui.component.AutosizeText import com.louisfn.somovie.ui.component.DefaultTopAppBar +import com.louisfn.somovie.ui.component.WormPagerIndicator import com.louisfn.somovie.ui.component.movie.MovieVoteAverageChart import com.louisfn.somovie.ui.theme.Dimens @@ -53,7 +53,7 @@ private const val BackdropRatio = 1.778f @Composable internal fun MovieDetailsHeader( headerUiState: HeaderUiState, - onPosterPositioned: (LayoutCoordinates) -> Unit, + onPosterPositionChange: (LayoutCoordinates) -> Unit, navigateUp: () -> Unit, modifier: Modifier = Modifier, ) { @@ -63,7 +63,7 @@ internal fun MovieDetailsHeader( ) { MovieDetailsHeaderContent( headerUiState = headerUiState, - onPosterPositioned = onPosterPositioned, + onPosterPositionChange = onPosterPositionChange, ) MovieDetailsTopBar( shareUrl = headerUiState.tmdbUrl, @@ -120,8 +120,8 @@ private fun MovieDetailsIconButton(onClick: () -> Unit, content: @Composable () @Composable private fun MovieDetailsHeaderContent( + onPosterPositionChange: (LayoutCoordinates) -> Unit, headerUiState: HeaderUiState, - onPosterPositioned: (LayoutCoordinates) -> Unit, ) { ConstraintLayout( modifier = Modifier @@ -150,7 +150,7 @@ private fun MovieDetailsHeaderContent( bottom.linkTo(backdropsPager.bottom) start.linkTo(parent.start, 24.dp) } - .onGloballyPositioned { onPosterPositioned(it) }, + .onGloballyPositioned { onPosterPositionChange(it) }, ) AutosizeText( @@ -254,9 +254,8 @@ private fun BackdropsPager( ) } - HorizontalPagerIndicator( + WormPagerIndicator( pagerState = pagerState, - pageCount = backdropPaths.size, activeColor = MaterialTheme.colors.onSurface, modifier = Modifier .align(Alignment.BottomCenter) diff --git a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsScreen.kt b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsScreen.kt index 18d4978..8000e91 100644 --- a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsScreen.kt +++ b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/MovieDetailsScreen.kt @@ -41,8 +41,8 @@ import java.time.Duration @Composable internal fun MovieDetailsScreen( - viewModel: MovieDetailsViewModel = hiltViewModel(), navigateUp: () -> Unit, + viewModel: MovieDetailsViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateLifecycleAware() @@ -79,7 +79,7 @@ private fun MovieDetailsScreen( end.linkTo(parent.end) }, navigateUp = navigateUp, - onPosterPositioned = { posterReducedCoordinates = it }, + onPosterPositionChange = { posterReducedCoordinates = it }, ) Box( @@ -123,8 +123,8 @@ private fun MovieDetailsScreen( @Composable private fun AddToWatchlistSmallFab( watchlistFabState: WatchlistFabState, - modifier: Modifier = Modifier, onClick: () -> Unit, + modifier: Modifier = Modifier, ) { Button( onClick = onClick, @@ -174,7 +174,10 @@ private fun MovieDetailsLoader(modifier: Modifier = Modifier) { } @Composable -private fun MovieDetailsRetry(modifier: Modifier = Modifier, onClick: () -> Unit) { +private fun MovieDetailsRetry( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { Retry( modifier = modifier, onClick = onClick, diff --git a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsDraggablePoster.kt b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsDraggablePoster.kt index 773c075..4b4580d 100644 --- a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsDraggablePoster.kt +++ b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsDraggablePoster.kt @@ -1,18 +1,19 @@ package com.louisfn.somovie.feature.moviedetails.poster import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.input.pointer.pointerInput +@Composable internal fun Modifier.draggablePoster( stateController: PosterStateController, -) = composed { +): Modifier { val currentStateController by rememberUpdatedState(stateController) - pointerInput(Unit) { + return pointerInput(Unit) { detectDragGestures( onDrag = { change, dragAmount -> change.consume() diff --git a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterFullScreen.kt b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterFullScreen.kt index e891d32..14cde86 100644 --- a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterFullScreen.kt +++ b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterFullScreen.kt @@ -57,7 +57,7 @@ fun MovieDetailsPosterFullScreen( posterPath = posterPath, posterState = posterState, posterReducedCoordinates = posterReducedCoordinates, - onPosterStateChanged = { posterState = it }, + onPosterStateChange = { posterState = it }, ) } } @@ -66,12 +66,12 @@ fun MovieDetailsPosterFullScreen( private fun Poster( posterPath: PosterPath, posterState: MovieDetailsPosterState, + onPosterStateChange: (MovieDetailsPosterState) -> Unit, posterReducedCoordinates: LayoutCoordinates, - onPosterStateChanged: (MovieDetailsPosterState) -> Unit, ) { val posterStateController = rememberPosterStateController( reducedCoordinates = posterReducedCoordinates, - onStateChanged = onPosterStateChanged, + onStateChange = onPosterStateChange, ) var onLoadImageSuccess by remember { mutableStateOf(false) } @@ -84,7 +84,7 @@ private fun Poster( .offset { posterStateController.offset.toIntOffset() } .clickable(withRipple = false) { if (onLoadImageSuccess) { - onPosterStateChanged(posterState.toggle()) + onPosterStateChange(posterState.toggle()) } } .draggablePoster(posterStateController), @@ -95,8 +95,8 @@ private fun Poster( @Composable private fun Poster( posterPath: PosterPath, - modifier: Modifier = Modifier, onLoadImageSuccess: () -> Unit, + modifier: Modifier = Modifier, ) { val configuration = LocalConfiguration.current DefaultAsyncImage( diff --git a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterStateController.kt b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterStateController.kt index 4d0adfb..7d2286d 100644 --- a/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterStateController.kt +++ b/feature/moviedetails/src/main/kotlin/com/louisfn/somovie/feature/moviedetails/poster/MovieDetailsPosterStateController.kt @@ -32,7 +32,7 @@ internal class PosterStateController( private val scope: CoroutineScope, private val minDragDistanceToReduce: Int, private val screenSize: Size, - private var onStateChanged: (MovieDetailsPosterState) -> Unit, + private var onStateChange: (MovieDetailsPosterState) -> Unit, ) { private var currentState: MovieDetailsPosterState = MovieDetailsPosterState.REDUCED @@ -87,7 +87,7 @@ internal class PosterStateController( if (currentState != MovieDetailsPosterState.EXPANDED) return if (dragOffset.getDistance() > minDragDistanceToReduce) { - onStateChanged(MovieDetailsPosterState.REDUCED) + onStateChange(MovieDetailsPosterState.REDUCED) } else { animateToExpandedState() } @@ -110,8 +110,8 @@ internal class PosterStateController( @Composable internal fun rememberPosterStateController( + onStateChange: (MovieDetailsPosterState) -> Unit, reducedCoordinates: LayoutCoordinates, - onStateChanged: (MovieDetailsPosterState) -> Unit, ): PosterStateController { val scope = rememberCoroutineScope() val minDragDistanceToReduce = MinDragDistanceToReduce.roundToPx() @@ -122,7 +122,7 @@ internal fun rememberPosterStateController( minDragDistanceToReduce = minDragDistanceToReduce, reducedCoordinates = reducedCoordinates, screenSize = screenSize, - onStateChanged = onStateChanged, + onStateChange = onStateChange, scope = scope, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b54b11..cb97040 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,13 @@ [versions] -accompanist = "0.32.0" -androidGradlePlugin = "8.7.1" +androidGradlePlugin = "8.7.2" androidxActivityCompose = "1.9.3" -androidxAnnotation = "1.9.0" -androidxCompose = "1.7.4" -androidxComposeMaterial = "1.7.4" -androidxConstraintlayout = "1.0.1" -androidxCoreKtx = "1.13.1" +androidxAnnotation = "1.9.1" +androidxCompose = "1.7.5" +androidxComposeMaterial = "1.7.5" +androidxConstraintlayout = "1.1.0" +androidxCoreKtx = "1.15.0" androidxDatastore = "1.1.1" -androidxLifecycle = "2.8.6" +androidxLifecycle = "2.8.7" androidxNavigation = "2.8.3" androidxPaging = "3.3.2" androidxPagingCompose = "3.3.2" @@ -17,35 +16,32 @@ androidxTest = "1.6.2" androidxTestCore = "1.6.1" androidxTestExt = "1.2.1" androidxWebkit = "1.12.1" -coil = "2.5.0" -coroutines = "1.8.1" -detekt = "1.23.6" -detektCompose = "0.3.3" +coil = "2.7.0" +coroutines = "1.9.0" +detekt = "1.23.7" +detektCompose = "0.4.17" faker = "1.16.0" -flipper = "0.250.0" +flipper = "0.271.0" gradleversions = "0.51.0" -hilt = "2.51.1" +hilt = "2.52" hiltAndroidX = "1.0.0" hiltCompose = "1.2.0" jacoco = "0.8.7" junit = "4.13.2" jvm = "1.7.20" -kotest = "5.8.0" -kotlin = "2.0.20" -ksp = "2.0.20-1.0.25" +kotest = "5.9.1" +kotlin = "2.0.21" +ksp = "2.0.21-1.0.26" ktlint = "1.0.1" leakcanary = "2.14" moshi = "1.15.1" okHttpLoggingInterceptor = "4.12.0" retrofit = "2.11.0" -soloader = "0.11.0" -spotless = "6.22.0" +soloader = "0.12.1" +spotless = "6.25.0" timber = "5.0.1" [libraries] -accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanist" } -accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } -accompanist-webview = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivityCompose" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" } androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidxComposeMaterial" } diff --git a/test/fixtures/src/main/kotlin/com/louisfn/somovie/test/fixtures/feature/login/FakeLogInManager.kt b/test/fixtures/src/main/kotlin/com/louisfn/somovie/test/fixtures/feature/login/FakeLogInManager.kt index 0b36616..72c7adf 100644 --- a/test/fixtures/src/main/kotlin/com/louisfn/somovie/test/fixtures/feature/login/FakeLogInManager.kt +++ b/test/fixtures/src/main/kotlin/com/louisfn/somovie/test/fixtures/feature/login/FakeLogInManager.kt @@ -19,9 +19,9 @@ class FakeLogInManager : LogInManager { override fun start() { } - override fun onApproved() { + override fun onApprove() { } - override fun onDenied() { + override fun onDeny() { } } diff --git a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/CompositionLocal.kt b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/CompositionLocal.kt index e23ed46..60f5e74 100644 --- a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/CompositionLocal.kt +++ b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/CompositionLocal.kt @@ -2,11 +2,6 @@ package com.louisfn.somovie.ui.common import androidx.compose.runtime.staticCompositionLocalOf import com.louisfn.somovie.ui.common.navigation.AppRouter -import com.squareup.moshi.Moshi - -val LocalMoshi = staticCompositionLocalOf { - error("Moshi not provided") -} val LocalAppRouter = staticCompositionLocalOf { error("AppRouter not provided") diff --git a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/extension/Modifier.kt b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/extension/Modifier.kt index ba41921..947011c 100644 --- a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/extension/Modifier.kt +++ b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/extension/Modifier.kt @@ -2,20 +2,19 @@ package com.louisfn.somovie.ui.common.extension import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed +@Composable fun Modifier.clickable(withRipple: Boolean, onClick: () -> Unit): Modifier = - composed { - if (withRipple) { - this.clickable { onClick() } - } else { - this.clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { - onClick() - } + if (withRipple) { + this.clickable { onClick() } + } else { + this.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + onClick() } } diff --git a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/modifier/Shake.kt b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/modifier/Shake.kt index facb8d7..ed9ac9d 100644 --- a/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/modifier/Shake.kt +++ b/ui/common/src/main/kotlin/com/louisfn/somovie/ui/common/modifier/Shake.kt @@ -7,15 +7,16 @@ import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.graphicsLayer private const val DefaultInitialScale = 1f private const val DefaultTargetScale = .9f private const val DefaultAnimationDuration = 50 +@Composable fun Modifier.shake( enabled: Boolean, initialScale: Float = DefaultInitialScale, @@ -24,22 +25,21 @@ fun Modifier.shake( durationMillis = DefaultAnimationDuration, easing = LinearEasing, ), -) = composed { - if (enabled) { - val infiniteTransition = rememberInfiniteTransition() - val scale by infiniteTransition.animateFloat( - initialValue = initialScale, - targetValue = targetScale, - animationSpec = infiniteRepeatable( - animation = animation, - repeatMode = RepeatMode.Reverse, - ), - ) - Modifier.graphicsLayer { - scaleX = scale - scaleY = scale - } - } else { - this +) = if (enabled) { + val infiniteTransition = rememberInfiniteTransition(label = "ShakeInfiniteTransition") + val scale by infiniteTransition.animateFloat( + initialValue = initialScale, + targetValue = targetScale, + animationSpec = infiniteRepeatable( + animation = animation, + repeatMode = RepeatMode.Reverse, + ), + label = "Shake", + ) + graphicsLayer { + scaleX = scale + scaleY = scale } +} else { + this } diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Button.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Button.kt index 8530d1a..fd22f4a 100644 --- a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Button.kt +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Button.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.Modifier @Composable fun DefaultTextButton( text: String, + onClick: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, - onClick: () -> Unit, ) { androidx.compose.material.TextButton( modifier = modifier, diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Retry.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Retry.kt index f939d81..a17768e 100644 --- a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Retry.kt +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/Retry.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import com.louisfn.somovie.ui.common.R as commonR @Composable -fun Retry(modifier: Modifier = Modifier, onClick: () -> Unit) { +fun Retry(onClick: () -> Unit, modifier: Modifier = Modifier) { Column( modifier = modifier .semantics { testTag = ComponentTestTag.Retry }, @@ -41,7 +41,7 @@ fun Retry(modifier: Modifier = Modifier, onClick: () -> Unit) { } @Composable -fun RetryButton(modifier: Modifier = Modifier, onClick: () -> Unit) { +fun RetryButton(onClick: () -> Unit, modifier: Modifier = Modifier) { Button( modifier = modifier, onClick = onClick, @@ -51,7 +51,7 @@ fun RetryButton(modifier: Modifier = Modifier, onClick: () -> Unit) { } @Composable -fun TextRetryButton(modifier: Modifier = Modifier, onClick: () -> Unit) { +fun TextRetryButton(onClick: () -> Unit, modifier: Modifier = Modifier) { DefaultTextButton( text = stringResource(id = commonR.string.common_retry_button), modifier = modifier diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WebView.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WebView.kt new file mode 100644 index 0000000..2f75458 --- /dev/null +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WebView.kt @@ -0,0 +1,726 @@ +package com.louisfn.somovie.ui.component + +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.view.ViewGroup.LayoutParams +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.louisfn.somovie.ui.component.LoadingState.Finished +import com.louisfn.somovie.ui.component.LoadingState.Loading +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * This is a fork of the accompanist library - https://google.github.io/accompanist/web + * + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param modifier A compose modifier + * @param captureBackPresses Set to true to have this Composable capture back presses and navigate + * the WebView back. + * @param navigator An optional navigator object that can be used to control the WebView's + * navigation from outside the composable. + * @param onCreate Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +fun WebView( + state: WebViewState, + modifier: Modifier = Modifier, + captureBackPresses: Boolean = true, + navigator: WebViewNavigator = rememberWebViewNavigator(), + onCreate: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, + chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() }, + factory: ((Context) -> WebView)? = null, +) { + BoxWithConstraints(modifier) { + // WebView changes it's layout strategy based on + // it's layoutParams. We convert from Compose Modifier to + // layout params here. + val width = + if (constraints.hasFixedWidth) + LayoutParams.MATCH_PARENT + else + LayoutParams.WRAP_CONTENT + val height = + if (constraints.hasFixedHeight) + LayoutParams.MATCH_PARENT + else + LayoutParams.WRAP_CONTENT + + val layoutParams = FrameLayout.LayoutParams( + width, + height, + ) + + WebView( + state, + layoutParams, + Modifier, + captureBackPresses, + navigator, + onCreate, + onDispose, + client, + chromeClient, + factory, + ) + } +} + +/** + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView. + * @param modifier A compose modifier + * @param captureBackPresses Set to true to have this Composable capture back presses and navigate + * the WebView back. + * @param navigator An optional navigator object that can be used to control the WebView's + * navigation from outside the composable. + * @param onCreate Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +fun WebView( + state: WebViewState, + layoutParams: FrameLayout.LayoutParams, + modifier: Modifier = Modifier, + captureBackPresses: Boolean = true, + navigator: WebViewNavigator = rememberWebViewNavigator(), + onCreate: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, + chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() }, + factory: ((Context) -> WebView)? = null, +) { + val webView = state.webView + + BackHandler(captureBackPresses && navigator.canGoBack) { + webView?.goBack() + } + + webView?.let { wv -> + LaunchedEffect(wv, navigator) { + with(navigator) { + wv.handleNavigationEvents() + } + } + + LaunchedEffect(wv, state) { + snapshotFlow { state.content }.collect { content -> + when (content) { + is WebContent.Url -> { + wv.loadUrl(content.url, content.additionalHttpHeaders) + } + + is WebContent.Data -> { + wv.loadDataWithBaseURL( + content.baseUrl, + content.data, + content.mimeType, + content.encoding, + content.historyUrl, + ) + } + + is WebContent.Post -> { + wv.postUrl( + content.url, + content.postData, + ) + } + + is WebContent.NavigatorOnly -> { + // NO-OP + } + } + } + } + } + + // Set the state of the client and chrome client + // This is done internally to ensure they always are the same instance as the + // parent Web composable + client.state = state + client.navigator = navigator + chromeClient.state = state + + AndroidView( + factory = { context -> + (factory?.invoke(context) ?: WebView(context)).apply { + onCreate(this) + + this.layoutParams = layoutParams + + state.viewState?.let { + this.restoreState(it) + } + + webChromeClient = chromeClient + webViewClient = client + }.also { state.webView = it } + }, + modifier = modifier, + onRelease = { + onDispose(it) + }, + ) +} + +/** + * AccompanistWebViewClient + * + * A parent class implementation of WebViewClient that can be subclassed to add custom behaviour. + * + * As Accompanist Web needs to set its own web client to function, it provides this intermediary + * class that can be overriden if further custom behaviour is required. + */ +open class AccompanistWebViewClient : WebViewClient() { + open lateinit var state: WebViewState + internal set + open lateinit var navigator: WebViewNavigator + internal set + + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + state.loadingState = Loading(0.0f) + state.errorsForCurrentRequest.clear() + state.pageTitle = null + state.pageIcon = null + + state.lastLoadedUrl = url + } + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + state.loadingState = Finished + } + + override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) { + super.doUpdateVisitedHistory(view, url, isReload) + + navigator.canGoBack = view.canGoBack() + navigator.canGoForward = view.canGoForward() + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + super.onReceivedError(view, request, error) + + if (error != null) { + state.errorsForCurrentRequest.add(WebViewError(request, error)) + } + } +} + +/** + * AccompanistWebChromeClient + * + * A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour. + * + * As Accompanist Web needs to set its own web client to function, it provides this intermediary + * class that can be overriden if further custom behaviour is required. + */ +open class AccompanistWebChromeClient : WebChromeClient() { + open lateinit var state: WebViewState + internal set + + override fun onReceivedTitle(view: WebView, title: String?) { + super.onReceivedTitle(view, title) + state.pageTitle = title + } + + override fun onReceivedIcon(view: WebView, icon: Bitmap?) { + super.onReceivedIcon(view, icon) + state.pageIcon = icon + } + + override fun onProgressChanged(view: WebView, newProgress: Int) { + super.onProgressChanged(view, newProgress) + if (state.loadingState is Finished) return + state.loadingState = Loading(newProgress / 100.0f) + } +} + +sealed class WebContent { + data class Url( + val url: String, + val additionalHttpHeaders: Map = emptyMap(), + ) : WebContent() + + data class Data( + val data: String, + val baseUrl: String? = null, + val encoding: String = UTF8, + val mimeType: String? = null, + val historyUrl: String? = null, + ) : WebContent() + + data class Post( + val url: String, + val postData: ByteArray, + ) : WebContent() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Post + + if (url != other.url) return false + if (!postData.contentEquals(other.postData)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + postData.contentHashCode() + return result + } + } + + data object NavigatorOnly : WebContent() +} + +/** + * Sealed class for constraining possible loading states. + * See [Loading] and [Finished]. + */ +sealed class LoadingState { + /** + * Describes a WebView that has not yet loaded for the first time. + */ + data object Initializing : LoadingState() + + /** + * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a + * [progress] property which is updated by the webview. + */ + data class Loading(val progress: Float) : LoadingState() + + /** + * Describes a webview that has finished loading content. + */ + data object Finished : LoadingState() +} + +/** + * A state holder to hold the state for the WebView. In most cases this will be remembered + * using the rememberWebViewState(uri) function. + */ +@Stable +class WebViewState(webContent: WebContent) { + var lastLoadedUrl: String? by mutableStateOf(null) + internal set + + /** + * The content being loaded by the WebView + */ + var content: WebContent by mutableStateOf(webContent) + + /** + * Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with + * progress) or the data loading has [LoadingState.Finished]. See [LoadingState] + */ + var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing) + internal set + + /** + * Whether the webview is currently loading data in its main frame + */ + val isLoading: Boolean + get() = loadingState !is Finished + + /** + * The title received from the loaded content of the current page + */ + var pageTitle: String? by mutableStateOf(null) + internal set + + /** + * the favicon received from the loaded content of the current page + */ + var pageIcon: Bitmap? by mutableStateOf(null) + internal set + + /** + * A list for errors captured in the last load. Reset when a new page is loaded. + * Errors could be from any resource (iframe, image, etc.), not just for the main page. + * For more fine grained control use the OnError callback of the WebView. + */ + val errorsForCurrentRequest: SnapshotStateList = mutableStateListOf() + + /** + * The saved view state from when the view was destroyed last. To restore state, + * use the navigator and only call loadUrl if the bundle is null. + * See WebViewSaveStateSample. + */ + var viewState: Bundle? = null + internal set + + // We need access to this in the state saver. An internal DisposableEffect or AndroidView + // onDestroy is called after the state saver and so can't be used. + internal var webView by mutableStateOf(null) +} + +/** + * Allows control over the navigation of a WebView from outside the composable. E.g. for performing + * a back navigation in response to the user clicking the "up" button in a TopAppBar. + * + * @see [rememberWebViewNavigator] + */ +@Stable +class WebViewNavigator(private val coroutineScope: CoroutineScope) { + private sealed interface NavigationEvent { + data object Back : NavigationEvent + data object Forward : NavigationEvent + data object Reload : NavigationEvent + data object StopLoading : NavigationEvent + + data class LoadUrl( + val url: String, + val additionalHttpHeaders: Map = emptyMap(), + ) : NavigationEvent + + data class LoadHtml( + val html: String, + val baseUrl: String? = null, + val mimeType: String? = null, + val encoding: String? = UTF8, + val historyUrl: String? = null, + ) : NavigationEvent + + data class PostUrl( + val url: String, + val postData: ByteArray, + ) : NavigationEvent { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PostUrl + + if (url != other.url) return false + if (!postData.contentEquals(other.postData)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + postData.contentHashCode() + return result + } + } + } + + private val navigationEvents: MutableSharedFlow = MutableSharedFlow(replay = 1) + + /** + * True when the web view is able to navigate backwards, false otherwise. + */ + var canGoBack: Boolean by mutableStateOf(false) + internal set + + /** + * True when the web view is able to navigate forwards, false otherwise. + */ + var canGoForward: Boolean by mutableStateOf(false) + internal set + + // Use Dispatchers.Main to ensure that the webview methods are called on UI thread + internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) { + navigationEvents.collect { event -> + when (event) { + is NavigationEvent.Back -> goBack() + is NavigationEvent.Forward -> goForward() + is NavigationEvent.Reload -> reload() + is NavigationEvent.StopLoading -> stopLoading() + is NavigationEvent.LoadHtml -> loadDataWithBaseURL( + event.baseUrl, + event.html, + event.mimeType, + event.encoding, + event.historyUrl, + ) + + is NavigationEvent.LoadUrl -> { + loadUrl(event.url, event.additionalHttpHeaders) + } + + is NavigationEvent.PostUrl -> { + postUrl(event.url, event.postData) + } + } + } + } + + fun loadUrl(url: String, additionalHttpHeaders: Map = emptyMap()) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadUrl( + url, + additionalHttpHeaders, + ), + ) + } + } + + fun loadHtml( + html: String, + baseUrl: String? = null, + mimeType: String? = null, + encoding: String? = UTF8, + historyUrl: String? = null, + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadHtml( + html, + baseUrl, + mimeType, + encoding, + historyUrl, + ), + ) + } + } + + fun postUrl( + url: String, + postData: ByteArray, + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.PostUrl( + url, + postData, + ), + ) + } + } + + /** + * Navigates the webview back to the previous page. + */ + fun navigateBack() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) } + } + + /** + * Navigates the webview forward after going back from a page. + */ + fun navigateForward() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) } + } + + /** + * Reloads the current page in the webview. + */ + fun reload() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) } + } + + /** + * Stops the current page load (if one is loading). + */ + fun stopLoading() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) } + } +} + +/** + * Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided + * override. + */ +@Composable +fun rememberWebViewNavigator( + coroutineScope: CoroutineScope = rememberCoroutineScope(), +): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) } + +/** + * A wrapper class to hold errors from the WebView. + */ +@Immutable +data class WebViewError( + /** + * The request the error came from. + */ + val request: WebResourceRequest?, + /** + * The error that was reported. + */ + val error: WebResourceError, +) + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param url The url to load in the WebView + * @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl]. + * Note that these headers are used for all subsequent requests of the WebView. + */ +@Composable +fun rememberWebViewState( + url: String, + additionalHttpHeaders: Map = emptyMap(), +): WebViewState = +// Rather than using .apply {} here we will recreate the state, this prevents + // a recomposition loop when the webview updates the url itself. + remember { + WebViewState( + WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders, + ), + ) + }.apply { + this.content = WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders, + ) + } + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param data The uri to load in the WebView + */ +@Composable +fun rememberWebViewStateWithHTMLData( + data: String, + baseUrl: String? = null, + encoding: String = UTF8, + mimeType: String? = null, + historyUrl: String? = null, +): WebViewState = + remember { + WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl)) + }.apply { + this.content = WebContent.Data( + data, baseUrl, encoding, mimeType, historyUrl, + ) + } + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param url The url to load in the WebView + * @param postData The data to be posted to the WebView with the url + */ +@Composable +fun rememberWebViewState( + url: String, + postData: ByteArray, +): WebViewState = +// Rather than using .apply {} here we will recreate the state, this prevents + // a recomposition loop when the webview updates the url itself. + remember { + WebViewState( + WebContent.Post( + url = url, + postData = postData, + ), + ) + }.apply { + this.content = WebContent.Post( + url = url, + postData = postData, + ) + } + +/** + * Creates a WebView state that is remembered across Compositions and saved + * across activity recreation. + * When using saved state, you cannot change the URL via recomposition. The only way to load + * a URL is via a WebViewNavigator. + */ +@Composable +fun rememberSaveableWebViewState(): WebViewState = + rememberSaveable(saver = WebStateSaver) { + WebViewState(WebContent.NavigatorOnly) + } + +val WebStateSaver: Saver = run { + val pageTitleKey = "pagetitle" + val lastLoadedUrlKey = "lastloaded" + val stateBundle = "bundle" + + mapSaver( + save = { + val viewState = Bundle().apply { it.webView?.saveState(this) } + mapOf( + pageTitleKey to it.pageTitle, + lastLoadedUrlKey to it.lastLoadedUrl, + stateBundle to viewState, + ) + }, + restore = { + WebViewState(WebContent.NavigatorOnly).apply { + this.pageTitle = it[pageTitleKey] as? String + this.lastLoadedUrl = it[lastLoadedUrlKey] as? String + this.viewState = it[stateBundle] as? Bundle + } + }, + ) +} + +private const val UTF8 = "utf-8" diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WormPagerIndicator.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WormPagerIndicator.kt new file mode 100644 index 0000000..76589b6 --- /dev/null +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/WormPagerIndicator.kt @@ -0,0 +1,88 @@ +package com.louisfn.somovie.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun WormPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier, + spacing: Dp = 8.dp, + size: Dp = 8.dp, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + color: Color = activeColor.copy(ContentAlpha.disabled), +) { + Box( + modifier = modifier, + contentAlignment = Alignment.CenterStart, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(spacing), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(pagerState.pageCount) { + Box( + modifier = Modifier + .size(size) + .background( + color = color, + shape = CircleShape, + ), + ) + } + } + + Box( + Modifier + .wormTransition( + pagerState, + spacing, + activeColor, + ) + .size(size), + ) + } +} + +private fun Modifier.wormTransition( + pagerState: PagerState, + spacing: Dp, + color: Color = Color.White, +) = drawBehind { + val distance = size.width + spacing.roundToPx() + val scrollPosition = pagerState.currentPage + pagerState.currentPageOffsetFraction + val wormOffset = scrollPosition % 1 * 2 + + val xPos = scrollPosition.toInt() * distance + val head = xPos + distance * 0f.coerceAtLeast(wormOffset - 1) + val tail = xPos + size.width + 1f.coerceAtMost(wormOffset) * distance + + val worm = RoundRect( + left = head, + top = 0f, + right = tail, + bottom = size.height, + cornerRadius = CornerRadius(50f), + ) + + val path = Path().apply { addRoundRect(worm) } + drawPath(path = path, color = color) +} diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeContainer.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeContainer.kt index 3781562..a56b76d 100644 --- a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeContainer.kt +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeContainer.kt @@ -40,9 +40,9 @@ fun SwipeContainer( swipeAnimationSpec: AnimationSpec = DefaultSwipeAnimationSpec, cancelAnimationSpec: AnimationSpec = DefaultRewindAnimationSpec, onDragging: (T, SwipeDirection, ratio: Float) -> Unit = { _, _, _ -> }, - onSwiped: (T, SwipeDirection) -> Unit = { _, _ -> }, - onDisappeared: (T, SwipeDirection) -> Unit = { _, _ -> }, - onCanceled: (T) -> Unit = {}, + onSwipe: (T, SwipeDirection) -> Unit = { _, _ -> }, + onDisappear: (T, SwipeDirection) -> Unit = { _, _ -> }, + onCancel: (T) -> Unit = {}, itemContent: @Composable (T) -> Unit, ) { Box(modifier = modifier) { @@ -57,10 +57,10 @@ fun SwipeContainer( velocityThreshold = velocityThreshold, swipeAnimationSpec = swipeAnimationSpec, cancelAnimationSpec = cancelAnimationSpec, - onSwiped = { direction -> onSwiped(item, direction) }, + onSwipe = { direction -> onSwipe(item, direction) }, onDragging = { direction, ratio -> onDragging(item, direction, ratio) }, - onCanceled = { onCanceled(item) }, - onDisappeared = { direction -> onDisappeared(item, direction) }, + onCancel = { onCancel(item) }, + onDisappear = { direction -> onDisappear(item, direction) }, itemContent = itemContent, ) } @@ -77,9 +77,9 @@ private fun SwipeableItem( swipeAnimationSpec: AnimationSpec, cancelAnimationSpec: AnimationSpec, onDragging: (SwipeDirection, ratio: Float) -> Unit, - onSwiped: (SwipeDirection) -> Unit, - onDisappeared: (SwipeDirection) -> Unit, - onCanceled: () -> Unit, + onSwipe: (SwipeDirection) -> Unit, + onDisappear: (SwipeDirection) -> Unit, + onCancel: () -> Unit, itemContent: @Composable (T) -> Unit, modifier: Modifier = Modifier, ) { @@ -93,9 +93,9 @@ private fun SwipeableItem( swipeAnimationSpec = swipeAnimationSpec, cancelAnimationSpec = cancelAnimationSpec, onDragging = onDragging, - onSwiped = onSwiped, - onDisappeared = onDisappeared, - onCanceled = onCanceled, + onSwipe = onSwipe, + onDisappear = onDisappear, + onCancel = onCancel, ) Box( diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeController.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeController.kt index 198ea59..64b7e99 100644 --- a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeController.kt +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeController.kt @@ -38,9 +38,9 @@ class SwipeController( private val cancelAnimationSpec: AnimationSpec, private val scope: CoroutineScope, private val onDragging: (SwipeDirection, ratio: Float) -> Unit, - private val onSwiped: (SwipeDirection) -> Unit, - private val onDisappeared: (SwipeDirection) -> Unit, - private val onCanceled: () -> Unit, + private val onSwipe: (SwipeDirection) -> Unit, + private val onDisappear: (SwipeDirection) -> Unit, + private val onCancel: () -> Unit, ) { private val velocityTracker = VelocityTracker() @@ -79,14 +79,14 @@ class SwipeController( val targetOffsetX = maxItemWidthWhenRotate.withSign(offset.x) val direction = SwipeDirection.fromOffset(targetOffsetX) - onSwiped(direction) + onSwipe(direction) offsetAnimatable.animateTo( targetValue = offset.copy(x = targetOffsetX), animationSpec = swipeAnimationSpec, ) - onDisappeared(direction) + onDisappear(direction) } else { - onCanceled() + onCancel() offsetAnimatable.animateTo(Offset.Zero, cancelAnimationSpec) } } @@ -109,12 +109,12 @@ internal fun rememberSwipeController( swipeAnimationSpec: AnimationSpec, cancelAnimationSpec: AnimationSpec, onDragging: (SwipeDirection, ratio: Float) -> Unit, - onSwiped: (SwipeDirection) -> Unit, - onDisappeared: (SwipeDirection) -> Unit, - onCanceled: () -> Unit, + onSwipe: (SwipeDirection) -> Unit, + onDisappear: (SwipeDirection) -> Unit, + onCancel: () -> Unit, ): SwipeController { val currentOnDragging by rememberUpdatedState(onDragging) - val currentOnSwipe by rememberUpdatedState(onSwiped) + val currentOnSwipe by rememberUpdatedState(onSwipe) val scope = rememberCoroutineScope() val velocityThresholdInPx = velocityThreshold.toPx() @@ -135,9 +135,9 @@ internal fun rememberSwipeController( cancelAnimationSpec = cancelAnimationSpec, scope = scope, onDragging = currentOnDragging, - onSwiped = currentOnSwipe, - onDisappeared = onDisappeared, - onCanceled = onCanceled, + onSwipe = currentOnSwipe, + onDisappear = onDisappear, + onCancel = onCancel, ) } } diff --git a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeableItem.kt b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeableItem.kt index 7ea9901..bb9cd36 100644 --- a/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeableItem.kt +++ b/ui/component/src/main/kotlin/com/louisfn/somovie/ui/component/swipe/SwipeableItem.kt @@ -1,19 +1,20 @@ package com.louisfn.somovie.ui.component.swipe import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.input.pointer.pointerInput +@Composable internal fun Modifier.swipeableItem( swipeController: SwipeController, -) = composed { - if (!swipeController.isGestureEnabled) return@composed this +): Modifier { + if (!swipeController.isGestureEnabled) return this val currentSwipeController by rememberUpdatedState(swipeController) - pointerInput(Unit) { + return pointerInput(Unit) { detectDragGestures( onDrag = { change, dragAmount -> change.consume()