diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 64560e5e..024dc000 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -53,6 +53,7 @@ kotlin { implementation(project(":feature:auth")) implementation(project(":feature:follower")) implementation(project(":feature:favourite")) + implementation(project(":feature:settings")) } } } @@ -100,6 +101,9 @@ android { debugImplementation(libs.compose.ui.tooling) } } +dependencies { + implementation(project(":feature:settings")) +} compose.desktop { application { diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 933aa219..53cac13a 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -11,6 +11,7 @@ import com.stslex.feature.film_feed.di.featureFeedModule import com.stslex.feature.follower.di.featureFollowerModule import com.stslex.feature.match_feed.di.featureMatchFeedModule import com.stslex.feature.profile.di.featureProfileModule +import com.stslex.feature.settings.di.featureSettingsModule import di.appModule import org.koin.compose.KoinApplication import org.koin.core.KoinApplication @@ -45,7 +46,8 @@ private fun KoinApplication.setupCommonModules() { featureMatchFeedModule, featureAuthModule, featureFollowerModule, - featureFavouriteModule + featureFavouriteModule, + featureSettingsModule ) ) } diff --git a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt index 6893088c..cdd16248 100644 --- a/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt +++ b/composeApp/src/commonMain/kotlin/navigator/AppNavigatorImpl.kt @@ -9,6 +9,7 @@ import com.stslex.feature.film.ui.FilmScreen import com.stslex.feature.follower.navigation.FollowerScreenArgs import com.stslex.feature.follower.ui.FollowerScreen import com.stslex.feature.match_feed.ui.MatchFeedScreen +import com.stslex.feature.settings.ui.SettingsScreen import main_screen.MainScreen class AppNavigatorImpl : AppNavigator { @@ -40,21 +41,17 @@ class AppNavigatorImpl : AppNavigator { is AppScreen.Film -> navigator.push(FilmScreen(screen.id)) AppScreen.MatchFeed -> navigator.push(MatchFeedScreen) is AppScreen.Favourite -> navigator.push(FavouriteScreen(uuid = screen.uuid)) - is AppScreen.Followers -> navigator.push( - FollowerScreen( - args = FollowerScreenArgs.Follower( - uuid = screen.uuid - ) - ) - ) - - is AppScreen.Following -> navigator.push( - FollowerScreen( - args = FollowerScreenArgs.Following( - uuid = screen.uuid - ) - ) - ) + is AppScreen.Followers -> navToFollowers(screen.uuid) + is AppScreen.Following -> navToFollowing(screen.uuid) + AppScreen.Settings -> navigator.push(SettingsScreen) } } + + private fun navToFollowing(uuid: String) { + navigator.push(FollowerScreen(args = FollowerScreenArgs.Following(uuid = uuid))) + } + + private fun navToFollowers(uuid: String) { + navigator.push(FollowerScreen(args = FollowerScreenArgs.Follower(uuid = uuid))) + } } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt index 2b302872..da3c1fc7 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt @@ -52,7 +52,7 @@ class ProfileClientImpl( override suspend fun getFollowers( request: PagingRequest ): UserFollowerResponse = client.request { - get("$HOST/$HOST_FOLLOW") { + get("$HOST/$HOST_FOLLOW/followers") { requestPaging(request) }.body() } @@ -60,7 +60,7 @@ class ProfileClientImpl( override suspend fun getFollowing( request: PagingRequest ): UserFollowerResponse = client.request { - get("$HOST/$HOST_FOLLOW") { + get("$HOST/$HOST_FOLLOW/following") { requestPaging(request) }.body() } diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/components/AppToolbar.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/components/AppToolbar.kt new file mode 100644 index 00000000..8aed75ca --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/components/AppToolbar.kt @@ -0,0 +1,61 @@ +package com.stslex.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.stslex.core.ui.theme.AppDimension + +@Composable +fun AppToolbar( + title: String, + modifier: Modifier = Modifier, + onBackClick: (() -> Unit)? = null, + actionIcon: @Composable (() -> Unit)? = null, + isActionVisible: Boolean = actionIcon != null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.background) + .padding(horizontal = AppDimension.Padding.medium), + ) { + if (onBackClick != null) { + IconButton( + modifier = Modifier.align(Alignment.CenterStart), + onClick = onBackClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + } + Text( + modifier = Modifier.align(Alignment.Center), + text = title, + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + if (isActionVisible && actionIcon != null) + Box( + modifier = Modifier.align(Alignment.CenterEnd) + ) { + actionIcon() + } + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt index b3451f53..8993e093 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/navigation/AppScreen.kt @@ -12,6 +12,8 @@ sealed interface AppScreen { data object Auth : AppScreen + data object Settings : AppScreen + data class Favourite( val uuid: String ) : AppScreen diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt index f0a1ad3a..b472ccf9 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt @@ -13,6 +13,8 @@ class ProfileRouterImpl( ) { when (event) { Navigation.LogIn -> navigator.navigate(AppScreen.Auth) + Navigation.Back -> navigator.navigate(AppScreen.Back) + Navigation.Settings -> navigator.navigate(AppScreen.Settings) is Navigation.Favourite -> navigator.navigate(AppScreen.Favourite(uuid = event.uuid)) is Navigation.Following -> navigator.navigate(AppScreen.Following(uuid = event.uuid)) is Navigation.Followers -> navigator.navigate(AppScreen.Followers(uuid = event.uuid)) diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt index 2839ca0f..8a5ecf35 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.SnackbarHostState @@ -73,9 +72,7 @@ private fun ProfileScreen( modifier: Modifier = Modifier, ) { BoxWithConstraints( - modifier = modifier - .fillMaxSize() - .systemBarsPadding(), + modifier = modifier.fillMaxSize(), ) { when (val screen = state.screen) { is ProfileScreenState.Content -> ProfileScreenContent( @@ -89,14 +86,14 @@ private fun ProfileScreen( repeatLastAction = { onAction(Action.RepeatLastAction) } ) - ProfileScreenState.Shimmer -> ProfileScreenShinner() + ProfileScreenState.Shimmer -> ProfileScreenShimmer() } AppSnackbarHost(snackbarHostState) } } @Composable -internal fun ProfileScreenShinner(modifier: Modifier = Modifier) { +internal fun ProfileScreenShimmer(modifier: Modifier = Modifier) { Box( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileLogoutButton.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileLogoutButton.kt deleted file mode 100644 index 21dede1d..00000000 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileLogoutButton.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.stslex.feature.profile.ui.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun ProfileLogoutButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - ElevatedButton( - modifier = modifier - .fillMaxWidth(), - onClick = onClick, - ) { - Text( - modifier = Modifier.align(Alignment.CenterVertically), - text = "Logout", - style = MaterialTheme.typography.titleMedium - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt index d15917a4..0e567490 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt @@ -1,42 +1,38 @@ package com.stslex.feature.profile.ui.components -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.feature.profile.ui.store.ProfileScreenState -import com.stslex.feature.profile.ui.store.ProfileStoreComponent +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action @Composable internal fun ProfileScreenContent( state: ProfileScreenState.Content, - onAction: (ProfileStoreComponent.Action) -> Unit, + onAction: (Action) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { - Column( - modifier = Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.Center, - ) { - ProfileAvatar(avatar = state.data.avatar) + Column(modifier = Modifier.fillMaxSize()) { + ProfileScreenToolbar( + nickname = state.data.username, + isCurrentUser = state.data.isCurrentUser, + onSettingsClick = { onAction(Action.SettingsClick) }, + onBackClick = { onAction(Action.BackButtonClick) } + ) - if (state.data.isCurrentUser) { - ProfileLogoutButton( - onClick = { onAction(ProfileStoreComponent.Action.Logout) }, - ) - } + ProfileAvatar(avatar = state.data.avatar) ProfileInfo( username = state.data.username, favouriteCount = state.data.favouriteCount, followingCount = state.data.following, followersCount = state.data.followers, - onFavouriteClick = { onAction(ProfileStoreComponent.Action.FavouriteClick) }, - onFollowingClick = { onAction(ProfileStoreComponent.Action.FollowingClick) }, - onFollowersClick = { onAction(ProfileStoreComponent.Action.FollowersClick) }, + onFavouriteClick = { onAction(Action.FavouriteClick) }, + onFollowingClick = { onAction(Action.FollowingClick) }, + onFollowersClick = { onAction(Action.FollowersClick) }, ) } diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt new file mode 100644 index 00000000..4e33b46c --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt @@ -0,0 +1,37 @@ +package com.stslex.feature.profile.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.stslex.core.ui.components.AppToolbar + +@Composable +fun ProfileScreenToolbar( + nickname: String, + isCurrentUser: Boolean, + onSettingsClick: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AppToolbar( + modifier = modifier, + title = nickname, + onBackClick = onBackClick, + isActionVisible = isCurrentUser, + actionIcon = { + IconButton( + onClick = onSettingsClick + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + } + ) +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt index 787bac78..bb366a4a 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt @@ -33,6 +33,8 @@ class ProfileStore( is Action.FavouriteClick -> actionFavouriteClick() is Action.FollowingClick -> actionFollowingClick() is Action.FollowersClick -> actionFollowersClick() + is Action.SettingsClick -> actionSettingsClick() + is Action.BackButtonClick -> actionBackClick() } } @@ -127,4 +129,12 @@ class ProfileStore( } ) } + + private fun actionSettingsClick() { + navigate(Navigation.Settings) + } + + private fun actionBackClick() { + navigate(Navigation.Back) + } } diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt index 23b44110..07a95b00 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt @@ -41,6 +41,10 @@ interface ProfileStoreComponent : Store { data object FollowingClick : Action data object FollowersClick : Action + + data object SettingsClick : Action + + data object BackButtonClick : Action } @Stable @@ -65,5 +69,9 @@ interface ProfileStoreComponent : Store { data class Followers( val uuid: String ) : Navigation + + data object Back : Navigation + + data object Settings : Navigation } } \ No newline at end of file diff --git a/feature/settings/.gitignore b/feature/settings/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/settings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts new file mode 100644 index 00000000..2c4126fb --- /dev/null +++ b/feature/settings/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.kotlinCocoapods) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + jvm("desktop") + + iosX64() + iosArm64() + iosSimulatorArm64() + cocoapods { + summary = "Some description for the Shared Module" + homepage = "Link to the Shared Module homepage" + version = "1.0" + ios.deploymentTarget = "16.0" + podfile = project.file(project.rootProject.projectDir.path + "/iosApp/FeatureSettingsPodfile") + framework { + baseName = "featureSettings" + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui")) + implementation(project(":core:network")) + implementation(project(":core:database")) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "com.stslex.feature.settings" + compileSdk = libs.versions.android.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} + +tasks.withType { + compilerOptions.freeCompilerArgs.addAll( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$projectDir/build/compose/metrics", + ) +} + +tasks.withType { + compilerOptions.freeCompilerArgs.addAll( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$projectDir/build/compose/reports", + ) +} \ No newline at end of file diff --git a/feature/settings/consumer-rules.pro b/feature/settings/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/settings/proguard-rules.pro b/feature/settings/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/settings/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt new file mode 100644 index 00000000..c6fde3ef --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt @@ -0,0 +1,20 @@ +package com.stslex.feature.settings.di + +import com.stslex.feature.settings.domain.SettingsInteractor +import com.stslex.feature.settings.domain.SettingsInteractorImpl +import com.stslex.feature.settings.navigation.SettingsRouter +import com.stslex.feature.settings.navigation.SettingsRouterImpl +import com.stslex.feature.settings.ui.store.SettingsStore +import org.koin.dsl.module + +val featureSettingsModule = module { + factory { + SettingsStore( + interactor = get(), + appDispatcher = get(), + router = get(), + ) + } + factory { SettingsRouterImpl(navigator = get()) } + factory { SettingsInteractorImpl(authController = get()) } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractor.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractor.kt new file mode 100644 index 00000000..10ca0edb --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractor.kt @@ -0,0 +1,6 @@ +package com.stslex.feature.settings.domain + +interface SettingsInteractor { + + suspend fun logOut() +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractorImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractorImpl.kt new file mode 100644 index 00000000..60bf33e5 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/domain/SettingsInteractorImpl.kt @@ -0,0 +1,12 @@ +package com.stslex.feature.settings.domain + +import com.stslex.core.network.utils.token.AuthController + +class SettingsInteractorImpl( + private val authController: AuthController +) : SettingsInteractor { + + override suspend fun logOut() { + authController.logOut() + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt new file mode 100644 index 00000000..6301907c --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt @@ -0,0 +1,6 @@ +package com.stslex.feature.settings.navigation + +import com.stslex.core.ui.navigation.Router +import com.stslex.feature.settings.ui.store.SettingsStoreComponent + +interface SettingsRouter : Router diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt new file mode 100644 index 00000000..6e7cad0e --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt @@ -0,0 +1,19 @@ +package com.stslex.feature.settings.navigation + +import com.stslex.core.ui.navigation.AppNavigator +import com.stslex.core.ui.navigation.AppScreen +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Navigation + +class SettingsRouterImpl( + private val navigator: AppNavigator +) : SettingsRouter { + + override fun invoke( + event: Navigation + ) { + when (event) { + Navigation.Back -> navigator.navigate(AppScreen.Back) + Navigation.LogOut -> navigator.navigate(AppScreen.Auth) + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt new file mode 100644 index 00000000..d3d2eb35 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt @@ -0,0 +1,90 @@ +package com.stslex.feature.settings.ui + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.stslex.core.ui.components.AppSnackbarHost +import com.stslex.core.ui.components.AppToolbar +import com.stslex.core.ui.theme.AppDimension +import com.stslex.feature.settings.ui.components.SettingsContent +import com.stslex.feature.settings.ui.store.SettingsStore +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Action +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Event +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.State + +object SettingsScreen : Screen { + + @Composable + override fun Content() { + val store = getScreenModel() + val state by remember { store.state }.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + store.event.collect { event -> + when (event) { + is Event.ShowSnackbar -> snackbarHostState.showSnackbar( + message = event.snackbar.message, + actionLabel = event.snackbar.action, + duration = event.snackbar.duration, + withDismissAction = event.snackbar.withDismissAction, + ) + } + } + } + SettingsScreen( + state = state, + onAction = store::sendAction, + snackbarHostState = snackbarHostState + ) + } +} + +@Composable +private fun SettingsScreen( + state: State, + onAction: (Action) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .statusBarsPadding(), + ) { + Column( + modifier = modifier.fillMaxSize(), + ) { + AppToolbar( + modifier = modifier, + title = "Settings", // todo("resources") + onBackClick = { onAction(Action.BackButtonClicked) } + ) + Spacer(modifier = Modifier.height(AppDimension.Padding.big)) + SettingsContent( + logOut = { onAction(Action.LogOut) } + ) + } + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + AppSnackbarHost( + snackbarHostState = snackbarHostState + ) + } +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/components/SettingsContent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/components/SettingsContent.kt new file mode 100644 index 00000000..c22396af --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/components/SettingsContent.kt @@ -0,0 +1,84 @@ +package com.stslex.feature.settings.ui.components + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import com.stslex.core.ui.theme.AppDimension +import com.stslex.feature.settings.ui.model.SettingsColumnItems + +@Composable +fun SettingsContent( + logOut: () -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize() + ) { + items( + count = SettingsColumnItems.entries.size, + key = { index -> SettingsColumnItems.entries[index].ordinal }, + contentType = { "SettingsColumnItems" } + ) { index -> + SettingsColumnItems.entries.getOrNull(index)?.let { item -> + SettingsColumnItem( + title = item.title, + iconVector = item.icon, + onClick = { + when (item) { + SettingsColumnItems.LOG_OUT -> logOut() + } + } + ) + } + } + } +} + +@Composable +private fun SettingsColumnItem( + title: String, + iconVector: ImageVector?, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ElevatedButton( + modifier = modifier + .padding(AppDimension.Padding.medium) + .fillMaxWidth(), + onClick = onClick, + shape = RoundedCornerShape(AppDimension.Radius.medium) + ) { + iconVector?.let { icon -> + Icon( + imageVector = icon, + contentDescription = title, + modifier = Modifier.align(Alignment.CenterVertically), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(AppDimension.Padding.medium)) + } + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f), + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/model/SettingsColumnItems.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/model/SettingsColumnItems.kt new file mode 100644 index 00000000..2d2a6351 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/model/SettingsColumnItems.kt @@ -0,0 +1,15 @@ +package com.stslex.feature.settings.ui.model + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.ui.graphics.vector.ImageVector + +enum class SettingsColumnItems( + val title: String, + val icon: ImageVector +) { + LOG_OUT( + title = "Log out", + icon = Icons.AutoMirrored.Filled.ExitToApp + ) +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt new file mode 100644 index 00000000..ac22dcb6 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt @@ -0,0 +1,61 @@ +package com.stslex.feature.settings.ui.store + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.mvi.BaseStore +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.feature.settings.domain.SettingsInteractor +import com.stslex.feature.settings.navigation.SettingsRouter +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Action +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Event +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Navigation +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.State + +class SettingsStore( + private val interactor: SettingsInteractor, + router: SettingsRouter, + appDispatcher: AppDispatcher +) : BaseStore( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL +) { + + override fun process(action: Action) { + when (action) { + Action.LogOut -> actionLogout() + Action.BackButtonClicked -> actionBackClick() + } + } + + private fun actionBackClick() { + navigate(Navigation.Back) + } + + private fun actionLogout() { + if (state.value.isLoading) return + updateState { currentState -> + currentState.copy( + isLoading = true + ) + } + + launch( + action = { + interactor.logOut() + }, + onSuccess = { + updateState { currentState -> + currentState.copy( + isLoading = false + ) + } + navigate(Navigation.LogOut) + }, + onError = { error -> + val message = error.message ?: "Logout error" + val snackbarType = Snackbar.Error(message) + sendEvent(Event.ShowSnackbar(snackbarType)) + } + ) + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt new file mode 100644 index 00000000..a444c1e4 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt @@ -0,0 +1,40 @@ +package com.stslex.feature.settings.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.Store +import com.stslex.core.ui.mvi.Store.Event.Snackbar + +interface SettingsStoreComponent : Store { + + @Stable + data class State( + val isLoading: Boolean + ) : Store.State { + + companion object { + + val INITIAL = State(isLoading = false) + } + } + + @Stable + sealed interface Action : Store.Action { + + data object LogOut : Action + + data object BackButtonClicked : Action + } + + @Stable + sealed interface Event : Store.Event { + + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : Store.Navigation { + + data object Back : Navigation + + data object LogOut : Navigation + } +} \ No newline at end of file diff --git a/feature/settings/src/test/java/com/stslex/feature/settings/ExampleUnitTest.kt b/feature/settings/src/test/java/com/stslex/feature/settings/ExampleUnitTest.kt new file mode 100644 index 00000000..4e63a596 --- /dev/null +++ b/feature/settings/src/test/java/com/stslex/feature/settings/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.stslex.feature.settings + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/iosApp/FeatureSettingsPodfile b/iosApp/FeatureSettingsPodfile new file mode 100644 index 00000000..ccf52ada --- /dev/null +++ b/iosApp/FeatureSettingsPodfile @@ -0,0 +1,5 @@ +target 'test' do + use_frameworks! + platform :ios, '16.0' + pod 'settings', :path => '../feature/settings' +end \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b698fac..0e7e0f9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,5 @@ include(":feature:profile") include(":feature:match_feed") include(":feature:auth") include(":feature:follower") -include(":feature:favourite") \ No newline at end of file +include(":feature:favourite") +include(":feature:settings")