diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 01b1730..f330181 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -18,6 +18,10 @@ jobs: - name: Checkout branch uses: actions/checkout@v2 + - run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + - name: set up JDK 17 uses: actions/setup-java@v3 with: @@ -28,11 +32,21 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - # local properties not needed now - # - name: set up LOCAL_PROPERTIES - # env: - # LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - # run: echo "$LOCAL_PROPERTIES" > ./local.properties + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + + - name: set up LOCAL_PROPERTIES + env: + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + run: echo "$LOCAL_PROPERTIES" > ./local.properties - name: Build with Gradle - run: ./gradlew assembleAndroidTest \ No newline at end of file + run: ./gradlew assembleAndroidTest diff --git a/.github/workflows/build_mac_os.yml b/.github/workflows/build_mac_os.yml index 349c595..146ac09 100644 --- a/.github/workflows/build_mac_os.yml +++ b/.github/workflows/build_mac_os.yml @@ -16,6 +16,10 @@ jobs: - name: Checkout branch uses: actions/checkout@v4 + - run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + - name: Set up JDK 21 uses: actions/setup-java@v4 with: @@ -28,6 +32,17 @@ jobs: - name: Set Xcode version run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + - name: set up LOCAL_PROPERTIES env: LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} @@ -37,4 +52,4 @@ jobs: run: ./gradlew kspKotlinMetadata - name: Build with Gradle - run: cd iosApp && xcodebuild -workspace ./iosApp.xcworkspace -scheme iosApp -configuration Debug -destination 'platform=iOS Simulator,OS=latest,name=iPhone 15' CODE_SIGNING_ALLOWED='NO' \ No newline at end of file + run: cd iosApp && xcodebuild -workspace ./iosApp.xcworkspace -scheme iosApp -configuration Debug -destination 'platform=iOS Simulator,OS=latest,name=iPhone 15' CODE_SIGNING_ALLOWED='NO' diff --git a/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt index 1de51fd..3c17f39 100644 --- a/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt @@ -14,6 +14,10 @@ import com.stslex.atten.convention.configureKsp import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.Properties class KMPApplicationConventionPlugin : Plugin { @@ -43,5 +47,61 @@ class KMPApplicationConventionPlugin : Plugin { versionCode = libs.findVersionInt("versionCode") } } + configureSigning() } +} + +fun Project.configureSigning() = extensions.configure { + signingConfigs { + val keystoreProperties = gradleKeystoreProperties(project.rootProject.projectDir) + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = project.getFile(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + with(getByName("debug")) { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = project.getFile(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") + isDebuggable = false + } + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + isDebuggable = true + } + } +} + + +fun Project.getFile(path: String): File { + val file = File(project.rootProject.projectDir, path) + if (file.isFile) { + return file + } else { + throw IllegalStateException("${file.name} is inValid") + } +} + +fun gradleKeystoreProperties(projectRootDir: File): Properties { + val properties = Properties() + val localProperties = File(projectRootDir, "keystore.properties") + + if (localProperties.isFile) { + InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader -> + properties.load(reader) + } + } + return properties } \ No newline at end of file diff --git a/commonApp/build.gradle.kts b/commonApp/build.gradle.kts index 67423a3..cd36879 100644 --- a/commonApp/build.gradle.kts +++ b/commonApp/build.gradle.kts @@ -13,9 +13,11 @@ kotlin { implementation(project(":core:database")) implementation(project(":core:paging")) implementation(project(":core:todo")) + implementation(project(":core:auth")) implementation(project(":feature:home")) implementation(project(":feature:details")) + implementation(project(":feature:settings")) } } } diff --git a/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt b/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt index a839a4a..bd8ebe5 100644 --- a/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt +++ b/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt @@ -10,15 +10,21 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.compose.LocalLifecycleOwner import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.defaultComponentContext +import com.stslex.atten.core.ui.kit.utils.ActivityHolderProducer import com.stslex.atten.host.DefaultRootComponent +import org.koin.android.ext.android.getKoin class MainActivity : ComponentActivity() { + private val activityProducer: ActivityHolderProducer by lazy { getKoin().get() } + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) val rootComponent = DefaultRootComponent(defaultComponentContext()) val windowController = WindowCompat.getInsetsController(window, window.decorView) + + activityProducer.produce(this) setContent { App( rootComponent = rootComponent, @@ -28,6 +34,11 @@ class MainActivity : ComponentActivity() { ) } } + + override fun onDestroy() { + super.onDestroy() + activityProducer.produce(null) + } } @Preview diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt index 90259f3..0be757f 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt @@ -1,11 +1,14 @@ package com.stslex.atten.di +import com.stslex.atten.core.auth.di.ModuleCoreAuth import com.stslex.atten.core.core.di.ModuleCore import com.stslex.atten.core.database.di.ModuleCoreDatabase import com.stslex.atten.core.paging.di.ModuleCorePaging import com.stslex.atten.core.todo.di.ModuleCoreToDo +import com.stslex.atten.core.ui.kit.utils.ModuleCoreUiUtils import com.stslex.atten.feature.details.di.ModuleFeatureDetails import com.stslex.atten.feature.home.di.ModuleFeatureHome +import com.stslex.atten.feature.settings.di.ModuleFeatureSettings import org.koin.core.module.Module import org.koin.ksp.generated.module @@ -14,6 +17,9 @@ val appModules: List = listOf( ModuleCoreDatabase().module, ModuleCoreToDo().module, ModuleCorePaging().module, + ModuleCoreUiUtils().module, + ModuleCoreAuth().module, ModuleFeatureHome().module, ModuleFeatureDetails().module, + ModuleFeatureSettings().module ) diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt index cb6ff8c..4befaf1 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.stslex.atten.feature.settings.ui.SettingsScreen import com.stslex.atten.feature.details.ui.DetailsScreen import com.stslex.atten.feature.home.ui.HomeScreen @@ -21,6 +22,7 @@ internal fun AppNavigationHost( when (val instance = created.instance) { is RootComponent.Child.Details -> DetailsScreen(instance.component) is RootComponent.Child.Home -> HomeScreen(instance.component) + is RootComponent.Child.Settings -> SettingsScreen(instance.component) } } } \ No newline at end of file diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt index e6955bf..b1bddb1 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt @@ -8,6 +8,7 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.navigate import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.Value +import com.stslex.atten.feature.settings.mvi.SettingsComponent import com.stslex.atten.core.ui.navigation.Config import com.stslex.atten.core.ui.navigation.Router import com.stslex.atten.feature.details.ui.mvi.DetailsComponent @@ -40,6 +41,7 @@ class DefaultRootComponent( ): Child = when (config) { is Config.Home -> Child.Home(HomeComponent.create(context.router)) is Config.Detail -> Child.Details(DetailsComponent.create(context.router, config.uuid)) + is Config.Settings -> Child.Settings(SettingsComponent.create(context.router)) } @OptIn(DelicateDecomposeApi::class) diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt index 72027d9..3bb9c9e 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt @@ -3,6 +3,7 @@ package com.stslex.atten.host import com.arkivanov.decompose.Cancellation import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.value.Value +import com.stslex.atten.feature.settings.mvi.SettingsComponent import com.stslex.atten.core.ui.navigation.Config import com.stslex.atten.feature.details.ui.mvi.DetailsComponent import com.stslex.atten.feature.home.ui.mvi.HomeComponent @@ -18,6 +19,8 @@ interface RootComponent { data class Home(val component: HomeComponent) : Child data class Details(val component: DetailsComponent) : Child + + data class Settings(val component: SettingsComponent) : Child } } diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts new file mode 100644 index 0000000..d12c79b --- /dev/null +++ b/core/auth/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.convention.kmp.library.compose) +} + +kotlin { + sourceSets.apply { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui:kit")) + } + androidMain.dependencies { + implementation(libs.gms.auth) + } + } +} \ No newline at end of file diff --git a/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt new file mode 100644 index 0000000..27e3413 --- /dev/null +++ b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt @@ -0,0 +1,94 @@ +package com.stslex.atten.core.auth.controller + +import android.app.Activity +import android.content.IntentSender.SendIntentException +import androidx.activity.ComponentActivity +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import com.google.android.gms.auth.api.identity.AuthorizationRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.Scope +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal actual class GoogleAuthControllerImpl actual constructor( + private val callback: GoogleAuthCallback, + private val activityHolder: ActivityHolder +) : GoogleAuthController { + + private lateinit var launcher: ManagedActivityResultLauncher + private val activity: ComponentActivity + get() = requireNotNull(activityHolder.activity as? ComponentActivity) + + @Composable + actual override fun RegisterLauncher() { + launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + logger.d("activity_result: $result, ${result.data}") + val activity = checkNotNull(activityHolder.activity as? ComponentActivity) + val result = if (result.resultCode == Activity.RESULT_OK) { + val authorizationResult = Identity + .getAuthorizationClient(activity) + .getAuthorizationResultFromIntent(result.data) + val uiResult = GoogleAuthResult( + accessToken = authorizationResult.accessToken, + serverAuthCode = authorizationResult.serverAuthCode + ) + Result.success(uiResult) + } else { + val msg = "auth fail with ${result.resultCode} code" + Result.failure(IllegalStateException(msg)) + } + callback.process(result) + } + } + + actual override fun auth( + block: (Result) -> Unit + ) { + callback { block(it) } + + val authRequest = AuthorizationRequest.Builder() + .setRequestedScopes(listOf(Scope(Scopes.EMAIL))) + .build() + + Identity.getAuthorizationClient(activity) + .authorize(authRequest) + .addOnSuccessListener { result -> + logger.d("on success: $result") + if (result.hasResolution()) { + try { + val pendingIntent = checkNotNull(result.pendingIntent) + val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build() + launcher.launch(intentSenderRequest) + } catch (e: SendIntentException) { + logger.e(e, "Couldn't start Authorization UI: " + e.localizedMessage) + } + } else { + val uiResult = GoogleAuthResult( + accessToken = result.accessToken, + serverAuthCode = result.serverAuthCode + ) + callback.process(Result.success(uiResult)) + } + } + .addOnFailureListener { + logger.e(it) + callback.process(Result.failure(it)) + } + } + + companion object { + + private val logger = Log.tag(TAG) + private const val TAG = "GOOGLE_AUTH_CONTROLLER" + } +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt new file mode 100644 index 0000000..696c0a1 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt @@ -0,0 +1,3 @@ +package com.stslex.atten.core.auth.callback + +internal interface GoogleAuthCallback : GoogleAuthReceiverCallback, GoogleAuthTransmitterCallback \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt new file mode 100644 index 0000000..a31bb14 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt @@ -0,0 +1,19 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult +import org.koin.core.annotation.Factory + +@Factory +internal class GoogleAuthCallbackImpl : GoogleAuthCallback { + + private var callback: (Result) -> Unit = {} + + override operator fun invoke(block: (Result) -> Unit) { + callback = block + } + + override fun process(result: Result) { + callback.invoke(result) + } +} + diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt new file mode 100644 index 0000000..1b2215f --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult + +fun interface GoogleAuthReceiverCallback { + + operator fun invoke(block: (Result) -> Unit) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt new file mode 100644 index 0000000..ebba9fe --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult + +internal interface GoogleAuthTransmitterCallback { + + fun process(result: Result) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt new file mode 100644 index 0000000..696bde4 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt @@ -0,0 +1,13 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.model.GoogleAuthResult + +interface GoogleAuthController { + + @Composable + fun RegisterLauncher() + + fun auth(block: (Result) -> Unit) +} + diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt new file mode 100644 index 0000000..3472510 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt @@ -0,0 +1,17 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal expect class GoogleAuthControllerImpl( + callback: GoogleAuthCallback, + activityHolder: ActivityHolder +) : GoogleAuthController { + + @Composable + override fun RegisterLauncher() + + override fun auth(block: (Result) -> Unit) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt new file mode 100644 index 0000000..527b7b8 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt @@ -0,0 +1,23 @@ +package com.stslex.atten.core.auth.di + +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.controller.GoogleAuthController +import com.stslex.atten.core.auth.controller.GoogleAuthControllerImpl +import com.stslex.atten.core.ui.kit.utils.ActivityHolder +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@ComponentScan("com.stslex.atten.core.auth") +class ModuleCoreAuth { + + @Single + internal fun googleAuthController( + callback: GoogleAuthCallback, + holder: ActivityHolder + ): GoogleAuthController = GoogleAuthControllerImpl( + callback = callback, + activityHolder = holder + ) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt new file mode 100644 index 0000000..4e50773 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt @@ -0,0 +1,9 @@ +package com.stslex.atten.core.auth.model + +import androidx.compose.runtime.Stable + +@Stable +data class GoogleAuthResult( + val serverAuthCode: String?, + val accessToken: String?, +) diff --git a/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt new file mode 100644 index 0000000..ca6eaa7 --- /dev/null +++ b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt @@ -0,0 +1,21 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal actual class GoogleAuthControllerImpl actual constructor( + callback: GoogleAuthCallback, + activityHolder: ActivityHolder +) : GoogleAuthController { + + @Composable + actual override fun RegisterLauncher() { + // do nothing - need for android + } + + actual override fun auth(block: (Result) -> Unit) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt b/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt index 2112539..bab9f23 100644 --- a/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt +++ b/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt @@ -11,11 +11,6 @@ import org.koin.core.scope.Scope @ComponentScan("com.stslex.atten.core.database") class ModuleCoreDatabase { -// val module = module { -// single { getDatabase() } -// single { get().getTodoDao() } -// } - @Single fun appDatabase(scope: Scope): AppDatabase = scope.getDatabase() diff --git a/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt b/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt new file mode 100644 index 0000000..3f2a125 --- /dev/null +++ b/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt @@ -0,0 +1,20 @@ +package com.stslex.atten.core.ui.kit.utils + +import java.lang.ref.WeakReference + +actual class ActivityHolderImpl actual constructor() : ActivityHolder, ActivityHolderProducer { + + private var _activity: WeakReference? = null + + actual override val activity: Any? + get() = _activity?.get() + + actual override fun produce(activity: Any?) { + if (activity == null) { + _activity?.clear() + _activity = null + } else { + _activity = WeakReference(activity) + } + } +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt deleted file mode 100644 index 3bd46b9..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -fun interface Router { - operator fun invoke(event: E) -} diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt deleted file mode 100644 index 3da4639..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.stslex.atten.core.core.coroutine.dispatcher.AppDispatcher -import com.stslex.atten.core.core.coroutine.scope.AppCoroutineScope -import com.stslex.atten.core.core.logger.Log -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Action -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Event -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Navigation -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.State -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -abstract class Store( - private val router: Router, - appDispatcher: AppDispatcher, - initialState: S -) : ViewModel(), StoreAbstraction { - - private val screenName = this::class.simpleName - - private val _event: MutableSharedFlow = MutableSharedFlow() - override val event: SharedFlow = _event.asSharedFlow() - - private val _state: MutableStateFlow = MutableStateFlow(initialState) - override val state: StateFlow = _state.asStateFlow() - - protected val scope: AppCoroutineScope = AppCoroutineScope( - scope = viewModelScope, - appDispatcher = appDispatcher - ) - - private var _lastAction: A? = null - protected val lastAction: A? - get() = _lastAction - - override fun dispatch(action: A) { - if (lastAction != action && action !is Action.RepeatLastAction) { - _lastAction = action - } - Log.d("$screenName dispatchAction: $action", TAG) - process(action) - } - - /** Process the action. This method should be overridden in the child class.*/ - protected abstract fun process(action: A) - - /** - * Updates the state of the screen. - * @param update - function that updates the state - * */ - protected fun updateState(update: (S) -> S) { - _state.update(update) - } - - /** - * Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher. - * @param event - event to be sent - * @see AppDispatcher - * */ - protected fun sendEvent(event: E) { - Log.d("$screenName sendEvent: $event", TAG) - scope.launch { - this@Store._event.emit(event) - } - } - - /** - * Navigates to the specified screen. The router is called with the specified event. - * @param event - event to be passed to the router - * @see Router - * */ - protected fun consumeNavigation(event: N) { - Log.d("$screenName consumeNavigation: $event", TAG) - router(event) - } - - /** - * Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher. - * @param onError - error handler - * @param onSuccess - success handler - * @param action - action to be executed - * @return Job - * @see Job - * @see AppDispatcher - * */ - protected fun launch( - onError: suspend (Throwable) -> Unit = {}, - onSuccess: suspend CoroutineScope.(T) -> Unit = {}, - action: suspend CoroutineScope.() -> T, - ): Job = scope.launch( - onError = onError, - onSuccess = onSuccess, - action = action - ) - - /** - * Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher. - * @param onError - error handler - * @param each - action for each element of the flow - * @return Job - * @see Flow - * @see Job - * @see AppDispatcher - * */ - protected fun Flow.launch( - onError: suspend (cause: Throwable) -> Unit = {}, - each: suspend (T) -> Unit - ): Job = scope.launch( - flow = this, - onError = onError, - each = each, - ) - - companion object { - private const val TAG = "Store" - } -} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt deleted file mode 100644 index ad547cd..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Action -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Event -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.State -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow - -interface StoreAbstraction { - - /** Flow of the state of the screen. */ - val state: StateFlow - - /** Flow of events that are sent to the screen. */ - val event: SharedFlow - - /** - * Sends an action to the store. Checks if the action is not the same as the last action. - * If the action is not the same as the last action, the last action is updated. - * The action is then processed. - * @param action - action to be sent - */ - fun dispatch(action: A) -} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt deleted file mode 100644 index 9ae6379..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.Stable -import com.stslex.atten.core.ui.kit.components.snackbar.SnackbarType - -interface StoreComponent { - - interface State : StoreComponent - - interface Navigation : StoreComponent - - interface Event : StoreComponent { - - @Stable - sealed class Snackbar( - open val message: String, - open val duration: SnackbarDuration, - open val withDismissAction: Boolean, - val action: String, - ) : Event { - - @Stable - data class Error( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.ERROR.label, - duration = duration, - withDismissAction = withDismissAction - ) - - @Stable - data class Success( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.SUCCESS.label, - duration = duration, - withDismissAction = withDismissAction - ) - - @Stable - data class Info( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.INFO.label, - duration = duration, - withDismissAction = withDismissAction - ) - } - } - - interface Action : StoreComponent { - interface RepeatLastAction : Action - } -} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt new file mode 100644 index 0000000..e0decfd --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +interface ActivityHolder { + + val activity: Any? +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt new file mode 100644 index 0000000..94610d0 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +expect class ActivityHolderImpl() : ActivityHolder, ActivityHolderProducer { + override val activity: Any? + override fun produce(activity: Any?) +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt new file mode 100644 index 0000000..e324346 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +interface ActivityHolderProducer { + + fun produce(activity: Any?) +} diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt new file mode 100644 index 0000000..a3841f6 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt @@ -0,0 +1,16 @@ +package com.stslex.atten.core.ui.kit.utils + +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +class ModuleCoreUiUtils { + + @Single + fun activityHolder(): ActivityHolder = ActivityHolderImpl() + + @Single + fun activityHolderProducer( + holder: ActivityHolder + ): ActivityHolderProducer = holder as ActivityHolderProducer +} \ No newline at end of file diff --git a/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt b/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt new file mode 100644 index 0000000..d3fdb13 --- /dev/null +++ b/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt @@ -0,0 +1,11 @@ +package com.stslex.atten.core.ui.kit.utils + +actual class ActivityHolderImpl actual constructor() : + ActivityHolder, + ActivityHolderProducer { + actual override val activity: Any? + get() = TODO("Not yet implemented") + + actual override fun produce(activity: Any?) { + } +} \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt index 88ca4cb..1975980 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt @@ -5,6 +5,9 @@ import com.stslex.atten.core.ui.mvi.processor.StoreProcessor import com.stslex.atten.core.ui.navigation.Component import org.koin.core.component.KoinScopeComponent import org.koin.core.module.Module +import org.koin.core.qualifier.qualifier +import org.koin.core.scope.Scope +import kotlin.reflect.KClass /** * Feature is a Koin feature module that provides a StoreProcessor. @@ -12,17 +15,26 @@ import org.koin.core.module.Module * * @see [StoreProcessor] * */ -interface Feature, TComponent : Component> : - KoinScopeComponent { +abstract class Feature, TComponent : Component>( + scopeClass: KClass<*> +) : KoinScopeComponent { - val loadModule: Boolean - get() = false + abstract val module: Module + + private val scopeName: String = requireNotNull(scopeClass.qualifiedName) { + "Scope name is null. Please check the SettingsFeature class." + } - val bindWithLifecycle: Boolean - get() = true + open val loadModule: Boolean + get() = false - val module: Module + final override val scope: Scope by lazy { + getKoin().getOrCreateScope( + scopeId = scopeName, + qualifier = qualifier(scopeName) + ) + } @Composable - fun processor(component: TComponent): TProcessor + abstract fun processor(component: TComponent): TProcessor } \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt deleted file mode 100644 index b9a3468..0000000 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.stslex.atten.core.ui.mvi - -fun interface Router { - - operator fun invoke(event: E) -} diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt index 20d03b6..2b15d74 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt @@ -1,4 +1,4 @@ -package com.stslex.wizard.core.ui.mvi.v2.processor +package com.stslex.atten.core.ui.mvi.processor import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt index 8b6cc20..345b607 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt @@ -11,7 +11,6 @@ import com.stslex.atten.core.ui.mvi.Store.Event import com.stslex.atten.core.ui.mvi.Store.State import com.stslex.atten.core.ui.navigation.Component import com.stslex.wizard.core.ui.mvi.v2.processor.ActionProcessor -import com.stslex.wizard.core.ui.mvi.v2.processor.EffectsProcessor import kotlinx.coroutines.CoroutineScope import org.koin.compose.viewmodel.koinViewModel import org.koin.core.component.KoinScopeComponent diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt index 16c7b9b..ec42983 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import com.stslex.atten.core.ui.mvi.Store import com.stslex.wizard.core.ui.mvi.v2.processor.ActionProcessor -import com.stslex.wizard.core.ui.mvi.v2.processor.EffectsProcessor import kotlinx.coroutines.CoroutineScope @Immutable diff --git a/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt b/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt index a8bd5e6..9cae8e5 100644 --- a/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt +++ b/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt @@ -20,4 +20,7 @@ sealed interface Config { data class Detail( val uuid: String ) : Config + + @Serializable + data object Settings : Config } \ No newline at end of file diff --git a/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt b/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt index 7094425..93615cf 100644 --- a/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt +++ b/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt @@ -9,8 +9,6 @@ import com.stslex.atten.feature.details.ui.mvi.DetailsStore.Action import com.stslex.atten.feature.details.ui.mvi.DetailsStore.Event import com.stslex.atten.feature.details.ui.mvi.DetailsStore.State import org.koin.core.module.Module -import org.koin.core.qualifier.qualifier -import org.koin.core.scope.Scope import org.koin.ksp.generated.module internal typealias DetailsStoreProcessor = StoreProcessor @@ -21,21 +19,12 @@ internal typealias DetailsStoreProcessor = StoreProcessor * * @see [com.stslex.atten.feature.details.ui.mvi.DetailsStore] * */ -internal object DetailsFeature : Feature { +internal object DetailsFeature : Feature( + scopeClass = DetailsScope::class +) { override val module: Module by lazy { ModuleFeatureDetails().module } - private val scopeName = requireNotNull(DetailsScope::class.qualifiedName) { - "Scope name is null. Please check the DetailsFeature class." - } - - override val scope: Scope by lazy { - getKoin().getOrCreateScope( - scopeId = scopeName, - qualifier = qualifier(scopeName) - ) - } - @Composable override fun processor( component: DetailsComponent diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt index 1c4768b..a42db6a 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt @@ -9,8 +9,6 @@ import com.stslex.atten.feature.home.ui.mvi.HomeStore.Action import com.stslex.atten.feature.home.ui.mvi.HomeStore.Event import com.stslex.atten.feature.home.ui.mvi.HomeStore.State import org.koin.core.module.Module -import org.koin.core.qualifier.qualifier -import org.koin.core.scope.Scope import org.koin.ksp.generated.module internal typealias HomeStoreProcessor = StoreProcessor @@ -21,21 +19,12 @@ internal typealias HomeStoreProcessor = StoreProcessor * * @see [com.stslex.atten.feature.home.ui.mvi.HomeStore] * */ -internal object HomeFeature : Feature { +internal object HomeFeature : Feature( + scopeClass = HomeScope::class +) { override val module: Module by lazy { ModuleFeatureHome().module } - private val scopeName = requireNotNull(HomeScope::class.qualifiedName) { - "Scope name is null. Please check the HomeFeature class." - } - - override val scope: Scope by lazy { - getKoin().getOrCreateScope( - scopeId = scopeName, - qualifier = qualifier(scopeName) - ) - } - @Composable override fun processor( component: HomeComponent diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt index b214bf2..8482fe4 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt @@ -56,6 +56,9 @@ fun HomeScreen(component: HomeComponent) { }, onItemLongCLick = { id -> processor.consume(Action.Click.OnSelectItemClicked(id)) + }, + onSettingsClick = { + processor.consume(Action.Click.OnSettingsClicked) } ) } diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt index f6c2418..60e8bcb 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt @@ -9,7 +9,6 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,13 +20,19 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.atten.core.paging.model.PagingConfig import com.stslex.atten.core.paging.states.PagingUiState @@ -43,7 +48,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import org.jetbrains.compose.ui.tooling.preview.Preview -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable internal fun HomeWidget( state: State, @@ -53,16 +58,86 @@ internal fun HomeWidget( onLoadNext: () -> Unit, onCreateItemClick: () -> Unit, onDeleteItemsClick: () -> Unit, + onSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box( + Scaffold( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .systemBarsPadding(), - ) { + topBar = { + TopAppBar( + title = { + Text( + text = "Home", + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton( + onClick = onSettingsClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) + }, + floatingActionButtonPosition = FabPosition.EndOverlay, + floatingActionButton = { + val buttonShapeRadius by animateDpAsState( + targetValue = if (state.selectedItems.isNotEmpty()) { + AppDimension.Radius.largest + } else { + AppDimension.Radius.medium + } + ) + CardWithAnimatedBorder( + modifier = Modifier + .padding(AppDimension.Padding.big) + .wrapContentSize() + .width(IntrinsicSize.Max) + .height(IntrinsicSize.Max), + onClick = { + if (state.selectedItems.isNotEmpty()) { + onDeleteItemsClick() + } else { + onCreateItemClick() + } + }, + isAnimated = state.selectedItems.isNotEmpty(), + cornerRadius = buttonShapeRadius, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + disableBorderColor = MaterialTheme.colorScheme.onSecondaryContainer + ) { + AnimatedContent( + modifier = Modifier.padding(AppDimension.Padding.big), + targetState = state.selectedItems.isNotEmpty(), + transitionSpec = { + fadeIn().plus(scaleIn()) togetherWith + fadeOut().plus(scaleOut()) + } + ) { isDeleting -> + Icon( + imageVector = if (isDeleting) { + Icons.Filled.Delete + } else { + Icons.Filled.Create + }, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + } + ) { paddingValues -> PagingColumn( modifier = Modifier + .padding(paddingValues) .fillMaxSize() .padding( horizontal = AppDimension.Padding.medium, @@ -86,53 +161,6 @@ internal fun HomeWidget( Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) } } - - val buttonShapeRadius by animateDpAsState( - targetValue = if (state.selectedItems.isNotEmpty()) { - AppDimension.Radius.largest - } else { - AppDimension.Radius.medium - } - ) - - CardWithAnimatedBorder( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(AppDimension.Padding.big) - .wrapContentSize() - .width(IntrinsicSize.Max) - .height(IntrinsicSize.Max), - onClick = { - if (state.selectedItems.isNotEmpty()) { - onDeleteItemsClick() - } else { - onCreateItemClick() - } - }, - isAnimated = state.selectedItems.isNotEmpty(), - cornerRadius = buttonShapeRadius, - backgroundColor = MaterialTheme.colorScheme.secondaryContainer, - disableBorderColor = MaterialTheme.colorScheme.onSecondaryContainer - ) { - AnimatedContent( - modifier = Modifier.padding(AppDimension.Padding.big), - targetState = state.selectedItems.isNotEmpty(), - transitionSpec = { - fadeIn().plus(scaleIn()) togetherWith - fadeOut().plus(scaleOut()) - } - ) { isDeleting -> - Icon( - imageVector = if (isDeleting) { - Icons.Filled.Delete - } else { - Icons.Filled.Create - }, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } } } @@ -163,7 +191,8 @@ internal fun HomeScreenPreview() { onItemClicked = {}, onCreateItemClick = {}, onItemLongCLick = {}, - onDeleteItemsClick = {} + onDeleteItemsClick = {}, + onSettingsClick = {} ) } } \ No newline at end of file diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt index fef8c81..3a891a4 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt @@ -102,6 +102,9 @@ interface HomeStore : Store { @Stable data object OnCreateItemClicked : Click + + @Stable + data object OnSettingsClicked : Click } @Stable @@ -110,6 +113,9 @@ interface HomeStore : Store { @Stable data class NavigateToDetail(val id: String) : Navigation + @Stable + data object Settings : Navigation + @Stable data object Back : Navigation } diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt index 8a6e016..3bf8974 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt @@ -1,12 +1,12 @@ package com.stslex.atten.feature.home.ui.mvi.handlers +import com.stslex.atten.core.ui.mvi.handler.Handler import com.stslex.atten.feature.home.di.HomeScope import com.stslex.atten.feature.home.domain.interactor.HomeScreenInteractor import com.stslex.atten.feature.home.domain.model.CreateTodoDomainModel import com.stslex.atten.feature.home.ui.mvi.HomeHandlerStore import com.stslex.atten.feature.home.ui.mvi.HomeStore.Action import com.stslex.atten.feature.home.ui.mvi.HomeStore.Event -import com.stslex.atten.core.ui.mvi.handler.Handler import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import org.koin.core.annotation.Factory @@ -27,9 +27,14 @@ internal class ClickHandler( is Action.Click.OnDeleteItemsClicked -> actionOnDeleteItemClicked() is Action.Click.OnItemClicked -> actionOnItemClicked(action) is Action.Click.OnSelectItemClicked -> actionOnSelectItemClicked(action) + is Action.Click.OnSettingsClicked -> actionSettingsClick() } } + private fun HomeHandlerStore.actionSettingsClick() { + consume(Action.Navigation.Settings) + } + private fun HomeHandlerStore.actionOnItemClicked( action: Action.Click.OnItemClicked ) { diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt index a361dc5..cb33d4f 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt @@ -15,6 +15,7 @@ internal class HomeComponentImpl( when (action) { is Action.Navigation.Back -> router.popBack() is Action.Navigation.NavigateToDetail -> router.navTo(Config.Detail(action.id)) + is Action.Navigation.Settings -> router.navTo(Config.Settings) } } } \ 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 0000000..513ce26 --- /dev/null +++ b/feature/settings/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.convention.kmp.feature) +} + +kotlin { + sourceSets.commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui:kit")) + implementation(project(":core:ui:mvi")) + implementation(project(":core:ui:navigation")) + implementation(project(":core:auth")) + } +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt new file mode 100644 index 0000000..ed6eb84 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.feature.settings.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.stslex.atten.feature.settings") +class ModuleFeatureSettings diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt new file mode 100644 index 0000000..6d3e6fc --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt @@ -0,0 +1,32 @@ +package com.stslex.atten.feature.settings.di + +import androidx.compose.runtime.Composable +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.core.ui.mvi.Feature +import com.stslex.atten.core.ui.mvi.processor.StoreProcessor +import com.stslex.atten.core.ui.mvi.processor.rememberStoreProcessor +import org.koin.core.module.Module +import org.koin.ksp.generated.module + +internal typealias SettingsStoreProcessor = StoreProcessor + +/** + * SettingsFeature is a Koin feature module that provides the SettingsStore processor. + * It is responsible for managing the state and actions related to the profile feature. + * + * @see [com.stslex.atten.feature.settings.mvi.SettingsStore] + * */ +internal object SettingsFeature : Feature( + scopeClass = SettingsScope::class +) { + + override val module: Module by lazy { ModuleFeatureSettings().module } + + @Composable + override fun processor( + component: SettingsComponent + ): SettingsStoreProcessor = rememberStoreProcessor(component) +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt new file mode 100644 index 0000000..2b27a72 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.feature.settings.di + +import org.koin.core.annotation.Scope + +@Scope(SettingsScope::class) +class SettingsScope \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt new file mode 100644 index 0000000..c3869a7 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt @@ -0,0 +1,15 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.handlers.SettingsComponentImpl +import com.stslex.atten.core.ui.mvi.handler.Handler +import com.stslex.atten.core.ui.navigation.Component +import com.stslex.atten.core.ui.navigation.Router + +interface SettingsComponent : Component, Handler { + + companion object { + + fun create(router: Router): SettingsComponent = SettingsComponentImpl(router) + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt new file mode 100644 index 0000000..8fc000c --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.core.ui.mvi.handler.HandlerStore + +interface SettingsHandlerStore : HandlerStore, SettingsStore \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt new file mode 100644 index 0000000..01403cd --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt @@ -0,0 +1,56 @@ +package com.stslex.atten.feature.settings.mvi + +import androidx.compose.runtime.Stable +import com.stslex.atten.core.ui.mvi.Store +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State + +interface SettingsStore : Store { + + @Stable + data class State( + val username: String = "", + ) : Store.State { + + companion object { + + val INIT = State() + } + } + + @Stable + sealed interface Action : Store.Action { + + @Stable + sealed interface Lifecycle : Action { + + @Stable + data object Init : Lifecycle + + @Stable + data object Dispose : Lifecycle + } + + @Stable + sealed interface Click : Action { + + @Stable + data object Back : Click + + @Stable + data object Login : Click + } + + @Stable + sealed interface Navigation : Action { + + @Stable + data object NavBack : Navigation + } + } + + @Stable + sealed interface Event : Store.Event +} + diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt new file mode 100644 index 0000000..5199969 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt @@ -0,0 +1,39 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.di.SettingsScope +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.feature.settings.mvi.handlers.ClickHandler +import com.stslex.atten.feature.settings.mvi.handlers.LifecycleHandler +import com.stslex.atten.core.core.coroutine.dispatcher.AppDispatcher +import com.stslex.atten.core.ui.mvi.BaseStore +import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.Qualifier +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@KoinViewModel +@Scope(SettingsScope::class) +@Scoped +@Qualifier(SettingsScope::class) +class SettingsStoreImpl( + @InjectedParam component: SettingsComponent, + appDispatcher: AppDispatcher, + clickHandler: ClickHandler, + lifecycleHandler: LifecycleHandler, +) : SettingsHandlerStore, BaseStore( + name = "SETTINGS", + initialState = State.INIT, + appDispatcher = appDispatcher, + handlerCreator = { action -> + when (action) { + is Action.Click -> clickHandler + is Action.Lifecycle -> lifecycleHandler + is Action.Navigation -> component + } + }, + disposeActions = listOf(Action.Lifecycle.Dispose), + initialActions = listOf(Action.Lifecycle.Init), +) \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt new file mode 100644 index 0000000..51f5b49 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt @@ -0,0 +1,37 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +import com.stslex.atten.core.auth.controller.GoogleAuthController +import com.stslex.atten.core.ui.mvi.handler.Handler +import com.stslex.atten.feature.settings.di.SettingsScope +import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@Factory +@Scope(SettingsScope::class) +@Scoped() +class ClickHandler( + private val authController: GoogleAuthController +) : Handler { + + override fun SettingsHandlerStore.invoke(action: Action.Click) { + when (action) { + Action.Click.Back -> actionBack() + Action.Click.Login -> actionLogin() + } + } + + private fun SettingsHandlerStore.actionBack() { + consume(Action.Navigation.NavBack) + } + + private fun SettingsHandlerStore.actionLogin() { + authController.auth { result -> + result + .onSuccess { logger.i("success: $it") } + .onFailure { logger.e(it, "auth error") } + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt new file mode 100644 index 0000000..de98454 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt @@ -0,0 +1,44 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +import com.stslex.atten.core.ui.mvi.handler.Handler +import com.stslex.atten.feature.settings.di.SettingsScope +import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import kotlinx.coroutines.delay +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@Factory +@Scope(SettingsScope::class) +@Scoped() +class LifecycleHandler : Handler { + + override fun SettingsHandlerStore.invoke(action: Action.Lifecycle) { + when (action) { + Action.Lifecycle.Init -> actionInit() + Action.Lifecycle.Dispose -> actionDispose() + } + } + + private fun SettingsHandlerStore.actionInit() { + // Handle initialization logic + updateState { currentState -> + currentState.copy( + username = "loading..." // Initial state before fetching username + ) + } + launch { + delay(10_000) + updateState { currentState -> + currentState.copy( + username = "User123" // Simulate fetching username + ) + } + } + } + + private fun SettingsHandlerStore.actionDispose() { + // Handle disposal logic + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt new file mode 100644 index 0000000..f3f4e5f --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt @@ -0,0 +1,18 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +import com.arkivanov.decompose.ComponentContext +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action.Navigation +import com.stslex.atten.core.ui.navigation.Router + +class SettingsComponentImpl( + private val router: Router +) : SettingsComponent, ComponentContext by router { + + override fun SettingsHandlerStore.invoke(action: Navigation) { + when (action) { + Navigation.NavBack -> router.popBack() + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt new file mode 100644 index 0000000..2de4ea8 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt @@ -0,0 +1,21 @@ +package com.stslex.atten.feature.settings.ui + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.controller.GoogleAuthController +import com.stslex.atten.core.ui.mvi.NavComponentScreen +import com.stslex.atten.feature.settings.di.SettingsFeature +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import org.koin.compose.getKoin + +@Composable +fun SettingsScreen( + component: SettingsComponent +) { + NavComponentScreen(SettingsFeature, component) { processor -> + getKoin().get().RegisterLauncher() + SettingsWidget( + state = processor.state.value, + consume = processor::consume, + ) + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt new file mode 100644 index 0000000..7c0aeb2 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt @@ -0,0 +1,53 @@ +package com.stslex.atten.feature.settings.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.feature.settings.ui.components.SettingsTopbar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsWidget( + state: State, + consume: (Action) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + SettingsTopbar( + title = state.username, + onBackClick = { consume(Action.Click.Back) }, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Text( + text = "Settings Screen", + style = MaterialTheme.typography.bodyLarge, + ) + + Button( + onClick = { consume(Action.Click.Login) }, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(text = "Login") + } + + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt new file mode 100644 index 0000000..7fbed48 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt @@ -0,0 +1,39 @@ +package com.stslex.atten.feature.settings.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsTopbar( + title: String, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5e91bdb..02d01a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,8 @@ decompose = "3.3.0" essenty = "2.5.0" parcelize = "0.2.4" +gms = "21.3.0" + [libraries] android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -86,6 +88,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" } +gms-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "gms" } + decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } decompose-extensions = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" } essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" } diff --git a/settings.gradle.kts b/settings.gradle.kts index db57c59..34ef1e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ pluginManagement { } } +@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositories { google() @@ -25,6 +26,8 @@ include(":core:ui:mvi") include(":core:database") include(":core:paging") include(":core:todo") +include(":core:auth") include(":feature:home") -include(":feature:details") \ No newline at end of file +include(":feature:details") +include(":feature:settings") \ No newline at end of file