diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b86273d9..6e52cd12 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 143d87b6..008b0094 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -34,11 +34,14 @@
+
+
+
@@ -60,6 +63,7 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b2c751a3..33be0745 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/README.md b/README.md
index 4af8c09f..c2698047 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,9 @@ To use different Navigation library need to change in ```build.gradle.kts``` at
```
buildConfigField(
- "com.velord.config.NavigationLib",
+ "com.velord.infrastructure.config.NavigationLib",
"NAVIGATION_LIB",
- "com.velord.config.NavigationLib.{Voyager}" or "Jetpack" or "Destinations" or "Compose" or "Nav3"
+ "com.velord.infrastructure.config.NavigationLib.{Voyager}" or "Jetpack" or "Destinations" or "Compose" or "Nav3"
)
```
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ddfed30e..f238ed3d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.convention.android.viewbinding)
id(libs.plugins.kotlin.plugin.parcelize.get().pluginId)
alias(libs.plugins.kotlin.plugin.serialization)
+ alias(libs.plugins.ksp)
alias(libs.plugins.convention.koin)
alias(libs.plugins.google.gms.services)
alias(libs.plugins.google.firebase.crashlytic)
@@ -63,9 +64,9 @@ android {
named("debug") {
buildConfigField("Boolean", "IS_LOGGING_ENABLED", "true")
buildConfigField(
- "com.velord.config.NavigationLib",
+ "com.velord.infrastructure.config.NavigationLib",
"NAVIGATION_LIB",
- "com.velord.config.NavigationLib.Nav3"
+ "com.velord.infrastructure.config.NavigationLib.Nav3"
)
}
named("release") {
@@ -77,9 +78,9 @@ android {
)
buildConfigField("Boolean", "IS_LOGGING_ENABLED", "false")
buildConfigField(
- "com.velord.config.NavigationLib",
+ "com.velord.infrastructure.config.NavigationLib",
"NAVIGATION_LIB",
- "com.velord.config.NavigationLib.Destinations"
+ "com.velord.infrastructure.config.NavigationLib.Destinations"
)
}
}
@@ -134,30 +135,36 @@ android {
}
dependencies {
- implementation(project(":model"))
+ implementation(projects.model)
// Module Infrastructure
- implementation(project(":infrastructure:util"))
- implementation(project(":infrastructure:navigation"))
- implementation(project(":infrastructure:di"))
- implementation(project(":infrastructure:config"))
+ implementation(projects.infrastructure.util)
+ implementation(projects.infrastructure.navigation)
+ implementation(projects.infrastructure.di)
+ implementation(projects.infrastructure.config)
+ // Module Domain
+ implementation(projects.domain.usecaseEvent)
// Module Core
- implementation(project(":core:core-ui"))
- implementation(project(":core:core-navigation"))
- implementation(project(":core:core-resource"))
+ implementation(projects.core.coreUi)
+ implementation(projects.core.coreNavigation)
+ implementation(projects.core.coreResource)
// Module Data
- implementation(project(":data:os"))
+ implementation(projects.data.os)
+ implementation(projects.data.appstate)
// Module UI
- implementation(project(":ui:sharedviewmodel"))
+ implementation(projects.ui.sharedviewmodel)
// Module UI Feature
- implementation(project(":ui:feature-bottomnavigation"))
- implementation(project(":ui:feature-splash"))
+ implementation(projects.ui.featureBottomnavigation)
+ implementation(projects.ui.featureSplash)
// Module UI Widget
- implementation(project(":ui:widget-refreshableimage"))
- implementation(project(":ui:widget-counter"))
- // Templates
+ implementation(projects.ui.widgetRefreshableimage)
+ implementation(projects.ui.widgetCounter)
+ // Template
implementation(libs.bundles.kotlin.module)
implementation(libs.bundles.androidx.module)
- implementation(libs.bundles.compose.ui)
+ implementation(libs.bundles.compose.ui.core)
+ // Koin annotations
+ implementation(libs.koin.annotation)
+ ksp(libs.koin.ksp)
// Tool
coreLibraryDesugaring(libs.android.desugar)
// Other
diff --git a/app/dependencies/productionReleaseRuntimeClasspath.txt b/app/dependencies/productionReleaseRuntimeClasspath.txt
index 7f6950b5..3419cc6b 100644
--- a/app/dependencies/productionReleaseRuntimeClasspath.txt
+++ b/app/dependencies/productionReleaseRuntimeClasspath.txt
@@ -97,7 +97,7 @@ androidx.credentials:credentials:1.5.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.1.0
-androidx.databinding:viewbinding:9.0.1
+androidx.databinding:viewbinding:9.1.1
androidx.datastore:datastore-android:1.2.0
androidx.datastore:datastore-core-android:1.2.0
androidx.datastore:datastore-core-okio-jvm:1.2.0
@@ -230,6 +230,10 @@ cafe.adriel.voyager:voyager-tab-navigator-android:1.1.0-beta03
cafe.adriel.voyager:voyager-tab-navigator:1.1.0-beta03
cafe.adriel.voyager:voyager-transitions-android:1.1.0-beta03
cafe.adriel.voyager:voyager-transitions:1.1.0-beta03
+co.touchlab:kermit-android:2.1.0
+co.touchlab:kermit-core-android:2.1.0
+co.touchlab:kermit-core:2.1.0
+co.touchlab:kermit:2.1.0
co.touchlab:stately-concurrency-jvm:2.1.0
co.touchlab:stately-concurrency:2.1.0
co.touchlab:stately-concurrent-collections-jvm:2.1.0
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2a505be1..cbc6ab69 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,7 +40,7 @@
@@ -54,7 +54,7 @@
@@ -65,7 +65,7 @@
@@ -80,4 +80,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/velord/composescreenexample/ui/main/MainViewModel.kt b/app/src/main/java/com/velord/composescreenexample/ui/main/MainViewModel.kt
deleted file mode 100644
index da7e4298..00000000
--- a/app/src/main/java/com/velord/composescreenexample/ui/main/MainViewModel.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.velord.composescreenexample.ui.main
-
-import android.content.Context
-import androidx.lifecycle.SavedStateHandle
-import com.velord.core.ui.compose.glance.GlanceWidgetThemeSustainer
-import com.velord.core.ui.compose.glance.updateAll
-import com.velord.model.setting.ThemeConfig
-import com.velord.refreshableimage.RefreshableImageWidget
-import com.velord.sharedviewmodel.CoroutineScopeViewModel
-import com.velord.widgetcounter.CounterWidget
-import kotlinx.coroutines.launch
-import org.koin.android.annotation.KoinViewModel
-
-@KoinViewModel
-class MainViewModel(
- private val context: Context,
- private val savedStateHandle: SavedStateHandle
-) : CoroutineScopeViewModel() {
-
- private val widgets = listOf>(
- RefreshableImageWidget(), CounterWidget()
- )
-
- fun updateTheme(themeConfig: ThemeConfig?) {
- if (themeConfig == null) return
-
- updateAllWidgets(themeConfig)
- updateDataStore()
- }
-
- private fun updateAllWidgets(themeConfig: ThemeConfig) = launch {
- widgets.updateAll(context, themeConfig)
- }
-
- private fun updateDataStore() {
- // TODO: save theme to data store
- }
-}
diff --git a/app/src/main/java/com/velord/composescreenexample/App.kt b/app/src/main/kotlin/com/velord/composescreenexample/App.kt
similarity index 80%
rename from app/src/main/java/com/velord/composescreenexample/App.kt
rename to app/src/main/kotlin/com/velord/composescreenexample/App.kt
index cf5fb017..08af2a98 100644
--- a/app/src/main/java/com/velord/composescreenexample/App.kt
+++ b/app/src/main/kotlin/com/velord/composescreenexample/App.kt
@@ -4,14 +4,14 @@ import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
-import android.util.Log
+import co.touchlab.kermit.Logger
import com.google.firebase.crashlytics.FirebaseCrashlytics
-import com.velord.config.BuildConfigResolver
-import com.velord.config.NavigationLib
-import com.velord.di.startKoin
-import com.velord.navigation.voyager.initVoyager
-import com.velord.os.MemoryLeakMonitor
-import com.velord.os.MemoryLogger
+import com.velord.data.os.memory.MemoryLeakMonitor
+import com.velord.data.os.memory.MemoryLogger
+import com.velord.infrastructure.config.BuildConfigResolver
+import com.velord.infrastructure.config.NavigationLib
+import com.velord.infrastructure.di.startKoin
+import com.velord.infrastructure.navigation.voyager.initVoyager
import org.koin.android.ext.android.inject
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@@ -34,36 +34,37 @@ internal class MemoryLoggerImpl : MemoryLogger {
override fun log(message: String, isError: Boolean) {
if (isError) {
- Log.e(TAG, message)
+ log.e { message }
crashlytics.log("E/$TAG: $message")
} else {
- Log.d(TAG, message)
+ log.d { message }
crashlytics.log("D/$TAG: $message")
}
}
override fun recordException(throwable: Throwable) {
- Log.e(TAG, "Recording non-fatal exception", throwable)
+ log.e(throwable) { "Recording non-fatal exception" }
crashlytics.recordException(throwable)
}
override fun setCustomKey(key: String, value: Int) {
- Log.d(TAG, "Key [$key] = $value")
+ log.d { "Key [$key] = $value" }
crashlytics.setCustomKey(key, value)
}
override fun setCustomKey(key: String, value: Long) {
- Log.d(TAG, "Key [$key] = $value")
+ log.d { "Key [$key] = $value" }
crashlytics.setCustomKey(key, value)
}
override fun setCustomKey(key: String, value: String) {
- Log.d(TAG, "Key [$key] = $value")
+ log.d { "Key [$key] = $value" }
crashlytics.setCustomKey(key, value)
}
companion object {
private const val TAG = "MemoryLogger"
+ private val log = Logger.withTag(TAG)
}
}
diff --git a/app/src/main/java/com/velord/composescreenexample/ui/main/MainActivity.kt b/app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainActivity.kt
similarity index 76%
rename from app/src/main/java/com/velord/composescreenexample/ui/main/MainActivity.kt
rename to app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainActivity.kt
index f9238e5e..9bd3741f 100644
--- a/app/src/main/java/com/velord/composescreenexample/ui/main/MainActivity.kt
+++ b/app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainActivity.kt
@@ -17,19 +17,20 @@ import androidx.navigation.fragment.NavHostFragment
import com.velord.composescreenexample.BuildConfig
import com.velord.composescreenexample.R
import com.velord.composescreenexample.databinding.ActivityMainBinding
-import com.velord.config.NavigationLib
import com.velord.core.ui.util.setContentWithTheme
-import com.velord.navigation.CreateNavigationViaDestinations
-import com.velord.navigation.CreateNavigationViaNav3
-import com.velord.navigation.CreateNavigationViaVanilla
-import com.velord.navigation.CreateNavigationViaVoyager
-import com.velord.sharedviewmodel.ThemeViewModel
-import com.velord.splash.SplashScreen
-import com.velord.splash.SplashViewModel
-import com.velord.splash.installSplash
+import com.velord.core.ui.util.setToastOverlayWithTheme
+import com.velord.infrastructure.config.NavigationLib
+import com.velord.infrastructure.navigation.CreateNavigationViaDestinations
+import com.velord.infrastructure.navigation.CreateNavigationViaNav3
+import com.velord.infrastructure.navigation.CreateNavigationViaVanilla
+import com.velord.infrastructure.navigation.CreateNavigationViaVoyager
+import com.velord.ui.feature.splash.SplashScreen
+import com.velord.ui.feature.splash.SplashViewModel
+import com.velord.ui.feature.splash.installSplash
+import com.velord.ui.sharedviewmodel.ThemeViewModel
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
-import com.velord.navigation.R as RNavigation
+import com.velord.infrastructure.navigation.R as RNavigation
class MainActivity : AppCompatActivity() {
@@ -38,7 +39,8 @@ class MainActivity : AppCompatActivity() {
private val fragmentContainer = R.id.navHostFragment
fun startIntent(context: Context, bundle: Bundle) = Intent(
- context, MainActivity::class.java
+ context,
+ MainActivity::class.java,
).apply {
putExtra(NAVIGATION_EXTRA, bundle)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
@@ -48,6 +50,10 @@ class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModel()
private val themeViewModel: ThemeViewModel by viewModel()
private val splashViewModel: SplashViewModel by viewModel()
+// Activity root
+// ├─ mainNavHost // Compose navigation(Voyager, Vanilla, Destinations, Nav3)
+// ├─ navHostFragment // Jetpack navigation
+// └─ toastOverlay // Global toast, always above both
private var binding: ActivityMainBinding? = null
override fun onDestroy() {
@@ -63,12 +69,22 @@ class MainActivity : AppCompatActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding?.root)
-
- handleIntent(savedInstanceState)
+ binding?.apply {
+ setContent()
+ handleIntent(savedInstanceState)
+ }
initObserving()
}
+ context(b: ActivityMainBinding)
+ private fun setContent() {
+ b.apply {
+ setContentView(root)
+ toastOverlay.setToastOverlayWithTheme(toastEventFlow = viewModel.toastConfigFlow)
+ }
+ }
+
+ context(b: ActivityMainBinding)
private fun handleIntent(savedInstanceState: Bundle?) {
/** The Intent provided by getIntent() (and its extras) always persist the same
* as it has been provided to activity first time(even process death has occurred).
@@ -97,6 +113,7 @@ class MainActivity : AppCompatActivity() {
}
}
+ context(b: ActivityMainBinding)
private fun setNavGraph() {
when (BuildConfig.NAVIGATION_LIB) {
NavigationLib.Voyager -> setNavGraphViaVoyager()
@@ -108,8 +125,9 @@ class MainActivity : AppCompatActivity() {
}
}
+ context(b: ActivityMainBinding)
private fun setNavGraphViaVoyager() {
- binding?.apply {
+ b.apply {
navHostFragment.isVisible = false
mainNavHost.apply {
isVisible = true
@@ -121,10 +139,16 @@ class MainActivity : AppCompatActivity() {
}
}
+ context(b: ActivityMainBinding)
private fun setNavGraphViaJetpack(
@IdRes destination: Int? = null,
bundle: Bundle? = bundleOf()
) {
+ b.apply {
+ mainNavHost.isVisible = false
+ navHostFragment.isVisible = true
+ }
+
val navHostFragment =
(supportFragmentManager.findFragmentById(fragmentContainer) as? NavHostFragment)
?: supportFragmentManager.fragments[0] as NavHostFragment
@@ -181,7 +205,8 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
themeViewModel.uiStateFlow.collect { theme ->
- viewModel.updateTheme(theme.androidThemeConfig?.config)
+ val action = MainUiAction.UpdateTheme(theme.appThemeConfig?.config)
+ viewModel.onAction(action)
}
}
}
diff --git a/app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainViewModel.kt b/app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainViewModel.kt
new file mode 100644
index 00000000..4d529136
--- /dev/null
+++ b/app/src/main/kotlin/com/velord/composescreenexample/ui/main/MainViewModel.kt
@@ -0,0 +1,79 @@
+package com.velord.composescreenexample.ui.main
+
+import android.content.Context
+import com.velord.core.ui.compose.glance.GlanceWidgetThemeSustainer
+import com.velord.core.ui.compose.glance.updateAll
+import com.velord.model.ToastConfig
+import com.velord.model.setting.ThemeConfig
+import com.velord.ui.sharedviewmodel.CoroutineScopeViewModel
+import com.velord.ui.widget.counter.CounterWidget
+import com.velord.ui.widget.refreshableimage.RefreshableImageWidget
+import com.velord.usecase.event.GetToastConfigFlowUC
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+import org.koin.android.annotation.KoinViewModel
+import org.koin.core.annotation.Provided
+
+sealed interface MainUiAction {
+ data class UpdateTheme(val themeConfig: ThemeConfig?) : MainUiAction
+}
+
+// Only one valid place for "Provided" annotation.
+// As this ViewModule can not be placed inside manual setup in viewModelModule DI.
+// Also, "app" module uses ksp generation.
+// Mixing them together derives telling compile time verification that GetToastConfigFlowUC
+// will be provided during runtime.
+@KoinViewModel
+class MainViewModel(
+ private val context: Context,
+ @Provided private val getToastConfigFlowUC: GetToastConfigFlowUC,
+) : CoroutineScopeViewModel() {
+
+ val toastConfigFlow = MutableSharedFlow()
+
+ private val widgets = listOf>(
+ RefreshableImageWidget(), CounterWidget()
+ )
+
+ private val actionFlow = MutableSharedFlow()
+
+ init {
+ observe()
+ }
+
+ fun onAction(action: MainUiAction) {
+ launch {
+ actionFlow.emit(action)
+ }
+ }
+
+ private fun observe() {
+ launch {
+ actionFlow.collect { action ->
+ when (action) {
+ is MainUiAction.UpdateTheme -> updateTheme(action.themeConfig)
+ }
+ }
+ }
+ launch {
+ getToastConfigFlowUC().collect { toastConfig ->
+ toastConfigFlow.emit(toastConfig)
+ }
+ }
+ }
+
+ private fun updateTheme(themeConfig: ThemeConfig?) {
+ if (themeConfig == null) return
+
+ updateAllWidgets(themeConfig)
+ updateDataStore()
+ }
+
+ private fun updateAllWidgets(themeConfig: ThemeConfig) = launch {
+ widgets.updateAll(context, themeConfig)
+ }
+
+ private fun updateDataStore() {
+ // TODO: save theme to data store
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 30139965..d413ba10 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -19,4 +19,12 @@
android:layout_height="match_parent"
app:defaultNavHost="true" />
+
+
\ No newline at end of file
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index fedee3d4..b4d7481f 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -3,8 +3,8 @@ plugins {
}
java {
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
+ sourceCompatibility = JavaVersion.VERSION_24
+ targetCompatibility = JavaVersion.VERSION_24
}
gradlePlugin {
@@ -37,14 +37,18 @@ gradlePlugin {
id = "velord.feature.ui.koin"
implementationClass = "com.velord.buildlogic.FeatureUiKoinConventionPlugin"
}
- register("domainUsecaseConvention") {
- id = "velord.domain.usecase"
- implementationClass = "com.velord.buildlogic.DomainUsecaseConventionPlugin"
- }
register("widgetGlanceConvention") {
id = "velord.widget.glance"
implementationClass = "com.velord.buildlogic.WidgetGlanceConventionPlugin"
}
+ register("kmpLibraryConvention") {
+ id = "velord.kmp.library"
+ implementationClass = "com.velord.buildlogic.KmpLibraryConventionPlugin"
+ }
+ register("domainUsecaseKmpConvention") {
+ id = "velord.domain.usecase.kmp"
+ implementationClass = "com.velord.buildlogic.DomainUsecaseKmpConventionPlugin"
+ }
}
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidApplicationConventionPlugin.kt
index b98f5a5b..1899d680 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidApplicationConventionPlugin.kt
@@ -6,8 +6,10 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidApplicationConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("com.android.application")
+ applyPlugin("android-application")
+ implementationBundle("logging-kmp")
extensions.configure {
configureApplicationAndroid(this)
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidComposeConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidComposeConventionPlugin.kt
index aadcf987..52fdb23d 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidComposeConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidComposeConventionPlugin.kt
@@ -8,16 +8,17 @@ import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
class AndroidComposeConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
+ applyPlugin("kotlin-plugin-compose")
- pluginManager.withPlugin("com.android.application") {
+ withPlugin("android-application") {
extensions.configure {
buildFeatures.compose = true
}
}
- pluginManager.withPlugin("com.android.library") {
+ withPlugin("android-library") {
extensions.configure {
buildFeatures.compose = true
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidConvention.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidConvention.kt
index 64dc9529..7b0c312a 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidConvention.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidConvention.kt
@@ -7,10 +7,10 @@ import org.gradle.api.Project
internal fun Project.configureApplicationAndroid(applicationExtension: ApplicationExtension) {
applicationExtension.apply {
- compileSdk = version("targetApi").toInt()
+ compileSdk = versionInt("targetApi")
defaultConfig {
- minSdk = version("minApi").toInt()
+ minSdk = versionInt("minApi")
}
compileOptions {
@@ -30,10 +30,10 @@ internal fun Project.configureApplicationAndroid(applicationExtension: Applicati
internal fun Project.configureLibraryAndroid(libraryExtension: LibraryExtension) {
libraryExtension.apply {
- compileSdk = version("targetApi").toInt()
+ compileSdk = versionInt("targetApi")
defaultConfig {
- minSdk = version("minApi").toInt()
+ minSdk = versionInt("minApi")
}
compileOptions {
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidLibraryConventionPlugin.kt
index 8006bd2d..4c9e3e6a 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidLibraryConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidLibraryConventionPlugin.kt
@@ -6,8 +6,10 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidLibraryConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("com.android.library")
+ applyPlugin("android-library")
+ implementationBundle("logging-kmp")
extensions.configure {
configureLibraryAndroid(this)
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidViewBindingConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidViewBindingConventionPlugin.kt
index 7159a91c..4a7b7a4a 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidViewBindingConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/AndroidViewBindingConventionPlugin.kt
@@ -7,14 +7,15 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidViewBindingConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.withPlugin("com.android.application") {
+ withPlugin("android-application") {
extensions.configure {
buildFeatures.viewBinding = true
}
}
- pluginManager.withPlugin("com.android.library") {
+ withPlugin("android-library") {
extensions.configure {
buildFeatures.viewBinding = true
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseConventionPlugin.kt
deleted file mode 100644
index 4038842b..00000000
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseConventionPlugin.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.velord.buildlogic
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-class DomainUsecaseConventionPlugin : Plugin {
- override fun apply(target: Project) = with(target) {
- pluginManager.apply("velord.android.library")
-
- addProjectDependency("implementation", ":model")
- addBundle("implementation", "kotlin-module")
- }
-}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseKmpConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseKmpConventionPlugin.kt
new file mode 100644
index 00000000..1f8cdf0f
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/DomainUsecaseKmpConventionPlugin.kt
@@ -0,0 +1,18 @@
+package com.velord.buildlogic
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+
+class DomainUsecaseKmpConventionPlugin : Plugin {
+
+ override fun apply(target: Project) = with(target) {
+ applyPlugin("convention-kmp-library")
+
+ extensions.configure {
+ commonMainImplementationProject(target, ProjectModule.MODEL)
+ commonMainImplementationLibrary(target, "kotlin-coroutine-core")
+ }
+ }
+}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiConventionPlugin.kt
index fbd9dc73..3dcd7a8d 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiConventionPlugin.kt
@@ -4,12 +4,13 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
class FeatureUiConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("velord.android.library")
- pluginManager.apply("velord.android.compose")
- pluginManager.apply("velord.android.viewbinding")
+ applyPlugin("convention-android-library")
+ applyPlugin("convention-android-compose")
+ applyPlugin("convention-android-viewbinding")
- addBundle("implementation", "kotlin-module")
- addBundle("implementation", "androidx-module")
+ implementationBundle("kotlin-module")
+ implementationBundle("androidx-module")
}
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiKoinConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiKoinConventionPlugin.kt
index 63ca4b8d..6bab4d16 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiKoinConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/FeatureUiKoinConventionPlugin.kt
@@ -4,8 +4,9 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
class FeatureUiKoinConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("velord.feature.ui")
- pluginManager.apply("velord.koin")
+ applyPlugin("convention-feature-ui")
+ applyPlugin("convention-koin")
}
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/KmpLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/KmpLibraryConventionPlugin.kt
new file mode 100644
index 00000000..17021d75
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/KmpLibraryConventionPlugin.kt
@@ -0,0 +1,34 @@
+package com.velord.buildlogic
+
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.kotlin.dsl.configure
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+
+class KmpLibraryConventionPlugin : Plugin {
+
+ override fun apply(target: Project) {
+ with(target) {
+ applyPlugin("kotlin-multiplatform")
+ applyPlugin("android-multiplatform-library")
+
+ val targetJvmVersion = versionInt("jvmTarget")
+
+ extensions.configure {
+ jvm("desktop")
+
+ jvmToolchain(targetJvmVersion)
+
+ val ext = (this as ExtensionAware).extensions
+ ext.configure(
+ "androidLibrary"
+ ) {
+ compileSdk = versionInt("targetApi")
+ minSdk = versionInt("minApi")
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/KoinConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/KoinConventionPlugin.kt
index b06e6cb7..1afc3297 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/KoinConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/KoinConventionPlugin.kt
@@ -4,24 +4,15 @@ import com.google.devtools.ksp.gradle.KspExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.dependencies
class KoinConventionPlugin : Plugin {
- override fun apply(target: Project) = with(target) {
- pluginManager.apply("com.google.devtools.ksp")
- val versionCatalog = libs
- val koinBom = versionCatalog.findLibrary("koin-bom").get().get()
- val koinBundle = versionCatalog.findBundle("koin").get().get()
- val koinKsp = versionCatalog.findLibrary("koin-ksp").get().get()
+ override fun apply(target: Project) = with(target) {
+ applyPlugin("ksp")
- dependencies {
- add("implementation", platform(koinBom))
- koinBundle.forEach { dependency ->
- add("implementation", dependency)
- }
- add("ksp", koinKsp)
- }
+ implementationPlatformLibrary("koin-bom")
+ implementationBundle("koin")
+ kspLibrary("koin-ksp")
extensions.configure {
arg("KOIN_CONFIG_CHECK", "true")
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectExtensions.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectExtensions.kt
index 68945ddc..ac40ec2c 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectExtensions.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectExtensions.kt
@@ -3,24 +3,85 @@ package com.velord.buildlogic
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.plugin.use.PluginDependency
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
internal val Project.libs: VersionCatalog
get() = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
-internal fun Project.version(name: String): String =
- libs.findVersion(name).get().requiredVersion
+internal fun Project.version(name: String): String = libs.findVersion(name).get().requiredVersion
-internal fun Project.addBundle(configurationName: String, bundleName: String) {
- val bundle = libs.findBundle(bundleName).get().get()
- bundle.forEach { dependency ->
- dependencies.add(configurationName, dependency)
+internal fun Project.versionInt(name: String): Int = version(name).toInt()
+
+internal fun Project.plugin(alias: String): PluginDependency = libs.findPlugin(alias).get().get()
+
+internal fun Project.pluginId(alias: String): String = plugin(alias).pluginId
+
+internal fun Project.applyPlugin(alias: String) {
+ pluginManager.apply(pluginId(alias))
+}
+
+internal fun Project.withPlugin(alias: String, action: () -> Unit) {
+ pluginManager.withPlugin(pluginId(alias)) {
+ action()
+ }
+}
+
+internal fun Project.library(libraryName: String) = libs.findLibrary(libraryName).get()
+
+private fun Project.bundle(bundleName: String) = libs.findBundle(bundleName).get().get()
+
+internal fun Project.implementationBundle(bundleName: String) {
+ bundle(bundleName).forEach { dependency ->
+ dependencies.add("implementation", dependency)
+ }
+}
+
+internal fun Project.implementationLibrary(libraryName: String) {
+ dependencies.add("implementation", library(libraryName))
+}
+
+internal fun Project.implementationPlatformLibrary(libraryName: String) {
+ dependencies.add("implementation", dependencies.platform(library(libraryName)))
+}
+
+internal fun Project.implementationProject(projectPath: String) {
+ dependencies.add("implementation", projectDependency(projectPath))
+}
+
+internal fun Project.kspLibrary(libraryName: String) {
+ dependencies.add("ksp", library(libraryName))
+}
+
+internal fun Project.implementationProject(project: ProjectModule) {
+ implementationProject(project.path)
+}
+
+internal fun Project.projectDependency(projectPath: String) = project(projectPath)
+
+internal fun Project.projectDependency(project: ProjectModule) = projectDependency(project.path)
+
+internal fun KotlinMultiplatformExtension.commonMainImplementationLibrary(
+ project: Project,
+ libraryName: String,
+) {
+ sourceSets.commonMain.dependencies {
+ implementation(project.library(libraryName))
}
}
-internal fun Project.addLibraryDependency(configurationName: String, libraryName: String) {
- dependencies.add(configurationName, libs.findLibrary(libraryName).get().get())
+internal fun KotlinMultiplatformExtension.commonMainImplementationProject(
+ project: Project,
+ projectPath: String,
+) {
+ sourceSets.commonMain.dependencies {
+ implementation(project.projectDependency(projectPath))
+ }
}
-internal fun Project.addProjectDependency(configurationName: String, projectPath: String) {
- dependencies.add(configurationName, project(projectPath))
+internal fun KotlinMultiplatformExtension.commonMainImplementationProject(
+ project: Project,
+ projectPath: ProjectModule,
+) {
+ commonMainImplementationProject(project, projectPath.path)
}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectModule.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectModule.kt
new file mode 100644
index 00000000..19c1aa07
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/ProjectModule.kt
@@ -0,0 +1,8 @@
+package com.velord.buildlogic
+
+internal enum class ProjectModule(val path: String) {
+ MODEL(":model"),
+ INFRASTRUCTURE_UTIL(":infrastructure:util"),
+ CORE_RESOURCE(":core:core-resource"),
+ CORE_UI(":core:core-ui"),
+}
diff --git a/build-logic/src/main/kotlin/com/velord/buildlogic/WidgetGlanceConventionPlugin.kt b/build-logic/src/main/kotlin/com/velord/buildlogic/WidgetGlanceConventionPlugin.kt
index e6272682..cf01f775 100644
--- a/build-logic/src/main/kotlin/com/velord/buildlogic/WidgetGlanceConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/com/velord/buildlogic/WidgetGlanceConventionPlugin.kt
@@ -4,17 +4,18 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
class WidgetGlanceConventionPlugin : Plugin {
+
override fun apply(target: Project) = with(target) {
- pluginManager.apply("velord.android.library")
- pluginManager.apply("velord.android.compose")
+ applyPlugin("convention-android-library")
+ applyPlugin("convention-android-compose")
- addProjectDependency("implementation", ":infrastructure:util")
- addProjectDependency("implementation", ":core:core-resource")
- addProjectDependency("implementation", ":core:core-ui")
+ implementationProject(ProjectModule.INFRASTRUCTURE_UTIL)
+ implementationProject(ProjectModule.CORE_RESOURCE)
+ implementationProject(ProjectModule.CORE_UI)
- addBundle("implementation", "kotlin-module")
- addBundle("implementation", "androidx-glance")
- addBundle("implementation", "androidx-workmanager")
- addBundle("implementation", "compose-ui")
+ implementationBundle("kotlin-module")
+ implementationBundle("androidx-glance")
+ implementationBundle("androidx-workmanager")
+ implementationBundle("compose-ui.core")
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 3e7d5737..70303ca3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,6 +14,8 @@ plugins {
alias(libs.plugins.google.firebase.crashlytic) apply false
}
+private val targetJvmVersion = libs.versions.jvmTarget.get()
+
extensions.configure {
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
baseline.set(rootProject.file("config/detekt/baseline.xml"))
@@ -37,7 +39,7 @@ subprojects {
}
tasks.withType(Detekt::class.java).configureEach {
- jvmTarget.set("21")
+ jvmTarget.set(targetJvmVersion)
exclude("**/build/**")
reports {
@@ -49,11 +51,10 @@ subprojects {
}
tasks.withType(DetektCreateBaselineTask::class.java).configureEach {
- jvmTarget.set("21")
+ jvmTarget.set(targetJvmVersion)
exclude("**/build/**")
enabled = false
}
-
}
pluginManager.withPlugin("com.android.application") {
@@ -63,6 +64,10 @@ subprojects {
pluginManager.withPlugin("com.android.library") {
configureDetekt()
}
+
+ pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
+ configureDetekt()
+ }
}
val detektProjectBaseline by tasks.registering(DetektCreateBaselineTask::class) {
@@ -73,7 +78,8 @@ val detektProjectBaseline by tasks.registering(DetektCreateBaselineTask::class)
multiPlatformEnabled.set(false)
noJdk.set(false)
parallel.set(true)
- jvmTarget.set("21")
+ // STRICT TOML COMPLIANCE
+ jvmTarget.set(targetJvmVersion)
setSource(files(subprojects.map { it.projectDir }))
config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
baseline.set(file("$rootDir/config/detekt/baseline.xml"))
@@ -86,4 +92,4 @@ val detektProjectBaseline by tasks.registering(DetektCreateBaselineTask::class)
// Fast way to clean project
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
-}
+}
\ No newline at end of file
diff --git a/core/core-navigation/build.gradle.kts b/core/core-navigation/build.gradle.kts
index f6708360..aad706be 100644
--- a/core/core-navigation/build.gradle.kts
+++ b/core/core-navigation/build.gradle.kts
@@ -1,19 +1,29 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.convention.android.compose)
- alias(libs.plugins.convention.android.viewbinding)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.jetbrains.compose)
+ alias(libs.plugins.kotlin.plugin.compose)
}
-android {
- namespace = "com.velord.core.navigation"
-}
+kotlin {
+ android {
+ namespace = "com.velord.core.navigation"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.kotlin.serialization.json)
+ implementation(libs.compose.runtime)
+ implementation(libs.compose.ui)
+ implementation(libs.compose.resources)
+ implementation(libs.voyager.navigator)
+ }
-dependencies {
- // Template
- implementation(libs.bundles.kotlin.module)
- implementation(libs.bundles.androidx.lifecycle.runtime)
- implementation(libs.bundles.compose.all)
- // Lib
- implementation(libs.bundles.voyager)
- implementation(libs.androidx.navigation.fragment)
+ androidMain.dependencies {
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.fragment.ktx)
+ implementation(libs.androidx.navigation.fragment)
+ }
+ }
}
diff --git a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/util/Fragment.kt b/core/core-navigation/src/androidMain/kotlin/com/velord/core/navigation/fragment/util/Fragment.kt
similarity index 75%
rename from core/core-navigation/src/main/java/com/velord/core/navigation/fragment/util/Fragment.kt
rename to core/core-navigation/src/androidMain/kotlin/com/velord/core/navigation/fragment/util/Fragment.kt
index 52972b0b..66e6196e 100644
--- a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/util/Fragment.kt
+++ b/core/core-navigation/src/androidMain/kotlin/com/velord/core/navigation/fragment/util/Fragment.kt
@@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
+import com.velord.core.navigation.fragment.NAVIGATION_PAYLOAD
import com.velord.core.navigation.fragment.NavigationDataFragment
import kotlin.reflect.KProperty
@@ -16,8 +17,8 @@ interface NavigationBundleDelegate {
operator fun provideDelegate(thisRef: R, prop: KProperty<*>): Lazy
}
-inline fun createNavigationBundleDelegate(
- crossinline getBundle: R.() -> Bundle
+inline fun createNavigationBundleDelegate(
+ crossinline getBundle: R.() -> Bundle,
): NavigationBundleDelegate = object : NavigationBundleDelegate {
override fun provideDelegate(thisRef: R, prop: KProperty<*>): Lazy = lazy {
val bundle = thisRef.getBundle()
@@ -26,23 +27,20 @@ inline fun createNavigationBundleDelegate(
}
}
-inline fun R.getNavigationData(): NavigationBundleDelegate =
+inline fun R.getNavigationData(): NavigationBundleDelegate =
createNavigationBundleDelegate { requireArguments() }
-inline fun R.getNavigationData(): NavigationBundleDelegate =
+inline fun R.getNavigationData(): NavigationBundleDelegate =
createNavigationBundleDelegate {
intent.extras ?: throw IllegalStateException("Activity $this does not have any arguments.")
}
-
-
fun Fragment.activityNavController() =
activity?.supportFragmentManager?.fragments?.firstOrNull()?.findNavController()
fun NavController.navigate(nav: NavigationDataFragment) {
- val bundle: Bundle? = if (nav.bundle != null) {
- val name = nav.bundle::class.simpleName ?: DEFAULT_BUNDLE_NAME
- bundleOf(name to nav.bundle)
+ val bundle: Bundle? = if (nav.payload != null) {
+ bundleOf(NAVIGATION_PAYLOAD to nav.payload)
} else {
null
}
diff --git a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/NavigationDataFragment.kt b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/NavigationDataFragment.kt
similarity index 54%
rename from core/core-navigation/src/main/java/com/velord/core/navigation/fragment/NavigationDataFragment.kt
rename to core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/NavigationDataFragment.kt
index 15df7178..1ed363b1 100644
--- a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/NavigationDataFragment.kt
+++ b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/NavigationDataFragment.kt
@@ -1,8 +1,8 @@
package com.velord.core.navigation.fragment
-import android.os.Bundle
+const val NAVIGATION_PAYLOAD = "navigationPayload"
data class NavigationDataFragment(
val id: Int,
- val bundle: Bundle? = null // Was Parcelable
+ val payload: String? = null,
)
diff --git a/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt
new file mode 100644
index 00000000..83242adb
--- /dev/null
+++ b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt
@@ -0,0 +1,17 @@
+package com.velord.core.navigation.fragment.entryPoint
+
+import kotlinx.serialization.json.Json
+
+enum class SettingsSourceFragment {
+ SettingsGraph,
+ CameraRecording,
+ ;
+
+ fun encode(): String = Json.encodeToString(this)
+
+ companion object {
+ const val ARGUMENT = "source"
+
+ fun decode(payload: String): SettingsSourceFragment = Json.decodeFromString(payload)
+ }
+}
diff --git a/core/core-navigation/src/main/java/com/velord/core/navigation/voyager/NavigationDataVoyager.kt b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/NavigationDataVoyager.kt
similarity index 83%
rename from core/core-navigation/src/main/java/com/velord/core/navigation/voyager/NavigationDataVoyager.kt
rename to core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/NavigationDataVoyager.kt
index b1e544f1..1f665a3f 100644
--- a/core/core-navigation/src/main/java/com/velord/core/navigation/voyager/NavigationDataVoyager.kt
+++ b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/NavigationDataVoyager.kt
@@ -1,7 +1,7 @@
package com.velord.core.navigation.voyager
import androidx.compose.runtime.Composable
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.compose.runtime.collectAsState
import cafe.adriel.voyager.core.registry.rememberScreen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
@@ -9,13 +9,13 @@ import kotlinx.coroutines.flow.Flow
data class NavigationDataVoyager(
val screen: SharedScreenVoyager,
- val useRoot: Boolean = false
+ val useRoot: Boolean = false,
)
@Composable
fun ObserveNavigation(eventFlow: Flow) {
val navigator = LocalNavigator.current
- val navigationDataState = eventFlow.collectAsStateWithLifecycle(initialValue = null)
+ val navigationDataState = eventFlow.collectAsState(initial = null)
navigationDataState.value?.let {
val screen = rememberScreen(it.screen)
if (it.useRoot) {
diff --git a/core/core-navigation/src/main/java/com/velord/core/navigation/voyager/SharedScreenVoyager.kt b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
similarity index 90%
rename from core/core-navigation/src/main/java/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
rename to core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
index eca1b4d3..bf58747f 100644
--- a/core/core-navigation/src/main/java/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
+++ b/core/core-navigation/src/commonMain/kotlin/com/velord/core/navigation/voyager/SharedScreenVoyager.kt
@@ -1,11 +1,10 @@
package com.velord.core.navigation.voyager
-import androidx.annotation.StringRes
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.registry.ScreenProvider
+import org.jetbrains.compose.resources.StringResource
sealed class SharedScreenVoyager : ScreenProvider {
-
sealed class BottomNavigationTab : SharedScreenVoyager() {
data object Camera : BottomNavigationTab()
data object Demo : BottomNavigationTab()
@@ -13,7 +12,7 @@ sealed class SharedScreenVoyager : ScreenProvider {
}
data class Test(
- @StringRes val title: Int,
+ val title: StringResource,
val modifier: Modifier = Modifier,
val onClick: () -> Unit = {},
) : SharedScreenVoyager()
diff --git a/core/core-navigation/src/main/AndroidManifest.xml b/core/core-navigation/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/core/core-navigation/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt b/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt
deleted file mode 100644
index 583814ba..00000000
--- a/core/core-navigation/src/main/java/com/velord/core/navigation/fragment/entryPoint/SettingsSourceFragment.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.velord.core.navigation.fragment.entryPoint
-
-enum class SettingsSourceFragment {
- SettingsGraph, CameraRecording
-}
-
-const val SETTINGS_SOURCE = "source"
diff --git a/core/core-resource/build.gradle.kts b/core/core-resource/build.gradle.kts
index 07fa284c..6ad9fe0e 100644
--- a/core/core-resource/build.gradle.kts
+++ b/core/core-resource/build.gradle.kts
@@ -1,14 +1,28 @@
plugins {
- alias(libs.plugins.convention.android.library)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.jetbrains.compose)
+ alias(libs.plugins.kotlin.plugin.compose)
+ alias(libs.plugins.kotlin.plugin.serialization)
}
-android {
- namespace = "com.velord.core.resource"
+compose.resources {
+ publicResClass = true
+ packageOfResClass = "com.velord.core.resource"
}
-dependencies {
- // Templates
- implementation(libs.bundles.kotlin.all)
- implementation(libs.bundles.androidx.module)
- implementation(libs.androidx.core.splashscreen)
+kotlin {
+ android {
+ namespace = "com.velord.core.resource"
+ androidResources {
+ enable = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ api(libs.compose.resources)
+ implementation(compose.runtime)
+ }
+
+ }
}
diff --git a/core/core-resource/src/main/res/anim/do_not_slide.xml b/core/core-resource/src/androidMain/res/anim/do_not_slide.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/do_not_slide.xml
rename to core/core-resource/src/androidMain/res/anim/do_not_slide.xml
diff --git a/core/core-resource/src/main/res/anim/scale_enter.xml b/core/core-resource/src/androidMain/res/anim/scale_enter.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/scale_enter.xml
rename to core/core-resource/src/androidMain/res/anim/scale_enter.xml
diff --git a/core/core-resource/src/main/res/anim/scale_exit.xml b/core/core-resource/src/androidMain/res/anim/scale_exit.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/scale_exit.xml
rename to core/core-resource/src/androidMain/res/anim/scale_exit.xml
diff --git a/core/core-resource/src/main/res/anim/slide_fade_enter.xml b/core/core-resource/src/androidMain/res/anim/slide_fade_enter.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_fade_enter.xml
rename to core/core-resource/src/androidMain/res/anim/slide_fade_enter.xml
diff --git a/core/core-resource/src/main/res/anim/slide_in_down.xml b/core/core-resource/src/androidMain/res/anim/slide_in_down.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_in_down.xml
rename to core/core-resource/src/androidMain/res/anim/slide_in_down.xml
diff --git a/core/core-resource/src/main/res/anim/slide_in_left.xml b/core/core-resource/src/androidMain/res/anim/slide_in_left.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_in_left.xml
rename to core/core-resource/src/androidMain/res/anim/slide_in_left.xml
diff --git a/core/core-resource/src/main/res/anim/slide_in_right.xml b/core/core-resource/src/androidMain/res/anim/slide_in_right.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_in_right.xml
rename to core/core-resource/src/androidMain/res/anim/slide_in_right.xml
diff --git a/core/core-resource/src/main/res/anim/slide_in_up.xml b/core/core-resource/src/androidMain/res/anim/slide_in_up.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_in_up.xml
rename to core/core-resource/src/androidMain/res/anim/slide_in_up.xml
diff --git a/core/core-resource/src/main/res/anim/slide_out_left.xml b/core/core-resource/src/androidMain/res/anim/slide_out_left.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_out_left.xml
rename to core/core-resource/src/androidMain/res/anim/slide_out_left.xml
diff --git a/core/core-resource/src/main/res/anim/slide_out_right.xml b/core/core-resource/src/androidMain/res/anim/slide_out_right.xml
similarity index 100%
rename from core/core-resource/src/main/res/anim/slide_out_right.xml
rename to core/core-resource/src/androidMain/res/anim/slide_out_right.xml
diff --git a/core/core-resource/src/main/res/animator/hide.xml b/core/core-resource/src/androidMain/res/animator/hide.xml
similarity index 100%
rename from core/core-resource/src/main/res/animator/hide.xml
rename to core/core-resource/src/androidMain/res/animator/hide.xml
diff --git a/core/core-resource/src/main/res/animator/show.xml b/core/core-resource/src/androidMain/res/animator/show.xml
similarity index 100%
rename from core/core-resource/src/main/res/animator/show.xml
rename to core/core-resource/src/androidMain/res/animator/show.xml
diff --git a/core/core-resource/src/main/res/drawable/counter_widget_preview.png b/core/core-resource/src/androidMain/res/drawable/counter_widget_preview.png
similarity index 100%
rename from core/core-resource/src/main/res/drawable/counter_widget_preview.png
rename to core/core-resource/src/androidMain/res/drawable/counter_widget_preview.png
diff --git a/core/core-resource/src/main/res/drawable/ic_brand.xml b/core/core-resource/src/androidMain/res/drawable/ic_brand.xml
similarity index 100%
rename from core/core-resource/src/main/res/drawable/ic_brand.xml
rename to core/core-resource/src/androidMain/res/drawable/ic_brand.xml
diff --git a/core/core-resource/src/main/res/drawable/ic_launcher_background.xml b/core/core-resource/src/androidMain/res/drawable/ic_launcher_background.xml
similarity index 100%
rename from core/core-resource/src/main/res/drawable/ic_launcher_background.xml
rename to core/core-resource/src/androidMain/res/drawable/ic_launcher_background.xml
diff --git a/core/core-resource/src/main/res/drawable/ic_launcher_foreground.xml b/core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground.xml
similarity index 100%
rename from core/core-resource/src/main/res/drawable/ic_launcher_foreground.xml
rename to core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground.xml
diff --git a/core/core-resource/src/main/res/drawable/ic_launcher_foreground_dark.xml b/core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground_dark.xml
similarity index 100%
rename from core/core-resource/src/main/res/drawable/ic_launcher_foreground_dark.xml
rename to core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground_dark.xml
diff --git a/core/core-resource/src/main/res/drawable/ic_launcher_foreground_light.xml b/core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground_light.xml
similarity index 100%
rename from core/core-resource/src/main/res/drawable/ic_launcher_foreground_light.xml
rename to core/core-resource/src/androidMain/res/drawable/ic_launcher_foreground_light.xml
diff --git a/core/core-resource/src/main/res/drawable/refreshable_image_widget_preview.png b/core/core-resource/src/androidMain/res/drawable/refreshable_image_widget_preview.png
similarity index 100%
rename from core/core-resource/src/main/res/drawable/refreshable_image_widget_preview.png
rename to core/core-resource/src/androidMain/res/drawable/refreshable_image_widget_preview.png
diff --git a/core/core-resource/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/core-resource/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to core/core-resource/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/core/core-resource/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/core/core-resource/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to core/core-resource/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/core/core-resource/src/main/res/mipmap-hdpi/ic_launcher.webp b/core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-hdpi/ic_launcher.webp
rename to core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
diff --git a/core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_background.webp
rename to core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp
diff --git a/core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
rename to core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
diff --git a/core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-hdpi/ic_launcher_round.webp
rename to core/core-resource/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
diff --git a/core/core-resource/src/main/res/mipmap-mdpi/ic_launcher.webp b/core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-mdpi/ic_launcher.webp
rename to core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
diff --git a/core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_background.webp
rename to core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp
diff --git a/core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
rename to core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
diff --git a/core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-mdpi/ic_launcher_round.webp
rename to core/core-resource/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
diff --git a/core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher.webp b/core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher.webp
rename to core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
diff --git a/core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
rename to core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp
diff --git a/core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
rename to core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
diff --git a/core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
rename to core/core-resource/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
diff --git a/core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
similarity index 100%
rename from core/core-resource/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
rename to core/core-resource/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
diff --git a/core/core-resource/src/main/res/transition/slide_up.xml b/core/core-resource/src/androidMain/res/transition/slide_up.xml
similarity index 100%
rename from core/core-resource/src/main/res/transition/slide_up.xml
rename to core/core-resource/src/androidMain/res/transition/slide_up.xml
diff --git a/core/core-resource/src/main/res/values-night/colors.xml b/core/core-resource/src/androidMain/res/values-night/colors.xml
similarity index 100%
rename from core/core-resource/src/main/res/values-night/colors.xml
rename to core/core-resource/src/androidMain/res/values-night/colors.xml
diff --git a/core/core-resource/src/main/res/values-v31/themes.xml b/core/core-resource/src/androidMain/res/values-v31/themes.xml
similarity index 100%
rename from core/core-resource/src/main/res/values-v31/themes.xml
rename to core/core-resource/src/androidMain/res/values-v31/themes.xml
diff --git a/core/core-resource/src/main/res/values-v33/themes.xml b/core/core-resource/src/androidMain/res/values-v33/themes.xml
similarity index 100%
rename from core/core-resource/src/main/res/values-v33/themes.xml
rename to core/core-resource/src/androidMain/res/values-v33/themes.xml
diff --git a/core/core-resource/src/main/res/values/colors.xml b/core/core-resource/src/androidMain/res/values/colors.xml
similarity index 100%
rename from core/core-resource/src/main/res/values/colors.xml
rename to core/core-resource/src/androidMain/res/values/colors.xml
diff --git a/core/core-resource/src/main/res/values/dimens.xml b/core/core-resource/src/androidMain/res/values/dimens.xml
similarity index 100%
rename from core/core-resource/src/main/res/values/dimens.xml
rename to core/core-resource/src/androidMain/res/values/dimens.xml
diff --git a/core/core-resource/src/main/res/values/ids.xml b/core/core-resource/src/androidMain/res/values/ids.xml
similarity index 100%
rename from core/core-resource/src/main/res/values/ids.xml
rename to core/core-resource/src/androidMain/res/values/ids.xml
diff --git a/core/core-resource/src/androidMain/res/values/strings.xml b/core/core-resource/src/androidMain/res/values/strings.xml
new file mode 100644
index 00000000..7c2d9242
--- /dev/null
+++ b/core/core-resource/src/androidMain/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+ ComposeScreenExample
+ Loading
+ Error
+ Counter Widget
+ Image Widget
+
diff --git a/core/core-resource/src/main/res/values/themes.xml b/core/core-resource/src/androidMain/res/values/themes.xml
similarity index 98%
rename from core/core-resource/src/main/res/values/themes.xml
rename to core/core-resource/src/androidMain/res/values/themes.xml
index 6eb3eb90..38e265a6 100644
--- a/core/core-resource/src/main/res/values/themes.xml
+++ b/core/core-resource/src/androidMain/res/values/themes.xml
@@ -17,4 +17,4 @@
This is required. -->
- @style/Theme.ComposeScreenExample
-
\ No newline at end of file
+
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher.webp b/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..182a73dd
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher_round.webp b/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..92fb6c55
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-hdpi/ic_launcher_round.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher.webp b/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..50ed3395
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher_round.webp b/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..03c45851
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-mdpi/ic_launcher_round.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher.webp b/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..d2ccfdc5
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher_round.webp b/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..fc81e920
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xhdpi/ic_launcher_round.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher.webp b/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..ba4c51fc
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher_round.webp b/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b7cc92d1
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xxhdpi/ic_launcher_round.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher.webp b/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..1033ce8a
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher_round.webp b/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..dfa34fd8
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable-xxxhdpi/ic_launcher_round.webp differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/counter_widget_preview.png b/core/core-resource/src/commonMain/composeResources/drawable/counter_widget_preview.png
new file mode 100644
index 00000000..8d9227ff
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable/counter_widget_preview.png differ
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/ic_brand.xml b/core/core-resource/src/commonMain/composeResources/drawable/ic_brand.xml
new file mode 100644
index 00000000..7d4a656e
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/drawable/ic_brand.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_background.xml b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..fdc1a45b
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_background.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground.xml b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..c2e2d5b1
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_dark.xml b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_dark.xml
new file mode 100644
index 00000000..9a106458
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_dark.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_light.xml b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_light.xml
new file mode 100644
index 00000000..c2e2d5b1
--- /dev/null
+++ b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_foreground_light.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/core-resource/src/main/ic_launcher-playstore.png b/core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_playstore.png
similarity index 100%
rename from core/core-resource/src/main/ic_launcher-playstore.png
rename to core/core-resource/src/commonMain/composeResources/drawable/ic_launcher_playstore.png
diff --git a/core/core-resource/src/commonMain/composeResources/drawable/refreshable_image_widget_preview.png b/core/core-resource/src/commonMain/composeResources/drawable/refreshable_image_widget_preview.png
new file mode 100644
index 00000000..7f2371b8
Binary files /dev/null and b/core/core-resource/src/commonMain/composeResources/drawable/refreshable_image_widget_preview.png differ
diff --git a/core/core-resource/src/main/res/values/strings.xml b/core/core-resource/src/commonMain/composeResources/values/strings.xml
similarity index 95%
rename from core/core-resource/src/main/res/values/strings.xml
rename to core/core-resource/src/commonMain/composeResources/values/strings.xml
index 26d12377..e7cacb71 100644
--- a/core/core-resource/src/main/res/values/strings.xml
+++ b/core/core-resource/src/commonMain/composeResources/values/strings.xml
@@ -50,14 +50,14 @@
Disable Os theme switcher first
Error
Loading
- Count: %d
+ Count: %1$d
Counter Widget
+
-
Refresh Image
- Widget Size:\nW: %1f x H: %2f
+ Widget Size:\nW: %1$s x H: %2$s
Image Widget
- Downloaded from:\n%s
+ Downloaded from:\n%1$s
Rear
Front
Waiting
@@ -106,7 +106,7 @@
\nSqlite / Room is required
\nOne of MVVM / MVI / Redux architecture is required;
\nClean Architecture is required;
-\nDependency injection is required;
+\nDependency injection is required
\nJetpack Compose is required
\nUnit-tests are required;
\n\nNotes
@@ -116,10 +116,10 @@
\nYou are free to implement your own design
\nMake sure that anyone can build your project.
- Range: %1s
+ Range: %1$s
https://github.com/AppSci/android-test-task?tab=readme-ov-file
Open Dialog Demo
- This demo is Deprecated on this navigation lib: %s
+ This demo is Deprecated on this navigation lib: %1$s
Cancel
Confirm
Ok
diff --git a/core/core-resource/src/main/AndroidManifest.xml b/core/core-resource/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/core/core-resource/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/core/core-ui/build.gradle.kts b/core/core-ui/build.gradle.kts
index 2a144f81..0ff2c0b7 100644
--- a/core/core-ui/build.gradle.kts
+++ b/core/core-ui/build.gradle.kts
@@ -1,27 +1,59 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.convention.android.compose)
- alias(libs.plugins.convention.android.viewbinding)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.jetbrains.compose)
+ alias(libs.plugins.kotlin.plugin.compose)
}
-android {
- namespace = "com.velord.core.ui"
-}
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-Xcontext-parameters")
+ }
-dependencies {
- // Module
- implementation(project(":model"))
- implementation(project(":infrastructure:util"))
- implementation(project(":core:core-resource"))
- implementation(project(":ui:sharedviewmodel"))
- // Template
- implementation(libs.bundles.kotlin.core)
- implementation(libs.bundles.androidx.module)
- implementation(libs.bundles.coil)
- implementation(libs.bundles.ui)
- // Lib
- implementation(libs.androidx.navigation.ui) // Material Dialog lib
- // Compose
- implementation(libs.androidx.glance)
- implementation(libs.androidx.glance.appwidget)
-}
+ android {
+ namespace = "com.velord.core.ui"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ // Module
+ implementation(projects.model)
+ implementation(projects.infrastructure.util)
+ implementation(projects.core.coreResource)
+ implementation(projects.ui.sharedviewmodel)
+ // Template
+ implementation(libs.bundles.compose.ui.core)
+ implementation(libs.bundles.compose.foundation)
+ implementation(libs.bundles.compose.core)
+ implementation(libs.bundles.compose.material3.all)
+ implementation(libs.androidx.graphics.shapes)
+ // Lib
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.coil.compose)
+ }
+
+ desktopMain.dependencies {
+ // Template
+ implementation(libs.bundles.kotlin.core)
+ }
+
+ androidMain.dependencies {
+ // Module
+ implementation(projects.model)
+ implementation(projects.infrastructure.util)
+ implementation(projects.core.coreResource)
+ implementation(projects.ui.sharedviewmodel)
+ // Template
+ implementation(libs.bundles.kotlin.core)
+ implementation(libs.bundles.androidx.module)
+ implementation(libs.bundles.coil)
+ implementation(libs.bundles.ui)
+ // Lib
+ implementation(libs.kermit)
+ implementation(libs.androidx.navigation.ui) // Material Dialog lib
+ implementation(libs.androidx.core.animation)
+ // Glance
+ implementation(libs.androidx.glance)
+ implementation(libs.androidx.glance.appwidget)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/TimeInterpolator.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/animation/interpolation/TimeInterpolator.kt
similarity index 73%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/TimeInterpolator.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/animation/interpolation/TimeInterpolator.kt
index 01d5742c..25819b1b 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/TimeInterpolator.kt
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/animation/interpolation/TimeInterpolator.kt
@@ -1,4 +1,4 @@
-package com.velord.core.ui.compose.animation.interpolator
+package com.velord.core.ui.compose.animation.interpolation
import android.animation.TimeInterpolator
import androidx.compose.animation.core.Easing
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/glance/GlanceColorProviders.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/GlanceColorProviders.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/glance/GlanceColorProviders.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/GlanceColorProviders.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/glance/GlanceWidgetThemeSustainer.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/GlanceWidgetThemeSustainer.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/glance/GlanceWidgetThemeSustainer.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/GlanceWidgetThemeSustainer.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/glance/MainGlanceTheme.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/MainGlanceTheme.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/glance/MainGlanceTheme.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/glance/MainGlanceTheme.kt
diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/theme/AndroidMainTheme.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/theme/AndroidMainTheme.kt
new file mode 100644
index 00000000..303822a6
--- /dev/null
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/compose/theme/AndroidMainTheme.kt
@@ -0,0 +1,61 @@
+package com.velord.core.ui.compose.theme
+
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.LocalActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import com.velord.core.ui.util.color.defineScrimAndDarkScrimColorForSystemBar
+
+@Composable
+internal actual fun platformDynamicColorScheme(
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+ apiAvailable: Boolean,
+): ColorScheme? {
+ if (dynamicColor.not() || apiAvailable.not()) return null
+
+ val context = LocalContext.current
+
+ return if (useDarkTheme) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+}
+
+@Composable
+internal actual fun PlatformSystemBarsEffect(
+ colorScheme: ColorScheme,
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+) {
+ val activity = LocalActivity.current as? ComponentActivity ?: return
+
+ LaunchedEffect(
+ activity,
+ colorScheme,
+ useDarkTheme,
+ dynamicColor,
+ ) {
+ val colors = colorScheme.defineScrimAndDarkScrimColorForSystemBar(makeTransparent = true)
+
+ val light = SystemBarStyle.light(
+ scrim = colors.scrim.toArgb(),
+ darkScrim = colors.darkScrim.toArgb(),
+ )
+ val dark = SystemBarStyle.dark(scrim = colors.scrim.toArgb())
+ val statusBarStyle = if (useDarkTheme) dark else light
+
+ activity.enableEdgeToEdge(
+ statusBarStyle = statusBarStyle,
+ navigationBarStyle = statusBarStyle,
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/AlertDialog.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/AlertDialog.kt
new file mode 100644
index 00000000..5dc793a2
--- /dev/null
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/AlertDialog.kt
@@ -0,0 +1,38 @@
+package com.velord.core.ui.dialog
+
+import android.content.Context
+import android.content.DialogInterface
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+fun Context.alertDialog(
+ cancelable: Boolean = true,
+ title: String? = null,
+ message: String? = null,
+ positiveText: String? = null,
+ negativeText: String? = null,
+ negativeCallback: (() -> Unit)? = null,
+ positiveCallback: () -> Unit = {}
+) {
+ val dialog = MaterialAlertDialogBuilder(this).setCancelable(cancelable).create()
+
+ positiveText?.let {
+ dialog.setButton(
+ DialogInterface.BUTTON_POSITIVE,
+ it
+ ) { _, _ ->
+ positiveCallback.invoke()
+ }
+ }
+ title?.let(dialog::setTitle)
+ message?.let { dialog.setMessage(message) }
+ negativeText?.let {
+ dialog.setButton(
+ DialogInterface.BUTTON_NEGATIVE,
+ it
+ ) { _, _ ->
+ negativeCallback?.invoke()
+ }
+ }
+
+ dialog.show()
+}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/dialog/CheckPermission.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
similarity index 67%
rename from core/core-ui/src/main/java/com/velord/core/ui/dialog/CheckPermission.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
index 47d4bf03..610335b9 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/dialog/CheckPermission.kt
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/dialog/CheckPermission.kt
@@ -4,22 +4,29 @@ import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.activity.result.ActivityResultLauncher
-import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
-import com.velord.core.resource.R
-import com.velord.util.context.createSettingsIntent
+import com.velord.core.resource.Res
+import com.velord.core.resource.decline
+import com.velord.core.resource.give_access_to_camera
+import com.velord.core.resource.give_access_to_microphone
+import com.velord.core.resource.go_to_app_settings
+import com.velord.core.resource.require_camera_permission
+import com.velord.core.resource.require_microphone_permission
+import com.velord.infrastructure.util.context.createSettingsIntent
+import kotlinx.coroutines.runBlocking
+import org.jetbrains.compose.resources.getString
private fun Context.askUserActivatePermissionInSettings(
- @StringRes title: Int,
- @StringRes message: Int,
+ title: org.jetbrains.compose.resources.StringResource,
+ message: org.jetbrains.compose.resources.StringResource,
onDecline: () -> Unit
) {
alertDialog(
- title = title,
- message = message,
- positiveText = R.string.go_to_app_settings,
- negativeText = R.string.decline,
+ title = runBlocking { getString(title) },
+ message = runBlocking { getString(message) },
+ positiveText = runBlocking { getString(Res.string.go_to_app_settings) },
+ negativeText = runBlocking { getString(Res.string.decline) },
positiveCallback = {
startActivity(createSettingsIntent())
},
@@ -32,8 +39,8 @@ fun Context.showGoToSettingsForMic(
onDecline: () -> Unit
) {
askUserActivatePermissionInSettings(
- title = R.string.require_microphone_permission,
- message = R.string.give_access_to_microphone,
+ title = Res.string.require_microphone_permission,
+ message = Res.string.give_access_to_microphone,
onDecline = onDecline
)
}
@@ -42,8 +49,8 @@ fun Context.showGoToSettingsForCamera(
onDecline: () -> Unit
) {
askUserActivatePermissionInSettings(
- title = R.string.require_camera_permission,
- message = R.string.give_access_to_camera,
+ title = Res.string.require_camera_permission,
+ message = Res.string.give_access_to_camera,
onDecline = onDecline
)
}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/permission/CheckCameraAndAudioRecordPermission.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
similarity index 73%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/permission/CheckCameraAndAudioRecordPermission.kt
rename to core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
index 5109805c..1d394577 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/permission/CheckCameraAndAudioRecordPermission.kt
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/permission/CheckCameraAndAudioRecordPermission.kt
@@ -1,8 +1,7 @@
-package com.velord.core.ui.util.permission
+package com.velord.core.ui.permission
import android.Manifest
import android.content.Context
-import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
@@ -11,23 +10,27 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
+import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.shouldShowRationale
import com.velord.core.ui.dialog.showGoToSettingsForCamera
import com.velord.core.ui.dialog.showGoToSettingsForMic
import com.velord.core.ui.util.ObserveSharedFlow
-import com.velord.util.permission.AndroidPermissionState
+import com.velord.infrastructure.util.permission.PermissionGrantState
+import com.velord.infrastructure.util.persmission.toPermissionGrantState
import kotlinx.coroutines.flow.MutableSharedFlow
+import com.google.accompanist.permissions.PermissionState as AccompanistPermissionState
+
+private val log = Logger.withTag("CheckCameraAndAudioRecordPermission")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CheckCameraAndAudioRecordPermission(
triggerCheckEvent: MutableSharedFlow,
- onCameraUpdateState: (AndroidPermissionState) -> Unit,
- onMicroUpdateState: (AndroidPermissionState) -> Unit,
+ onCameraUpdateState: (PermissionGrantState) -> Unit,
+ onMicroUpdateState: (PermissionGrantState) -> Unit,
) {
val context = LocalContext.current
// Fix the issue when the user first time asked for permission.
@@ -42,7 +45,7 @@ fun CheckCameraAndAudioRecordPermission(
Manifest.permission.RECORD_AUDIO,
),
onPermissionsResult = { _ ->
- Log.d("CheckCameraAndAudioRecordPermission", "onPermissionsResult")
+ log.d { "onPermissionsResult" }
if (permissionAlreadyRequestedState.value.not()) {
permissionAlreadyRequestedState.value = true
}
@@ -65,31 +68,35 @@ fun CheckCameraAndAudioRecordPermission(
}
LaunchedEffect(permissionsState) {
- Log.d("CheckCameraAndAudioRecordPermission", "LaunchedEffect permissionsState")
+ log.d { "LaunchedEffect permissionsState" }
permissionsState.launchMultiplePermissionRequest()
}
LaunchedEffect(permissionAlreadyRequestedState.value) {
if (permissionAlreadyRequestedState.value.not()) return@LaunchedEffect
- Log.d("CheckCameraAndAudioRecordPermission", "LaunchedEffect permissionAlreadyRequestedState")
+ log.d { "LaunchedEffect permissionAlreadyRequestedState" }
checkCamera(permissionAlreadyRequestedState, cameraState, context)
checkAudioRecord(permissionAlreadyRequestedState, microState, context)
}
cameraState.value?.let {
- Log.d("CheckCameraAndAudioRecordPermission", "Camera: ${it.status}")
- val androidPermState = it.status.toAndroidPermissionState(permissionAlreadyRequestedState.value)
- onCameraUpdateState(androidPermState)
+ log.d { "Camera: ${it.status}" }
+ val permissionGrantState = it.status.toPermissionGrantState(
+ isRequestedBefore = permissionAlreadyRequestedState.value
+ )
+ onCameraUpdateState(permissionGrantState)
}
microState.value?.let {
- Log.d("CheckCameraAndAudioRecordPermission", "Micro: ${it.status}")
- val androidPermState = it.status.toAndroidPermissionState(permissionAlreadyRequestedState.value)
- onMicroUpdateState(androidPermState)
+ log.d { "Micro: ${it.status}" }
+ val permissionGrantState = it.status.toPermissionGrantState(
+ isRequestedBefore = permissionAlreadyRequestedState.value
+ )
+ onMicroUpdateState(permissionGrantState)
}
ObserveSharedFlow(flow = triggerCheckEvent) {
- Log.d("CheckCameraAndAudioRecordPermission", "ObserveTrigger != null")
+ log.d { "ObserveTrigger != null" }
permissionsState.launchMultiplePermissionRequest()
checkCamera(permissionAlreadyRequestedState, cameraState, context)
@@ -100,7 +107,7 @@ fun CheckCameraAndAudioRecordPermission(
@OptIn(ExperimentalPermissionsApi::class)
private fun checkCamera(
permissionAlreadyRequestedState: State,
- cameraState: State,
+ cameraState: State,
context: Context
) {
baseCheck(
@@ -114,7 +121,7 @@ private fun checkCamera(
@OptIn(ExperimentalPermissionsApi::class)
private fun checkAudioRecord(
permissionAlreadyRequestedState: State,
- microState: State,
+ microState: State,
context: Context
) {
baseCheck(
@@ -128,7 +135,7 @@ private fun checkAudioRecord(
@OptIn(ExperimentalPermissionsApi::class)
private fun baseCheck(
permissionAlreadyRequestedState: State,
- permState: State,
+ permState: State,
onCompletelyDenied: () -> Unit,
tag: String
) {
@@ -136,7 +143,7 @@ private fun baseCheck(
val isNotGranted = it.status.isGranted.not()
val isNotShowRationale = it.status.shouldShowRationale.not()
val isRequestedBefore = permissionAlreadyRequestedState.value
- Log.d("CheckCameraAndAudioRecordPermission", "$tag: ${it.status.toAndroidPermissionState(isRequestedBefore)}")
+ log.d { "$tag: ${it.status.toPermissionGrantState(isRequestedBefore)}" }
if (isNotGranted && isNotShowRationale && isRequestedBefore) {
onCompletelyDenied()
}
diff --git a/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt
new file mode 100644
index 00000000..3cc97044
--- /dev/null
+++ b/core/core-ui/src/androidMain/kotlin/com/velord/core/ui/util/AndroidSetContent.kt
@@ -0,0 +1,91 @@
+package com.velord.core.ui.util
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.velord.core.ui.compose.component.ToastHost
+import com.velord.core.ui.compose.theme.MainTheme
+import com.velord.infrastructure.util.context.getActivity
+import com.velord.model.ToastConfig
+import com.velord.model.setting.AppThemeConfig
+import com.velord.ui.sharedviewmodel.ThemeUiState
+import com.velord.ui.sharedviewmodel.ThemeViewModel
+import kotlinx.coroutines.flow.Flow
+
+fun ComponentActivity.setContentWithTheme(
+ screen: @Composable ComposeView.() -> Unit,
+): ComposeView = ComposeView(this).setContentWithTheme(screen)
+
+fun Fragment.setContentWithTheme(
+ screen: @Composable ComposeView.() -> Unit,
+): ComposeView = ComposeView(requireContext()).setContentWithTheme(screen)
+
+context(_: LifecycleOwner)
+fun ComposeView.setContentWithTheme(
+ screen: @Composable ComposeView.() -> Unit,
+): ComposeView = setThemedContent {
+ screen()
+}
+
+context(_: LifecycleOwner)
+fun ComposeView.setToastOverlayWithTheme(
+ toastEventFlow: Flow,
+): ComposeView = setThemedContent {
+ ToastHost(toastEventFlow = toastEventFlow) {
+ // Empty content. This ComposeView is only the global toast overlay.
+ }
+}
+
+context(_: LifecycleOwner)
+private fun ComposeView.setThemedContent(
+ content: @Composable ComposeView.() -> Unit,
+): ComposeView = apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+
+ setContent {
+ AppThemeContainer {
+ this@setThemedContent.content()
+ }
+ }
+}
+
+@Composable
+private fun AppThemeContainer(
+ content: @Composable () -> Unit,
+) {
+ val activity = LocalContext.current.getActivity()
+ val themeViewModel = viewModel(
+ viewModelStoreOwner = activity as ViewModelStoreOwner,
+ )
+ val themeState: State = themeViewModel.uiStateFlow.collectAsStateWithLifecycle()
+
+ val theme = themeState.value?.appThemeConfig ?: AppThemeConfig.DEFAULT
+
+ CompositionLocalProvider(LocalTheme provides theme) {
+ val localThemeConfig = LocalTheme.current
+
+ val isDark = if (localThemeConfig.config.abideToOs) {
+ isSystemInDarkTheme()
+ } else {
+ localThemeConfig.config.useDarkTheme
+ }
+
+ MainTheme(
+ abideToOsTheme = localThemeConfig.config.abideToOs,
+ useDarkTheme = isDark,
+ dynamicColor = localThemeConfig.config.useDynamicColor,
+ specialTheme = localThemeConfig.config.current,
+ content = content,
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/annotation/ConstructorLikeFunction.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/annotation/ConstructorLikeFunction.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/annotation/ConstructorLikeFunction.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/annotation/ConstructorLikeFunction.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/easing/AnticipateOvershootEasing.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/easing/AnticipateOvershootEasing.kt
new file mode 100644
index 00000000..308887f7
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/easing/AnticipateOvershootEasing.kt
@@ -0,0 +1,44 @@
+package com.velord.core.ui.compose.animation.easing
+
+import androidx.compose.animation.core.Easing
+import com.velord.core.ui.annotation.ConstructorLikeFunction
+
+private const val DEFAULT_TENSION = 2f
+private const val DEFAULT_EXTRA_TENSION = 1.5f
+private const val HALF_FRACTION = 0.5f
+private const val DOUBLE_FRACTION = 2f
+
+@ConstructorLikeFunction
+fun AnticipateOvershootInterpolatorEasing(
+ tension: Float = DEFAULT_TENSION,
+ extraTension: Float = DEFAULT_EXTRA_TENSION,
+): Easing = AnticipateOvershootEasing(tension = tension, extraTension = extraTension)
+
+class AnticipateOvershootEasing(
+ tension: Float = DEFAULT_TENSION,
+ extraTension: Float = DEFAULT_EXTRA_TENSION,
+) : Easing {
+
+ private val totalTension = tension * extraTension
+
+ override fun transform(fraction: Float): Float = if (fraction < HALF_FRACTION) {
+ val anticipateCalc = anticipate(value = fraction * DOUBLE_FRACTION, tension = totalTension)
+ HALF_FRACTION * anticipateCalc
+ } else {
+ val overshootCalc = overshoot(
+ value = fraction * DOUBLE_FRACTION - DOUBLE_FRACTION,
+ tension = totalTension,
+ )
+ HALF_FRACTION * (overshootCalc + DOUBLE_FRACTION)
+ }
+
+ private fun anticipate(
+ value: Float,
+ tension: Float,
+ ): Float = value * value * ((tension + 1f) * value - tension)
+
+ private fun overshoot(
+ value: Float,
+ tension: Float,
+ ): Float = value * value * ((tension + 1f) * value + tension)
+}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/easing/HesitateEasing.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/easing/HesitateEasing.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/animation/easing/HesitateEasing.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/easing/HesitateEasing.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt
new file mode 100644
index 00000000..6baa5f22
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt
@@ -0,0 +1,26 @@
+package com.velord.core.ui.compose.animation.interpolator
+
+import androidx.compose.animation.core.Easing
+import com.velord.core.ui.annotation.ConstructorLikeFunction
+import kotlin.math.PI
+import kotlin.math.sin
+
+private const val DEFAULT_TENSION = 50f
+
+@ConstructorLikeFunction
+fun CircularSpringInterpolatorEasing(
+ tension: Float = DEFAULT_TENSION,
+): Easing = CircularSpringInterpolator(tension)
+
+class CircularSpringInterpolator(
+ private val tension: Float = DEFAULT_TENSION,
+) : Easing {
+
+ override fun transform(fraction: Float): Float {
+ val sinusOne = sin(tension * fraction)
+ val sinusTwo = sin(PI.toFloat() * fraction)
+
+ return sinusOne * sinusTwo + fraction
+ }
+}
+
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt
new file mode 100644
index 00000000..0415a5e9
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt
@@ -0,0 +1,33 @@
+@file:Suppress("MagicNumber")
+
+package com.velord.core.ui.compose.animation.interpolator
+
+import androidx.compose.animation.core.Easing
+import com.velord.core.ui.annotation.ConstructorLikeFunction
+import kotlin.math.PI
+import kotlin.math.pow
+import kotlin.math.sin
+
+private const val DEFAULT_STIFFNESS = 0.35f
+private const val DECAY_FACTOR = 10
+private const val STIFFNESS_DIVISOR = 4
+
+@ConstructorLikeFunction
+fun SpringOverflowInterpolatorEasing(
+ stiffness: Float = DEFAULT_STIFFNESS,
+): Easing = SpringOverflowInterpolator(stiffness)
+
+class SpringOverflowInterpolator(
+ private val stiffness: Float = DEFAULT_STIFFNESS,
+) : Easing {
+
+ override fun transform(fraction: Float): Float {
+ val twoPowTen = 2.0.pow((-DECAY_FACTOR * fraction).toDouble())
+ val stiffnessByInput = fraction - stiffness / STIFFNESS_DIVISOR
+ val doublePi = 2 * PI
+ val sinus = sin(stiffnessByInput * doublePi / stiffness)
+
+ return (twoPowTen * sinus + 1).toFloat()
+ }
+}
+
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/AnimatableLabeledIcon.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/AnimatableLabeledIcon.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/component/AnimatableLabeledIcon.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/AnimatableLabeledIcon.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/CollapsingToolbar.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/CollapsingToolbar.kt
similarity index 98%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/component/CollapsingToolbar.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/CollapsingToolbar.kt
index 442b1274..28a70220 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/CollapsingToolbar.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/CollapsingToolbar.kt
@@ -157,10 +157,7 @@ internal fun CollapsingToolbar(
.onGloballyPositioned {
onHeightReceive(it.size.height.dp)
}
- .bottomBorder(
- thickness = 1.dp,
- color = MaterialTheme.colorScheme.primary,
- )
+ .bottomBorder(thickness = 1.dp, color = MaterialTheme.colorScheme.primary)
,
title = {
Row(
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/FullSizeBackground.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/FullSizeBackground.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/component/FullSizeBackground.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/FullSizeBackground.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt
new file mode 100644
index 00000000..29238b86
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt
@@ -0,0 +1,50 @@
+@file:Suppress("FunctionName")
+
+package com.velord.core.ui.compose.component
+
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+private val DEFAULT_CORNER_RADIUS = 8.dp
+
+fun DrawScope.LinearGradientShaderCanvas(
+ animatedValue: Float,
+ startColor: Color = Color.Transparent,
+ centerColor: Color = Color.White,
+ endColor: Color = Color.Transparent,
+ cornerRadius: Dp = DEFAULT_CORNER_RADIUS,
+ gradientColorAndPosition: List> = listOf(
+ startColor to 0f,
+ centerColor to animatedValue,
+ endColor to 1f,
+ ),
+) {
+ val width = size.width
+ val height = size.height
+ val cornerRadiusPx = cornerRadius.toPx()
+
+ val colorStops = gradientColorAndPosition
+ .map { colorAndPosition ->
+ colorAndPosition.second to colorAndPosition.first
+ }
+ .toTypedArray()
+
+ drawRoundRect(
+ brush = Brush.linearGradient(
+ colorStops = colorStops,
+ start = Offset.Zero,
+ end = Offset(width, height),
+ tileMode = TileMode.Clamp,
+ ),
+ cornerRadius = CornerRadius(
+ x = cornerRadiusPx,
+ y = cornerRadiusPx,
+ ),
+ )
+}
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/PervasiveArcFromBottomLayout.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/PervasiveArcFromBottomLayout.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/component/PervasiveArcFromBottomLayout.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/PervasiveArcFromBottomLayout.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/ToastHost.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/ToastHost.kt
new file mode 100644
index 00000000..3506cdbe
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/component/ToastHost.kt
@@ -0,0 +1,220 @@
+package com.velord.core.ui.compose.component
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.unit.dp
+import com.velord.model.ToastConfig
+import com.velord.model.ToastDuration
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.milliseconds
+
+private const val TOAST_ENTER_DURATION_MILLIS = 360
+private const val TOAST_EXIT_DURATION_MILLIS = 180
+private const val TOAST_ENTER_DELAY_MILLIS = 30
+
+private const val TOAST_ENTER_INITIAL_SCALE = 0.94f
+private const val TOAST_EXIT_TARGET_SCALE = 0.98f
+
+private val ToastEnterEasing = CubicBezierEasing(
+ a = 0.16f,
+ b = 1f,
+ c = 0.3f,
+ d = 1f,
+)
+
+private val ToastExitEasing = CubicBezierEasing(
+ a = 0.7f,
+ b = 0f,
+ c = 0.84f,
+ d = 0f,
+)
+
+internal data class ToastMessage(
+ val id: Int,
+ val text: String,
+)
+
+@Stable
+class ToastHostState internal constructor(private val scope: CoroutineScope) {
+
+ internal var message by mutableStateOf(null)
+ private set
+
+ private var nextId = 0
+ private var dismissJob: Job? = null
+
+ fun show(
+ text: String,
+ duration: ToastDuration = ToastDuration.Short,
+ ) {
+ dismissJob?.cancel()
+
+ message = ToastMessage(id = nextId++, text = text)
+
+ dismissJob = scope.launch {
+ delay(duration.millis.milliseconds)
+ dismiss()
+ }
+ }
+
+ fun dismiss() {
+ dismissJob?.cancel()
+ message = null
+ }
+}
+
+@Composable
+fun rememberToastHostState(): ToastHostState {
+ val scope = rememberCoroutineScope()
+
+ return remember(scope) {
+ ToastHostState(scope)
+ }
+}
+
+@Composable
+fun ToastHost(
+ toastEventFlow: Flow,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.BottomCenter,
+ content: @Composable BoxScope.() -> Unit,
+) {
+ val toastHostState = rememberToastHostState()
+
+ LaunchedEffect(toastEventFlow) {
+ toastEventFlow.collect { config ->
+ toastHostState.show(text = config.message, duration = config.duration)
+ }
+ }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ content()
+
+ Box(
+ modifier = Modifier.matchParentSize(),
+ contentAlignment = alignment,
+ ) {
+ ToastContent(state = toastHostState)
+ }
+ }
+}
+
+@Composable
+private fun ToastContent(state: ToastHostState) {
+ AnimatedContent(
+ targetState = state.message,
+ label = "ToastContent",
+ contentKey = { message -> message?.id },
+ transitionSpec = {
+ val enter = fadeIn(
+ animationSpec = tween(
+ durationMillis = TOAST_ENTER_DURATION_MILLIS,
+ delayMillis = TOAST_ENTER_DELAY_MILLIS,
+ easing = ToastEnterEasing,
+ ),
+ ) + slideInVertically(
+ animationSpec = tween(
+ durationMillis = TOAST_ENTER_DURATION_MILLIS,
+ easing = ToastEnterEasing,
+ ),
+ initialOffsetY = { height -> height / 3 },
+ ) + scaleIn(
+ animationSpec = tween(
+ durationMillis = TOAST_ENTER_DURATION_MILLIS,
+ easing = ToastEnterEasing,
+ ),
+ initialScale = TOAST_ENTER_INITIAL_SCALE,
+ transformOrigin = TransformOrigin(
+ pivotFractionX = 0.5f,
+ pivotFractionY = 1f,
+ ),
+ )
+
+ val exit = fadeOut(
+ animationSpec = tween(
+ durationMillis = TOAST_EXIT_DURATION_MILLIS,
+ easing = ToastExitEasing,
+ ),
+ ) + slideOutVertically(
+ animationSpec = tween(
+ durationMillis = TOAST_EXIT_DURATION_MILLIS,
+ easing = ToastExitEasing,
+ ),
+ targetOffsetY = { height -> height / 4 },
+ ) + scaleOut(
+ animationSpec = tween(
+ durationMillis = TOAST_EXIT_DURATION_MILLIS,
+ easing = ToastExitEasing,
+ ),
+ targetScale = TOAST_EXIT_TARGET_SCALE,
+ transformOrigin = TransformOrigin(
+ pivotFractionX = 0.5f,
+ pivotFractionY = 1f,
+ ),
+ )
+
+ enter togetherWith exit using SizeTransform(clip = false)
+ },
+ contentAlignment = Alignment.Center
+ ) { message ->
+ if (message == null) return@AnimatedContent
+
+ ToastSurface(text = message.text)
+ }
+}
+
+@Composable
+private fun ToastSurface(text: String) {
+ Surface(
+ modifier = Modifier.padding(
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 16.dp,
+ ),
+ shape = MaterialTheme.shapes.medium,
+ color = MaterialTheme.colorScheme.inverseSurface,
+ tonalElevation = 6.dp,
+ shadowElevation = 6.dp,
+ ) {
+ Text(
+ text = text,
+ modifier = Modifier.padding(
+ horizontal = 16.dp,
+ vertical = 10.dp,
+ ),
+ color = MaterialTheme.colorScheme.inverseOnSurface,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/path/ArcAtBottomCenterPath.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/ArcAtBottomCenterPath.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/path/ArcAtBottomCenterPath.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/ArcAtBottomCenterPath.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/path/Mapper.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/Mapper.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/path/Mapper.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/Mapper.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/path/TicketPath.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/TicketPath.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/path/TicketPath.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/TicketPath.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/path/WavePath.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/WavePath.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/path/WavePath.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/path/WavePath.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/polygon/HeartPolygon.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/polygon/HeartPolygon.kt
new file mode 100644
index 00000000..272608e5
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/polygon/HeartPolygon.kt
@@ -0,0 +1,67 @@
+@file:Suppress("MagicNumber")
+
+package com.velord.core.ui.compose.polygon
+
+import androidx.compose.ui.geometry.Offset
+import androidx.graphics.shapes.CornerRounding
+import androidx.graphics.shapes.RoundedPolygon
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.sin
+
+internal fun Float.toRadians(): Float = this * PI.toFloat() / 180f
+
+internal val PointZero: Offset = Offset.Zero
+
+internal fun radialToCartesian(
+ radius: Float,
+ angleRadians: Float,
+ center: Offset = PointZero,
+): Offset = directionVectorOffset(angleRadians) * radius + center
+
+internal fun directionVectorOffset(angleRadians: Float): Offset = Offset(
+ x = cos(angleRadians.toDouble()).toFloat(),
+ y = sin(angleRadians.toDouble()).toFloat(),
+)
+
+fun RoundedPolygon.Companion.heart(): RoundedPolygon {
+ val radius = 1f
+ val radiusSides = 0.8f
+ val innerRadius = 0.1f
+
+ val right = radialToCartesian(radiusSides, 0f.toRadians())
+ val top = radialToCartesian(radius, 90f.toRadians())
+ val left = radialToCartesian(radiusSides, 180f.toRadians())
+ val bottomLeft = radialToCartesian(radius, 250f.toRadians())
+ val innerBottom = radialToCartesian(innerRadius, 270f.toRadians())
+ val bottomRight = radialToCartesian(radius, 290f.toRadians())
+
+ val vertices = floatArrayOf(
+ right.x,
+ right.y,
+ top.x,
+ top.y,
+ left.x,
+ left.y,
+ bottomLeft.x,
+ bottomLeft.y,
+ innerBottom.x,
+ innerBottom.y,
+ bottomRight.x,
+ bottomRight.y,
+ )
+
+ val roundingNormal = 0.6f
+ val roundingNone = 0f
+
+ val rounding = listOf(
+ CornerRounding(roundingNormal),
+ CornerRounding(roundingNone),
+ CornerRounding(roundingNormal),
+ CornerRounding(roundingNormal),
+ CornerRounding(roundingNone),
+ CornerRounding(roundingNormal),
+ )
+
+ return RoundedPolygon(vertices = vertices, perVertexRounding = rounding)
+}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewCombined.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewCombined.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewCombined.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewCombined.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewDevice.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewDevice.kt
similarity index 60%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewDevice.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewDevice.kt
index f9606d36..e93149cc 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewDevice.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewDevice.kt
@@ -1,6 +1,5 @@
package com.velord.core.ui.compose.preview
-import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
private const val GROUP_NAME = "devices"
@@ -8,16 +7,19 @@ private const val GROUP_NAME = "devices"
@Preview(
name = "Phone Landscape",
group = GROUP_NAME,
- device = "spec:shape=Normal,width=891,height=311,unit=dp,dpi=420"
+ widthDp = 891,
+ heightDp = 311,
)
@Preview(
name = "Phone Portrait",
group = GROUP_NAME,
- device = Devices.PIXEL_4,
+ widthDp = 411,
+ heightDp = 891,
)
@Preview(
name = "Tablet",
group = GROUP_NAME,
- device = Devices.TABLET
+ widthDp = 800,
+ heightDp = 1280,
)
-annotation class PreviewDevice
+annotation class PreviewDevice
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewFont.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewFont.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/preview/PreviewFont.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/preview/PreviewFont.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/shape/ArcAtBottomCenterShape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/ArcAtBottomCenterShape.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/shape/ArcAtBottomCenterShape.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/ArcAtBottomCenterShape.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/shape/MorphShape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/MorphShape.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/shape/MorphShape.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/MorphShape.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/shape/Shape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/shape/Shape.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/Shape.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/shape/TicketShape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/TicketShape.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/shape/TicketShape.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/shape/TicketShape.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt
new file mode 100644
index 00000000..4e26a2bd
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/MainTheme.kt
@@ -0,0 +1,64 @@
+package com.velord.core.ui.compose.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import com.velord.core.ui.compose.shape.MainShapes
+import com.velord.core.ui.compose.theme.color.DarkColorScheme
+import com.velord.core.ui.compose.theme.color.LightColorScheme
+import com.velord.model.setting.AppThemeConfig
+import com.velord.model.setting.SpecialTheme
+
+private fun SpecialTheme.toColorScheme(): ColorScheme = when (this) {
+ SpecialTheme.DARK -> DarkColorScheme
+ SpecialTheme.LIGHT -> LightColorScheme
+}
+
+@Composable
+fun MainTheme(
+ abideToOsTheme: Boolean = true,
+ useDarkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = true,
+ specialTheme: SpecialTheme,
+ content: @Composable () -> Unit,
+) {
+ val dynamicColorScheme = platformDynamicColorScheme(
+ useDarkTheme = useDarkTheme,
+ dynamicColor = dynamicColor,
+ apiAvailable = AppThemeConfig.DEFAULT.isSystemDynamicColorAvailable,
+ )
+
+ val colorScheme: ColorScheme = when {
+ abideToOsTheme.not() && dynamicColorScheme != null -> dynamicColorScheme
+ abideToOsTheme -> if (useDarkTheme) DarkColorScheme else LightColorScheme
+ else -> specialTheme.toColorScheme()
+ }
+
+ PlatformSystemBarsEffect(
+ colorScheme = colorScheme,
+ useDarkTheme = useDarkTheme,
+ dynamicColor = dynamicColor,
+ )
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ shapes = MainShapes,
+ typography = MainTypography,
+ content = content,
+ )
+}
+
+@Composable
+internal expect fun platformDynamicColorScheme(
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+ apiAvailable: Boolean,
+): ColorScheme?
+
+@Composable
+internal expect fun PlatformSystemBarsEffect(
+ colorScheme: ColorScheme,
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+)
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/RippleTheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/RippleTheme.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/theme/RippleTheme.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/RippleTheme.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/Typography.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/Typography.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/theme/Typography.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/Typography.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/color/ColorScheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/theme/color/ColorScheme.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorScheme.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/color/ColorTokens.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorTokens.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/compose/theme/color/ColorTokens.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/compose/theme/color/ColorTokens.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/ObserveSharedFlow.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/ObserveSharedFlow.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/ObserveSharedFlow.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/ObserveSharedFlow.kt
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/ScreenSize.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/ScreenSize.kt
new file mode 100644
index 00000000..7246c877
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/ScreenSize.kt
@@ -0,0 +1,27 @@
+package com.velord.core.ui.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
+
+data class ScreenSize(
+ val width: Number,
+ val height: Number,
+)
+
+@Composable
+fun getScreenWidthAndHeightInDp(): ScreenSize {
+ val size = LocalWindowInfo.current.containerDpSize
+ val width = size.width.value
+ val height = size.height.value
+ return ScreenSize(width, height)
+}
+
+@Composable
+fun getScreenWidthAndHeightInPx(): ScreenSize {
+ val configuration = LocalWindowInfo.current.containerDpSize
+ val density = LocalDensity.current
+ val width = with(density) { configuration.width.roundToPx() }
+ val height = with(density) { configuration.height.roundToPx() }
+ return ScreenSize(width, height)
+}
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/SetContent.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/SetContent.kt
new file mode 100644
index 00000000..8fca40f1
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/SetContent.kt
@@ -0,0 +1,6 @@
+package com.velord.core.ui.util
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import com.velord.model.setting.AppThemeConfig
+
+val LocalTheme = staticCompositionLocalOf { AppThemeConfig.DEFAULT }
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/Color.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/Color.kt
similarity index 75%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/Color.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/Color.kt
index 32d26807..9e69615e 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/Color.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/Color.kt
@@ -1,4 +1,4 @@
-package com.velord.core.ui.util
+package com.velord.core.ui.util.color
import androidx.compose.ui.graphics.Color
@@ -14,3 +14,10 @@ fun Color.toBlackOrWhite(): Color {
}
fun Int.inverseColor(): Int = this xor COLOR_MASK
+
+fun Color.inverseColor(): Color = Color(
+ red = 1f - red,
+ green = 1f - green,
+ blue = 1f - blue,
+ alpha = alpha,
+)
diff --git a/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/ColorScheme.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/ColorScheme.kt
new file mode 100644
index 00000000..fd1b2c30
--- /dev/null
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/color/ColorScheme.kt
@@ -0,0 +1,17 @@
+package com.velord.core.ui.util.color
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.ui.graphics.Color
+
+internal data class SystemBarScrimColors(
+ val scrim: Color,
+ val darkScrim: Color,
+)
+
+internal fun ColorScheme.defineScrimAndDarkScrimColorForSystemBar(
+ makeTransparent: Boolean = true
+): SystemBarScrimColors {
+ val scrim = if (makeTransparent) Color.Transparent else this.surface
+ val darkScrim = if (makeTransparent) Color.Transparent else scrim.inverseColor()
+ return SystemBarScrimColors(scrim, darkScrim)
+}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/BlinkingShadow.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/BlinkingShadow.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/BlinkingShadow.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/BlinkingShadow.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Hanging.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Hanging.kt
similarity index 88%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Hanging.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Hanging.kt
index 7e251c44..79742afe 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Hanging.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Hanging.kt
@@ -15,9 +15,9 @@ import androidx.compose.ui.graphics.graphicsLayer
object HangingDefaults {
- val shift: HangingShift = HangingShift.Default
+ val shift: HangingShift = HangingShift.DEFAULT
- val animation: HangingAnimation = HangingAnimation.Default
+ val animation: HangingAnimation = HangingAnimation.DEFAULT
val pivotPoint: HangingPivotPoint = HangingPivotPoint.Top
@@ -28,7 +28,7 @@ object HangingDefaults {
): HangingShift = HangingShift(startRotationAngle, endRotationAngle)
fun animation(
- duration: Int = HangingAnimation.DefaultDuration,
+ duration: Int = HangingAnimation.DEFAULT_DURATION,
easing: Easing = LinearEasing,
): HangingAnimation = HangingAnimation(duration, easing)
@@ -44,7 +44,7 @@ class HangingShift internal constructor(
internal val rightRotationAngle: Int
) {
companion object {
- val Default = HangingShift(leftRotationAngle = 25, rightRotationAngle = -25)
+ val DEFAULT = HangingShift(leftRotationAngle = 25, rightRotationAngle = -25)
}
}
@@ -54,12 +54,9 @@ class HangingAnimation internal constructor(
internal val easing: Easing
) {
companion object {
- internal const val DefaultDuration = 1000
+ internal const val DEFAULT_DURATION = 1000
- val Default = HangingAnimation(
- duration = DefaultDuration,
- easing = LinearEasing,
- )
+ val DEFAULT = HangingAnimation(duration = DEFAULT_DURATION, easing = LinearEasing)
}
}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt
similarity index 93%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt
index e4b550dd..a725df21 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/RecomposeHighlighter.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlin.math.min
+import kotlin.time.Duration.Companion.milliseconds
private const val HIGHLIGHT_DURATION = 3000L
/**
@@ -50,8 +51,8 @@ fun Modifier.recomposeHighlighter(
// Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
// as the key is really just to cause the timer to restart every composition).
LaunchedEffect(totalCompositions[0]) {
- delay(highlightDuration)
- totalCompositionsAtLastTimeout.value = totalCompositions[0]
+ delay(highlightDuration.milliseconds)
+ totalCompositionsAtLastTimeout.longValue = totalCompositions[0]
}
Modifier.drawWithCache {
@@ -61,8 +62,8 @@ fun Modifier.recomposeHighlighter(
// Below is to draw the highlight, if necessary. A lot of the logic is copied from
// Modifier.border
- val numCompositionsSinceTimeout =
- totalCompositions[0] - totalCompositionsAtLastTimeout.value
+ val numCompositionsSinceTimeout = totalCompositions[0] -
+ totalCompositionsAtLastTimeout.longValue
val hasValidBorderParams = size.minDimension > 0f
if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Shimmering.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Shimmering.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Shimmering.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Shimmering.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Swelling.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Swelling.kt
similarity index 100%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/Swelling.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/Swelling.kt
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/WaveShape.kt b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/WaveShape.kt
similarity index 99%
rename from core/core-ui/src/main/java/com/velord/core/ui/util/modifier/WaveShape.kt
rename to core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/WaveShape.kt
index 62bdc3f8..9bc7f45b 100644
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/modifier/WaveShape.kt
+++ b/core/core-ui/src/commonMain/kotlin/com/velord/core/ui/util/modifier/WaveShape.kt
@@ -21,7 +21,7 @@ enum class WaveSide {
Bottom;
companion object {
- val Default = Top
+ val DEFAULT = Top
}
}
diff --git a/core/core-ui/src/desktopMain/kotlin/com/velord/core/ui/compose/theme/DesktopMainTheme.kt b/core/core-ui/src/desktopMain/kotlin/com/velord/core/ui/compose/theme/DesktopMainTheme.kt
new file mode 100644
index 00000000..794f010a
--- /dev/null
+++ b/core/core-ui/src/desktopMain/kotlin/com/velord/core/ui/compose/theme/DesktopMainTheme.kt
@@ -0,0 +1,18 @@
+package com.velord.core.ui.compose.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun platformDynamicColorScheme(
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+ apiAvailable: Boolean,
+): ColorScheme? = null
+
+@Composable
+internal actual fun PlatformSystemBarsEffect(
+ colorScheme: ColorScheme,
+ useDarkTheme: Boolean,
+ dynamicColor: Boolean,
+) = Unit
\ No newline at end of file
diff --git a/core/core-ui/src/main/AndroidManifest.xml b/core/core-ui/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/core/core-ui/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt b/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt
deleted file mode 100644
index 6035b4f4..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/CircularSpringInterpolator.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.velord.core.ui.compose.animation.interpolator
-
-import android.view.animation.Interpolator
-import androidx.compose.animation.core.Easing
-import com.velord.core.ui.annotation.ConstructorLikeFunction
-import kotlin.math.sin
-
-@ConstructorLikeFunction
-fun CircularSpringInterpolatorEasing(tension: Float = 50f): Easing =
- CircularSpringInterpolator(tension).toEasing()
-
-class CircularSpringInterpolator(private val tension: Float = 50f) : Interpolator {
- override fun getInterpolation(input: Float): Float {
- val sinusOne = sin(tension * input)
- val sinusTwo = sin(Math.PI * input)
- return (sinusOne * sinusTwo + input).toFloat()
- }
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt b/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt
deleted file mode 100644
index 54ad3839..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/animation/interpolator/SpringOverflowInterpolator.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-@file:Suppress("MagicNumber")
-
-package com.velord.core.ui.compose.animation.interpolator
-
-import android.view.animation.Interpolator
-import androidx.compose.animation.core.Easing
-import com.velord.core.ui.annotation.ConstructorLikeFunction
-import kotlin.math.pow
-import kotlin.math.sin
-
-@ConstructorLikeFunction
-fun SpringOverflowInterpolatorEasing(stiffness: Float = 0.35f): Easing =
- SpringOverflowInterpolator(stiffness).toEasing()
-
-class SpringOverflowInterpolator(
- private val stiffness: Float = 0.35f
-) : Interpolator {
-
- override fun getInterpolation(input: Float): Float {
- val twoPowTen = 2.0.pow((-DECAY_FACTOR * input).toDouble())
- val stiffnessByInput = (input - stiffness / STIFFNESS_DIVISOR)
- val doublePi = 2 * Math.PI
- val sinus = sin(stiffnessByInput * doublePi / stiffness)
- return (twoPowTen * sinus + 1).toFloat()
- }
-
- companion object {
- private const val DECAY_FACTOR = 10
- private const val STIFFNESS_DIVISOR = 4
- }
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt b/core/core-ui/src/main/java/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt
deleted file mode 100644
index 8fbb4187..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/component/LinearGradientShaderCanvas.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-@file:Suppress("FunctionName")
-
-package com.velord.core.ui.compose.component
-
-import android.graphics.RectF
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.LinearGradientShader
-import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.graphics.TileMode
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
-import androidx.compose.ui.graphics.nativeCanvas
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.core.graphics.withSave
-
-private val DEFAULT_CORNER_RADIUS = 8.dp
-
-fun DrawScope.LinearGradientShaderCanvas(
- animatedValue: Float,
- startColor: Color = Color.Transparent,
- centerColor: Color = Color.White,
- endColor: Color = Color.Transparent,
- cornerRadius: Dp = DEFAULT_CORNER_RADIUS,
- gradientColorAndPosition: List> = listOf(
- startColor to 0f,
- centerColor to animatedValue,
- endColor to 1f
- )
-) {
- drawIntoCanvas { canvas ->
- val width = size.width
- val height = size.height
- val cornerRadiusPx = cornerRadius.toPx()
- val shader = LinearGradientShader(
- from = Offset(0f, 0f),
- to = Offset(width, height),
- colors = gradientColorAndPosition.map { it.first },
- colorStops = gradientColorAndPosition.map { it.second },
- tileMode = TileMode.Clamp
- )
- val paint = Paint().asFrameworkPaint()
- paint.shader = shader
-
- canvas.nativeCanvas.withSave {
- val rect = RectF(0f, 0f, width, height)
- drawRoundRect(rect, cornerRadiusPx, cornerRadiusPx, paint)
- }
- }
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/polygon/HeartPolygon.kt b/core/core-ui/src/main/java/com/velord/core/ui/compose/polygon/HeartPolygon.kt
deleted file mode 100644
index 6e894600..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/polygon/HeartPolygon.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-@file:Suppress("MagicNumber")
-
-package com.velord.core.ui.compose.polygon
-
-import android.graphics.PointF
-import androidx.core.graphics.plus
-import androidx.core.graphics.times
-import androidx.graphics.shapes.CornerRounding
-import androidx.graphics.shapes.RoundedPolygon
-import kotlin.math.PI
-import kotlin.math.cos
-import kotlin.math.sin
-
-internal fun Float.toRadians() = this * PI.toFloat() / 180f
-
-internal val PointZero = PointF(0f, 0f)
-internal fun radialToCartesian(
- radius: Float,
- angleRadians: Float,
- center: PointF = PointZero
-) = directionVectorPointF(angleRadians) * radius + center
-
-internal fun directionVectorPointF(angleRadians: Float) =
- PointF(cos(angleRadians), sin(angleRadians))
-
-fun RoundedPolygon.Companion.heart(): RoundedPolygon {
- val radius = 1f
- val radiusSides = 0.8f
- val innerRadius = 0.1f
- val vertices = floatArrayOf(
- radialToCartesian(radiusSides, 0f.toRadians()).x,
- radialToCartesian(radiusSides, 0f.toRadians()).y,
- radialToCartesian(radius, 90f.toRadians()).x,
- radialToCartesian(radius, 90f.toRadians()).y,
- radialToCartesian(radiusSides, 180f.toRadians()).x,
- radialToCartesian(radiusSides, 180f.toRadians()).y,
- radialToCartesian(radius, 250f.toRadians()).x,
- radialToCartesian(radius, 250f.toRadians()).y,
- radialToCartesian(innerRadius, 270f.toRadians()).x,
- radialToCartesian(innerRadius, 270f.toRadians()).y,
- radialToCartesian(radius, 290f.toRadians()).x,
- radialToCartesian(radius, 290f.toRadians()).y,
- )
-
- val roundingNormal = 0.6f
- val roundingNone = 0f
- val rounding = listOf(
- CornerRounding(roundingNormal),
- CornerRounding(roundingNone),
- CornerRounding(roundingNormal),
- CornerRounding(roundingNormal),
- CornerRounding(roundingNone),
- CornerRounding(roundingNormal),
- )
-
- return RoundedPolygon(
- vertices = vertices,
- perVertexRounding = rounding
- )
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/MainTheme.kt b/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/MainTheme.kt
deleted file mode 100644
index c5697eee..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/compose/theme/MainTheme.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.velord.core.ui.compose.theme
-
-import androidx.activity.ComponentActivity
-import androidx.activity.SystemBarStyle
-import androidx.activity.compose.LocalActivity
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.ColorScheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.platform.LocalContext
-import com.velord.core.ui.compose.shape.MainShapes
-import com.velord.core.ui.compose.theme.color.DarkColorScheme
-import com.velord.core.ui.compose.theme.color.LightColorScheme
-import com.velord.core.ui.util.defineScrimAndDarkScrimColorForSystemBar
-import com.velord.model.setting.AndroidThemeConfig
-import com.velord.model.setting.SpecialTheme
-
-private fun SpecialTheme.toColorScheme(): ColorScheme = when (this) {
- SpecialTheme.DARK -> DarkColorScheme
- SpecialTheme.LIGHT -> LightColorScheme
-}
-
-@Composable
-fun MainTheme(
- abideToOsTheme: Boolean = true,
- useDarkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
- specialTheme: SpecialTheme,
- content: @Composable () -> Unit
-) {
- val apiAvailable = AndroidThemeConfig.isSystemDynamicColorAvailable()
- val colorScheme: ColorScheme = when {
- abideToOsTheme.not() && dynamicColor && apiAvailable -> {
- val context = LocalContext.current
- if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
- abideToOsTheme -> if (useDarkTheme) DarkColorScheme else LightColorScheme
- else -> specialTheme.toColorScheme()
- }
-
- val context = LocalActivity.current as ComponentActivity
- LaunchedEffect(key1 = useDarkTheme, key2 = dynamicColor) {
- val (scrim, darkScrim) = colorScheme
- .defineScrimAndDarkScrimColorForSystemBar(makeTransparent = true)
- val light = SystemBarStyle.light(
- scrim = scrim,
- darkScrim = darkScrim
- )
- val dark = SystemBarStyle.dark(scrim = scrim)
- val statusBarStyle = if (useDarkTheme) dark else light
- context.enableEdgeToEdge(
- statusBarStyle = statusBarStyle,
- navigationBarStyle = statusBarStyle
- )
- }
-
- MaterialTheme(
- colorScheme = colorScheme,
- shapes = MainShapes,
- typography = MainTypography,
- content = content
- )
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/dialog/AlertDialog.kt b/core/core-ui/src/main/java/com/velord/core/ui/dialog/AlertDialog.kt
deleted file mode 100644
index 9876d19d..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/dialog/AlertDialog.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.velord.core.ui.dialog
-
-import android.content.Context
-import android.content.DialogInterface
-import androidx.annotation.StringRes
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-fun Context.alertDialog(
- cancelable: Boolean = true,
- @StringRes title: Int? = null,
- @StringRes message: Int? = null,
- @StringRes positiveText: Int? = null,
- @StringRes negativeText: Int? = null,
- negativeCallback: (() -> Unit)? = null,
- positiveCallback: () -> Unit = {}
-) {
- val messageStr = message?.let { getString(it) }
- alertDialog(
- cancelable = cancelable,
- title = title,
- message = messageStr,
- positiveText = positiveText,
- negativeText = negativeText,
- negativeCallback = negativeCallback,
- positiveCallback = positiveCallback
- )
-}
-
-fun Context.alertDialog(
- cancelable: Boolean = true,
- @StringRes title: Int? = null,
- message: String? = null,
- @StringRes positiveText: Int? = null,
- @StringRes negativeText: Int? = null,
- negativeCallback: (() -> Unit)? = null,
- positiveCallback: () -> Unit = {}
-) {
- val dialog = MaterialAlertDialogBuilder(this)
- .setCancelable(cancelable)
- .create()
-
- positiveText?.let {
- dialog.setButton(
- DialogInterface.BUTTON_POSITIVE,
- this.getString(it)
- ) { _, _ ->
- positiveCallback.invoke()
- }
- }
- title?.let { dialog.setTitle(it) }
- message?.let { dialog.setMessage(message) }
- negativeText?.let {
- dialog.setButton(
- DialogInterface.BUTTON_NEGATIVE,
- this.getString(negativeText)
- ) { _, _ ->
- negativeCallback?.invoke()
- }
- }
-
- dialog.show()
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/ColorScheme.kt b/core/core-ui/src/main/java/com/velord/core/ui/util/ColorScheme.kt
deleted file mode 100644
index 6bb38a1d..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/ColorScheme.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.velord.core.ui.util
-
-import androidx.compose.material3.ColorScheme
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-
-internal fun ColorScheme.defineScrimAndDarkScrimColorForSystemBar(
- makeTransparent: Boolean = true
-): Pair {
- val scrim = if (makeTransparent) Color.Transparent.toArgb() else this.surface.toArgb()
- val darkScrim = if (makeTransparent) Color.Transparent.toArgb() else scrim.inverseColor()
- return scrim to darkScrim
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/ScreenSize.kt b/core/core-ui/src/main/java/com/velord/core/ui/util/ScreenSize.kt
deleted file mode 100644
index 38ed58cf..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/ScreenSize.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.velord.core.ui.util
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.dp
-
-data class ScreenSize(
- val width: Number,
- val height: Number
-)
-
-@Composable
-fun getScreenWidthAndHeightInDp(): ScreenSize {
- val configuration = LocalConfiguration.current
- val width = configuration.screenWidthDp
- val height = configuration.screenHeightDp
- return ScreenSize(width, height)
-}
-
-@Composable
-fun getScreenWidthAndHeightInPx(): ScreenSize {
- val configuration = LocalConfiguration.current
- val density = LocalDensity.current
- val width = with(density) { configuration.screenWidthDp.dp.roundToPx() }
- val height = with(density) { configuration.screenHeightDp.dp.roundToPx() }
- return ScreenSize(width, height)
-}
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/SetContent.kt b/core/core-ui/src/main/java/com/velord/core/ui/util/SetContent.kt
deleted file mode 100644
index b9af9e9b..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/SetContent.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.velord.core.ui.util
-
-import android.annotation.SuppressLint
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.State
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.velord.core.ui.compose.theme.MainTheme
-import com.velord.model.setting.AndroidThemeConfig
-import com.velord.sharedviewmodel.ThemeUiState
-import com.velord.sharedviewmodel.ThemeViewModel
-import com.velord.util.context.getActivity
-
-fun ComponentActivity.setContentWithTheme(
- screen: @Composable ComposeView.() -> Unit
-): ComposeView = ComposeView(this).setContentWithTheme(screen)
-
-fun Fragment.setContentWithTheme(
- screen: @Composable ComposeView.() -> Unit
-): ComposeView = ComposeView(requireContext()).setContentWithTheme(screen)
-
-context(_: LifecycleOwner)
-@SuppressLint("UnrememberedMutableState")
-fun ComposeView.setContentWithTheme(
- screen: @Composable ComposeView.() -> Unit
-): ComposeView = apply {
- setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
- setContent {
- val activity = LocalContext.current.getActivity()
- val themeViewModel = viewModel(activity as ViewModelStoreOwner)
- val themeState: State = themeViewModel.uiStateFlow.collectAsStateWithLifecycle()
-
- val theme = themeState.value?.androidThemeConfig ?: AndroidThemeConfig.DEFAULT
- CompositionLocalProvider(LocalTheme provides theme) {
- val localThemeConfig = LocalTheme.current
- val isDark = if (localThemeConfig.config.abideToOs) {
- isSystemInDarkTheme()
- } else {
- localThemeConfig.config.useDarkTheme
- }
- MainTheme(
- abideToOsTheme = localThemeConfig.config.abideToOs,
- useDarkTheme = isDark,
- dynamicColor = localThemeConfig.config.useDynamicColor,
- specialTheme = localThemeConfig.config.current
- ) {
- screen()
- }
- }
- }
-}
-
-val LocalTheme = staticCompositionLocalOf { AndroidThemeConfig.DEFAULT }
diff --git a/core/core-ui/src/main/java/com/velord/core/ui/util/permission/PermissionState.kt b/core/core-ui/src/main/java/com/velord/core/ui/util/permission/PermissionState.kt
deleted file mode 100644
index 5afa1103..00000000
--- a/core/core-ui/src/main/java/com/velord/core/ui/util/permission/PermissionState.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.velord.core.ui.util.permission
-
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.PermissionStatus
-import com.velord.util.permission.AndroidPermissionState
-
-@OptIn(ExperimentalPermissionsApi::class)
-fun PermissionStatus.toAndroidPermissionState(isRequestedBefore: Boolean): AndroidPermissionState = when (this) {
- is PermissionStatus.Granted -> AndroidPermissionState.Granted
- is PermissionStatus.Denied -> when {
- this.shouldShowRationale -> AndroidPermissionState.Rationale
- this.shouldShowRationale.not() && isRequestedBefore -> AndroidPermissionState.PossiblePermanentDeny
- else -> AndroidPermissionState.Denied
- }
-}
diff --git a/data/appstate/build.gradle.kts b/data/appstate/build.gradle.kts
index c2210442..1452b9b0 100644
--- a/data/appstate/build.gradle.kts
+++ b/data/appstate/build.gradle.kts
@@ -1,16 +1,36 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.kotlin.plugin.serialization)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.appstate"
+kotlin {
+ android {
+ namespace = "com.velord.data.appstate"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Modules
- implementation(project(":model"))
- // Templates
- implementation(libs.bundles.kotlin.module)
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
diff --git a/data/appstate/src/main/java/com/velord/appstate/AppStateService.kt b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
similarity index 54%
rename from data/appstate/src/main/java/com/velord/appstate/AppStateService.kt
rename to data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
index 2749ef86..ef13d456 100644
--- a/data/appstate/src/main/java/com/velord/appstate/AppStateService.kt
+++ b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/AppStateDataSource.kt
@@ -1,26 +1,35 @@
-package com.velord.appstate
+package com.velord.data.appstate
+import com.velord.model.ToastConfig
import com.velord.model.movie.Movie
import com.velord.model.movie.MovieSortOption
import com.velord.model.movie.SortType
import com.velord.model.setting.ThemeConfig
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import org.koin.core.annotation.Single
-interface AppStateService {
+interface AppStateDataSource {
val themeConfigFlow: MutableStateFlow
val movieRosterFlow: MutableStateFlow>
val movieFavoriteRosterFlow: MutableStateFlow>
val movieSortFlow: MutableStateFlow>
+ val toastConfigFlow: MutableSharedFlow
}
-@Single
-class AppStateServiceImpl : AppStateService {
+@Single(binds = [AppStateDataSource::class])
+class AppStateDataSourceImpl : AppStateDataSource {
+
override val themeConfigFlow = MutableStateFlow(ThemeConfig.DEFAULT)
+
override val movieRosterFlow = MutableStateFlow>(emptyList())
override val movieFavoriteRosterFlow = MutableStateFlow>(emptyList())
- override val movieSortFlow = MutableStateFlow(listOf(
- MovieSortOption(SortType.DateDescending, isSelected = true),
- MovieSortOption(SortType.DateAscending, isSelected = false),
- ))
+ override val movieSortFlow = MutableStateFlow(
+ listOf(
+ MovieSortOption(SortType.DateDescending, isSelected = true),
+ MovieSortOption(SortType.DateAscending, isSelected = false),
+ ),
+ )
+
+ override val toastConfigFlow = MutableSharedFlow()
}
diff --git a/data/appstate/src/main/java/com/velord/appstate/DI.kt b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/DI.kt
similarity index 67%
rename from data/appstate/src/main/java/com/velord/appstate/DI.kt
rename to data/appstate/src/commonMain/kotlin/com/velord/data/appstate/DI.kt
index 8e33ac25..62e9fbee 100644
--- a/data/appstate/src/main/java/com/velord/appstate/DI.kt
+++ b/data/appstate/src/commonMain/kotlin/com/velord/data/appstate/DI.kt
@@ -1,10 +1,10 @@
@file:Suppress("MatchingDeclarationName")
-package com.velord.appstate
+package com.velord.data.appstate
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Module
-@ComponentScan("com.velord.appstate")
+@ComponentScan("com.velord.data.appstate")
class AppStateModule
diff --git a/data/appstate/src/main/AndroidManifest.xml b/data/appstate/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/appstate/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/backend/build.gradle.kts b/data/backend/build.gradle.kts
index 151b4d94..406eb7e7 100644
--- a/data/backend/build.gradle.kts
+++ b/data/backend/build.gradle.kts
@@ -1,20 +1,49 @@
plugins {
- alias(libs.plugins.convention.android.library)
+ alias(libs.plugins.convention.kmp.library)
alias(libs.plugins.kotlin.plugin.serialization)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.backend"
+kotlin {
+ android {
+ namespace = "com.velord.data.backend"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(libs.kotlin.serialization.json)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.client.negotiation)
+ implementation(libs.ktor.serialization)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ }
+
+ androidMain.dependencies {
+ implementation(libs.ktor.client.okhttp)
+ }
+
+ desktopMain.dependencies {
+ implementation(libs.ktor.client.okhttp)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Module
- implementation(project(":model"))
- implementation(project(":infrastructure:util"))
- implementation(project(":data:datastore"))
- // Templates
- implementation(libs.androidx.core.ktx)
- implementation(libs.bundles.kotlin.core)
- implementation(libs.bundles.network.all)
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
diff --git a/data/backend/src/androidMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt b/data/backend/src/androidMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt
new file mode 100644
index 00000000..e676645c
--- /dev/null
+++ b/data/backend/src/androidMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt
@@ -0,0 +1,6 @@
+package com.velord.data.backend.ktor
+
+import io.ktor.client.engine.HttpClientEngineFactory
+import io.ktor.client.engine.okhttp.OkHttp
+
+internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = OkHttp
diff --git a/data/backend/src/main/java/com/velord/backend/ktor/BaseHttpClient.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/BaseHttpClient.kt
similarity index 72%
rename from data/backend/src/main/java/com/velord/backend/ktor/BaseHttpClient.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/BaseHttpClient.kt
index bd6cc069..18450c3c 100644
--- a/data/backend/src/main/java/com/velord/backend/ktor/BaseHttpClient.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/BaseHttpClient.kt
@@ -1,13 +1,11 @@
-package com.velord.backend.ktor
+package com.velord.data.backend.ktor
-import android.content.Context
-import com.velord.backend.model.BaseUrl
+import com.velord.data.backend.model.BaseUrl
import io.ktor.client.HttpClient
-import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
-import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
@@ -21,24 +19,26 @@ import io.ktor.http.URLProtocol
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
+import kotlin.time.Clock
private const val DEVICE_TIME_HEADER = "deviceTime"
private const val TIMEOUT = 2000L
-class BaseHttpClient(
- context: Context,
- private val baseUrl: BaseUrl
-) {
+internal expect fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*>
- private val client: HttpClient
-
- init {
- client = configureClient()
+private val defaultLogger = object : Logger {
+ override fun log(message: String) {
+ println(message)
}
+}
+
+class BaseHttpClient(private val baseUrl: BaseUrl) {
+
+ private val client: HttpClient = configureClient()
- private fun configureClient() = HttpClient(OkHttp).config {
+ private fun configureClient() = HttpClient(platformHttpClientEngineFactory()).config {
install(Logging) {
- logger = Logger.DEFAULT
+ logger = defaultLogger
level = LogLevel.BODY
filter { request ->
request.url.host.contains(baseUrl.host)
@@ -56,7 +56,7 @@ class BaseHttpClient(
allowStructuredMapKeys = true
prettyPrint = true
useArrayPolymorphism = true
- }
+ },
)
}
defaultRequest {
@@ -65,7 +65,7 @@ class BaseHttpClient(
url.protocol = URLProtocol.HTTPS
url(baseUrl.full)
- header(DEVICE_TIME_HEADER, System.currentTimeMillis().toString())
+ header(DEVICE_TIME_HEADER, Clock.System.now().toEpochMilliseconds().toString())
}
}
diff --git a/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/DI.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/DI.kt
new file mode 100644
index 00000000..f273c088
--- /dev/null
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/DI.kt
@@ -0,0 +1,21 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.velord.data.backend.ktor
+
+import com.velord.data.backend.model.BaseUrl
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.dsl.module
+
+private const val PROTOCOL_HTTPS = "https"
+private const val MOVIE_HOST = "api.themoviedb.org"
+
+private fun createMovieBaseUrl() = BaseUrl(protocol = PROTOCOL_HTTPS, host = MOVIE_HOST)
+
+val httpModule = module {
+ single { BaseHttpClient(createMovieBaseUrl()) }
+}
+
+@Module
+@ComponentScan("com.velord.data.backend.ktor")
+class BackendModule
diff --git a/data/backend/src/main/java/com/velord/backend/ktor/MovieNetworkService.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/MovieNetworkDataSource.kt
similarity index 67%
rename from data/backend/src/main/java/com/velord/backend/ktor/MovieNetworkService.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/MovieNetworkDataSource.kt
index 22af14dc..c6c6b653 100644
--- a/data/backend/src/main/java/com/velord/backend/ktor/MovieNetworkService.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/ktor/MovieNetworkDataSource.kt
@@ -1,28 +1,28 @@
-package com.velord.backend.ktor
+package com.velord.data.backend.ktor
-import com.velord.backend.model.MoviePageRequest
-import com.velord.backend.model.MovieRosterResponse
+import com.velord.data.backend.model.MoviePageRequest
+import com.velord.data.backend.model.MovieRosterResponse
import io.ktor.client.call.body
import io.ktor.client.request.header
import io.ktor.http.path
import org.koin.core.annotation.Provided
import org.koin.core.annotation.Single
-interface MovieNetworkService {
+interface MovieNetworkDataSource {
suspend fun getMovie(page: MoviePageRequest): MovieRosterResponse
}
private const val AUTHORIZATION_HEADER = "Authorization"
private const val BEARER =
"Bearer eyJhbGciOiJIUzI1NiJ9" +
- ".eyJhdWQiOiJjYjNiOGJhNDk4ZGFiOTUzYmZhYzVhMTI4YzQ0ZWM2ZSIsIm5iZiI6MTcyMTMzNTMzOC4yMDgxMzYs" +
- "InN1YiI6IjY2OTk3ZDE0OTU3YjM2NWNjOGZkNzIwOCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ" +
- ".VoVoq1XiABNy-3i8BUe-WXvg7Sp5AjDfrF-yFMmh1eM"
+ ".eyJhdWQiOiJjYjNiOGJhNDk4ZGFiOTUzYmZhYzVhMTI4YzQ0ZWM2ZSIsIm5iZiI6MTcyMTMzNTMzOC4yMDgxMzYs" +
+ "InN1YiI6IjY2OTk3ZDE0OTU3YjM2NWNjOGZkNzIwOCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ" +
+ ".VoVoq1XiABNy-3i8BUe-WXvg7Sp5AjDfrF-yFMmh1eM"
@Single
-class MovieNetworkServiceImpl(
- @Provided private val client: BaseHttpClient
-) : MovieNetworkService {
+class MovieNetworkDataSourceImpl(
+ @Provided private val client: BaseHttpClient,
+) : MovieNetworkDataSource {
override suspend fun getMovie(page: MoviePageRequest): MovieRosterResponse {
val url = "/3/discover/movie"
diff --git a/data/backend/src/main/java/com/velord/backend/model/BaseUrl.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/BaseUrl.kt
similarity index 64%
rename from data/backend/src/main/java/com/velord/backend/model/BaseUrl.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/model/BaseUrl.kt
index d5675304..d1e3e4c6 100644
--- a/data/backend/src/main/java/com/velord/backend/model/BaseUrl.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/BaseUrl.kt
@@ -1,8 +1,8 @@
-package com.velord.backend.model
+package com.velord.data.backend.model
data class BaseUrl(
val protocol: String,
- val host: String
+ val host: String,
) {
val full: String
get() = "$protocol://$host"
diff --git a/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/ErrorResponse.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/ErrorResponse.kt
new file mode 100644
index 00000000..0f755f0c
--- /dev/null
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/ErrorResponse.kt
@@ -0,0 +1,6 @@
+package com.velord.data.backend.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ErrorResponse(val success: Boolean = true, val errors: Map = emptyMap())
diff --git a/data/backend/src/main/java/com/velord/backend/model/MoviePageRequest.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MoviePageRequest.kt
similarity index 81%
rename from data/backend/src/main/java/com/velord/backend/model/MoviePageRequest.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MoviePageRequest.kt
index 917aff40..2d436de0 100644
--- a/data/backend/src/main/java/com/velord/backend/model/MoviePageRequest.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MoviePageRequest.kt
@@ -1,4 +1,4 @@
-package com.velord.backend.model
+package com.velord.data.backend.model
import com.velord.model.movie.FilterType
diff --git a/data/backend/src/main/java/com/velord/backend/model/MovieResponse.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieResponse.kt
similarity index 90%
rename from data/backend/src/main/java/com/velord/backend/model/MovieResponse.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieResponse.kt
index 62773cd0..1e76f743 100644
--- a/data/backend/src/main/java/com/velord/backend/model/MovieResponse.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieResponse.kt
@@ -1,4 +1,4 @@
-package com.velord.backend.model
+package com.velord.data.backend.model
import com.velord.model.movie.Movie
import kotlinx.serialization.SerialName
@@ -25,7 +25,7 @@ data class MovieResponse(
title = title,
description = description,
isLiked = false,
- date = Movie.toCalendar(date),
+ date = Movie.toInstant(date),
rating = rating,
voteCount = voteCount,
imagePath = imageUrl,
diff --git a/data/backend/src/main/java/com/velord/backend/model/MovieRosterResponse.kt b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieRosterResponse.kt
similarity index 79%
rename from data/backend/src/main/java/com/velord/backend/model/MovieRosterResponse.kt
rename to data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieRosterResponse.kt
index e8d6392d..fad56ea0 100644
--- a/data/backend/src/main/java/com/velord/backend/model/MovieRosterResponse.kt
+++ b/data/backend/src/commonMain/kotlin/com/velord/data/backend/model/MovieRosterResponse.kt
@@ -1,4 +1,4 @@
-package com.velord.backend.model
+package com.velord.data.backend.model
import kotlinx.serialization.Serializable
diff --git a/data/backend/src/desktopMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt b/data/backend/src/desktopMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt
new file mode 100644
index 00000000..e676645c
--- /dev/null
+++ b/data/backend/src/desktopMain/kotlin/com/velord/data/backend/ktor/PlatformHttpClient.kt
@@ -0,0 +1,6 @@
+package com.velord.data.backend.ktor
+
+import io.ktor.client.engine.HttpClientEngineFactory
+import io.ktor.client.engine.okhttp.OkHttp
+
+internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = OkHttp
diff --git a/data/backend/src/main/AndroidManifest.xml b/data/backend/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/backend/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/backend/src/main/java/com/velord/backend/ktor/DI.kt b/data/backend/src/main/java/com/velord/backend/ktor/DI.kt
deleted file mode 100644
index b7cd4045..00000000
--- a/data/backend/src/main/java/com/velord/backend/ktor/DI.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-@file:Suppress("MatchingDeclarationName")
-
-package com.velord.backend.ktor
-
-import com.velord.backend.model.BaseUrl
-import org.koin.core.annotation.ComponentScan
-import org.koin.core.annotation.Module
-import org.koin.dsl.module
-
-private const val PROTOCOL_HTTPS = "https"
-private const val MOVIE_HOST = "api.themoviedb.org"
-
-private fun createMovieBaseUrl() = BaseUrl(
- protocol = PROTOCOL_HTTPS,
- host = MOVIE_HOST
-)
-
-val httpModule = module {
- single { BaseHttpClient(get(), createMovieBaseUrl()) }
-}
-
-@Module
-@ComponentScan("com.velord.backend.ktor")
-class BackendModule
diff --git a/data/backend/src/main/java/com/velord/backend/model/ErrorResponse.kt b/data/backend/src/main/java/com/velord/backend/model/ErrorResponse.kt
deleted file mode 100644
index a701b663..00000000
--- a/data/backend/src/main/java/com/velord/backend/model/ErrorResponse.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.velord.backend.model
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class ErrorResponse(
- val success: Boolean = true,
- val errors: Map = emptyMap()
-)
diff --git a/data/datastore/build.gradle.kts b/data/datastore/build.gradle.kts
index 067be896..caba2410 100644
--- a/data/datastore/build.gradle.kts
+++ b/data/datastore/build.gradle.kts
@@ -1,17 +1,39 @@
plugins {
- alias(libs.plugins.convention.android.library)
+ alias(libs.plugins.convention.kmp.library)
alias(libs.plugins.kotlin.plugin.serialization)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.datastore"
+kotlin {
+ android {
+ namespace = "com.velord.data.datastore"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ implementation(libs.androidx.datastore.core.okio)
+ implementation(libs.kotlin.serialization.json)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Modules
- implementation(project(":model"))
- // Templates
- implementation(libs.androidx.datastore)
- implementation(libs.kotlin.serialization.json)
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
diff --git a/data/datastore/src/androidMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt b/data/datastore/src/androidMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
new file mode 100644
index 00000000..6f82c753
--- /dev/null
+++ b/data/datastore/src/androidMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
@@ -0,0 +1,11 @@
+package com.velord.data.datastore
+
+import android.content.Context
+import okio.Path
+import okio.Path.Companion.toPath
+import org.koin.core.context.GlobalContext
+
+internal actual fun appSettingStorePath(): Path {
+ val appContext: Context = GlobalContext.get().get()
+ return "${appContext.filesDir.absolutePath}/setting.json".toPath()
+}
diff --git a/data/datastore/src/main/java/com/velord/datastore/DI.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DI.kt
similarity index 51%
rename from data/datastore/src/main/java/com/velord/datastore/DI.kt
rename to data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DI.kt
index 724dd4e7..7994c07f 100644
--- a/data/datastore/src/main/java/com/velord/datastore/DI.kt
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DI.kt
@@ -1,10 +1,8 @@
-@file:Suppress("MatchingDeclarationName")
-
-package com.velord.datastore
+package com.velord.data.datastore
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Module
-@ComponentScan("com.velord.datastore")
+@ComponentScan("com.velord.data.datastore")
class DataStoreModule
diff --git a/data/datastore/src/main/java/com/velord/datastore/DataStoreService.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
similarity index 72%
rename from data/datastore/src/main/java/com/velord/datastore/DataStoreService.kt
rename to data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
index 086575e8..60abe744 100644
--- a/data/datastore/src/main/java/com/velord/datastore/DataStoreService.kt
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/DataStoreDataSource.kt
@@ -1,6 +1,6 @@
-package com.velord.datastore
+package com.velord.data.datastore
-import com.velord.datastore.appSetting.AppSettingDataStore
+import com.velord.data.datastore.appSetting.AppSettingDataStore
import com.velord.model.setting.AppSetting
import com.velord.model.setting.ThemeConfig
import kotlinx.coroutines.flow.Flow
@@ -8,16 +8,16 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
-interface DataStoreService {
+interface DataStoreDataSource {
suspend fun checkAppFirstLaunch(): Boolean
suspend fun setThemeConfig(theme: ThemeConfig)
suspend fun getAppSettingFlow(): Flow
}
-@Single
-class DataStoreServiceImpl(
- private val appSetting: AppSettingDataStore
-) : DataStoreService {
+@Single(binds = [DataStoreDataSource::class])
+class DataStoreDataSourceImpl(
+ private val appSetting: AppSettingDataStore,
+) : DataStoreDataSource {
private suspend fun setFirstLaunch() {
appSetting.updateData {
@@ -30,12 +30,13 @@ class DataStoreServiceImpl(
if (isFirstLaunch) setFirstLaunch()
isFirstLaunch
- }.first()
+ }
+ .first()
override suspend fun setThemeConfig(theme: ThemeConfig) {
appSetting.updateData {
- it.copy(theme = theme)
- }
+ it.copy(theme = theme)
+ }
}
override suspend fun getAppSettingFlow(): Flow = appSetting.flow
diff --git a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
new file mode 100644
index 00000000..71e65727
--- /dev/null
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
@@ -0,0 +1,5 @@
+package com.velord.data.datastore
+
+import okio.Path
+
+internal expect fun appSettingStorePath(): Path
diff --git a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStore.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStore.kt
new file mode 100644
index 00000000..23ea9548
--- /dev/null
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStore.kt
@@ -0,0 +1,13 @@
+package com.velord.data.datastore.appSetting
+
+import com.velord.model.setting.AppSetting
+import kotlinx.coroutines.flow.Flow
+
+interface AppSettingDataStore {
+
+ val flow: Flow
+
+ suspend fun updateData(
+ transform: suspend (value: AppSetting) -> AppSetting,
+ ): Result
+}
diff --git a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreImpl.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreImpl.kt
new file mode 100644
index 00000000..153a6188
--- /dev/null
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreImpl.kt
@@ -0,0 +1,31 @@
+package com.velord.data.datastore.appSetting
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.okio.OkioStorage
+import com.velord.data.datastore.appSettingStorePath
+import com.velord.model.setting.AppSetting
+import kotlinx.coroutines.flow.Flow
+import okio.FileSystem
+import org.koin.core.annotation.Single
+
+@Single
+class AppSettingDataStoreImpl : AppSettingDataStore {
+
+ private val dataStore: DataStore = DataStoreFactory.create(
+ storage = OkioStorage(
+ fileSystem = FileSystem.SYSTEM,
+ serializer = AppSettingDataStoreSerializer,
+ producePath = ::appSettingStorePath,
+ ),
+ )
+
+ override val flow: Flow
+ get() = dataStore.data
+
+ override suspend fun updateData(
+ transform: suspend (value: AppSetting) -> AppSetting,
+ ): Result = Result.runCatching {
+ dataStore.updateData { transform(it) }
+ }
+}
diff --git a/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreSerializer.kt b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreSerializer.kt
new file mode 100644
index 00000000..ec169a38
--- /dev/null
+++ b/data/datastore/src/commonMain/kotlin/com/velord/data/datastore/appSetting/AppSettingDataStoreSerializer.kt
@@ -0,0 +1,23 @@
+package com.velord.data.datastore.appSetting
+
+import androidx.datastore.core.okio.OkioSerializer
+import com.velord.model.setting.AppSetting
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
+import okio.BufferedSink
+import okio.BufferedSource
+
+object AppSettingDataStoreSerializer : OkioSerializer {
+
+ override val defaultValue: AppSetting = AppSetting.DEFAULT
+
+ override suspend fun readFrom(source: BufferedSource): AppSetting = try {
+ Json.decodeFromString(deserializer = AppSetting.serializer(), string = source.readUtf8())
+ } catch (exception: SerializationException) {
+ defaultValue
+ }
+
+ override suspend fun writeTo(t: AppSetting, sink: BufferedSink) {
+ sink.writeUtf8(Json.encodeToString(serializer = AppSetting.serializer(), value = t))
+ }
+}
diff --git a/data/datastore/src/desktopMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt b/data/datastore/src/desktopMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
new file mode 100644
index 00000000..28c8afad
--- /dev/null
+++ b/data/datastore/src/desktopMain/kotlin/com/velord/data/datastore/PlatformStorePath.kt
@@ -0,0 +1,11 @@
+package com.velord.data.datastore
+
+import okio.FileSystem
+import okio.Path
+import okio.Path.Companion.toPath
+
+internal actual fun appSettingStorePath(): Path {
+ val dir = "${System.getProperty("user.home")}/.velord".toPath()
+ FileSystem.SYSTEM.createDirectories(dir)
+ return "$dir/setting.json".toPath()
+}
diff --git a/data/datastore/src/main/AndroidManifest.xml b/data/datastore/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/datastore/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStore.kt b/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStore.kt
deleted file mode 100644
index b8afc8e5..00000000
--- a/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStore.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.velord.datastore.appSetting
-
-import android.content.Context
-import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
-import androidx.datastore.dataStore
-import com.velord.model.setting.AppSetting
-import kotlinx.coroutines.flow.Flow
-import org.koin.core.annotation.Single
-
-interface AppSettingDataStore {
- val flow: Flow
- suspend fun updateData(transform: suspend (t: AppSetting) -> AppSetting): Result
-}
-
-@Single
-class AppSettingDataStoreImpl(
- private val context: Context
-) : AppSettingDataStore {
-
- private val Context.dataStore by dataStore(
- fileName = "setting",
- serializer = AppSettingDataStoreSerializer,
- corruptionHandler = ReplaceFileCorruptionHandler {
- AppSetting.DEFAULT
- }
- )
-
- override val flow: Flow
- get() = context.dataStore.data
-
- override suspend fun updateData(
- transform: suspend (t: AppSetting) -> AppSetting
- ) = Result.runCatching {
- context.dataStore.updateData {
- transform(it)
- }
- }
-}
diff --git a/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStoreSerializer.kt b/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStoreSerializer.kt
deleted file mode 100644
index cb86a81b..00000000
--- a/data/datastore/src/main/java/com/velord/datastore/appSetting/AppSettingDataStoreSerializer.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.velord.datastore.appSetting
-
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.Serializer
-import com.velord.model.setting.AppSetting
-import kotlinx.serialization.SerializationException
-import kotlinx.serialization.json.Json
-import java.io.InputStream
-import java.io.OutputStream
-
-object AppSettingDataStoreSerializer : Serializer {
-
- override val defaultValue: AppSetting = AppSetting.DEFAULT
-
- override suspend fun readFrom(input: InputStream): AppSetting = try {
- Json.decodeFromString(
- deserializer = AppSetting.serializer(),
- string = input.readBytes().decodeToString()
- )
- } catch (e: SerializationException) {
-// Possible variant
-// e.printStackTrace()
-// defaultValue
- throw CorruptionException("Unable to read ${AppSetting::class.simpleName}", e)
- }
-
- override suspend fun writeTo(t: AppSetting, output: OutputStream) {
- output.write(
- Json.encodeToString(
- serializer = AppSetting.serializer(),
- value = t
- ).encodeToByteArray()
- )
- }
-}
diff --git a/data/db/build.gradle.kts b/data/db/build.gradle.kts
index e9d7fdde..120c855b 100644
--- a/data/db/build.gradle.kts
+++ b/data/db/build.gradle.kts
@@ -1,16 +1,39 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.db"
+kotlin {
+ android {
+ namespace = "com.velord.data.db"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ implementation(libs.androidx.room.runtime)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Modules
- implementation(project(":model"))
- // Room
- ksp(libs.androidx.room.compiler)
- implementation(libs.bundles.androidx.room.coroutines)
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.androidx.room.compiler)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.androidx.room.compiler)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
diff --git a/data/db/src/androidMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt b/data/db/src/androidMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
new file mode 100644
index 00000000..9ae064ff
--- /dev/null
+++ b/data/db/src/androidMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
@@ -0,0 +1,12 @@
+package com.velord.data.db
+
+import android.content.Context
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import org.koin.core.context.GlobalContext
+
+internal actual fun appDatabaseBuilder(): RoomDatabase.Builder {
+ val appContext: Context = GlobalContext.get().get()
+ val dbFile = appContext.getDatabasePath("velord.db")
+ return Room.databaseBuilder(context = appContext, name = dbFile.absolutePath)
+}
diff --git a/data/db/src/main/java/com/velord/db/AppDatabase.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/AppDatabase.kt
similarity index 65%
rename from data/db/src/main/java/com/velord/db/AppDatabase.kt
rename to data/db/src/commonMain/kotlin/com/velord/data/db/AppDatabase.kt
index 86600daa..d41fe599 100644
--- a/data/db/src/main/java/com/velord/db/AppDatabase.kt
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/AppDatabase.kt
@@ -1,9 +1,9 @@
-package com.velord.db
+package com.velord.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
-import com.velord.db.movie.MovieDao
-import com.velord.db.movie.MovieEntity
+import com.velord.data.db.movie.MovieDao
+import com.velord.data.db.movie.MovieEntity
@Database(entities = [MovieEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
diff --git a/data/db/src/main/java/com/velord/db/DI.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/DI.kt
similarity index 53%
rename from data/db/src/main/java/com/velord/db/DI.kt
rename to data/db/src/commonMain/kotlin/com/velord/data/db/DI.kt
index 64085cce..d881d05c 100644
--- a/data/db/src/main/java/com/velord/db/DI.kt
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/DI.kt
@@ -1,30 +1,25 @@
@file:Suppress("MatchingDeclarationName")
-package com.velord.db
+package com.velord.data.db
-import android.content.Context
-import androidx.room.Room
-import com.velord.db.movie.MovieDao
+import com.velord.data.db.movie.MovieDao
+import kotlinx.coroutines.Dispatchers
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
import org.koin.dsl.module
-private fun createDatabase(context: Context): AppDatabase = Room
- .databaseBuilder(
- context,
- AppDatabase::class.java,
- "velord"
- )
+private fun createDatabase(): AppDatabase = appDatabaseBuilder()
.fallbackToDestructiveMigration(false)
+ .setQueryCoroutineContext(Dispatchers.IO)
.build()
private fun provideMovieDao(database: AppDatabase): MovieDao = database.movieDao()
val databaseModule = module {
- single { createDatabase(get()) }
+ single { createDatabase() }
single { provideMovieDao(get()) }
}
@Module
-@ComponentScan("com.velord.db")
+@ComponentScan("com.velord.data.db")
class DbModule
diff --git a/data/db/src/commonMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
new file mode 100644
index 00000000..7a489f5c
--- /dev/null
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
@@ -0,0 +1,5 @@
+package com.velord.data.db
+
+import androidx.room.RoomDatabase
+
+internal expect fun appDatabaseBuilder(): RoomDatabase.Builder
diff --git a/data/db/src/main/java/com/velord/db/movie/MovieDao.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieDao.kt
similarity index 87%
rename from data/db/src/main/java/com/velord/db/movie/MovieDao.kt
rename to data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieDao.kt
index d377070d..38ff3890 100644
--- a/data/db/src/main/java/com/velord/db/movie/MovieDao.kt
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieDao.kt
@@ -1,4 +1,4 @@
-package com.velord.db.movie
+package com.velord.data.db.movie
import androidx.room.Dao
import androidx.room.Delete
@@ -17,24 +17,26 @@ interface MovieDao {
@Query("SELECT * FROM MovieEntity")
suspend fun getAll(): List
- @Query("SELECT * FROM MovieEntity WHERE isLiked = 1 " +
+ @Query(
+ "SELECT * FROM MovieEntity WHERE isLiked = 1 " +
"ORDER BY " +
"CASE WHEN :orderBy = 'date' AND :sortOrder = 0 THEN date END DESC, " +
- "CASE WHEN :orderBy = 'date' AND :sortOrder = 1 THEN date END ASC "
+ "CASE WHEN :orderBy = 'date' AND :sortOrder = 1 THEN date END ASC ",
)
fun getAllLikedFlow(
orderBy: String,
sortOrder: Int,
): Flow>
- @Query("SELECT * FROM MovieEntity WHERE " +
+ @Query(
+ "SELECT * FROM MovieEntity WHERE " +
"rating BETWEEN :ratingStart AND :ratingEnd " +
"AND voteCount BETWEEN :voteCountStart AND :voteCountEnd " +
"ORDER BY " +
"CASE WHEN :orderBy = 'date' AND :sortOrder = 0 THEN date END DESC, " +
"CASE WHEN :orderBy = 'date' AND :sortOrder = 1 THEN date END ASC " +
"LIMIT :pageSize " +
- "OFFSET :offset"
+ "OFFSET :offset",
)
suspend fun getFirstPage(
ratingStart: Float,
@@ -44,7 +46,7 @@ interface MovieDao {
orderBy: String,
sortOrder: Int,
pageSize: Int,
- offset: Int
+ offset: Int,
): List
@Update
diff --git a/data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieEntity.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieEntity.kt
new file mode 100644
index 00000000..8f4085da
--- /dev/null
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/MovieEntity.kt
@@ -0,0 +1,49 @@
+package com.velord.data.db.movie
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.velord.model.movie.Movie
+
+@Entity(tableName = "MovieEntity")
+data class MovieEntity(
+ @PrimaryKey
+ val id: Int,
+ @ColumnInfo
+ val title: String,
+ @ColumnInfo
+ val description: String,
+ @ColumnInfo
+ val isLiked: Boolean,
+ @ColumnInfo
+ val date: String,
+ @ColumnInfo
+ val rating: Float,
+ @ColumnInfo
+ val voteCount: Int,
+ @ColumnInfo
+ val imagePath: String?,
+) {
+
+ fun toDomain(): Movie = Movie(
+ id = id,
+ title = title,
+ description = description,
+ isLiked = isLiked,
+ date = Movie.toInstant(date),
+ rating = rating,
+ voteCount = voteCount,
+ imagePath = imagePath,
+ )
+}
+
+internal fun Movie.toEntity(): MovieEntity = MovieEntity(
+ id = id,
+ title = title,
+ description = description,
+ isLiked = isLiked,
+ date = Movie.toRaw(date),
+ rating = rating,
+ voteCount = voteCount,
+ imagePath = imagePath,
+)
diff --git a/data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSource.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSource.kt
new file mode 100644
index 00000000..afc288cb
--- /dev/null
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSource.kt
@@ -0,0 +1,19 @@
+package com.velord.data.db.movie.dataSource
+
+import com.velord.model.movie.FilterType
+import com.velord.model.movie.Movie
+import com.velord.model.movie.SortType
+import kotlinx.coroutines.flow.Flow
+
+interface MovieDbDataSource {
+ suspend fun getPage(
+ page: Int,
+ sortType: SortType,
+ filterRoster: List,
+ ): List
+
+ suspend fun insertAll(movies: List)
+ suspend fun update(movie: Movie)
+ fun getAllLikedFlow(sortType: SortType): Flow>
+ suspend fun clear()
+}
diff --git a/data/db/src/main/java/com/velord/db/movie/MovieDbService.kt b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSourceImpl.kt
similarity index 60%
rename from data/db/src/main/java/com/velord/db/movie/MovieDbService.kt
rename to data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSourceImpl.kt
index 7372312c..846ac674 100644
--- a/data/db/src/main/java/com/velord/db/movie/MovieDbService.kt
+++ b/data/db/src/commonMain/kotlin/com/velord/data/db/movie/dataSource/MovieDbDataSourceImpl.kt
@@ -1,12 +1,14 @@
-package com.velord.db.movie
+package com.velord.data.db.movie.dataSource
+import com.velord.data.db.movie.MovieDao
+import com.velord.data.db.movie.MovieEntity
+import com.velord.data.db.movie.toEntity
import com.velord.model.movie.FilterType
import com.velord.model.movie.Movie
import com.velord.model.movie.MoviePagination
import com.velord.model.movie.SortType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
-import org.koin.core.annotation.Provided
import org.koin.core.annotation.Single
private fun SortType.toSortOrder(): Int = when (this) {
@@ -14,39 +16,25 @@ private fun SortType.toSortOrder(): Int = when (this) {
SortType.DateDescending -> 0
}
-interface MovieDbService {
- suspend fun getPage(
- page: Int,
- sortType: SortType,
- filterRoster: List
- ): List
- suspend fun insertAll(movies: List)
- suspend fun update(movie: Movie)
- fun getAllLikedFlow(sortType: SortType): Flow>
- suspend fun clear()
-}
-
-@Single
-class MovieDbServiceImpl(
- @Provided private val db: MovieDao
-) : MovieDbService {
+@Single(binds = [MovieDbDataSource::class])
+class MovieDbDataSourceImpl internal constructor(private val dao: MovieDao) : MovieDbDataSource {
override suspend fun getPage(
page: Int,
sortType: SortType,
- filterRoster: List
+ filterRoster: List,
): List {
val sortOrder = sortType.toSortOrder()
val rating: FilterType.Rating = filterRoster
.firstOrNull { it is FilterType.Rating } as? FilterType.Rating
- ?: FilterType.Rating.Default
+ ?: FilterType.Rating.DEFAULT
val voteCount: FilterType.VoteCount = filterRoster
.firstOrNull { it is FilterType.VoteCount } as? FilterType.VoteCount
- ?: FilterType.VoteCount.Default
+ ?: FilterType.VoteCount.DEFAULT
val offset = MoviePagination.calculateOffset(page)
- val movieFromDbRoster = db.getFirstPage(
+ val movieFromDbRoster = dao.getFirstPage(
ratingStart = rating.start,
ratingEnd = rating.end,
voteCountStart = voteCount.start,
@@ -54,29 +42,34 @@ class MovieDbServiceImpl(
sortOrder = sortOrder,
orderBy = "date",
pageSize = MoviePagination.PAGE_COUNT,
- offset = offset
- )
+ offset = offset,
+ ).map(MovieEntity::toDomain)
- return movieFromDbRoster.map { it.toDomain() }
+ return movieFromDbRoster
}
override suspend fun insertAll(movies: List) {
- db.insertAll(*movies.map { MovieEntity(it) }.toTypedArray())
+ val entityRoster = movies
+ .map(Movie::toEntity)
+ .toTypedArray()
+ dao.insertAll(*entityRoster)
}
override suspend fun update(movie: Movie) {
- db.update(MovieEntity(movie))
+ dao.update(movie.toEntity())
}
override fun getAllLikedFlow(sortType: SortType): Flow> {
val sortOrder = sortType.toSortOrder()
- return db.getAllLikedFlow(
+ return dao.getAllLikedFlow(
sortOrder = sortOrder,
orderBy = "date",
- ).map { it.map { entity -> entity.toDomain() } }
+ ).map { roster ->
+ roster.map(MovieEntity::toDomain)
+ }
}
override suspend fun clear() {
- db.clear()
+ dao.clear()
}
}
diff --git a/data/db/src/desktopMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt b/data/db/src/desktopMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
new file mode 100644
index 00000000..d21afe3c
--- /dev/null
+++ b/data/db/src/desktopMain/kotlin/com/velord/data/db/PlatformDatabaseBuilder.kt
@@ -0,0 +1,11 @@
+package com.velord.data.db
+
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import java.io.File
+
+internal actual fun appDatabaseBuilder(): RoomDatabase.Builder {
+ val dbFile = File(System.getProperty("user.home"), ".velord/velord.db")
+ dbFile.parentFile?.mkdirs()
+ return Room.databaseBuilder(name = dbFile.absolutePath)
+}
diff --git a/data/db/src/main/AndroidManifest.xml b/data/db/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/db/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/db/src/main/java/com/velord/db/movie/MovieEntity.kt b/data/db/src/main/java/com/velord/db/movie/MovieEntity.kt
deleted file mode 100644
index 69aae62e..00000000
--- a/data/db/src/main/java/com/velord/db/movie/MovieEntity.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.velord.db.movie
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import com.velord.model.movie.Movie
-
-@Entity(tableName = "MovieEntity")
-data class MovieEntity(
- @PrimaryKey
- val id: Int,
- @ColumnInfo
- val title: String,
- @ColumnInfo
- val description: String,
- @ColumnInfo
- val isLiked: Boolean,
- @ColumnInfo
- val date: String,
- @ColumnInfo
- val rating: Float,
- @ColumnInfo
- val voteCount: Int,
- @ColumnInfo
- val imagePath: String?,
-) {
-
- fun toDomain(): Movie {
- return Movie(
- id = id,
- title = title,
- description = description,
- isLiked = isLiked,
- date = Movie.toCalendar(date),
- rating = rating,
- voteCount = voteCount,
- imagePath = imagePath,
- )
- }
-
- companion object {
- operator fun invoke(movie: Movie): MovieEntity = MovieEntity(
- id = movie.id,
- title = movie.title,
- description = movie.description,
- isLiked = movie.isLiked,
- date = Movie.toRaw(movie.date),
- rating = movie.rating,
- voteCount = movie.voteCount,
- imagePath = movie.imagePath,
- )
- }
-}
diff --git a/data/gateway/build.gradle.kts b/data/gateway/build.gradle.kts
index 2c51d7a4..4880ca4d 100644
--- a/data/gateway/build.gradle.kts
+++ b/data/gateway/build.gradle.kts
@@ -1,21 +1,43 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.gateway"
+kotlin {
+ android {
+ namespace = "com.velord.data.gateway"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(projects.data.appstate)
+ implementation(projects.data.backend)
+ implementation(projects.data.datastore)
+ implementation(projects.data.db)
+ implementation(projects.data.os)
+ implementation(projects.domain.usecaseMovie)
+ implementation(projects.domain.usecaseCamera)
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.kermit)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Modules
- implementation(project(":model"))
- // Data
- implementation(project(":data:datastore"))
- implementation(project(":data:appstate"))
- implementation(project(":data:backend"))
- implementation(project(":data:db"))
- // Use case
- implementation(project(":domain:usecase-setting"))
- implementation(project(":domain:usecase-movie"))
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt
new file mode 100644
index 00000000..a3fb3aa7
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/DI.kt
@@ -0,0 +1,23 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.velord.data.gateway
+
+import com.velord.data.appstate.AppStateModule
+import com.velord.data.backend.ktor.BackendModule
+import com.velord.data.datastore.DataStoreModule
+import com.velord.data.db.DbModule
+import com.velord.data.os.OsModule
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module(
+ includes = [
+ AppStateModule::class,
+ DbModule::class,
+ BackendModule::class,
+ DataStoreModule::class,
+ OsModule::class
+ ],
+)
+@ComponentScan("com.velord.data.gateway")
+class GatewayModule
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/camera/CameraGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/camera/CameraGateway.kt
new file mode 100644
index 00000000..43303efb
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/camera/CameraGateway.kt
@@ -0,0 +1,17 @@
+package com.velord.data.gateway.camera
+
+import com.velord.data.os.camera.CameraDataSource
+import com.velord.model.camera.RecordingSession
+import com.velord.model.camera.VideoCaptureWrapper
+import org.koin.core.annotation.Single
+
+@Single
+class CameraGateway(private val dataSource: CameraDataSource) {
+ fun startRecording(
+ videoCapture: VideoCaptureWrapper,
+ audioEnabled: Boolean,
+ ): RecordingSession = dataSource.startRecording(
+ videoCapture = videoCapture,
+ audioEnabled = audioEnabled,
+ )
+}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieByGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieByGateway.kt
new file mode 100644
index 00000000..9979c2fb
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieByGateway.kt
@@ -0,0 +1,41 @@
+package com.velord.data.gateway.movie
+
+import com.velord.model.movie.SortType
+import com.velord.usecase.movie.model.MovieFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import org.koin.core.annotation.Single
+
+@Single
+class MovieByGateway(
+ private val movieGateway: MovieGateway,
+ private val movieSortGateway: MovieSortGateway,
+ private val movieFavoriteReader: MovieFavoriteReader,
+) {
+
+ fun getBySort(): MovieFlow {
+ val all = movieGateway.getFlow()
+ val sort = movieSortGateway.getSelectedFlow()
+ return MovieFlow(
+ all.combine(sort) { movies, sortOption ->
+ when (sortOption.type) {
+ SortType.DateDescending -> movies.sortedByDescending { it.date }
+ SortType.DateAscending -> movies.sortedBy { it.date }
+ }
+ }.catch {},
+ )
+ }
+
+ fun getByFavorite(): MovieFlow {
+ val favorite = movieFavoriteReader.getFlow()
+ val sort = movieSortGateway.getSelectedFlow()
+ return MovieFlow(
+ favorite.combine(sort) { movies, sortOption ->
+ when (sortOption.type) {
+ SortType.DateDescending -> movies.sortedByDescending { it.date }
+ SortType.DateAscending -> movies.sortedBy { it.date }
+ }
+ }.catch {},
+ )
+ }
+}
diff --git a/data/gateway/src/main/java/com/velord/gateway/movie/MovieFavoriteGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteGateway.kt
similarity index 57%
rename from data/gateway/src/main/java/com/velord/gateway/movie/MovieFavoriteGateway.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteGateway.kt
index dec0d215..0f34f9e5 100644
--- a/data/gateway/src/main/java/com/velord/gateway/movie/MovieFavoriteGateway.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteGateway.kt
@@ -1,10 +1,9 @@
-package com.velord.gateway.movie
+package com.velord.data.gateway.movie
-import android.util.Log
-import com.velord.appstate.AppStateService
-import com.velord.db.movie.MovieDbService
+import co.touchlab.kermit.Logger
+import com.velord.data.appstate.AppStateDataSource
+import com.velord.data.db.movie.dataSource.MovieDbDataSource
import com.velord.model.movie.Movie
-import com.velord.usecase.movie.dataSource.MovieFavoriteDS
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -14,22 +13,22 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
-@Single
+private val log = Logger.withTag("MovieFavoriteGateway")
+
+@Single(binds = [MovieFavoriteReader::class])
class MovieFavoriteGateway(
- private val appState: AppStateService,
- private val db: MovieDbService,
- private val movieSortGateway: MovieSortGateway
-) : MovieFavoriteDS {
+ private val appState: AppStateDataSource,
+ private val db: MovieDbDataSource,
+ private val movieSortGateway: MovieSortGateway,
+) : MovieFavoriteReader {
private val errorHandler = CoroutineExceptionHandler { _, throwable ->
- Log.d("MovieFavoriteGateway", "CoroutineExceptionHandler: $throwable")
+ log.d { "CoroutineExceptionHandler: $throwable" }
}
private val scope = CoroutineScope(Job() + Dispatchers.IO + errorHandler)
init {
scope.launch {
- // Do we need sorting here ?
- // We always need full list of liked movies
val sortType = movieSortGateway.getSelected().type
db.getAllLikedFlow(sortType).collect {
appState.movieFavoriteRosterFlow.value = it
@@ -37,21 +36,22 @@ class MovieFavoriteGateway(
}
}
- override fun get(): List = appState.movieFavoriteRosterFlow.value
+ fun get(): List = appState.movieFavoriteRosterFlow.value
override fun getFlow(): Flow> = appState.movieFavoriteRosterFlow
- override suspend fun update(movie: Movie) {
+ suspend fun update(movie: Movie) {
+ val updated = movie.copy(isLiked = movie.isLiked.not())
appState.movieRosterFlow.update { roster ->
roster.map {
- if (it.id == movie.id) {
- movie
+ if (it.id == updated.id) {
+ updated
} else {
it
}
}
}
- db.update(movie)
+ db.update(updated)
}
}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieDS.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteReader.kt
similarity index 55%
rename from domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieDS.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteReader.kt
index 8af56899..4f6ea917 100644
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieDS.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieFavoriteReader.kt
@@ -1,9 +1,8 @@
-package com.velord.usecase.movie.dataSource
+package com.velord.data.gateway.movie
import com.velord.model.movie.Movie
import kotlinx.coroutines.flow.Flow
-interface MovieDS {
+interface MovieFavoriteReader {
fun getFlow(): Flow>
- fun get(): List
}
diff --git a/data/gateway/src/main/java/com/velord/gateway/movie/MovieGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieGateway.kt
similarity index 53%
rename from data/gateway/src/main/java/com/velord/gateway/movie/MovieGateway.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieGateway.kt
index 24a2e8a6..af0e239c 100644
--- a/data/gateway/src/main/java/com/velord/gateway/movie/MovieGateway.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieGateway.kt
@@ -1,20 +1,17 @@
-package com.velord.gateway.movie
+package com.velord.data.gateway.movie
-import com.velord.appstate.AppStateService
+import com.velord.data.appstate.AppStateDataSource
import com.velord.model.movie.Movie
-import com.velord.usecase.movie.dataSource.MovieDS
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.update
import org.koin.core.annotation.Single
@Single
-class MovieGateway(
- private val appState: AppStateService
-) : MovieDS {
+class MovieGateway(private val appState: AppStateDataSource) {
- override fun getFlow(): Flow> = appState.movieRosterFlow
+ fun getFlow(): Flow> = appState.movieRosterFlow
- override fun get(): List = appState.movieRosterFlow.value
+ fun get(): List = appState.movieRosterFlow.value
fun clearInMemory() {
appState.movieRosterFlow.value = emptyList()
diff --git a/data/gateway/src/main/java/com/velord/gateway/movie/MoviePaginationGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MoviePaginationGateway.kt
similarity index 58%
rename from data/gateway/src/main/java/com/velord/gateway/movie/MoviePaginationGateway.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MoviePaginationGateway.kt
index b6ad6b72..2f229cf0 100644
--- a/data/gateway/src/main/java/com/velord/gateway/movie/MoviePaginationGateway.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MoviePaginationGateway.kt
@@ -1,14 +1,13 @@
-package com.velord.gateway.movie
+package com.velord.data.gateway.movie
-import android.util.Log
-import com.velord.backend.ktor.MovieNetworkService
-import com.velord.backend.model.MoviePageRequest
-import com.velord.db.movie.MovieDbService
+import co.touchlab.kermit.Logger
+import com.velord.data.backend.ktor.MovieNetworkDataSource
+import com.velord.data.backend.model.MoviePageRequest
+import com.velord.data.db.movie.dataSource.MovieDbDataSource
import com.velord.model.movie.FilterType
import com.velord.model.movie.MoviePagination
import com.velord.model.movie.MovieRosterSize
-import com.velord.usecase.movie.dataSource.LoadNewPageMovieDS
-import com.velord.usecase.movie.dataSource.RefreshMovieDS
+import com.velord.usecase.movie.model.MovieLoadNewPageResult
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -18,66 +17,71 @@ import org.koin.core.annotation.Single
private const val INITIAL_PAGE = 1
private const val TAG = "MoviePaginationGateway"
+private val log = Logger.withTag(TAG)
-// Load First Page at init
-// Than load from Db
-// If Page is not full load form network
@Single
class MoviePaginationGateway(
- private val http: MovieNetworkService,
- private val db: MovieDbService,
+ private val http: MovieNetworkDataSource,
+ private val db: MovieDbDataSource,
private val movieGateway: MovieGateway,
- private val movieSortGateway: MovieSortGateway
-) : LoadNewPageMovieDS, RefreshMovieDS {
+ private val movieSortGateway: MovieSortGateway,
+) {
private val errorHandler = CoroutineExceptionHandler { _, throwable ->
- Log.d(TAG, "Error: $throwable")
+ log.d { "Error: $throwable" }
}
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + errorHandler)
private var currentPage = INITIAL_PAGE
init {
observe()
- }
-
- init {
scope.launch {
load()
}
}
- override suspend fun load(): MovieRosterSize {
- Log.d(TAG, "loadNewPage: $currentPage")
+ suspend fun load(): MovieLoadNewPageResult = try {
+ log.d { "loadNewPage: $currentPage" }
val fromDb = loadFromDb(currentPage)
val isPageFull = fromDb.value == MoviePagination.PAGE_COUNT
- Log.d(TAG, "loadNewPage fromDb: $fromDb")
+ log.d { "loadNewPage fromDb: $fromDb" }
val newSize = if (isPageFull) {
fromDb
} else {
loadFromNetwork(currentPage)
}
- return newSize
+ val countOfNewItems = newSize.value
+ if (countOfNewItems < MoviePagination.PAGE_COUNT) {
+ MovieLoadNewPageResult.Exhausted
+ } else {
+ MovieLoadNewPageResult.Success
+ }
+ } catch (exception: Exception) {
+ MovieLoadNewPageResult.LoadPageFailed(exception.message.orEmpty())
}
- override suspend fun refresh(): MovieRosterSize {
+ suspend fun refresh(): MovieLoadNewPageResult = try {
movieGateway.clearInMemory()
db.clear()
currentPage = INITIAL_PAGE
- return load()
+ load()
+ MovieLoadNewPageResult.Success
+ } catch (exception: Exception) {
+ MovieLoadNewPageResult.LoadPageFailed(exception.message.orEmpty())
}
private suspend fun loadFromNetwork(page: Int): MovieRosterSize {
val newPage = MoviePageRequest(
page = page,
- rating = FilterType.Rating.Default,
- voteCount = FilterType.VoteCount.Default
+ rating = FilterType.Rating.DEFAULT,
+ voteCount = FilterType.VoteCount.DEFAULT,
)
val movieRoster = http.getMovie(newPage)
val newRoster = movieRoster.results.map { it.toDomain() }
- Log.d(TAG, "loadFromNetwork newRoster: $newRoster")
- Log.d(TAG, "loadFromNetwork size: ${newRoster.size}")
+ log.d { "loadFromNetwork newRoster: $newRoster" }
+ log.d { "loadFromNetwork size: ${newRoster.size}" }
movieGateway.update { movies ->
(movies + newRoster).toSet().toList()
}
@@ -88,17 +92,18 @@ class MoviePaginationGateway(
private suspend fun loadFromDb(page: Int): MovieRosterSize {
val sortType = movieSortGateway.getSelected().type
- val filterRoster = FilterType.createAll()
+ val filterRoster = FilterType.ALL
val fromDb = db.getPage(
page = page,
sortType = sortType,
- filterRoster = filterRoster
+ filterRoster = filterRoster,
)
- Log.d(TAG, "loadFromDb: $fromDb")
+ log.d { "loadFromDb: $fromDb" }
movieGateway.update { movies ->
(movies + fromDb).toSet().toList()
}
+
return MovieRosterSize(fromDb.size)
}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieSortGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieSortGateway.kt
new file mode 100644
index 00000000..4c2906a8
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/MovieSortGateway.kt
@@ -0,0 +1,35 @@
+package com.velord.data.gateway.movie
+
+import com.velord.data.appstate.AppStateDataSource
+import com.velord.model.movie.MovieSortOption
+import com.velord.usecase.movie.model.MovieSortOptionFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.update
+import org.koin.core.annotation.Single
+
+@Single
+class MovieSortGateway(private val appState: AppStateDataSource) {
+
+ fun getFlow(): MovieSortOptionFlow = MovieSortOptionFlow(appState.movieSortFlow)
+
+ fun getSelectedFlow(): Flow = appState.movieSortFlow
+ .mapNotNull { roster -> roster.firstOrNull { it.isSelected } }
+
+ fun update(newOption: MovieSortOption) {
+ val updated = newOption.copy(isSelected = true)
+ appState.movieSortFlow.update {
+ it.map { option ->
+ if (updated.type == option.type) {
+ updated
+ } else {
+ option.copy(isSelected = false)
+ }
+ }
+ }
+ }
+
+ fun getSelected(): MovieSortOption = appState.movieSortFlow.value
+ .firstOrNull { it.isSelected }
+ ?: MovieSortOption.DEFAULT
+}
diff --git a/data/gateway/src/main/java/com/velord/gateway/movie/TestData.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/TestData.kt
similarity index 81%
rename from data/gateway/src/main/java/com/velord/gateway/movie/TestData.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/TestData.kt
index ae4d6e91..f4eb71e2 100644
--- a/data/gateway/src/main/java/com/velord/gateway/movie/TestData.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/movie/TestData.kt
@@ -1,16 +1,15 @@
@file:Suppress("MagicNumber")
-package com.velord.gateway.movie
+
+package com.velord.data.gateway.movie
import com.velord.model.movie.Movie
-import java.util.Calendar
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atStartOfDayIn
+import kotlin.time.Instant
-private fun createCalendar(year: Int, month: Int, day: Int): Calendar {
- val calendar = Calendar.getInstance()
- calendar.set(Calendar.YEAR, year)
- calendar.set(Calendar.MONTH, month - 1) // Months are 0-based in Calendar
- calendar.set(Calendar.DAY_OF_MONTH, day)
- return calendar
-}
+private fun createDate(year: Int, month: Int, day: Int): Instant =
+ LocalDate(year, month, day).atStartOfDayIn(TimeZone.UTC)
internal val testMovieRoster = listOf(
Movie(
@@ -18,9 +17,9 @@ internal val testMovieRoster = listOf(
title = "Star Wars",
description = "A long time ago in a galaxy far, far away...",
isLiked = true,
- createCalendar(1977, 1, 1),
+ createDate(1977, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
)
@@ -30,18 +29,18 @@ internal val testMovieRoster2 = listOf(
title = "Star Wars",
description = "A long time ago in a galaxy far, far away...",
isLiked = true,
- createCalendar(1977, 1, 1),
+ createDate(1977, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 2,
title = "The Lord of the Rings",
description = "One ring",
isLiked = false,
- createCalendar(2001, 6, 1),
+ createDate(2001, 6, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 3,
@@ -49,9 +48,9 @@ internal val testMovieRoster2 = listOf(
description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption " +
"through acts of common decency.",
isLiked = false,
- createCalendar(1994, 9, 23),
+ createDate(1994, 9, 23),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 4,
@@ -59,18 +58,18 @@ internal val testMovieRoster2 = listOf(
description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine " +
"empire to his reluctant son.",
isLiked = false,
- createCalendar(1972, 1, 1),
+ createDate(1972, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 5,
title = "The Dark Knight",
description = "When the menace known as the Joker wreaks havoc and chaos on the",
isLiked = true,
- createCalendar(2008, 7, 18),
+ createDate(2008, 7, 18),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 6,
@@ -78,9 +77,9 @@ internal val testMovieRoster2 = listOf(
description = "A computer hacker learns from mysterious rebels about the true nature of his reality " +
"and his role in the war against its controllers.",
isLiked = false,
- createCalendar(1999, 1, 1),
+ createDate(1999, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 7,
@@ -88,9 +87,9 @@ internal val testMovieRoster2 = listOf(
description = "A thief who steals corporate secrets through the use of dream-sharing technology is " +
"given the inverse task of planting an idea into the mind of a C.E.O.",
isLiked = true,
- createCalendar(2010, 7, 16),
+ createDate(2010, 7, 16),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 8,
@@ -98,9 +97,9 @@ internal val testMovieRoster2 = listOf(
description = "A team of explorers travel through a wormhole in space in an attempt to ensure " +
"humanity's survival.",
isLiked = false,
- createCalendar(2014, 1, 1),
+ createDate(2014, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 9,
@@ -108,9 +107,9 @@ internal val testMovieRoster2 = listOf(
description = "After a tragic accident, two stage magicians engage in a battle to create the ultimate " +
"illusion while sacrificing everything they have to outwit each other.",
isLiked = true,
- createCalendar(2006, 1, 1),
+ createDate(2006, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 10,
@@ -118,9 +117,9 @@ internal val testMovieRoster2 = listOf(
description = "An undercover cop and a mole in the police attempt to identify each other while " +
"infiltrating an Irish gang in South Boston.",
isLiked = false,
- createCalendar(2006, 1, 1),
+ createDate(2006, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 11,
@@ -128,9 +127,9 @@ internal val testMovieRoster2 = listOf(
description = "Lion cub and future king Simba searches for his identity. His eagerness to please others " +
"and penchant for testing his boundaries sometimes gets him into trouble.",
isLiked = false,
- createCalendar(1994, 1, 1),
+ createDate(1994, 1, 1),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 13,
@@ -138,9 +137,9 @@ internal val testMovieRoster2 = listOf(
description = "The story of the Corleone family in America, focusing on the transformation of Michael " +
"Corleone from reluctant family outsider to ruthless mafia patriarch.",
isLiked = false,
- createCalendar(1974, 12, 15),
+ createDate(1974, 12, 15),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 14,
@@ -148,9 +147,9 @@ internal val testMovieRoster2 = listOf(
description = "Will Hunting, a young janitor at MIT, has a gift for mathematics but needs help from " +
"a therapist to find direction in his life.",
isLiked = false,
- createCalendar(1997, 12, 10),
+ createDate(1997, 12, 10),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 15,
@@ -158,9 +157,9 @@ internal val testMovieRoster2 = listOf(
description = "The true story of Oskar Schindler, a German businessman who saved the lives of over " +
"1100 Jews during the Holocaust by employing them in his factories.",
isLiked = false,
- createCalendar(1993, 12, 15),
+ createDate(1993, 12, 15),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 16,
@@ -168,9 +167,9 @@ internal val testMovieRoster2 = listOf(
description = "A jury must come to a unanimous decision in the case of a young man accused " +
"of murdering his father.",
isLiked = false,
- createCalendar(1957, 4, 11),
+ createDate(1957, 4, 11),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 17,
@@ -178,9 +177,9 @@ internal val testMovieRoster2 = listOf(
description = "The lives of two mob hit men, a boxer, and a wife of a gangster become intertwined " +
"in a series of violent events.",
isLiked = false,
- createCalendar(1994, 9, 23),
+ createDate(1994, 9, 23),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 18,
@@ -188,18 +187,18 @@ internal val testMovieRoster2 = listOf(
description = "Gandalf and Aragorn lead the World of Men into a final battle against Sauron's evil forces. " +
"Frodo and Sam must travel deep into Mordor to destroy the One Ring and end the threat of Sauron forever.",
isLiked = false,
- createCalendar(2003, 12, 17),
+ createDate(2003, 12, 17),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 19,
title = "Parasite",
description = "The members of a poor family scheme to become employed by a wealthy family.",
isLiked = false,
- createCalendar(2019, 5, 30),
+ createDate(2019, 5, 30),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 20,
@@ -208,9 +207,9 @@ internal val testMovieRoster2 = listOf(
"must face the evil Prince Humperdinck, his three deadly henchmen, and a cunning Sicilian " +
"in order to win her hand in marriage.",
isLiked = false,
- createCalendar(1987, 9, 25),
+ createDate(1987, 9, 25),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 21,
@@ -219,9 +218,9 @@ internal val testMovieRoster2 = listOf(
"their relationship, he undergoes the same procedure to erase her from his mind. However, as the " +
"procedure begins, Joel realizes that he may not want to forget everything.",
isLiked = false,
- createCalendar(2004, 3, 10),
+ createDate(2004, 3, 10),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 22,
@@ -230,9 +229,9 @@ internal val testMovieRoster2 = listOf(
"are transformed into pigs by the witch Yubaba, Chihiro takes a job working in Yubaba's bathhouse " +
"to find a way to free herself and her parents and return to the human world.",
isLiked = false,
- createCalendar(2001, 7, 20),
+ createDate(2001, 7, 20),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 23,
@@ -241,9 +240,9 @@ internal val testMovieRoster2 = listOf(
"What starts as an emotional reunion turns into an unexpected journey as Billi grapples with " +
"cultural differences and family secrets.",
isLiked = false,
- createCalendar(2019, 4, 26),
+ createDate(2019, 4, 26),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 24,
@@ -251,19 +250,19 @@ internal val testMovieRoster2 = listOf(
description = "In a post-apocalyptic wasteland, a woman named Imperator Furiosa rebels against a " +
"tyrannical ruler in order to help five enslaved women escape from his clutches.",
isLiked = false,
- createCalendar(2015, 5, 14),
+ createDate(2015, 5, 14),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 25,
- title = "Amélie",
- description = "A shy waitress in a Parisian café decides to anonymously change the lives of the people " +
+ title = "Amelie",
+ description = "A shy waitress in a Parisian cafe decides to anonymously change the lives of the people " +
"around her for the better, but struggles to find happiness for herself.",
isLiked = false,
- createCalendar(2001, 4, 25),
+ createDate(2001, 4, 25),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 26,
@@ -272,9 +271,9 @@ internal val testMovieRoster2 = listOf(
"security chief, Shu Lien. However, the sword is stolen, and Li Mu Bai must find it before " +
"it falls into the wrong hands.",
isLiked = false,
- createCalendar(2016, 2, 8),
+ createDate(2016, 2, 8),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 27,
@@ -282,9 +281,9 @@ internal val testMovieRoster2 = listOf(
description = "The story of a legendary concierge at a renowned European hotel between the wars " +
"and his relationship with a young lobby boy.",
isLiked = false,
- createCalendar(2014, 2, 6),
+ createDate(2014, 2, 6),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 28,
@@ -292,19 +291,19 @@ internal val testMovieRoster2 = listOf(
description = "In a dystopian future where Earth is overrun with garbage, a lonely robot named WALL-E " +
"encounters a sleek robot named EVE and follows her into outer space on a quest to save humanity.",
isLiked = false,
- createCalendar(2008, 6, 27),
+ createDate(2008, 6, 27),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
- id = 29, // Continuing from previous ID
+ id = 29,
title = "Avengers: Endgame",
description = "After the devastating events of Avengers: Infinity War (2018), the remaining Avengers " +
"assemble once more in order to undo Thanos' actions and restore balance to the universe.",
isLiked = false,
- createCalendar(2019, 4, 26),
+ createDate(2019, 4, 26),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 30,
@@ -314,9 +313,9 @@ internal val testMovieRoster2 = listOf(
"and must reunite with an old friend to learn more about her past while saving the planet from a " +
"dangerous threat.",
isLiked = false,
- createCalendar(2019, 3, 8),
+ createDate(2019, 3, 8),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 31,
@@ -324,9 +323,9 @@ internal val testMovieRoster2 = listOf(
description = "A kind-hearted street urchin named Aladdin embarks on an adventure after finding " +
"a magical lamp that houses a powerful genie.",
isLiked = false,
- createCalendar(2019, 5, 24),
+ createDate(2019, 5, 24),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 32,
@@ -334,9 +333,9 @@ internal val testMovieRoster2 = listOf(
description = "A live-action remake of the 1994 Disney classic, telling the story of Simba's " +
"journey to become king.",
isLiked = false,
- createCalendar(2019, 7, 19),
+ createDate(2019, 7, 19),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 33,
@@ -345,9 +344,9 @@ internal val testMovieRoster2 = listOf(
"takes a road trip with her family. Along the way, they encounter a new toy named Forky " +
"who doesn't understand why he's a toy.",
isLiked = false,
- createCalendar(2019, 6, 21),
+ createDate(2019, 6, 21),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 34,
@@ -356,9 +355,9 @@ internal val testMovieRoster2 = listOf(
"vacation to Europe with his friends. However, his plans are put on hold when Nick Fury arrives " +
"needing his help to battle a new threat.",
isLiked = false,
- createCalendar(2019, 7, 2),
+ createDate(2019, 7, 2),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 35,
@@ -367,9 +366,9 @@ internal val testMovieRoster2 = listOf(
"grown up and moved away from Derry, Maine. However, they are all psychically linked by Pennywise " +
"the Dancing Clown, and they must reunite to stop him once and for all.",
isLiked = false,
- createCalendar(2019, 9, 6),
+ createDate(2019, 9, 6),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 36,
@@ -378,9 +377,9 @@ internal val testMovieRoster2 = listOf(
"broken by the indifference and cruelty he experiences every day. This mistreatment pushes him " +
"further into madness and leads him to become a violent criminal known as the Joker.",
isLiked = false,
- createCalendar(2019, 10, 4),
+ createDate(2019, 10, 4),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 37,
@@ -388,9 +387,9 @@ internal val testMovieRoster2 = listOf(
description = "A family gathering turns into a murder investigation when renowned crime novelist " +
"Harlan Thrombey is found dead at his estate.",
isLiked = false,
- createCalendar(2019, 11, 27),
+ createDate(2019, 11, 27),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 38,
@@ -398,9 +397,9 @@ internal val testMovieRoster2 = listOf(
description = "A young boy in Nazi Germany during World War II has an imaginary friend who is Hitler. " +
"He begins to question his beliefs when his widowed mother hides a young Jewish girl in their attic.",
isLiked = false,
- createCalendar(2019, 10, 18),
+ createDate(2019, 10, 18),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 39,
@@ -409,9 +408,9 @@ internal val testMovieRoster2 = listOf(
"interference and their own egos to build a revolutionary race car for Ford in order to defeat " +
"Ferrari at the 24 Hours of Le Mans in 1966.",
isLiked = false,
- createCalendar(2019, 11, 15),
+ createDate(2019, 11, 15),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 40,
@@ -419,9 +418,9 @@ internal val testMovieRoster2 = listOf(
description = "A cynical journalist tasked with profiling Mister Rogers must confront his own childhood " +
"trauma while learning about empathy kindness and forgiveness.",
isLiked = false,
- createCalendar(2019, 11, 22),
+ createDate(2019, 11, 22),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 41,
@@ -429,9 +428,9 @@ internal val testMovieRoster2 = listOf(
description = "Two lighthouse keepers on a remote New England island in the late 19th century " +
"slowly descend into madness.",
isLiked = false,
- createCalendar(2019, 10, 18),
+ createDate(2019, 10, 18),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 42,
@@ -439,18 +438,18 @@ internal val testMovieRoster2 = listOf(
description = "Jo March, Meg March, Amy March, and Beth March are four sisters who come of age " +
"in America during the Civil War.",
isLiked = false,
- createCalendar(2019, 12, 25),
+ createDate(2019, 12, 25),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 43,
title = "Marriage Story",
description = "A married couple navigates a contentious divorce.",
isLiked = false,
- createCalendar(2019, 11, 6),
+ createDate(2019, 11, 6),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 44,
@@ -458,9 +457,9 @@ internal val testMovieRoster2 = listOf(
description = "A prison warden must confront the psychological toll of her job when she is tasked " +
"with overseeing the execution of a death row inmate.",
isLiked = false,
- createCalendar(2019, 1, 27),
+ createDate(2019, 1, 27),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 45,
@@ -469,9 +468,9 @@ internal val testMovieRoster2 = listOf(
"What starts as an emotional reunion turns into an unexpected journey as Billi grapples with " +
"cultural differences and family secrets.",
isLiked = false,
- createCalendar(2019, 4, 26),
+ createDate(2019, 4, 26),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 46,
@@ -480,18 +479,17 @@ internal val testMovieRoster2 = listOf(
"married to a stranger. During this time, she develops a forbidden romance with a female painter " +
"who is sent to paint her portrait.",
isLiked = false,
- createCalendar(2019, 5, 15),
+ createDate(2019, 5, 15),
rating = 7.66f,
- voteCount = 100
+ voteCount = 100,
),
Movie(
id = 47,
title = "Pain and Glory",
description = "A fading Spanish filmmaker reflects on his life and career as he struggles with illness.",
isLiked = false,
- createCalendar(2019, 5, 18),
+ createDate(2019, 5, 18),
rating = 7.66f,
- voteCount = 100
- )
+ voteCount = 100,
+ ),
)
-
diff --git a/data/gateway/src/main/java/com/velord/gateway/setting/GetThemeConfigGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/GetThemeConfigGateway.kt
similarity index 54%
rename from data/gateway/src/main/java/com/velord/gateway/setting/GetThemeConfigGateway.kt
rename to data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/GetThemeConfigGateway.kt
index f31f4eb8..cc940adc 100644
--- a/data/gateway/src/main/java/com/velord/gateway/setting/GetThemeConfigGateway.kt
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/GetThemeConfigGateway.kt
@@ -1,27 +1,29 @@
-package com.velord.gateway.setting
+package com.velord.data.gateway.setting
-import com.velord.appstate.AppStateService
-import com.velord.datastore.DataStoreService
+import com.velord.data.appstate.AppStateDataSource
+import com.velord.data.datastore.DataStoreDataSource
import com.velord.model.setting.ThemeConfig
-import com.velord.usecase.setting.GetThemeConfigDS
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
-@Single
+@Single(binds = [ThemeConfigSaver::class])
class GetThemeConfigGateway(
- private val dataStore: DataStoreService,
- private val appState: AppStateService
-) : GetThemeConfigDS {
+ private val dataStore: DataStoreDataSource,
+ private val appState: AppStateDataSource,
+) : ThemeConfigSaver {
private var isInitialized = false
- override suspend fun getFlow(): Flow {
+ suspend fun getFlow(): Flow {
if (isInitialized.not()) {
isInitialized = true
runCatching {
- appState.themeConfigFlow.value = dataStore.getAppSettingFlow().map { it.theme }.first()
+ appState.themeConfigFlow.value = dataStore
+ .getAppSettingFlow()
+ .map { it.theme }
+ .first()
}
}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt
new file mode 100644
index 00000000..98d865c7
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/SwitchThemeConfigGateway.kt
@@ -0,0 +1,26 @@
+package com.velord.data.gateway.setting
+
+import com.velord.model.setting.ThemeConfig
+import org.koin.core.annotation.Single
+
+@Single
+class SwitchThemeConfigGateway(private val themeConfigSaver: ThemeConfigSaver) {
+
+ suspend fun switchAbideToOs(config: ThemeConfig) {
+ val newConfig = config.copy(abideToOs = config.abideToOs.not())
+ themeConfigSaver.save(newConfig)
+ }
+
+ suspend fun switchDynamicColor(config: ThemeConfig) {
+ val newConfig = config.copy(useDynamicColor = config.useDynamicColor.not())
+ themeConfigSaver.save(newConfig)
+ }
+
+ suspend fun switchDarkTheme(config: ThemeConfig) {
+ val newConfig = config.copy(
+ useDarkTheme = config.useDarkTheme.not(),
+ current = ThemeConfig.getOppositeTheme(config.current),
+ )
+ themeConfigSaver.save(newConfig)
+ }
+}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/ThemeConfigSaver.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/ThemeConfigSaver.kt
new file mode 100644
index 00000000..0816ce3d
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/setting/ThemeConfigSaver.kt
@@ -0,0 +1,7 @@
+package com.velord.data.gateway.setting
+
+import com.velord.model.setting.ThemeConfig
+
+interface ThemeConfigSaver {
+ suspend fun save(config: ThemeConfig)
+}
diff --git a/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/toast/ToastGateway.kt b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/toast/ToastGateway.kt
new file mode 100644
index 00000000..d7a9b447
--- /dev/null
+++ b/data/gateway/src/commonMain/kotlin/com/velord/data/gateway/toast/ToastGateway.kt
@@ -0,0 +1,16 @@
+package com.velord.data.gateway.toast
+
+import com.velord.data.appstate.AppStateDataSource
+import com.velord.model.ToastConfig
+import kotlinx.coroutines.flow.Flow
+import org.koin.core.annotation.Single
+
+@Single
+class ToastGateway(private val appState: AppStateDataSource) {
+
+ fun getFlow(): Flow = appState.toastConfigFlow
+
+ suspend fun show(config: ToastConfig) {
+ appState.toastConfigFlow.emit(config)
+ }
+}
diff --git a/data/gateway/src/main/AndroidManifest.xml b/data/gateway/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/gateway/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/gateway/src/main/java/com/velord/gateway/DI.kt b/data/gateway/src/main/java/com/velord/gateway/DI.kt
deleted file mode 100644
index 3f6bb2d5..00000000
--- a/data/gateway/src/main/java/com/velord/gateway/DI.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-@file:Suppress("MatchingDeclarationName")
-
-package com.velord.gateway
-
-import com.velord.appstate.AppStateModule
-import com.velord.backend.ktor.BackendModule
-import com.velord.datastore.DataStoreModule
-import com.velord.db.DbModule
-import org.koin.core.annotation.ComponentScan
-import org.koin.core.annotation.Module
-
-@Module(
- includes = [
- AppStateModule::class,
- DbModule::class,
- BackendModule::class,
- DataStoreModule::class
- ]
-)
-@ComponentScan("com.velord.gateway")
-class GatewayModule
diff --git a/data/gateway/src/main/java/com/velord/gateway/movie/MovieSortGateway.kt b/data/gateway/src/main/java/com/velord/gateway/movie/MovieSortGateway.kt
deleted file mode 100644
index 4969beed..00000000
--- a/data/gateway/src/main/java/com/velord/gateway/movie/MovieSortGateway.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.velord.gateway.movie
-
-import com.velord.appstate.AppStateService
-import com.velord.model.movie.MovieSortOption
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.update
-import org.koin.core.annotation.Single
-
-@Single
-class MovieSortGateway(
- private val appState: AppStateService
-) : MovieSortDS {
-
- override fun getFlow(): Flow> = appState.movieSortFlow
-
- override fun getSelectedFlow(): Flow = appState.movieSortFlow
- .map { roster -> roster.firstOrNull { it.isSelected } }
- .filterNotNull()
-
- override fun update(newOption: MovieSortOption) {
- appState.movieSortFlow.update {
- it.map { option ->
- if (newOption.type == option.type) {
- newOption
- } else {
- option.copy(isSelected = false)
- }
- }
- }
- }
-
- override fun getSelected(): MovieSortOption = appState.movieSortFlow.value
- .firstOrNull { it.isSelected }
- ?: MovieSortOption.Default
-}
diff --git a/data/os/build.gradle.kts b/data/os/build.gradle.kts
index e84be2d9..3c6c478a 100644
--- a/data/os/build.gradle.kts
+++ b/data/os/build.gradle.kts
@@ -1,15 +1,47 @@
plugins {
- alias(libs.plugins.convention.android.library)
- alias(libs.plugins.convention.koin)
+ alias(libs.plugins.convention.kmp.library)
+ alias(libs.plugins.ksp)
}
-android {
- namespace = "com.velord.os"
+kotlin {
+ android {
+ namespace = "com.velord.data.os"
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(projects.model)
+ implementation(libs.koin.core)
+ api(libs.koin.annotation)
+ }
+
+ androidMain.dependencies {
+ implementation(projects.model)
+ implementation(projects.core.coreResource)
+ implementation(projects.domain.usecaseCamera)
+ implementation(libs.kotlin.coroutine.core)
+ implementation(libs.koin.core)
+ implementation(libs.koin.annotation)
+ implementation(libs.androidx.camera.video)
+ }
+
+ named("commonMain").configure {
+ kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
+ }
+ }
}
dependencies {
- // Templates
- implementation(libs.bundles.kotlin.module)
+ add("kspCommonMainMetadata", libs.koin.ksp)
+ add("kspAndroid", libs.koin.ksp)
+ add("kspDesktop", libs.koin.ksp)
+}
+
+tasks.matching {
+ it.name.startsWith("ksp") &&
+ it.name != "kspCommonMainKotlinMetadata"
+}.configureEach {
+ dependsOn("kspCommonMainKotlinMetadata")
}
ksp {
diff --git a/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
new file mode 100644
index 00000000..3f75cd3f
--- /dev/null
+++ b/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
@@ -0,0 +1,70 @@
+package com.velord.data.os.camera
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.os.Environment
+import androidx.camera.video.FileDescriptorOutputOptions
+import androidx.camera.video.FileOutputOptions
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.VideoRecordEvent
+import com.velord.core.resource.R
+import com.velord.model.camera.RecordingSession
+import com.velord.model.camera.VideoCaptureWrapper
+import com.velord.model.file.FileName
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+import org.koin.core.scope.Scope
+import java.io.File
+@Module
+actual class CameraPlatformModule {
+ @Single
+ actual fun provideCameraDataSource(scope: Scope): CameraDataSource =
+ AndroidCameraDataSource(scope.get())
+}
+
+private class AndroidCameraDataSource(private val context: Context) : CameraDataSource {
+
+ @SuppressLint("MissingPermission")
+ override fun startRecording(
+ videoCapture: VideoCaptureWrapper,
+ audioEnabled: Boolean,
+ ): RecordingSession {
+ val outputOptions = createFileOutputOptions()
+ val recording = videoCapture.value.output
+ .prepareRecording(context, outputOptions)
+ .apply { if (audioEnabled) withAudioEnabled() }
+ .start(context.mainExecutor, ::onVideoRecordEvent)
+ return CameraXRecordingSession(recording)
+ }
+
+ private fun createFileOutputOptions(): FileOutputOptions {
+ val moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
+ val appDir = File(moviesDir, context.getString(R.string.app_name)).apply {
+ mkdirs()
+ }
+ val outputFile = File(appDir, FileName().value)
+ return FileOutputOptions.Builder(outputFile).build()
+ }
+
+ private fun onVideoRecordEvent(newEvent: VideoRecordEvent) {
+ if (newEvent is VideoRecordEvent.Finalize) {
+ val isNone = newEvent.error == VideoRecordEvent.Finalize.ERROR_NONE
+ if (isNone) return
+ deleteFailedOutput(newEvent)
+ }
+ }
+
+ private fun deleteFailedOutput(finalizeEvent: VideoRecordEvent.Finalize) {
+ when (val outputOptions = finalizeEvent.outputOptions) {
+ is FileOutputOptions -> outputOptions.file.delete()
+ is MediaStoreOutputOptions -> {
+ val outputUri = finalizeEvent.outputResults.outputUri
+ if (outputUri != Uri.EMPTY) {
+ context.contentResolver.delete(outputUri, null, null)
+ }
+ }
+ is FileDescriptorOutputOptions -> Unit
+ }
+ }
+}
diff --git a/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraXRecordingSession.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraXRecordingSession.kt
new file mode 100644
index 00000000..ee8ea7ce
--- /dev/null
+++ b/data/os/src/androidMain/kotlin/com/velord/data/os/camera/CameraXRecordingSession.kt
@@ -0,0 +1,11 @@
+package com.velord.data.os.camera
+
+import androidx.camera.video.Recording
+import com.velord.model.camera.RecordingSession
+
+internal class CameraXRecordingSession(private val recording: Recording) : RecordingSession {
+
+ override fun stop() {
+ recording.stop()
+ }
+}
diff --git a/data/os/src/main/kotlin/com/velord/os/MemoryDumpProvider.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
similarity index 61%
rename from data/os/src/main/kotlin/com/velord/os/MemoryDumpProvider.kt
rename to data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
index 4b44ee1c..e4bfb1bc 100644
--- a/data/os/src/main/kotlin/com/velord/os/MemoryDumpProvider.kt
+++ b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
@@ -1,20 +1,20 @@
-package com.velord.os
+package com.velord.data.os.memory
import android.os.Debug
import android.system.Os
import android.system.OsConstants
-import org.koin.core.annotation.Single
import java.io.BufferedReader
import java.io.FileReader
import kotlin.math.roundToLong
-@Single
-internal class MemoryDumpProvider(private val logger: MemoryLogger) {
+internal actual class MemoryDumpProvider actual constructor(
+ private val logger: MemoryLogger,
+) {
private val memoryInfo = Debug.MemoryInfo()
private val pageSize by lazy { Os.sysconf(OsConstants._SC_PAGESIZE) }
- fun takeDump(reason: DumpReason, trimLevel: Int? = null): MemoryDump {
+ actual fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump {
Debug.getMemoryInfo(memoryInfo)
val (rss, shared) = getRssAndShared()
@@ -25,26 +25,41 @@ internal class MemoryDumpProvider(private val logger: MemoryLogger) {
shared = shared,
swap = swap,
pageTable = pageTable,
- uss = rss - shared
+ uss = rss - shared,
)
val pss = PssBreakdown(
- total = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_TOTAL).toLongCompat().kbToMb(),
- javaHeap = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_JAVA_HEAP).toLongCompat().kbToMb(),
- nativeHeap = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_NATIVE_HEAP).toLongCompat().kbToMb(),
- graphics = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_GRAPHICS).toLongCompat().kbToMb(),
- code = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_CODE).toLongCompat().kbToMb(),
- stack = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_STACK).toLongCompat().kbToMb(),
- system = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_SYSTEM).toLongCompat().kbToMb(),
- other = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_OTHER).toLongCompat().kbToMb()
+ total = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_TOTAL)
+ .toLongCompat()
+ .kbToMb(),
+ javaHeap = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_JAVA_HEAP)
+ .toLongCompat()
+ .kbToMb(),
+ nativeHeap = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_NATIVE_HEAP)
+ .toLongCompat()
+ .kbToMb(),
+ graphics = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_GRAPHICS)
+ .toLongCompat()
+ .kbToMb(),
+ code = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_CODE)
+ .toLongCompat()
+ .kbToMb(),
+ stack = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_STACK)
+ .toLongCompat()
+ .kbToMb(),
+ system = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_SYSTEM)
+ .toLongCompat()
+ .kbToMb(),
+ other = memoryInfo.getMemoryStat(PssBreakdown.KEY_PSS_OTHER)
+ .toLongCompat()
+ .kbToMb(),
)
- val snapshot = MemorySnapshot(core = core, pss = pss)
- val dump = MemoryDump(
+ val snapshot = MemorySnapshot(core = core, pss = pss)
+ return MemoryDump(
reason = reason,
trimLevel = trimLevel,
timestamp = System.currentTimeMillis(),
- snapshot = snapshot
+ snapshot = snapshot,
)
- return dump
}
private fun getRssAndShared(): Pair = try {
@@ -55,8 +70,8 @@ internal class MemoryDumpProvider(private val logger: MemoryLogger) {
val shared = (parts.getOrNull(2)?.toLongOrNull() ?: 0L) * pageSize
rss.bytesToMb() to shared.bytesToMb()
}
- } catch (e: Exception) {
- logger.log("Error reading /proc/self/statm: ${e.message}")
+ } catch (exception: Exception) {
+ logger.log("Error reading /proc/self/statm: ${exception.message}")
0L to 0L
}
@@ -67,23 +82,25 @@ internal class MemoryDumpProvider(private val logger: MemoryLogger) {
BufferedReader(FileReader("/proc/self/status")).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
- if (line!!.startsWith("VmSwap:")) {
- swap = parseValue(line)
- } else if (line.startsWith("VmPTE:")) {
- pageTable = parseValue(line)
+ when {
+ line!!.startsWith("VmSwap:") -> {
+ swap = parseValue(line)
+ }
+ line.startsWith("VmPTE:") -> {
+ pageTable = parseValue(line)
+ }
}
if (swap > 0 && pageTable > 0) break
}
}
- } catch (e: Exception) {
- logger.log("Error reading /proc/self/status: ${e.message}")
+ } catch (exception: Exception) {
+ logger.log("Error reading /proc/self/status: ${exception.message}")
}
+
return swap.kbToMb() to pageTable.kbToMb()
}
- private fun parseValue(line: String): Long =
- line.replace(DIGIT_REGEX, "").toLongOrNull() ?: 0L
-
+ private fun parseValue(line: String): Long = line.replace(DIGIT_REGEX, "").toLongOrNull() ?: 0L
private fun String?.toLongCompat(): Long = this?.toLongOrNull() ?: 0L
private fun Long.bytesToMb(): Long = (this.toDouble() / BYTES_IN_MB).roundToLong()
@@ -92,6 +109,6 @@ internal class MemoryDumpProvider(private val logger: MemoryLogger) {
companion object {
private val DIGIT_REGEX = Regex("[^0-9]")
private const val KB_IN_MB = 1024.0
- private const val BYTES_IN_MB = 1048576.0 // 1024 * 1024
+ private const val BYTES_IN_MB = 1048576.0
}
}
diff --git a/data/os/src/main/kotlin/com/velord/os/MemoryLeakMonitor.kt b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
similarity index 72%
rename from data/os/src/main/kotlin/com/velord/os/MemoryLeakMonitor.kt
rename to data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
index 5eda3fd4..e57b08a8 100644
--- a/data/os/src/main/kotlin/com/velord/os/MemoryLeakMonitor.kt
+++ b/data/os/src/androidMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
@@ -1,4 +1,4 @@
-package com.velord.os
+package com.velord.data.os.memory
import android.content.ComponentCallbacks2
import android.content.Context
@@ -11,23 +11,24 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
+import org.koin.core.context.GlobalContext
import kotlin.time.Duration.Companion.minutes
@Single
-class MemoryLeakMonitor(
- private val context: Context,
+actual class MemoryLeakMonitor actual constructor(
private val logger: MemoryLogger,
) : ComponentCallbacks2 {
+ private val context: Context = GlobalContext.get().get().applicationContext
private val memoryDumpProvider = MemoryDumpProvider(logger)
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
logger.log("MemoryLeakMonitor coroutine error: ${exception.message}")
}
private val monitorScope = CoroutineScope(
- SupervisorJob() + Dispatchers.Default + exceptionHandler
+ SupervisorJob() + Dispatchers.Default + exceptionHandler,
)
- fun start() {
+ actual fun start() {
context.registerComponentCallbacks(this)
setupIntervalLogging()
@@ -37,7 +38,7 @@ class MemoryLeakMonitor(
private fun setupIntervalLogging() {
monitorScope.launch {
while (isActive) {
- val snapshot = memoryDumpProvider.takeDump(DumpReason.INTERVAL)
+ val snapshot = memoryDumpProvider.takeDump(DumpReason.INTERVAL, trimLevel = null)
publishMemoryDump(snapshot)
delay(LOGGING_INTERVAL)
}
@@ -50,11 +51,11 @@ class MemoryLeakMonitor(
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
if (exception is OutOfMemoryError) {
try {
- val snapshot = memoryDumpProvider.takeDump(DumpReason.OOM)
+ val snapshot = memoryDumpProvider.takeDump(DumpReason.OOM, trimLevel = null)
publishMemoryDump(snapshot)
logger.recordException(exception)
- } catch (e: Exception) {
- logger.log("Failed to capture OOM snapshot: ${e.message}")
+ } catch (monitorException: Exception) {
+ logger.log("Failed to capture OOM snapshot: ${monitorException.message}")
}
}
defaultHandler?.uncaughtException(thread, exception)
@@ -64,7 +65,7 @@ class MemoryLeakMonitor(
override fun onTrimMemory(level: Int) {
val snapshot = memoryDumpProvider.takeDump(
reason = DumpReason.TRIM_MEMORY,
- trimLevel = level
+ trimLevel = level,
)
publishMemoryDump(snapshot)
}
@@ -72,7 +73,7 @@ class MemoryLeakMonitor(
override fun onConfigurationChanged(newConfig: Configuration) = Unit
@Deprecated("Deprecated in Java")
- override fun onLowMemory() = Unit // No-op
+ override fun onLowMemory() = Unit
private fun publishMemoryDump(dump: MemoryDump) {
logger.log("$dump")
@@ -83,20 +84,20 @@ class MemoryLeakMonitor(
trimLevel?.let { setCustomKey(KEY_MEM_TRIM_LEVEL, it) }
setCustomKey(KEY_TIMESTAMP, timestamp)
- setCustomKey(KEY_MEM_RSS_MB, dump.snapshot.core.rss)
- setCustomKey(KEY_MEM_SHARED_MB, dump.snapshot.core.shared)
- setCustomKey(KEY_MEM_SWAP_MB, dump.snapshot.core.swap)
- setCustomKey(KEY_MEM_PAGE_TABLE_MB, dump.snapshot.core.pageTable)
- setCustomKey(KEY_MEM_USS_MB, dump.snapshot.core.uss)
-
- setCustomKey(KEY_MEM_PSS_TOTAL_MB, dump.snapshot.pss.total)
- setCustomKey(KEY_MEM_PSS_JAVA_MB, dump.snapshot.pss.javaHeap)
- setCustomKey(KEY_MEM_PSS_NATIVE_MB, dump.snapshot.pss.nativeHeap)
- setCustomKey(KEY_MEM_PSS_GRAPHICS_MB, dump.snapshot.pss.graphics)
- setCustomKey(KEY_MEM_PSS_CODE_MB, dump.snapshot.pss.code)
- setCustomKey(KEY_MEM_PSS_STACK_MB, dump.snapshot.pss.stack)
- setCustomKey(KEY_MEM_PSS_SYSTEM_MB, dump.snapshot.pss.system)
- setCustomKey(KEY_MEM_PSS_OTHER_MB, dump.snapshot.pss.other)
+ setCustomKey(KEY_MEM_RSS_MB, snapshot.core.rss)
+ setCustomKey(KEY_MEM_SHARED_MB, snapshot.core.shared)
+ setCustomKey(KEY_MEM_SWAP_MB, snapshot.core.swap)
+ setCustomKey(KEY_MEM_PAGE_TABLE_MB, snapshot.core.pageTable)
+ setCustomKey(KEY_MEM_USS_MB, snapshot.core.uss)
+
+ setCustomKey(KEY_MEM_PSS_TOTAL_MB, snapshot.pss.total)
+ setCustomKey(KEY_MEM_PSS_JAVA_MB, snapshot.pss.javaHeap)
+ setCustomKey(KEY_MEM_PSS_NATIVE_MB, snapshot.pss.nativeHeap)
+ setCustomKey(KEY_MEM_PSS_GRAPHICS_MB, snapshot.pss.graphics)
+ setCustomKey(KEY_MEM_PSS_CODE_MB, snapshot.pss.code)
+ setCustomKey(KEY_MEM_PSS_STACK_MB, snapshot.pss.stack)
+ setCustomKey(KEY_MEM_PSS_SYSTEM_MB, snapshot.pss.system)
+ setCustomKey(KEY_MEM_PSS_OTHER_MB, snapshot.pss.other)
}
}
}
diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt
new file mode 100644
index 00000000..3115a356
--- /dev/null
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/DI.kt
@@ -0,0 +1,11 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.velord.data.os
+
+import com.velord.data.os.camera.CameraPlatformModule
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+
+@Module(includes = [CameraPlatformModule::class])
+@ComponentScan("com.velord.data.os")
+class OsModule
diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
new file mode 100644
index 00000000..d57841f7
--- /dev/null
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
@@ -0,0 +1,20 @@
+package com.velord.data.os.camera
+
+import com.velord.model.camera.RecordingSession
+import com.velord.model.camera.VideoCaptureWrapper
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+import org.koin.core.scope.Scope
+
+interface CameraDataSource {
+ fun startRecording(
+ videoCapture: VideoCaptureWrapper,
+ audioEnabled: Boolean,
+ ) : RecordingSession
+}
+
+@Module
+expect class CameraPlatformModule() {
+ @Single
+ fun provideCameraDataSource(scope: Scope): CameraDataSource
+}
diff --git a/data/os/src/main/kotlin/com/velord/os/MemoryDump.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDump.kt
similarity index 80%
rename from data/os/src/main/kotlin/com/velord/os/MemoryDump.kt
rename to data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDump.kt
index 54df8b9d..3dbd4932 100644
--- a/data/os/src/main/kotlin/com/velord/os/MemoryDump.kt
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDump.kt
@@ -1,22 +1,24 @@
-package com.velord.os
+package com.velord.data.os.memory
internal data class MemoryDump(
val reason: DumpReason,
val trimLevel: Int? = null,
val timestamp: Long,
- val snapshot: MemorySnapshot
+ val snapshot: MemorySnapshot,
) {
override fun toString(): String {
val trimInfo = trimLevel?.let { " (Level: $it)" } ?: ""
return "[$reason]$trimInfo PSS: ${snapshot.pss.total}MB" +
- " | Java: ${snapshot.pss.javaHeap}MB" +
- " | USS: ${snapshot.core.uss}MB" +
- " | RSS: ${snapshot.core.rss}MB"
+ " | Java: ${snapshot.pss.javaHeap}MB" +
+ " | USS: ${snapshot.core.uss}MB" +
+ " | RSS: ${snapshot.core.rss}MB"
}
}
internal enum class DumpReason {
- INTERVAL, TRIM_MEMORY, OOM
+ INTERVAL,
+ TRIM_MEMORY,
+ OOM,
}
internal data class CoreOsMetric(
@@ -24,7 +26,6 @@ internal data class CoreOsMetric(
val shared: Long,
val swap: Long,
val pageTable: Long,
- // Calculated as RSS - Shared
val uss: Long,
)
@@ -36,7 +37,7 @@ internal data class PssBreakdown(
val code: Long,
val stack: Long,
val system: Long,
- val other: Long
+ val other: Long,
) {
companion object {
const val KEY_PSS_TOTAL = "summary.total-pss"
diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
new file mode 100644
index 00000000..77b7fa87
--- /dev/null
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
@@ -0,0 +1,5 @@
+package com.velord.data.os.memory
+
+internal expect class MemoryDumpProvider(logger: MemoryLogger) {
+ fun takeDump(reason: DumpReason, trimLevel: Int? = null): MemoryDump
+}
diff --git a/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
new file mode 100644
index 00000000..99486c04
--- /dev/null
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
@@ -0,0 +1,8 @@
+package com.velord.data.os.memory
+
+import org.koin.core.annotation.Single
+
+@Single
+expect class MemoryLeakMonitor(logger: MemoryLogger) {
+ fun start()
+}
diff --git a/data/os/src/main/kotlin/com/velord/os/MemoryLogger.kt b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt
similarity index 88%
rename from data/os/src/main/kotlin/com/velord/os/MemoryLogger.kt
rename to data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt
index a5e750e9..51b51016 100644
--- a/data/os/src/main/kotlin/com/velord/os/MemoryLogger.kt
+++ b/data/os/src/commonMain/kotlin/com/velord/data/os/memory/MemoryLogger.kt
@@ -1,10 +1,9 @@
-package com.velord.os
+package com.velord.data.os.memory
interface MemoryLogger {
fun log(message: String, isError: Boolean = false)
fun recordException(throwable: Throwable)
fun setCustomKey(key: String, value: Int)
-
fun setCustomKey(key: String, value: Long)
fun setCustomKey(key: String, value: String)
}
diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
new file mode 100644
index 00000000..16eafada
--- /dev/null
+++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/camera/CameraDataSource.kt
@@ -0,0 +1,23 @@
+package com.velord.data.os.camera
+
+import com.velord.model.camera.RecordingSession
+import com.velord.model.camera.VideoCaptureWrapper
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Single
+import org.koin.core.scope.Scope
+
+@Module
+actual class CameraPlatformModule {
+ @Single
+ actual fun provideCameraDataSource(scope: Scope): CameraDataSource = DesktopCameraDataSource()
+}
+
+private class DesktopCameraDataSource : CameraDataSource {
+
+ override fun startRecording(
+ videoCapture: VideoCaptureWrapper,
+ audioEnabled: Boolean,
+ ): RecordingSession {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
new file mode 100644
index 00000000..cc26d805
--- /dev/null
+++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryDumpProvider.kt
@@ -0,0 +1,33 @@
+package com.velord.data.os.memory
+
+internal actual class MemoryDumpProvider actual constructor(
+ private val logger: MemoryLogger,
+) {
+ actual fun takeDump(reason: DumpReason, trimLevel: Int?): MemoryDump {
+ logger.log("Memory dump is not implemented for desktop target.")
+ return MemoryDump(
+ reason = reason,
+ trimLevel = trimLevel,
+ timestamp = System.currentTimeMillis(),
+ snapshot = MemorySnapshot(
+ core = CoreOsMetric(
+ rss = 0L,
+ shared = 0L,
+ swap = 0L,
+ pageTable = 0L,
+ uss = 0L,
+ ),
+ pss = PssBreakdown(
+ total = 0L,
+ javaHeap = 0L,
+ nativeHeap = 0L,
+ graphics = 0L,
+ code = 0L,
+ stack = 0L,
+ system = 0L,
+ other = 0L,
+ ),
+ ),
+ )
+ }
+}
diff --git a/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
new file mode 100644
index 00000000..2383e44e
--- /dev/null
+++ b/data/os/src/desktopMain/kotlin/com/velord/data/os/memory/MemoryLeakMonitor.kt
@@ -0,0 +1,11 @@
+package com.velord.data.os.memory
+
+import org.koin.core.annotation.Single
+
+@Single
+actual class MemoryLeakMonitor actual constructor(private val logger: MemoryLogger) {
+
+ actual fun start() {
+ logger.log("Memory leak monitor is not implemented for desktop target.")
+ }
+}
diff --git a/data/os/src/main/AndroidManifest.xml b/data/os/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/data/os/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/data/os/src/main/kotlin/com/velord/os/DI.kt b/data/os/src/main/kotlin/com/velord/os/DI.kt
deleted file mode 100644
index 44fab26b..00000000
--- a/data/os/src/main/kotlin/com/velord/os/DI.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-@file:Suppress("MatchingDeclarationName")
-
-package com.velord.os
-
-import org.koin.core.annotation.ComponentScan
-import org.koin.core.annotation.Module
-
-@Module
-@ComponentScan("com.velord.os")
-class OsModule
diff --git a/domain/usecase-camera/build.gradle.kts b/domain/usecase-camera/build.gradle.kts
new file mode 100644
index 00000000..9e29befa
--- /dev/null
+++ b/domain/usecase-camera/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ alias(libs.plugins.convention.domain.usecase.kmp)
+}
+
+kotlin {
+ android {
+ namespace = "com.velord.usecase.camera"
+ }
+}
\ No newline at end of file
diff --git a/domain/usecase-camera/src/commonMain/kotlin/com/velord/usecase/camera/StartRecordingUC.kt b/domain/usecase-camera/src/commonMain/kotlin/com/velord/usecase/camera/StartRecordingUC.kt
new file mode 100644
index 00000000..4cb8606b
--- /dev/null
+++ b/domain/usecase-camera/src/commonMain/kotlin/com/velord/usecase/camera/StartRecordingUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.camera
+
+import com.velord.model.camera.RecordingSession
+import com.velord.model.camera.VideoCaptureWrapper
+
+fun interface StartRecordingUC {
+ operator fun invoke(
+ videoCapture: VideoCaptureWrapper,
+ audioEnabled: Boolean,
+ ): RecordingSession
+}
\ No newline at end of file
diff --git a/domain/usecase-event/build.gradle.kts b/domain/usecase-event/build.gradle.kts
new file mode 100644
index 00000000..8e3f034e
--- /dev/null
+++ b/domain/usecase-event/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ alias(libs.plugins.convention.domain.usecase.kmp)
+}
+
+kotlin {
+ android {
+ namespace = "com.velord.usecase.event"
+ }
+}
diff --git a/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/GetToastConfigFlowUC.kt b/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/GetToastConfigFlowUC.kt
new file mode 100644
index 00000000..25665402
--- /dev/null
+++ b/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/GetToastConfigFlowUC.kt
@@ -0,0 +1,6 @@
+package com.velord.usecase.event
+
+import com.velord.model.ToastConfig
+import kotlinx.coroutines.flow.Flow
+
+fun interface GetToastConfigFlowUC : () -> Flow
diff --git a/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/ShowToastUC.kt b/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/ShowToastUC.kt
new file mode 100644
index 00000000..a0e7f90a
--- /dev/null
+++ b/domain/usecase-event/src/commonMain/kotlin/com/velord/usecase/event/ShowToastUC.kt
@@ -0,0 +1,5 @@
+package com.velord.usecase.event
+
+import com.velord.model.ToastConfig
+
+fun interface ShowToastUC : suspend (ToastConfig) -> Unit
diff --git a/domain/usecase-movie/build.gradle.kts b/domain/usecase-movie/build.gradle.kts
index 5a32181a..daffbd80 100644
--- a/domain/usecase-movie/build.gradle.kts
+++ b/domain/usecase-movie/build.gradle.kts
@@ -1,13 +1,16 @@
plugins {
- alias(libs.plugins.convention.domain.usecase)
+ alias(libs.plugins.convention.domain.usecase.kmp)
+ alias(libs.plugins.mokkery)
}
-android {
- namespace = "com.velord.usecase.movie"
-}
+kotlin {
+ android {
+ namespace = "com.velord.usecase.movie"
+ }
-dependencies {
- // Test
- testImplementation(libs.junit)
- testImplementation(libs.bundles.test)
+ sourceSets {
+ commonTest.dependencies {
+ implementation(libs.bundles.test.kmp)
+ }
+ }
}
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetAllMovieUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetAllMovieUC.kt
new file mode 100644
index 00000000..df14cfc4
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetAllMovieUC.kt
@@ -0,0 +1,12 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieFlow
+
+/**
+ * Returns a reactive roster of all available movies.
+ *
+ * The returned [MovieFlow] is expected to emit whenever the underlying movie roster changes,
+ * including changes caused by persistence updates, pagination, refresh, or sort-option changes.
+ * This use case is read-only and does not trigger loading by itself.
+ */
+fun interface GetAllMovieUC: () -> MovieFlow
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetFavoriteMovieUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetFavoriteMovieUC.kt
new file mode 100644
index 00000000..38640350
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetFavoriteMovieUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieFlow
+
+/**
+ * Returns a reactive roster of liked movies only.
+ *
+ * The returned [MovieFlow] is expected to stay up to date with like/unlike operations and with
+ * sort-option changes applied to the favorite subset. This use case is read-only.
+ */
+fun interface GetFavoriteMovieUC : () -> MovieFlow
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetMovieSortOptionUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetMovieSortOptionUC.kt
new file mode 100644
index 00000000..2985613c
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/GetMovieSortOptionUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieSortOptionFlow
+
+/**
+ * Returns the full roster of movie sort options together with their current selection state.
+ *
+ * Consumers should collect the returned [MovieSortOptionFlow] to react to future selection
+ * changes instead of treating the value as a one-shot snapshot.
+ */
+fun interface GetMovieSortOptionUC : () -> MovieSortOptionFlow
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/LoadNewPageMovieUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/LoadNewPageMovieUC.kt
new file mode 100644
index 00000000..8cfd8e78
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/LoadNewPageMovieUC.kt
@@ -0,0 +1,13 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieLoadNewPageResult
+
+/**
+ * Requests loading of the next movie page and appends any newly available items to the backing
+ * roster.
+ *
+ * The returned [MovieLoadNewPageResult] reports whether a full page was loaded, the data source is
+ * exhausted, or the operation failed. Implementations are expected to manage pagination state
+ * internally.
+ */
+fun interface LoadNewPageMovieUC : suspend () -> MovieLoadNewPageResult
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/RefreshMovieUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/RefreshMovieUC.kt
new file mode 100644
index 00000000..03b46e24
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/RefreshMovieUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieLoadNewPageResult
+
+/**
+ * Rebuilds the movie roster from the first page.
+ *
+ * Implementations are expected to clear any in-memory and persisted pagination state before
+ * loading fresh data. The returned [MovieLoadNewPageResult] describes the refresh outcome.
+ */
+fun interface RefreshMovieUC : suspend () -> MovieLoadNewPageResult
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/SetMovieSortOptionUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/SetMovieSortOptionUC.kt
new file mode 100644
index 00000000..9626b33a
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/SetMovieSortOptionUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.MovieSortOption
+
+/**
+ * Marks the supplied sort option as selected.
+ *
+ * Callers should pass the option that represents the desired final selection. Implementations are
+ * expected to persist that choice and clear the selected flag from all competing options.
+ */
+fun interface SetMovieSortOptionUC : (MovieSortOption) -> Unit
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/UpdateMovieLikeUC.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/UpdateMovieLikeUC.kt
new file mode 100644
index 00000000..f3de6444
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/UpdateMovieLikeUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.Movie
+
+/**
+ * Toggles the like state for the supplied movie snapshot.
+ *
+ * Callers should provide the movie as it is currently rendered to the user. Implementations are
+ * expected to derive the opposite like state, update in-memory state, and persist the change.
+ */
+fun interface UpdateMovieLikeUC : suspend (Movie) -> Unit
diff --git a/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieFlow.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieFlow.kt
new file mode 100644
index 00000000..a9b00ee0
--- /dev/null
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieFlow.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.movie.model
+
+import com.velord.model.movie.Movie
+import com.velord.model.movie.MovieSortOption
+import kotlinx.coroutines.flow.Flow
+
+@JvmInline
+value class MovieFlow(val flow: Flow>)
+
+@JvmInline
+value class MovieSortOptionFlow(val flow: Flow>)
\ No newline at end of file
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/MovieLoadNewPageResult.kt b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieLoadNewPageResult.kt
similarity index 64%
rename from domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/MovieLoadNewPageResult.kt
rename to domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieLoadNewPageResult.kt
index 57eb1b25..a883e2f2 100644
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/MovieLoadNewPageResult.kt
+++ b/domain/usecase-movie/src/commonMain/kotlin/com/velord/usecase/movie/model/MovieLoadNewPageResult.kt
@@ -1,7 +1,7 @@
-package com.velord.usecase.movie.result
+package com.velord.usecase.movie.model
sealed class MovieLoadNewPageResult {
data object Success : MovieLoadNewPageResult()
- data object Exausted : MovieLoadNewPageResult()
+ data object Exhausted : MovieLoadNewPageResult()
data class LoadPageFailed(val message: String) : MovieLoadNewPageResult()
}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetAllMovieUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetAllMovieUCTest.kt
new file mode 100644
index 00000000..079714a8
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetAllMovieUCTest.kt
@@ -0,0 +1,136 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.Movie
+import com.velord.usecase.movie.model.MovieFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+import kotlin.time.Clock
+
+class GetAllMovieUCTest {
+
+ private val movies = listOf(
+ Movie(
+ id = 1,
+ title = "Movie 1",
+ description = "Description 1",
+ isLiked = false,
+ date = Clock.System.now(),
+ rating = 4.5f,
+ voteCount = 100,
+ imagePath = "imagePath1"
+ ),
+ Movie(
+ id = 2,
+ title = "Movie 2",
+ description = "Description 2",
+ isLiked = true,
+ date = Clock.System.now(),
+ rating = 3.8f,
+ voteCount = 50,
+ imagePath = "imagePath2"
+ )
+ )
+ private val heterogeneousMovies = listOf(
+ Movie(
+ id = 10,
+ title = "Classic Drama",
+ description = "Old movie",
+ isLiked = false,
+ date = Movie.toInstant("1999-12-31"),
+ rating = 1.5f,
+ voteCount = 7,
+ imagePath = null
+ ),
+ Movie(
+ id = 11,
+ title = "Future Sci-Fi",
+ description = "New movie",
+ isLiked = true,
+ date = Movie.toInstant("2025-08-20"),
+ rating = 9.8f,
+ voteCount = 9999,
+ imagePath = "/poster.png"
+ )
+ )
+
+ @Test
+ fun `invoke should return the exact movie flow from delegate`() = runTest {
+ val expectedFlow = flowOf(movies)
+ val useCase = GetAllMovieUC { MovieFlow(expectedFlow) }
+
+ val result = useCase()
+
+ assertSame(expectedFlow, result.flow)
+ assertEquals(movies, result.flow.first())
+ }
+
+ @Test
+ fun `invoke should preserve all delegate emissions`() = runTest {
+ val firstEmission = movies.take(1)
+ val secondEmission = movies
+ val useCase = GetAllMovieUC {
+ MovieFlow(flowOf(firstEmission, secondEmission))
+ }
+
+ val result = useCase()
+
+ assertEquals(listOf(firstEmission, secondEmission), result.flow.toList())
+ }
+
+ @Test
+ fun `invoke should call delegate on each invocation`() = runTest {
+ var invocationCount = 0
+ val useCase = GetAllMovieUC {
+ invocationCount += 1
+ MovieFlow(flowOf(listOf(movies[invocationCount - 1])))
+ }
+
+ val firstResult = useCase()
+ val secondResult = useCase()
+
+ assertEquals(2, invocationCount)
+ assertEquals(listOf(movies[0]), firstResult.flow.first())
+ assertEquals(listOf(movies[1]), secondResult.flow.first())
+ }
+
+ @Test
+ fun `invoke should support empty movie emission`() = runTest {
+ val useCase = GetAllMovieUC {
+ MovieFlow(flowOf(emptyList()))
+ }
+
+ val result = useCase()
+
+ assertEquals(emptyList(), result.flow.first())
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception before flow creation`() {
+ val useCase = GetAllMovieUC {
+ throw IllegalStateException("movie flow failed")
+ }
+
+ val error = assertFailsWith {
+ useCase()
+ }
+
+ assertEquals("movie flow failed", error.message)
+ }
+
+ @Test
+ fun `invoke should preserve heterogeneous movie payloads and order`() = runTest {
+ val useCase = GetAllMovieUC {
+ MovieFlow(flowOf(heterogeneousMovies))
+ }
+
+ val result = useCase()
+
+ assertEquals(heterogeneousMovies, result.flow.first())
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt
new file mode 100644
index 00000000..3cdb9d68
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt
@@ -0,0 +1,138 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.Movie
+import com.velord.usecase.movie.model.MovieFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+import kotlin.time.Clock
+
+class GetFavoriteMovieUCTest {
+
+ private val movies = listOf(
+ Movie(
+ id = 1,
+ title = "Movie 1",
+ description = "Description 1",
+ isLiked = false,
+ date = Clock.System.now(),
+ rating = 4.5f,
+ voteCount = 100,
+ imagePath = "imagePath1"
+ ),
+ Movie(
+ id = 2,
+ title = "Movie 2",
+ description = "Description 2",
+ isLiked = true,
+ date = Clock.System.now(),
+ rating = 3.8f,
+ voteCount = 50,
+ imagePath = "imagePath2"
+ )
+ )
+ private val heterogeneousMovies = listOf(
+ Movie(
+ id = 21,
+ title = "Indie Film",
+ description = "Low budget",
+ isLiked = true,
+ date = Movie.toInstant("2001-01-01"),
+ rating = 2.0f,
+ voteCount = 3,
+ imagePath = null
+ ),
+ Movie(
+ id = 22,
+ title = "Blockbuster",
+ description = "High budget",
+ isLiked = false,
+ date = Movie.toInstant("2024-11-11"),
+ rating = 8.7f,
+ voteCount = 15000,
+ imagePath = "/favorite.jpg"
+ )
+ )
+
+ @Test
+ fun `invoke should return the exact movie flow from delegate`() = runTest {
+ val expectedFlow = flowOf(movies)
+ val useCase = GetFavoriteMovieUC {
+ MovieFlow(expectedFlow)
+ }
+
+ val result = useCase()
+
+ assertSame(expectedFlow, result.flow)
+ assertEquals(movies, result.flow.first())
+ }
+
+ @Test
+ fun `invoke should preserve all delegate emissions`() = runTest {
+ val firstEmission = movies.take(1)
+ val secondEmission = movies
+ val useCase = GetFavoriteMovieUC {
+ MovieFlow(flowOf(firstEmission, secondEmission))
+ }
+
+ val result = useCase()
+
+ assertEquals(listOf(firstEmission, secondEmission), result.flow.toList())
+ }
+
+ @Test
+ fun `invoke should call delegate on each invocation`() = runTest {
+ var invocationCount = 0
+ val useCase = GetFavoriteMovieUC {
+ invocationCount += 1
+ MovieFlow(flowOf(listOf(movies[invocationCount - 1])))
+ }
+
+ val firstResult = useCase()
+ val secondResult = useCase()
+
+ assertEquals(2, invocationCount)
+ assertEquals(listOf(movies[0]), firstResult.flow.first())
+ assertEquals(listOf(movies[1]), secondResult.flow.first())
+ }
+
+ @Test
+ fun `invoke should support empty favorite movie emission`() = runTest {
+ val useCase = GetFavoriteMovieUC {
+ MovieFlow(flowOf(emptyList()))
+ }
+
+ val result = useCase()
+
+ assertEquals(emptyList(), result.flow.first())
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception before flow creation`() {
+ val useCase = GetFavoriteMovieUC {
+ throw IllegalStateException("favorite flow failed")
+ }
+
+ val error = assertFailsWith {
+ useCase()
+ }
+
+ assertEquals("favorite flow failed", error.message)
+ }
+
+ @Test
+ fun `invoke should preserve heterogeneous favorite movie payloads and order`() = runTest {
+ val useCase = GetFavoriteMovieUC {
+ MovieFlow(flowOf(heterogeneousMovies))
+ }
+
+ val result = useCase()
+
+ assertEquals(heterogeneousMovies, result.flow.first())
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt
new file mode 100644
index 00000000..92c00d6e
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt
@@ -0,0 +1,87 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.MovieSortOption
+import com.velord.model.movie.SortType
+import com.velord.usecase.movie.model.MovieSortOptionFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+
+class GetMovieSortOptionUCTest {
+
+ private val options = listOf(
+ MovieSortOption(SortType.DateAscending, false),
+ MovieSortOption(SortType.DateDescending, true)
+ )
+
+ @Test
+ fun `invoke should return the exact sort option flow from delegate`() = runTest {
+ val expectedFlow = flowOf(options)
+ val useCase = GetMovieSortOptionUC {
+ MovieSortOptionFlow(expectedFlow)
+ }
+
+ val result = useCase()
+
+ assertSame(expectedFlow, result.flow)
+ assertEquals(options, result.flow.first())
+ }
+
+ @Test
+ fun `invoke should preserve all delegate emissions`() = runTest {
+ val firstEmission = options.take(1)
+ val secondEmission = options
+ val useCase = GetMovieSortOptionUC {
+ MovieSortOptionFlow(flowOf(firstEmission, secondEmission))
+ }
+
+ val result = useCase()
+
+ assertEquals(listOf(firstEmission, secondEmission), result.flow.toList())
+ }
+
+ @Test
+ fun `invoke should call delegate on each invocation`() = runTest {
+ var invocationCount = 0
+ val useCase = GetMovieSortOptionUC {
+ invocationCount += 1
+ MovieSortOptionFlow(flowOf(listOf(options[invocationCount - 1])))
+ }
+
+ val firstResult = useCase()
+ val secondResult = useCase()
+
+ assertEquals(2, invocationCount)
+ assertEquals(listOf(options[0]), firstResult.flow.first())
+ assertEquals(listOf(options[1]), secondResult.flow.first())
+ }
+
+ @Test
+ fun `invoke should support empty sort option emission`() = runTest {
+ val useCase = GetMovieSortOptionUC {
+ MovieSortOptionFlow(flowOf(emptyList()))
+ }
+
+ val result = useCase()
+
+ assertEquals(emptyList(), result.flow.first())
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception before flow creation`() {
+ val useCase = GetMovieSortOptionUC {
+ throw IllegalStateException("sort option flow failed")
+ }
+
+ val error = assertFailsWith {
+ useCase()
+ }
+
+ assertEquals("sort option flow failed", error.message)
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt
new file mode 100644
index 00000000..e0332e2c
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt
@@ -0,0 +1,82 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieLoadNewPageResult
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class LoadNewPageMovieUCTest {
+
+ @Test
+ fun `invoke should return delegate result`() = runTest {
+ val useCase = LoadNewPageMovieUC {
+ MovieLoadNewPageResult.Success
+ }
+
+ val result = useCase()
+
+ assertEquals(MovieLoadNewPageResult.Success, result)
+ }
+
+ @Test
+ fun `invoke should call delegate on each invocation`() = runTest {
+ var invocationCount = 0
+ val useCase = LoadNewPageMovieUC {
+ invocationCount += 1
+ if (invocationCount == 1) {
+ MovieLoadNewPageResult.Success
+ } else {
+ MovieLoadNewPageResult.Exhausted
+ }
+ }
+
+ val firstResult = useCase()
+ val secondResult = useCase()
+
+ assertEquals(2, invocationCount)
+ assertEquals(MovieLoadNewPageResult.Success, firstResult)
+ assertEquals(MovieLoadNewPageResult.Exhausted, secondResult)
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception`() = runTest {
+ val useCase = LoadNewPageMovieUC {
+ throw IllegalStateException("load failed")
+ }
+
+ val error = assertFailsWith {
+ useCase()
+ }
+
+ assertEquals("load failed", error.message)
+ }
+
+ @Test
+ fun `invoke should return load page failed result from delegate`() = runTest {
+ val expectedResult = MovieLoadNewPageResult.LoadPageFailed("network error")
+ val useCase = LoadNewPageMovieUC {
+ expectedResult
+ }
+
+ val result = useCase()
+
+ assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun `invoke should support suspended delegate before returning`() = runTest {
+ var completed = false
+ val useCase = LoadNewPageMovieUC {
+ delay(1)
+ completed = true
+ MovieLoadNewPageResult.Exhausted
+ }
+
+ val result = useCase()
+
+ assertEquals(true, completed)
+ assertEquals(MovieLoadNewPageResult.Exhausted, result)
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/RefreshMovieUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/RefreshMovieUCTest.kt
new file mode 100644
index 00000000..960c1497
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/RefreshMovieUCTest.kt
@@ -0,0 +1,82 @@
+package com.velord.usecase.movie
+
+import com.velord.usecase.movie.model.MovieLoadNewPageResult
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class RefreshMovieUCTest {
+
+ @Test
+ fun `invoke should return delegate result`() = runTest {
+ val useCase = RefreshMovieUC {
+ MovieLoadNewPageResult.Success
+ }
+
+ val result = useCase()
+
+ assertEquals(MovieLoadNewPageResult.Success, result)
+ }
+
+ @Test
+ fun `invoke should call delegate on each invocation`() = runTest {
+ var invocationCount = 0
+ val useCase = RefreshMovieUC {
+ invocationCount += 1
+ if (invocationCount == 1) {
+ MovieLoadNewPageResult.Success
+ } else {
+ MovieLoadNewPageResult.LoadPageFailed("refresh exhausted")
+ }
+ }
+
+ val firstResult = useCase()
+ val secondResult = useCase()
+
+ assertEquals(2, invocationCount)
+ assertEquals(MovieLoadNewPageResult.Success, firstResult)
+ assertEquals(MovieLoadNewPageResult.LoadPageFailed("refresh exhausted"), secondResult)
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception`() = runTest {
+ val useCase = RefreshMovieUC {
+ throw IllegalStateException("refresh failed")
+ }
+
+ val error = assertFailsWith {
+ useCase()
+ }
+
+ assertEquals("refresh failed", error.message)
+ }
+
+ @Test
+ fun `invoke should return load page failed result from delegate`() = runTest {
+ val expectedResult = MovieLoadNewPageResult.LoadPageFailed("refresh denied")
+ val useCase = RefreshMovieUC {
+ expectedResult
+ }
+
+ val result = useCase()
+
+ assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun `invoke should support suspended delegate before returning`() = runTest {
+ var completed = false
+ val useCase = RefreshMovieUC {
+ delay(1)
+ completed = true
+ MovieLoadNewPageResult.Success
+ }
+
+ val result = useCase()
+
+ assertEquals(true, completed)
+ assertEquals(MovieLoadNewPageResult.Success, result)
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt
new file mode 100644
index 00000000..24fb0de5
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt
@@ -0,0 +1,81 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.MovieSortOption
+import com.velord.model.movie.SortType
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+
+class SetMovieSortOptionUCTest {
+
+ @Test
+ fun `invoke should pass selected option to delegate`() {
+ var capturedOption: MovieSortOption? = null
+ val expectedOption = MovieSortOption(SortType.DateAscending, false)
+ val useCase = SetMovieSortOptionUC { option ->
+ capturedOption = option
+ }
+
+ useCase(expectedOption)
+
+ assertSame(expectedOption, capturedOption)
+ assertEquals(expectedOption, capturedOption)
+ }
+
+ @Test
+ fun `invoke should preserve arguments across multiple invocations`() {
+ val capturedOptions = mutableListOf()
+ val firstOption = MovieSortOption.DEFAULT
+ val secondOption = MovieSortOption(SortType.DateDescending, true)
+ val useCase = SetMovieSortOptionUC { option ->
+ capturedOptions += option
+ }
+
+ useCase(firstOption)
+ useCase(secondOption)
+
+ assertEquals(listOf(firstOption, secondOption), capturedOptions)
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception`() {
+ val useCase = SetMovieSortOptionUC {
+ throw IllegalArgumentException("unsupported option")
+ }
+
+ val error = assertFailsWith {
+ useCase(MovieSortOption.DEFAULT)
+ }
+
+ assertEquals("unsupported option", error.message)
+ }
+
+ @Test
+ fun `invoke should forward already selected option unchanged`() {
+ var capturedOption: MovieSortOption? = null
+ val expectedOption = MovieSortOption(SortType.DateDescending, true)
+ val useCase = SetMovieSortOptionUC { option ->
+ capturedOption = option
+ }
+
+ useCase(expectedOption)
+
+ assertSame(expectedOption, capturedOption)
+ assertEquals(expectedOption, capturedOption)
+ }
+
+ @Test
+ fun `invoke should call delegate once per invocation`() {
+ var invocationCount = 0
+ val useCase = SetMovieSortOptionUC {
+ invocationCount += 1
+ }
+
+ useCase(MovieSortOption.DEFAULT)
+ useCase(MovieSortOption(SortType.DateAscending, false))
+ useCase(MovieSortOption(SortType.DateDescending, true))
+
+ assertEquals(3, invocationCount)
+ }
+}
diff --git a/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt
new file mode 100644
index 00000000..332eb5c6
--- /dev/null
+++ b/domain/usecase-movie/src/commonTest/kotlin/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt
@@ -0,0 +1,114 @@
+package com.velord.usecase.movie
+
+import com.velord.model.movie.Movie
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
+import kotlin.time.Clock
+
+class UpdateMovieLikeUCTest {
+
+ private val movie = Movie(
+ id = 1,
+ title = "Movie Title",
+ description = "Description",
+ isLiked = false,
+ date = Clock.System.now(),
+ rating = 4.5f,
+ voteCount = 100,
+ imagePath = "image.jpg"
+ )
+ private val heterogeneousMovie = Movie(
+ id = 99,
+ title = "Silent Documentary",
+ description = "No poster available",
+ isLiked = true,
+ date = Movie.toInstant("1984-04-14"),
+ rating = 0.5f,
+ voteCount = 1,
+ imagePath = null
+ )
+
+ @Test
+ fun `invoke should pass movie to delegate`() = runTest {
+ var capturedMovie: Movie? = null
+ val useCase = UpdateMovieLikeUC { updatedMovie ->
+ capturedMovie = updatedMovie
+ }
+
+ useCase(movie)
+
+ assertSame(movie, capturedMovie)
+ assertEquals(movie, capturedMovie)
+ }
+
+ @Test
+ fun `invoke should preserve movies across multiple invocations`() = runTest {
+ val capturedMovies = mutableListOf()
+ val secondMovie = movie.copy(id = 2, isLiked = true)
+ val useCase = UpdateMovieLikeUC { updatedMovie ->
+ capturedMovies += updatedMovie
+ }
+
+ useCase(movie)
+ useCase(secondMovie)
+
+ assertEquals(listOf(movie, secondMovie), capturedMovies)
+ }
+
+ @Test
+ fun `invoke should propagate delegate exception`() = runTest {
+ val useCase = UpdateMovieLikeUC {
+ throw IllegalStateException("update failed")
+ }
+
+ val error = assertFailsWith {
+ useCase(movie)
+ }
+
+ assertEquals("update failed", error.message)
+ }
+
+ @Test
+ fun `invoke should forward already liked movie unchanged`() = runTest {
+ var capturedMovie: Movie? = null
+ val likedMovie = movie.copy(isLiked = true)
+ val useCase = UpdateMovieLikeUC { updatedMovie ->
+ capturedMovie = updatedMovie
+ }
+
+ useCase(likedMovie)
+
+ assertSame(likedMovie, capturedMovie)
+ assertEquals(likedMovie, capturedMovie)
+ }
+
+ @Test
+ fun `invoke should support suspended delegate before completion`() = runTest {
+ var completed = false
+ val useCase = UpdateMovieLikeUC {
+ delay(1)
+ completed = true
+ }
+
+ useCase(movie)
+
+ assertEquals(true, completed)
+ }
+
+ @Test
+ fun `invoke should forward heterogeneous movies unchanged and in order`() = runTest {
+ val capturedMovies = mutableListOf()
+ val useCase = UpdateMovieLikeUC { updatedMovie ->
+ capturedMovies += updatedMovie
+ }
+
+ useCase(movie)
+ useCase(heterogeneousMovie)
+
+ assertEquals(listOf(movie, heterogeneousMovie), capturedMovies)
+ }
+}
diff --git a/domain/usecase-movie/src/main/AndroidManifest.xml b/domain/usecase-movie/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/domain/usecase-movie/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetAllMovieUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetAllMovieUC.kt
deleted file mode 100644
index bdff3c54..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetAllMovieUC.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieDS
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import com.velord.usecase.movie.result.GetMovieResult
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.combine
-
-class GetAllMovieUC(
- private val movieDS: MovieDS,
- private val movieSortDS: MovieSortDS
-) {
-
- operator fun invoke(): GetMovieResult = try {
- val merged = mergeMovieWithSort()
- GetMovieResult.Success(merged)
- } catch (e: Exception) {
- GetMovieResult.MergeError(e.message.orEmpty())
- }
-
- private fun mergeMovieWithSort(): Flow> {
- val all = movieDS.getFlow()
- val sort = movieSortDS.getSelectedFlow()
- return all.combine(sort) { movies, sortOption ->
- when (sortOption.type) {
- SortType.DateDescending -> movies.sortedByDescending { it.date }
- SortType.DateAscending -> movies.sortedBy { it.date }
- }
- }.catch {}
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetFavoriteMovieUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetFavoriteMovieUC.kt
deleted file mode 100644
index 8d0c6bfa..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetFavoriteMovieUC.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieFavoriteDS
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import com.velord.usecase.movie.result.GetFavoriteMovieResult
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.combine
-
-class GetFavoriteMovieUC(
- private val movieFavoriteDS: MovieFavoriteDS,
- private val movieSortDS: MovieSortDS
-) {
- operator fun invoke(): GetFavoriteMovieResult = try {
- val merged = mergeMovieWithSort()
- GetFavoriteMovieResult.Success(merged)
- } catch (e: Exception) {
- GetFavoriteMovieResult.MergeError(e.message.orEmpty())
- }
-
- private fun mergeMovieWithSort(): Flow> {
- val favorite = movieFavoriteDS.getFlow()
- val sort = movieSortDS.getSelectedFlow()
- return favorite.combine(sort) { movies, sortOption ->
- when (sortOption.type) {
- SortType.DateDescending -> movies.sortedByDescending { it.date }
- SortType.DateAscending -> movies.sortedBy { it.date }
- }
- }.catch {}
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetMovieSortOptionUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetMovieSortOptionUC.kt
deleted file mode 100644
index 3ac92a4d..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/GetMovieSortOptionUC.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MovieSortOption
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
-
-class GetMovieSortOptionUC(private val movieSortDS: MovieSortDS) {
- operator fun invoke(): Flow> = movieSortDS.getFlow().catch {}
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/LoadNewPageMovieUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/LoadNewPageMovieUC.kt
deleted file mode 100644
index b6a41a99..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/LoadNewPageMovieUC.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MoviePagination
-import com.velord.usecase.movie.dataSource.LoadNewPageMovieDS
-import com.velord.usecase.movie.result.MovieLoadNewPageResult
-
-class LoadNewPageMovieUC(private val dataSource: LoadNewPageMovieDS) {
-
- suspend operator fun invoke(): MovieLoadNewPageResult = try {
- val countOfNewItems = dataSource.load().value
-
- if (countOfNewItems < MoviePagination.PAGE_COUNT) {
- MovieLoadNewPageResult.Exausted
- } else {
- MovieLoadNewPageResult.Success
- }
- } catch (e: Exception) {
- MovieLoadNewPageResult.LoadPageFailed(e.message.orEmpty())
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/RefreshMovieUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/RefreshMovieUC.kt
deleted file mode 100644
index ddbc2401..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/RefreshMovieUC.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.usecase.movie.dataSource.RefreshMovieDS
-import com.velord.usecase.movie.result.MovieLoadNewPageResult
-
-class RefreshMovieUC(private val dataSource: RefreshMovieDS) {
-
- suspend operator fun invoke() = try {
- dataSource.refresh()
- MovieLoadNewPageResult.Success
- } catch (e: Exception) {
- MovieLoadNewPageResult.LoadPageFailed(e.message.orEmpty())
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/SetMovieSortOptionUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/SetMovieSortOptionUC.kt
deleted file mode 100644
index 9758cf95..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/SetMovieSortOptionUC.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MovieSortOption
-import com.velord.usecase.movie.dataSource.MovieSortDS
-
-class SetMovieSortOptionUC(private val movieSortDS: MovieSortDS) {
-
- operator fun invoke(newOption: MovieSortOption) {
- val updated = newOption.copy(isSelected = true)
- movieSortDS.update(updated)
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/UpdateMovieLikeUC.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/UpdateMovieLikeUC.kt
deleted file mode 100644
index 5c91e87a..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/UpdateMovieLikeUC.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.usecase.movie.dataSource.MovieFavoriteDS
-import com.velord.usecase.movie.result.UpdateMovieResult
-
-class UpdateMovieLikeUC(private val dataSource: MovieFavoriteDS) {
-
- suspend operator fun invoke(movie: Movie): UpdateMovieResult {
- val updated = movie.copy(isLiked = movie.isLiked.not())
- return try {
- dataSource.update(updated)
- UpdateMovieResult.Success
- } catch (e: Exception) {
- UpdateMovieResult.DbError(e.message.orEmpty())
- }
- }
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/LoadNewPageMovieDS.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/LoadNewPageMovieDS.kt
deleted file mode 100644
index b6a072fa..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/LoadNewPageMovieDS.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.velord.usecase.movie.dataSource
-
-import com.velord.model.movie.MovieRosterSize
-
-interface LoadNewPageMovieDS {
- suspend fun load(): MovieRosterSize
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieFavoriteDS.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieFavoriteDS.kt
deleted file mode 100644
index cc50a86f..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieFavoriteDS.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.velord.usecase.movie.dataSource
-
-import com.velord.model.movie.Movie
-import kotlinx.coroutines.flow.Flow
-
-interface MovieFavoriteDS {
- fun getFlow(): Flow>
- fun get(): List
- suspend fun update(movie: Movie)
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieSortDS.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieSortDS.kt
deleted file mode 100644
index 97e9c5c7..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/MovieSortDS.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.velord.usecase.movie.dataSource
-
-import com.velord.model.movie.MovieSortOption
-import kotlinx.coroutines.flow.Flow
-
-interface MovieSortDS {
- fun getFlow(): Flow>
- fun getSelectedFlow(): Flow
- fun update(newOption: MovieSortOption)
- fun getSelected(): MovieSortOption
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/RefreshMovieDS.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/RefreshMovieDS.kt
deleted file mode 100644
index a793bc4f..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/dataSource/RefreshMovieDS.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.velord.usecase.movie.dataSource
-
-import com.velord.model.movie.MovieRosterSize
-
-interface RefreshMovieDS {
- suspend fun refresh(): MovieRosterSize
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetFavoriteMovieResult.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetFavoriteMovieResult.kt
deleted file mode 100644
index 6551eb98..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetFavoriteMovieResult.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.velord.usecase.movie.result
-
-import com.velord.model.movie.Movie
-import kotlinx.coroutines.flow.Flow
-
-sealed class GetFavoriteMovieResult {
- data class Success(val flow: Flow>) : GetFavoriteMovieResult()
- data class MergeError(val message: String) : GetFavoriteMovieResult()
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetMovieResult.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetMovieResult.kt
deleted file mode 100644
index 2f055135..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/GetMovieResult.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.velord.usecase.movie.result
-
-import com.velord.model.movie.Movie
-import kotlinx.coroutines.flow.Flow
-
-sealed class GetMovieResult {
-
- data class Success(
- val flow: Flow>
- ) : GetMovieResult()
-
- data class DBError(
- val flow: Flow>,
- val message: String
- ) : GetMovieResult()
-
- data class MergeError(val message: String) : GetMovieResult()
-}
diff --git a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/UpdateMovieResult.kt b/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/UpdateMovieResult.kt
deleted file mode 100644
index a8cc0f08..00000000
--- a/domain/usecase-movie/src/main/java/com/velord/usecase/movie/result/UpdateMovieResult.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.velord.usecase.movie.result
-
-sealed class UpdateMovieResult {
- data object Success : UpdateMovieResult()
- data class DbError(val message: String) : UpdateMovieResult()
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetAllMovieUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetAllMovieUCTest.kt
deleted file mode 100644
index 6d100a8b..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetAllMovieUCTest.kt
+++ /dev/null
@@ -1,439 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.model.movie.MovieSortOption
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieDS
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import com.velord.usecase.movie.result.GetMovieResult
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.spyk
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.withTimeoutOrNull
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import java.util.Calendar
-
-class GetAllMovieUCTest {
-
-
- private val movie1 = Movie(
- 1, "Movie 1", "Description 1", false, Calendar.getInstance(), 4.5f, 100, "imagePath1"
- )
- private val movie2 = Movie(
- 2, "Movie 2", "Description 2", true,
- Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }, 3.8f, 50, "imagePath2"
- )
- private val movieList = listOf(movie1, movie2)
-
- private val movieDS = mockk {
- coEvery { get() } returns movieList
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(movieList)
- }
- private val movieSortDS = mockk {
- every { getSelectedFlow() } returns flowOf(DESCENDING)
- }
-
- @Test
- fun `invoke should return Success when movies are available and sorted by DateDescending`() = runTest {
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- // Get the list of movies from the flow
- val movies = (result as GetMovieResult.Success).flow.toList().flatten()
- assertEquals(2, movies.size)
- assertTrue(movies[0].date.after(movies[1].date)) // Verify descending date order
- }
-
- @Test
- fun `invoke should return Success when movies are available and sorted by DateAscending`() = runTest {
- // ... (Similar to the previous test, but with SortType.DateAscending)
- val movieSortDS = mockk {
- every { getSelectedFlow() } returns flowOf(ASCENDING)
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- val movies = (result as GetMovieResult.Success).flow.toList().flatten()
- assertEquals(2, movies.size)
- assertTrue(movies[0].date.before(movies[1].date)) // Verify ascending date order
- }
-
- @Test
- fun `invoke should return DBError when an exception occurs`() = runTest {
- val movieDS = mockk {
- coEvery { get() } returns emptyList()
- coEvery { loadFromDb() } throws Exception("Database error")
- every { getFlow() } returns flowOf(emptyList())
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.DBError)
- assertEquals("Database error", (result as GetMovieResult.DBError).message)
- assertTrue(result.flow.toList().flatten().isEmpty())
- }
-
- @Test
- fun `invoke should load movies from DB when initially empty`() = runTest {
- val movieDS = mockk {
- coEvery { get() } returns emptyList()
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(listOf(movie1))
- }
- val movieSortDS = mockk {
- every { getSelectedFlow() } returns flowOf(DESCENDING)
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- getAllMovieUC() // Call invoke to trigger loading from DB
-
- coVerify { movieDS.loadFromDb() } // Verify that loadFromDB was called
- }
-
- @Test
- fun `invoke should handle empty movie list`() = runTest {
- val movieDS = mockk {
- coEvery { get() } returns emptyList()
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(emptyList())
- coEvery { loadNewPage() } returns 0
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success) // Success is still expected
- val movies = (result as GetMovieResult.Success).flow.toList().flatten()
- assertTrue(movies.isEmpty()) // Verify that the resulting list is empty
- }
-
- @Test
- fun `invoke should handle movies with same date when sorting`() = runTest {
- val sameDate = Calendar.getInstance()
- val movieDS = mockk {
- coEvery { get() } returns listOf(
- Movie(1, "Movie 1", "Description 1", false, sameDate, 4.5f, 100),
- Movie(2, "Movie 2", "Description 2", true, sameDate, 3.8f, 50)
- )
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(listOf(
- Movie(1, "Movie 1", "Description 1", false, sameDate, 4.5f, 100),
- Movie(2, "Movie 2", "Description 2", true, sameDate, 3.8f, 50)
- ))
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- val movies = (result as GetMovieResult.Success).flow.toList().flatten()
- assertEquals(2, movies.size)
- // You might want to add assertions based on how you expect movies with the same date to be ordered
- }
-
- @Test
- fun `invoke should handle exception during sorting and return MergeError (mocking private function)`() =
- runTest {
- val exception = RuntimeException("Simulated sorting exception")
- // Create a spy object
- val getAllMovieUC = spyk(GetAllMovieUC(mockk(), mockk()))
-
- // Mock the private mergeMovieWithSort function to throw an exception
- every { getAllMovieUC["mergeMovieWithSort"]() } throws exception
-
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.MergeError)
- assertEquals(exception.message, (result as GetMovieResult.MergeError).message)
- }
-
- @Test
- fun `invoke should handle extremely large movie list efficiently`() = runTest {
- val largeMovieList = (1..100000).map {
- Movie(it, "Movie $it", "Desc $it", false, Calendar.getInstance(), (it % 5).toFloat(), it * 10)
- }
- val movieDS = mockk {
- coEvery { get() } returns largeMovieList
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(largeMovieList)
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
-
- // Measure the time taken to get the result
- val startTime = System.currentTimeMillis()
- val result = getAllMovieUC()
- val endTime = System.currentTimeMillis()
-
- assertTrue(result is GetMovieResult.Success)
- // Add an assertion to check if the sorting was performed within an acceptable time limit
- assertTrue(endTime - startTime < 500) // Adjust the time limit as needed
- }
-
- @Test
- fun `invoke should handle errors emitted by upstream flows and return Success with empty flow`() =
- runTest {
- val exception = RuntimeException("Simulated error from movieDS")
- val movieDS = mockk {
- every { getFlow() } returns flow {
- emit(emptyList())
- throw exception
- }
- coEvery { get() } returns emptyList()
- coEvery { loadFromDb() } returns 0
- coEvery { loadNewPage() } returns 0 // Mock loadNewPage()
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- val movies = (result as GetMovieResult.Success).flow.first()
- assertTrue(movies.isEmpty())
- coVerify { movieDS.loadNewPage() } // Verify that loadNewPage() was called
- }
-
- @Test
- fun `invoke should handle sort option changes and apply them correctly`() = runTest {
- val movieSortDS = mockk {
- every { getSelectedFlow() } returns flow {
- emit(DESCENDING)
- delay(50)
- emit(ASCENDING)
- }
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success) // Assert Success
- val resultFlow = (result as GetMovieResult.Success).flow // Extract the flow
-
- val firstEmission = withTimeoutOrNull(100) { resultFlow.first() }
- val secondEmission = withTimeoutOrNull(100) { resultFlow.drop(1).first() }
-
- assertNotNull(firstEmission)
- assertNotNull(secondEmission)
- // Use safe calls to handle potential nulls in the lists
- // descending
- assertTrue(firstEmission?.get(0)?.date?.after(firstEmission[1].date) ?: false)
- // ascending
- assertTrue(secondEmission?.get(0)?.date?.before(secondEmission[1].date) ?: false)
- }
-
- @Test
- fun `invoke should handle DBError when exception occurs during loadFromDB and flow is not empty`() =
- runTest {
- val exception = RuntimeException("Database error")
- val movieDS = mockk {
- coEvery { get() } returns emptyList()
- coEvery { loadFromDb() } throws exception
- every { getFlow() } returns flowOf(listOf(movie1)) // Non-empty flow
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.DBError)
- // Use safe call to handle potential null in the list
- assertEquals("Database error", (result as GetMovieResult.DBError).message)
- assertEquals(1, result.flow.firstOrNull()?.size ?: 0)
- }
-
- @Test
- fun `invoke should handle cancellation gracefully and not emit results after cancellation`() =
- runTest {
- val movieDS = mockk {
- every { getFlow() } returns flow {
- delay(500) // Simulate a long-running operation
- emit(listOf(movie1))
- }
- coEvery { loadFromDb() } returns 0
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val collectedMovies = mutableListOf()
- val job = launch {
- val result = getAllMovieUC()
- if (result is GetMovieResult.Success) {
- result.flow.collect { movies ->
- collectedMovies.addAll(movies)
- }
- }
- }
-
- delay(100) // Allow some time for the flow to start
- job.cancel() // Cancel the coroutine
- delay(600) // Wait longer than the simulated delay in the flow
-
- // Assert that no movies were collected after cancellation
- assertTrue(collectedMovies.isEmpty())
- }
-
- @Test
- fun `invoke should handle multiple emissions from movieDS getFlow`() = runTest {
- val movieDS = mockk {
- every { getFlow() } returns flow {
- emit(listOf(movie1))
- delay(50)
- emit(listOf(movie1, movie2))
- }
- coEvery { get() } returns listOf(movie1)
- coEvery { loadFromDb() } returns 0
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- val resultFlow = (result as GetMovieResult.Success).flow
-
- val firstEmission = withTimeoutOrNull(100) { resultFlow.first() }
- val secondEmission = withTimeoutOrNull(100) { resultFlow.drop(1).first() }
-
- assertNotNull(firstEmission)
- assertNotNull(secondEmission)
-
- assertEquals(1, firstEmission?.size) // First emission should have one movie
- assertEquals(2, secondEmission?.size) // Second emission should have two movies
- }
-
- @Test
- fun `invoke should handle very late emission from movieSortDS getSelectedFlow`() = runTest {
- val movieSortDS = mockk {
- every { getSelectedFlow() } returns flow {
- delay(500) // Simulate a very late emission
- emit(DESCENDING)
- }
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = withTimeoutOrNull(1000) { getAllMovieUC() }
-
- assertNotNull(result) // Assert that the result is null (timeout)
- assertTrue(result is GetMovieResult.Success) // Assert that the result is Success
- }
-
- @Test
- fun `invoke should handle concurrent emissions from both movieDS and movieSortDS`() = runTest {
- val movie3 = Movie(
- 3, "Movie 3", "Desc 3", false, Calendar.getInstance(), 4.2f, 80, "imagePath3"
- )
- val movie4 = Movie(
- 4, "Movie 4", "Desc 4", true,
- Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }, 3.5f, 60, "imagePath4"
- )
- val movieDS = mockk {
- every { getFlow() } returns flow {
- emit(listOf(movie3))
- delay(50)
- emit(listOf(movie3, movie4))
- }
- coEvery { get() } returns listOf(movie1)
- coEvery { loadFromDb() } returns 0
- }
- val movieSortDS = mockk {
- every { getSelectedFlow() } returns flow {
- delay(25) // Emit a sort option slightly after the first movie emission
- emit(DESCENDING)
- delay(75) // Emit another sort option after the second movie emission
- emit(ASCENDING)
- }
- }
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.Success)
- val resultFlow = (result as GetMovieResult.Success).flow
-
- val firstEmission = withTimeoutOrNull(100) { resultFlow.first() }
- val secondEmission = withTimeoutOrNull(150) { resultFlow.drop(1).first() }
-
- // Handle potential nulls from withTimeoutOrNull
- if (firstEmission == null || secondEmission == null) {
- fail("Emissions should not be null within the given timeouts")
- } else {
- // Assertions for non-null emissions
- assertEquals(1, firstEmission.size)
- assertEquals(2, secondEmission.size)
- assertTrue(secondEmission[0].date.after(secondEmission[1].date))
- }
- }
-
- @Test
- fun `invoke should handle exceptions in movieDS getFlow and return a MergeError`() = runTest {
- val movieDS = mockk {
- every { getFlow() } throws RuntimeException("Mocked Exception") // Simulate an exception
- }
- val movieSortDS = mockk() // We don't need to mock this for this test
-
- val getAllMovieUC = GetAllMovieUC(movieDS, movieSortDS)
- val result = getAllMovieUC()
-
- assertTrue(result is GetMovieResult.MergeError) // Assert that the result is a MergeError
- }
-
- @Test
- fun `invoke should handle movies with same date in both sort orders`() = runTest {
- val sameDate = Calendar.getInstance()
- val movie3 = Movie(3, "Movie 3", "Desc 3", false, sameDate, 4.2f,80, "imagePath3")
- val movie4 = Movie(4, "Movie 4", "Desc 4", true, sameDate, 3.5f, 60, "imagePath4")
- val movieListWithSameDate = listOf(movie3, movie4)
-
- val movieDS = mockk {
- coEvery { get() } returns movieListWithSameDate
- coEvery { loadFromDb() } returns 0
- every { getFlow() } returns flowOf(movieListWithSameDate)
- }
-
- //Test with descending order
- val movieSortDSDescending = mockk {
- every { getSelectedFlow() } returns flowOf(DESCENDING)
- }
- val getAllMovieUCDescending = GetAllMovieUC(movieDS, movieSortDSDescending)
- val resultDescending = getAllMovieUCDescending()
- assertTrue(resultDescending is GetMovieResult.Success)
- val moviesDescending = (resultDescending as GetMovieResult.Success).flow.first()
- // Order might be based on insertion order or another property
- assertEquals(movie3, moviesDescending[0])
- assertEquals(movie4, moviesDescending[1])
-
- // Test with ascending order
- val movieSortDSAscending = mockk {
- every { getSelectedFlow() } returns flowOf(ASCENDING)
- }
- val getAllMovieUCAscending = GetAllMovieUC(movieDS, movieSortDSAscending)
- val resultAscending = getAllMovieUCAscending()
- assertTrue(resultAscending is GetMovieResult.Success)
- val moviesAscending = (resultAscending as GetMovieResult.Success).flow.first()
- // Assuming you want to order by ID in ascending order
- assertEquals(movie3, moviesAscending[0])
- assertEquals(movie4, moviesAscending[1])
- }
-
- companion object {
- private val DESCENDING = MovieSortOption(SortType.DateDescending, true)
- private val ASCENDING = MovieSortOption(SortType.DateAscending, true)
- }
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt
deleted file mode 100644
index 3c15f27d..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetFavoriteMovieUCTest.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.model.movie.MovieSortOption
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieFavoriteDS
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import com.velord.usecase.movie.result.GetFavoriteMovieResult
-import io.mockk.coEvery
-import io.mockk.every
-import io.mockk.mockk
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import java.util.Calendar
-
-class GetFavoriteMovieUCTest {
-
- private val calendar1 = Calendar.getInstance()
- private val calendar2 = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) } // Add a day to calendar2
- private val movie1 = Movie(1, "Movie 1", "Description 1", false, calendar1, 4.5f, 100, "imagePath1")
- private val movie2 = Movie(2, "Movie 2", "Description 2", true, calendar2, 3.8f, 50, "imagePath2")
- private val expectedMovies = listOf(movie1, movie2)
-
- private val favoriteDS = mockk {
- every { getFlow() } returns flow { emit(expectedMovies) }
- every { get() } returns emptyList() // Mocking to avoid unnecessary interactions
- coEvery { update(any()) } returns Unit
- }
-
- private val expectedOptions = listOf(
- MovieSortOption(SortType.DateAscending, false),
- MovieSortOption(SortType.DateDescending, true)
- )
- private val movieSortDS = mockk {
- every { getSelectedFlow() } returns flowOf(expectedOptions[1]) // Default to DateDescending
- every { getFlow() } returns flow { emit(expectedOptions) }
- }
-
- @Test
- fun `invoke should return Success with sorted movies when favorites are available`() = runTest {
- val getFavoriteMovieUC = GetFavoriteMovieUC(favoriteDS, movieSortDS)
- val result = getFavoriteMovieUC()
-
- assertTrue(result is GetFavoriteMovieResult.Success)
- val movies = mutableListOf()
- (result as GetFavoriteMovieResult.Success).flow.collect { movies.addAll(it) }
- assertEquals(expectedMovies.size, movies.size)
- assertTrue(movies[0].date.after(movies[1].date)) // Verify sorting
- }
-
- @Test
- fun `invoke should return Success with empty list when no favorites are available`() = runTest {
- val emptyFavoriteDS = mockk {
- every { getFlow() } returns flowOf(emptyList())
- }
- val getFavoriteMovieUC = GetFavoriteMovieUC(emptyFavoriteDS, movieSortDS)
- val result = getFavoriteMovieUC()
-
- assertTrue(result is GetFavoriteMovieResult.Success)
- val movies = mutableListOf()
- (result as GetFavoriteMovieResult.Success).flow.collect { movies.addAll(it) }
- assertTrue(movies.isEmpty())
- }
-
- @Test
- fun `invoke should handle exceptions from favoriteDS and return MergeError`() = runTest {
- val exception = RuntimeException("Simulated error from favoriteDS")
- val errorFavoriteDS = mockk {
- every { getFlow() } throws exception
- }
- val getFavoriteMovieUC = GetFavoriteMovieUC(errorFavoriteDS, movieSortDS)
- val result = getFavoriteMovieUC()
-
- assertTrue(result is GetFavoriteMovieResult.MergeError)
- assertEquals(exception.message, (result as GetFavoriteMovieResult.MergeError).message)
- }
-
- @Test
- fun `invoke should handle exceptions during sorting and return Success with initial movies`() = runTest {
- val exception = RuntimeException("Simulated sorting exception")
- val errorFavoriteDS = mockk {
- every { getFlow() }returns flow {
- emit(expectedMovies)
- throw exception
- }
- }
- val getFavoriteMovieUC = GetFavoriteMovieUC(errorFavoriteDS, movieSortDS)
- val result = getFavoriteMovieUC()
-
- // Log the result object
- println("Result: $result")
-
- assertTrue(result is GetFavoriteMovieResult.Success)
- val movies = mutableListOf()
- (result as GetFavoriteMovieResult.Success).flow.collect { movies.addAll(it) }
-
- // Log the collected movies
- println("Collected Movies:$movies")
-
- // Sort expectedMovies in descending order of date
- val expectedSortedMovies = expectedMovies.sortedByDescending { it.date }
-
- assertEquals(expectedSortedMovies, movies) // Verify the initial emission
- }
-
- @Test
- fun `invoke should apply sort option changes correctly`() = runTest {
- val dynamicMovieSortDS = mockk {
- every { getSelectedFlow() } returns flow {
- emit(expectedOptions[0]) // Emit DateAscending first
- delay(50)
- emit(expectedOptions[1]) // Then emit DateDescending
- }
- }
- val getFavoriteMovieUC = GetFavoriteMovieUC(favoriteDS, dynamicMovieSortDS)
- val result = getFavoriteMovieUC()
-
- assertTrue(result is GetFavoriteMovieResult.Success)
- val resultFlow = (result as GetFavoriteMovieResult.Success).flow
- val emissions = mutableListOf>()
- resultFlow.take(2).collect { emissions.add(it) } // Collect two emissions
-
- assertEquals(2, emissions.size) // Verify we got two emissions
- // Ascending (first emission) - Check IDs since dates might be very close
- assertTrue(emissions[0][0].id < emissions[0][1].id)
- // Descending (second emission) - Check IDs since dates might be very close
- assertTrue(emissions[1][0].id > emissions[1][1].id)
- }
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt
deleted file mode 100644
index 7c8106f0..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/GetMovieSortOptionUCTest.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MovieSortOption
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import io.mockk.every
-import io.mockk.mockk
-import kotlinx.coroutines.flow.count
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class GetMovieSortOptionUCTest {
-
- @Test
- fun `invoke should return the flow from movieSortDS`() = runTest {
- val expectedOptions = listOf(
- MovieSortOption(SortType.DateAscending, false),
- MovieSortOption(SortType.DateDescending, true)
- )
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow { emit(expectedOptions) }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions = resultFlow.first()
-
- assertEquals(expectedOptions, actualOptions)
- }
-
- @Test
- fun `invoke should return an empty flow when movieSortDS emits an empty flow`() = runTest {
- val movieSortDS = mockk {
- every { getFlow() } returns emptyFlow()
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions = resultFlow.firstOrNull()
-
- assertEquals(null, actualOptions)
- }
-
- @Test
- fun `invoke should handle exceptions in movieSortDS getFlow and terminate`() = runTest {
- val expectedOptions = listOf(
- MovieSortOption(SortType.DateAscending, false)
- )
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow {
- emit(expectedOptions)
- throw RuntimeException("Mocked Exception")
- }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions1 = resultFlow.first()
- val actualOptions2 = resultFlow.drop(1).firstOrNull()
-
- assertEquals(expectedOptions, actualOptions1)
- assertEquals(null, actualOptions2) // Assert flow termination after exception
- }
-
- @Test
- fun `invoke should emit multiple lists from movieSortDS`() = runTest {
- val expectedOptions1 = listOf(
- MovieSortOption(SortType.DateAscending, false)
- )
- val expectedOptions2 = listOf(
- MovieSortOption(SortType.DateAscending, false),
- MovieSortOption(SortType.DateDescending, true)
- )
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow {
- emit(expectedOptions1)
- emit(expectedOptions2)
- }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions1 = resultFlow.first()
- val actualOptions2 = resultFlow.drop(1).first()
-
- assertEquals(expectedOptions1, actualOptions1)
- assertEquals(expectedOptions2, actualOptions2)
- }
-
- @Test
- fun `invoke should handle a delayed exception in movieSortDS getFlow and terminate`() = runTest {
- val expectedOptions =listOf(
- MovieSortOption(SortType.DateAscending, false)
- )
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow {
- emit(expectedOptions)
- kotlinx.coroutines.delay(100) // Introduce a delay
- throw RuntimeException("Mocked Exception")
- }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions = resultFlow.first()
- val exceptionThrown = try {
- resultFlow.drop(1).first()
- false
- } catch (_: RuntimeException) {
- true
- }
-
- assertEquals(expectedOptions, actualOptions)
- assertTrue(exceptionThrown) // Assert that an exception was thrown after the first emission
- }
-
- @Test
- fun `invoke should handle a flow that emits an empty list`() = runTest {
- val expectedOptions = emptyList() // Explicitly an empty list
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow { emit(expectedOptions) }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions = resultFlow.first()
-
- assertEquals(expectedOptions, actualOptions) // Asserting an empty list emission
- }
-
- @Test
- fun `invoke should handle a flow with multiple empty list emissions`() = runTest {
- val movieSortDS = mockk {
- every { getFlow() } returns flow {
- emit(emptyList())
- emit(emptyList())
- emit(emptyList())
- }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val emissionsCount = resultFlow.count()
-
- assertEquals(3, emissionsCount) // Verify three empty list emissions
- }
-
- @Test
- fun `invoke should handle a flow with mixed empty and non-empty list emissions`() = runTest {
- val expectedOptions = listOf(
- MovieSortOption(SortType.DateDescending, true)
- )
-
- val movieSortDS = mockk {
- every { getFlow() } returns flow {
- emit(emptyList())
- emit(expectedOptions)
- emit(emptyList())
- }
- }
-
- val getMovieSortOptionUC = GetMovieSortOptionUC(movieSortDS)
- val resultFlow = getMovieSortOptionUC()
-
- val actualOptions = resultFlow.drop(1).first() // Collect the non-empty emission
-
- assertEquals(expectedOptions, actualOptions)
- }
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt
deleted file mode 100644
index 954d042e..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/LoadNewPageMovieUCTest.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MoviePagination
-import com.velord.usecase.movie.dataSource.MovieDS
-import com.velord.usecase.movie.result.MovieLoadNewPageResult
-import io.mockk.coEvery
-import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class LoadNewPageMovieUCTest {
-
- @Test
- fun `invoke should return Success when dataSource loads a full page`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns MoviePagination.PAGE_COUNT
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
- @Test
- fun `invoke should return Exausted when dataSource loads less than a full page`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns MoviePagination.PAGE_COUNT - 10
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- assertEquals(MovieLoadNewPageResult.Exausted, result)
- }
-
- @Test
- fun `invoke should return LoadPageFailed when dataSource throws an exception`() = runTest {
- val errorMessage = "Test Error"
- val dataSource = mockk {
- coEvery { loadNewPage() } throws Exception(errorMessage)
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- assertEquals(MovieLoadNewPageResult.LoadPageFailed(errorMessage), result)
- }
-
- @Test
- @Suppress("MaxLineLength")
- fun `invoke should return LoadPageFailed with empty message when dataSource throws an exception with empty message`() =
- runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } throws Exception("")
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- assertEquals(MovieLoadNewPageResult.LoadPageFailed(""), result)
- }
-
- @Test
- fun `invoke should return LoadPageFailed when dataSource throws an exception with a very long message`() =
- runTest {
- val veryLongErrorMessage = "This is a very long error message that exceeds any reasonable length" +
- " and should be truncated or handled appropriately by the UI to avoid display issues."
- val dataSource = mockk {
- coEvery { loadNewPage() } throws Exception(veryLongErrorMessage)
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- // Assert that the full error message is still captured in the result
- assertEquals(MovieLoadNewPageResult.LoadPageFailed(veryLongErrorMessage), result)
- }
-
- @Test
- fun `invoke should return Exausted when dataSource loads zero new items`() = runTest{
- val dataSource = mockk {
- coEvery { loadNewPage() } returns 0 // Simulate loading zero items
- }
-
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- // Expect Exausted as no new items were loaded
- assertEquals(MovieLoadNewPageResult.Exausted, result)
- }
-
- @Test
- fun `invoke returns Success when loaded count is exactly PAGE_COUNT`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns MoviePagination.PAGE_COUNT
- }
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- assertEquals(MovieLoadNewPageResult.Success, loadNewPageMovieUC())
- }
-
- @Test
- fun`invoke returns Exausted when loaded count is one less than PAGE_COUNT`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns MoviePagination.PAGE_COUNT - 1
- }
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- assertEquals(MovieLoadNewPageResult.Exausted, loadNewPageMovieUC())
- }
-
- @Test
- fun `invoke returns Success when loaded count is one more than PAGE_COUNT`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns MoviePagination.PAGE_COUNT + 1
- }
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- assertEquals(MovieLoadNewPageResult.Success, loadNewPageMovieUC())
- }
-
- @Test
- fun `invoke returns Exausted when dataSource returns a negative count`() = runTest {
- val dataSource = mockk {
- coEvery { loadNewPage() } returns -5 // Simulate an unexpected negative count
- }
- val loadNewPageMovieUC = LoadNewPageMovieUC(dataSource)
- val result = loadNewPageMovieUC()
-
- // Expect Exausted as a negative count indicates no new items
- assertEquals(MovieLoadNewPageResult.Exausted, result)
- }
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/RefreshMovieUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/RefreshMovieUCTest.kt
deleted file mode 100644
index 569f2268..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/RefreshMovieUCTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.usecase.movie.dataSource.MovieDS
-import com.velord.usecase.movie.result.MovieLoadNewPageResult
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockk
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class RefreshMovieUCTest {
-
- @Test
- fun `invoke should return Success when refresh completes successfully`() = runTest {
- val dataSource= mockk {
- coEvery { refresh() } returns 10 // Assuming a successful refresh returns a positive number
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- coVerify(exactly = 1) { dataSource.refresh() }
- }
-
- @Test
- fun `invoke should return LoadPageFailed when refresh throws an exception`() = runTest {
- val errorMessage = "Refresh Error"
- val dataSource = mockk {
- coEvery { refresh() } throws Exception(errorMessage)
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.LoadPageFailed(errorMessage), result)
- }
-
- @Test
- fun `invoke should return LoadPageFailed with empty message when refresh throws an exception with empty message`() =
- runTest {
- val dataSource = mockk {
- coEvery { refresh() } throws Exception("")
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.LoadPageFailed(""), result)
- }
-
- @Test
- fun `invoke should return Success when refresh returns zero`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returns 0
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
- @Test
- fun `invoke should return Success when refresh returns a large positive number`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returns 1000
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
- @Test
- fun `invokeshould return Success when refresh returns the minimum integer value`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returns Int.MIN_VALUE
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
- @Test
- fun `invoke should return Success when refresh returns the maximum integer value`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returns Int.MAX_VALUE
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
- @Test
- fun `invoke should not modify the result if refresh is called multiple times`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returnsMany listOf(5, 10, 15)
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- repeat(3) {val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
- }
-
- @Test
- fun `invoke should handle consecutive calls to refresh`() = runTest {
- val dataSource = mockk {
- coEvery { refresh() } returnsMany listOf(5, 10)
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- assertEquals(MovieLoadNewPageResult.Success, refreshMovieUC())
- assertEquals(MovieLoadNewPageResult.Success, refreshMovieUC())
- }
-
- @Test
- fun `invoke should handle refresh being called after a delay`() = runTest {
- val dataSource = mockk{
- coEvery { refresh() } coAnswers {
- delay(1000) // Introduce a 1-second delay within refresh()
- 5
- }
- }
- val refreshMovieUC = RefreshMovieUC(dataSource)
- val result = refreshMovieUC()
- assertEquals(MovieLoadNewPageResult.Success, result)
- }
-
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt
deleted file mode 100644
index 18d1ef04..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/SetMovieSortOptionUCTest.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.MovieSortOption
-import com.velord.model.movie.SortType
-import com.velord.usecase.movie.dataSource.MovieSortDS
-import io.mockk.coVerify
-import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class SetMovieSortOptionUCTest {
-
- @Test
- fun `invoke should update movieSortDS with the selected option`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val newOption = MovieSortOption(SortType.DateAscending, false)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
-
- setMovieSortOptionUC(newOption)
-
- coVerify { movieSortDS.update(newOption.copy(isSelected = true)) }
- }
-
- @Test
- fun `invoke should update movieSortDS with already selected option`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val newOption = MovieSortOption(SortType.DateDescending, true)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
-
- setMovieSortOptionUC(newOption)
-
- coVerify { movieSortDS.update(newOption.copy(isSelected = true)) }
- }
-
- @Test
- fun `invoke should update movieSortDS with different options consecutively`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val option1 = MovieSortOption(SortType.DateAscending, false)
- val option2 = MovieSortOption(SortType.DateDescending, false)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
-
- setMovieSortOptionUC(option1)
- setMovieSortOptionUC(option2)
-
- coVerify {
- movieSortDS.update(option1.copy(isSelected = true))
- movieSortDS.update(option2.copy(isSelected = true))
- }
- }
-
- @Test
- fun `invoke should update movieSortDS with the same option multiple times`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val option = MovieSortOption(SortType.DateAscending, false)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
-
- repeat(3) { setMovieSortOptionUC(option) }
-
- coVerify(exactly = 3) { movieSortDS.update(option.copy(isSelected = true)) }
- }
-
- @Test
- fun `invoke should update movieSortDS with Default option`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
- setMovieSortOptionUC(MovieSortOption.Default)
-
- coVerify { movieSortDS.update(MovieSortOption.Default.copy(isSelected = true)) }
- }
-
- @Test
- fun `invoke should only update movieSortDS once per invocation`() = runTest {
- val movieSortDS = mockk(relaxed = true)
- val option = MovieSortOption(SortType.DateDescending, false)
- val setMovieSortOptionUC = SetMovieSortOptionUC(movieSortDS)
-
- setMovieSortOptionUC(option)
-
- coVerify(exactly = 1) { movieSortDS.update(any()) }
- }
-}
diff --git a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt b/domain/usecase-movie/src/test/java/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt
deleted file mode 100644
index 181f639f..00000000
--- a/domain/usecase-movie/src/test/java/com/velord/usecase/movie/UpdateMovieLikeUCTest.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.velord.usecase.movie
-
-import com.velord.model.movie.Movie
-import com.velord.usecase.movie.dataSource.MovieFavoriteDS
-import com.velord.usecase.movie.result.UpdateMovieResult
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import java.util.Calendar
-
-class UpdateMovieLikeUCTest {
-
- private val movie = Movie(
- 1, "Movie Title", "Description", false, Calendar.getInstance(),
- 4.5f, 100, "image.jpg"
- )
-
- @Test
- fun `invoke should toggle isLiked and update dataSource`() = runTest {
- val dataSource = mockk(relaxed = true)
- val useCase = UpdateMovieLikeUC(dataSource)
- val result = useCase(movie)
-
- assertEquals(UpdateMovieResult.Success, result)
- coVerify { dataSource.update(movie.copy(isLiked = true)) }
- }
-
- @Test
- fun `invoke should handle toggling isLiked from true to false`() = runTest {
- val dataSource = mockk(relaxed = true)
- val useCase = UpdateMovieLikeUC(dataSource)
- val result = useCase(movie.copy(isLiked = true))
-
- assertEquals(UpdateMovieResult.Success, result)
- coVerify { dataSource.update(movie.copy(isLiked = false)) }
- }
-
- @Test
- fun `invoke should return DbError when dataSource throws an exception`() = runTest {
- val errorMessage = "Database Error"
- val dataSource = mockk {
- coEvery { update(any()) } throws Exception(errorMessage)
- }
- val useCase = UpdateMovieLikeUC(dataSource)
- val result = useCase(movie)
-
- assertEquals(UpdateMovieResult.DbError(errorMessage), result)
- }
-
- @Test
- fun `invoke should return DbError with empty message when exception has no message`() = runTest {
- val dataSource = mockk {
- coEvery { update(any()) } throws Exception()
- }
- val useCase = UpdateMovieLikeUC(dataSource)
- val result = useCase(movie)
-
- assertEquals(UpdateMovieResult.DbError(""), result)
- }
-
- @Test
- fun `invoke should update dataSource with the correct movie ID`() = runTest {
- val dataSource = mockk(relaxed = true)
- val useCase = UpdateMovieLikeUC(dataSource)
- useCase(movie)
-
- coVerify { dataSource.update(movie.copy(isLiked = true)) }
- }
-
-
- @Test
- fun `invoke should handle consecutive toggles on the same movie`() = runTest {
- val dataSource = mockk(relaxed = true)
- val useCase = UpdateMovieLikeUC(dataSource)
-
- useCase(movie) // Toggle once
- val result = useCase(movie.copy(isLiked = true))// Toggle again, starting with isLiked = true
-
- assertEquals(UpdateMovieResult.Success, result)
- coVerify(exactly = 2) { dataSource.update(any()) } // Verify update is called twice
- coVerify { dataSource.update(movie.copy(isLiked = false)) } // Verify the final state is isLiked = false
- }
-
- @Test
- fun `invoke should handle movies with same ID but different other properties`() = runTest {
- val dataSource = mockk(relaxed = true)
- val useCase = UpdateMovieLikeUC(dataSource)
- val movie2 = movie.copy(title = "Different Title", rating = 3.0f) // Same ID, different properties
-
- useCase(movie)
- val result = useCase(movie2)
-
- assertEquals(UpdateMovieResult.Success, result)
- coVerify {
- dataSource.update(movie.copy(isLiked = true))
- dataSource.update(movie2.copy(isLiked = true)) // Verify both movies are updated independently
- }
- }
-}
diff --git a/domain/usecase-setting/build.gradle.kts b/domain/usecase-setting/build.gradle.kts
index b717e2b5..8de6924e 100644
--- a/domain/usecase-setting/build.gradle.kts
+++ b/domain/usecase-setting/build.gradle.kts
@@ -1,7 +1,9 @@
plugins {
- alias(libs.plugins.convention.domain.usecase)
+ alias(libs.plugins.convention.domain.usecase.kmp)
}
-android {
- namespace = "com.velord.usecase.setting"
-}
+kotlin {
+ android {
+ namespace = "com.velord.usecase.setting"
+ }
+}
\ No newline at end of file
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetThemeConfigUC.kt
new file mode 100644
index 00000000..51bc98fe
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/GetThemeConfigUC.kt
@@ -0,0 +1,12 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.ThemeConfig
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Returns a reactive stream of the current theme configuration.
+ *
+ * The first invocation may initialize in-memory theme state from persistence. After that,
+ * collectors are expected to receive future theme updates through the returned [Flow].
+ */
+fun interface GetThemeConfigUC : suspend () -> Flow
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt
new file mode 100644
index 00000000..8a585728
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt
@@ -0,0 +1,10 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.ThemeConfig
+
+/**
+ * Toggles the `abideToOs` flag on the supplied [ThemeConfig] and persists the updated state.
+ *
+ * The argument is treated as the current snapshot, not as a partial update.
+ */
+fun interface SwitchAbideToOsThemeConfigUC : suspend (ThemeConfig) -> Unit
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt
new file mode 100644
index 00000000..e675d232
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt
@@ -0,0 +1,11 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.ThemeConfig
+
+/**
+ * Toggles dark-theme usage on the supplied [ThemeConfig] and persists the updated state.
+ *
+ * Implementations are also expected to update the concrete current theme so it stays consistent
+ * with the new dark/light choice.
+ */
+fun interface SwitchDarkThemeConfigUC : suspend (ThemeConfig) -> Unit
diff --git a/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt
new file mode 100644
index 00000000..abc1d6bf
--- /dev/null
+++ b/domain/usecase-setting/src/commonMain/kotlin/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt
@@ -0,0 +1,10 @@
+package com.velord.usecase.setting
+
+import com.velord.model.setting.ThemeConfig
+
+/**
+ * Toggles dynamic-color usage on the supplied [ThemeConfig] and persists the updated state.
+ *
+ * The argument is treated as the current full snapshot.
+ */
+fun interface SwitchDynamicColorThemeConfigUC : suspend (ThemeConfig) -> Unit
diff --git a/domain/usecase-setting/src/main/AndroidManifest.xml b/domain/usecase-setting/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/domain/usecase-setting/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigDS.kt b/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigDS.kt
deleted file mode 100644
index afbc446f..00000000
--- a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigDS.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.velord.usecase.setting
-
-import com.velord.model.setting.ThemeConfig
-import kotlinx.coroutines.flow.Flow
-
-interface GetThemeConfigDS {
- suspend fun getFlow(): Flow
- suspend fun save(config: ThemeConfig)
-}
diff --git a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigUC.kt b/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigUC.kt
deleted file mode 100644
index 0acaa509..00000000
--- a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/GetThemeConfigUC.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.velord.usecase.setting
-
-import com.velord.model.setting.ThemeConfig
-import kotlinx.coroutines.flow.Flow
-
-class GetThemeConfigUC(private val dataSource: GetThemeConfigDS) {
- suspend operator fun invoke(): Flow = dataSource.getFlow()
-}
diff --git a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt b/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt
deleted file mode 100644
index 35068179..00000000
--- a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchAbideToOsThemeConfigUC.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.velord.usecase.setting
-
-import com.velord.model.setting.ThemeConfig
-
-class SwitchAbideToOsThemeConfigUC(private val dataSource: GetThemeConfigDS) {
-
- suspend operator fun invoke(config: ThemeConfig) {
- val newConfig = config.copy(abideToOs = config.abideToOs.not())
- dataSource.save(newConfig)
- }
-}
diff --git a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt b/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt
deleted file mode 100644
index 2b5b0e4c..00000000
--- a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDarkThemeConfigUC.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.velord.usecase.setting
-
-import com.velord.model.setting.ThemeConfig
-
-class SwitchDarkThemeConfigUC(private val dataSource: GetThemeConfigDS) {
-
- suspend operator fun invoke(config: ThemeConfig) {
- val newConfig = config.copy(
- useDarkTheme = config.useDarkTheme.not(),
- current = ThemeConfig.getOppositeTheme(config.current),
- )
- dataSource.save(newConfig)
- }
-}
diff --git a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt b/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt
deleted file mode 100644
index 44475ec4..00000000
--- a/domain/usecase-setting/src/main/java/com/velord/usecase/setting/SwitchDynamicColorThemeConfigUC.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.velord.usecase.setting
-
-import com.velord.model.setting.ThemeConfig
-
-class SwitchDynamicColorThemeConfigUC(private val dataSource: GetThemeConfigDS) {
-
- suspend operator fun invoke(config: ThemeConfig) {
- val newConfig = config.copy(useDynamicColor = config.useDynamicColor.not())
- dataSource.save(newConfig)
- }
-}
diff --git a/gradle.properties b/gradle.properties
index 1cab2a91..1378bc63 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -34,8 +34,5 @@ org.gradle.vfs.watch=true
# Disable the generation of different build classes in modules.
# This way we save time where, for example, there are no BuildConfig classes.
# The disadvantage of this is that you have to manually indicate in the module that a certain build feature is needed
-android.defaults.buildfeatures.buildconfig=false
-android.defaults.buildfeatures.aidl=false
-android.defaults.buildfeatures.renderscript=false
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
\ No newline at end of file
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
index 5c34300f..6b1a33e2 100644
--- a/gradle/gradle-daemon-jvm.properties
+++ b/gradle/gradle-daemon-jvm.properties
@@ -1,13 +1,12 @@
#This file is generated by updateDaemonJvm
-toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
-toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
-toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
-toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
-toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect
-toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect
-toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect
-toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect
-toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect
-toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect
-toolchainVendor=JETBRAINS
-toolchainVersion=21
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/4f4ebe4f162f6deb29540c4ebe629d79/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/79d5995ef1c3e4df39a3b2f545cada5e/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/61fdd87038cf8b094dadad8566609a29/redirect
+toolchainVendor=ADOPTIUM
+toolchainVersion=24
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 98796f74..42ad32ef 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,22 +1,22 @@
[versions]
# API
minApi = "26"
-targetApi = "36"
+targetApi = "37"
# Build
-buildTools = "30.0.3"
-gradle = "9.0.1"
+buildTools = "36.0.0"
+gradle = "9.2.1"
jvmTarget = "24"
jetifier = "1.0.0-beta10"
# DI
-dagger = "2.59.2"
-hilt = "2.59.2"
-hiltNavigation = "1.3.0"
-hiltNavigationCompose = "1.3.0"
+dagger = "2.60.1"
+hilt = "2.60.1"
+hiltNavigation = "1.4.0"
+hiltNavigationCompose = "1.4.0"
koinBom = "4.1.1"
koinKsp = "2.3.1"
# Google Firebase
-googleFirebaseBom = "34.10.0"
-googleFirebaseCrashlyticPlugin = "3.0.6"
+googleFirebaseBom = "34.15.0"
+googleFirebaseCrashlyticPlugin = "3.0.7"
# Google Other
googleGuava = "33.5.0-android"
googleKsp = "2.3.4"
@@ -28,7 +28,7 @@ googlePayCompose = "1.2.0"
googleWalletCompose = "0.1.0-beta01"
# Google GMS/Google Play Services
googleGmsAppSet = "16.1.0"
-googleGmsAuth = "21.4.0"
+googleGmsAuth = "21.6.0"
googleGmsAuthApiPhone = "18.3.0"
googleGmsBase = "18.10.0"
googleGmsLocation = "21.3.0"
@@ -36,14 +36,14 @@ googleGmsMap = "20.0.0"
googleGmsMlKit = "19.0.1"
googleGmsVision = "20.1.3"
googleGmsServicesPlugin = "4.4.4"
-googleGmsWallet = "19.5.0"
+googleGmsWallet = "20.0.0"
# Kotlin
-kotlin = "2.3.10"
-kotlinCoroutine = "1.10.2"
+kotlin = "2.4.0"
+kotlinCoroutine = "1.11.0"
kotlinCollectionsImmutable = "0.4.0"
kotlinDateTime = "0.7.1"
# Kotlin Json Serialization
-kotlinJsonSerialization = "1.10.0"
+kotlinJsonSerialization = "1.11.0"
kotlinJsonSerializationRetrofitConverter = "1.0.0"
## Android
androidDesugar = "2.1.5"
@@ -119,20 +119,20 @@ androidxGlance = "1.1.1" # For Glance, AppWidgets
androidxGlanceWearTiles = "1.0.0-alpha07"
## Graphics
androidxGraphicsCore = "1.0.4"
-androidxGraphicsPath = "1.0.1"
+androidxGraphicsPath = "1.1.0"
androidxGraphicsShapes = "1.1.0"
## G
androidxGridlayout = "1.1.0"
## L
androidxLegacySupport = "1.0.0"
-androidxLifecycle = "2.10.0"
+androidxLifecycle = "2.11.0"
## M
-androidxMedia3 = "1.9.2"
+androidxMedia3 = "1.10.1"
## N
-androidxNavigation = "2.9.7"
-androidxNavigation3 = "1.0.1"
+androidxNavigation = "2.9.8"
+androidxNavigation3 = "1.1.4"
## AndroidX Paging
-androidxPaging = "3.4.1"
+androidxPaging = "3.4.2"
## P
androidxPalette = "1.0.0" # There are no release notes for this artifact.
androidxPercentLayout = "1.0.0"
@@ -157,7 +157,7 @@ androidxRemoteCallback = "1.0.0-alpha03"
androidxResourceInspection = "1.0.1"
androidxRoom = "2.8.4"
## S
-androidxSavedState = "1.4.0"
+androidxSavedState = "1.5.0"
## Security
androidxSecurityCrypto = "1.1.0"
androidxSecurityAppAuthenticator = "1.0.0"
@@ -216,18 +216,19 @@ androidxWearProtoLayout = "1.3.0"
androidxWearTiles = "1.5.0"
androidxWearWatchface = "1.2.1"
## W
-androidxWebkit = "1.15.0"
+androidxWebkit = "1.16.0"
## Window
androidxWindow = "1.5.1"
androidxWindowExtension = "1.0.0"
## W
-androidxWorkManager = "2.11.1"
+androidxWorkManager = "2.11.2"
# Network
squareRetrofit = "3.0.0"
-squareOkhttp3Interceptor = "5.3.2"
-ktor = "3.4.1"
+squareOkhttp3Interceptor = "5.4.0"
+ktor = "3.5.1"
# Compose
-composeDefault = "1.10.4" # Foundation, Material, Animation, Ui, UiTooling, Runtime
+composePlugin = "1.11.1"
+composeDefault = "1.11.2" # Foundation, Material, Animation, Ui, UiTooling, Runtime
composeIcon = "1.7.8"
composeAccompanist = "0.37.3"
# composeCompiler = "1.5.14"
@@ -240,7 +241,7 @@ composeSmartToolFactoryCropper = "0.4.0"
composeSmartToolFactoryGesture = "3.0.0"
composeHaze = "1.7.2"
# Image loader
-coil = "3.4.0"
+coil = "3.5.0"
# Permission
sagarCoroutinePermission = "2.0.3"
accompanistPermission = "0.37.2"
@@ -249,16 +250,20 @@ composeDestinations = "2.3.0"
voyager = "1.1.0-beta03"
velordMultipleBackstack = "0.5.9.1"
# Logging
+kermit = "2.1.0"
slf4j = "1.7.36"
takt = "2.1.1"
rebugger = "1.0.1"
# Testing
junit = "4.13.2"
-mockk = "1.14.9"
+konsist = "0.17.3"
+mockk = "1.14.11"
+mokkery = "3.4.2"
turbine = "1.2.1"
# Code quality
-detekt = "2.0.0-alpha.2"
+detekt = "2.0.0-alpha.5"
dependencyGuard = "0.5.0"
+cameraVideo = "1.6.1"
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
@@ -288,6 +293,8 @@ androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorl
androidx-core-uwb = { module = "androidx.core.uwb:uwb", version.ref = "androidxCoreUWB" }
androidx-cursoradapter = { module = "androidx.cursoradapter:cursoradapter", version.ref = "androidxCursorAdapter" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidxDatastore" }
+androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "androidxDatastore" }
+androidx-datastore-core-okio = { module = "androidx.datastore:datastore-core-okio", version.ref = "androidxDatastore" }
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidxDocumentFile" }
androidx-draganddrop = { module = "androidx.draganddrop:draganddrop", version.ref = "androidxDragAndDrop" }
androidx-drawerlayout = { module = "androidx.drawerlayout:drawerlayout", version.ref = "androidxDrawerLayout" }
@@ -388,6 +395,7 @@ androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process",
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
# AndroidX Lifecycle ViewModel
+androidx-lifecycle-viewmodel-core = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" }
@@ -438,6 +446,7 @@ androidx-recylerview-selection = { module = "androidx.recyclerview:recyclerview-
androidx-resourceinspection-annotation = { module = "androidx.resourceinspection:resourceinspection-annotation", version.ref = "androidxResourceInspection" }
androidx-resourceinspection-processor = { module = "androidx.resourceinspection:resourceinspection-processor", version.ref = "androidxResourceInspection" }
# AndroidX Room
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
androidx-room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "androidxRoom" }
@@ -534,6 +543,7 @@ androidx-workmanager-multiprocess = { module = "androidx.work:work-multiprocess"
androidx-workmanager-testing = { module = "androidx.work:work-testing", version.ref = "androidxWorkManager" }
# Compose Core -------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------------------
+compose-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composePlugin" }
compose-animation = { module = "androidx.compose.animation:animation", version.ref = "composeDefault" }
# Compose Runtime
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeDefault" }
@@ -555,8 +565,8 @@ compose-material3-adaptive-layout = { module = "androidx.compose.material3.adapt
compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "composeMaterial3Adaptive" }
# Compose Ui
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "composeDefault" }
-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "composeDefault" }
-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "composeDefault" }
+compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composePlugin" }
+compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composePlugin" }
compose-ui-geometry = { module = "androidx.compose.ui:ui-geometry", version.ref = "composeDefault" }
compose-ui-geometry-android = { module = "androidx.compose.ui:ui-geometry-android", version.ref = "composeDefault" }
# Compose 3-rd party
@@ -579,7 +589,7 @@ hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", ve
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
# DI koin
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" }
-koin-core = { module = "io.insert-koin:koin-core" }
+koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinBom" }
koin-android = { module = "io.insert-koin:koin-android" }
koin-navigation = { module = "io.insert-koin:koin-androidx-navigation" }
koin-compose = { module = "io.insert-koin:koin-androidx-compose" }
@@ -660,16 +670,21 @@ voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", v
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
voyager-hilt = { module = "cafe.adriel.voyager:voyager-hilt", version.ref = "voyager" }
# Logging
+kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" }
takt = { module = "jp.wasabeef:takt", version.ref = "takt" }
rebugger = { module = "io.github.theapache64:rebugger", version.ref = "rebugger" }
# Testing
# Test
junit = { module = "junit:junit", version.ref = "junit" }
+konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" }
coroutine-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutine" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
mock = { module = "io.mockk:mockk", version.ref = "mockk" }
mock-agent = { module = "io.mockk:mockk-agent-jvm", version.ref = "mockk" }
+mokkery-coroutines = { module = "dev.mokkery:mokkery-coroutines", version.ref = "mokkery" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+camera-video = { group = "androidx.camera", name = "camera-video", version.ref = "cameraVideo" }
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
@@ -702,7 +717,7 @@ androidx-camera-all = [
"androidx-camera-lifecycle", "androidx-camera-video",
"androidx-camera-view", "androidx-camera-mlkit",
"androidx-camera-extension",
-# "androidx-camera-media3",
+ # "androidx-camera-media3",
"androidx-camera-viewfinder-core", "androidx-camera-viewfinder-compose",
"androidx-camera-viewfinder-view"
]
@@ -965,7 +980,7 @@ androidx-all = [
"androidx-camera-lifecycle", "androidx-camera-video",
"androidx-camera-view", "androidx-camera-mlkit",
"androidx-camera-extension",
-# "androidx-camera-media3", Conflicting with other dependencies
+ # "androidx-camera-media3", Conflicting with other dependencies
# AndroidX Camera Viewfinder
"androidx-camera-viewfinder-core", "androidx-camera-viewfinder-compose",
"androidx-camera-viewfinder-view",
@@ -1068,15 +1083,18 @@ androidx-all = [
# Compose
compose-core = ["compose-runtime", "compose-animation"]
# Compose Accompanist
-compose-accompanist-all = [
- "compose-accompanist-drawable",
- "compose-accompanist-permission"
-]
+compose-accompanist-all = ["compose-accompanist-drawable", "compose-accompanist-permission"]
# Compose Foundation
compose-foundation = ["compose-foundation", "compose-foundation-layout"]
# Compose Ui
-compose-ui = [
- "compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview",
+compose-ui-core = [
+ "compose-ui",
+ "compose-ui-tooling", "compose-ui-tooling-preview"
+]
+compose-ui-geometry = ["compose-ui-geometry", "compose-ui-geometry-android"]
+compose-ui-all = [
+ "compose-ui",
+ "compose-ui-tooling", "compose-ui-tooling-preview",
"compose-ui-geometry", "compose-ui-geometry-android"
]
# Compose Material
@@ -1231,6 +1249,7 @@ voyager = [
## Compose Destinations
compose-destinations = ["compose-destinations", "compose-destinations-bottomsheet"]
# Logging
+logging-kmp = ["kermit"]
logging = ["slf4j-android", "takt", "rebugger"]
# Test
test = [
@@ -1238,22 +1257,36 @@ test = [
"androidx-test-ext-junit", "androidx-arch-testing",
"turbine", "mock", "mock-agent"
]
+# KMP Test (commonTest)
+test-kmp = [
+ "kotlin-test", "coroutine-test",
+ "turbine", "mokkery-coroutines"
+]
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------
[plugins]
+# Android
android-application = { id = "com.android.application", version.ref = "gradle" }
android-library = { id = "com.android.library", version.ref = "gradle" }
+android-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "gradle" }
+# Kotlin
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-plugin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+# Compose
+jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "composePlugin" }
+# 3-rd Party
detekt = { id = "dev.detekt", version.ref = "detekt" }
dependency-guard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" }
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "googleKsp" }
+mokkery = { id = "dev.mokkery", version.ref = "mokkery" }
google-firebase-crashlytic = { id = "com.google.firebase.crashlytics", version.ref = "googleFirebaseCrashlyticPlugin" }
google-gms-services = { id = "com.google.gms.google-services", version.ref = "googleGmsServicesPlugin" }
# Convention Plugin
@@ -1264,5 +1297,6 @@ convention-android-viewbinding = { id = "velord.android.viewbinding" }
convention-koin = { id = "velord.koin" }
convention-feature-ui = { id = "velord.feature.ui" }
convention-feature-ui-koin = { id = "velord.feature.ui.koin" }
-convention-domain-usecase = { id = "velord.domain.usecase" }
+convention-domain-usecase-kmp = { id = "velord.domain.usecase.kmp" }
convention-widget-glance = { id = "velord.widget.glance" }
+convention-kmp-library = { id = "velord.kmp.library" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c61a118f..a351597e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/infrastructure/config/build.gradle.kts b/infrastructure/config/build.gradle.kts
index 3d9861d9..b87762b3 100644
--- a/infrastructure/config/build.gradle.kts
+++ b/infrastructure/config/build.gradle.kts
@@ -1,7 +1,9 @@
plugins {
- alias(libs.plugins.convention.android.library)
+ alias(libs.plugins.convention.kmp.library)
}
-android {
- namespace = "com.velord.config"
+kotlin {
+ android {
+ namespace = "com.velord.infrastructure.config"
+ }
}
diff --git a/infrastructure/config/src/main/java/com/velord/config/BuildConfigResolver.kt b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
similarity index 65%
rename from infrastructure/config/src/main/java/com/velord/config/BuildConfigResolver.kt
rename to infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
index 5acb8c38..a44cb7c7 100644
--- a/infrastructure/config/src/main/java/com/velord/config/BuildConfigResolver.kt
+++ b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/BuildConfigResolver.kt
@@ -1,4 +1,4 @@
-package com.velord.config
+package com.velord.infrastructure.config
interface BuildConfigResolver {
fun getNavigationLib(): NavigationLib
diff --git a/infrastructure/config/src/main/java/com/velord/config/NavigationLib.kt b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/NavigationLib.kt
similarity index 78%
rename from infrastructure/config/src/main/java/com/velord/config/NavigationLib.kt
rename to infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/NavigationLib.kt
index ef646e19..b35447bf 100644
--- a/infrastructure/config/src/main/java/com/velord/config/NavigationLib.kt
+++ b/infrastructure/config/src/commonMain/kotlin/com/velord/infrastructure/config/NavigationLib.kt
@@ -1,4 +1,4 @@
-package com.velord.config
+package com.velord.infrastructure.config
enum class NavigationLib {
Voyager,
diff --git a/infrastructure/config/src/main/AndroidManifest.xml b/infrastructure/config/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a..00000000
--- a/infrastructure/config/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/infrastructure/di/build.gradle.kts b/infrastructure/di/build.gradle.kts
index 8e1c718e..60c8f7bb 100644
--- a/infrastructure/di/build.gradle.kts
+++ b/infrastructure/di/build.gradle.kts
@@ -5,33 +5,36 @@ plugins {
}
android {
- namespace = "com.velord.di"
+ namespace = "com.velord.infrastructure.di"
}
dependencies {
- implementation(project(":infrastructure:config"))
+ implementation(projects.infrastructure.config)
+ implementation(projects.model)
// Module Domain
- implementation(project(":domain:usecase-setting"))
- implementation(project(":domain:usecase-movie"))
+ implementation(projects.domain.usecaseSetting)
+ implementation(projects.domain.usecaseMovie)
+ implementation(projects.domain.usecaseCamera)
+ implementation(projects.domain.usecaseEvent)
// Module Data Source
- implementation(project(":data:backend"))
- implementation(project(":data:datastore"))
- implementation(project(":data:appstate"))
- implementation(project(":data:gateway"))
- implementation(project(":data:db"))
- implementation(project(":data:os"))
+ implementation(projects.data.backend)
+ implementation(projects.data.datastore)
+ implementation(projects.data.appstate)
+ implementation(projects.data.gateway)
+ implementation(projects.data.db)
+ implementation(projects.data.os)
// Module UI
- implementation(project(":ui:sharedviewmodel"))
+ implementation(projects.ui.sharedviewmodel)
// Module UI Feature
- implementation(project(":ui:feature-demo"))
- implementation(project(":ui:feature-camerarecording"))
- implementation(project(":ui:feature-bottomnavigation"))
- implementation(project(":ui:feature-setting"))
- implementation(project(":ui:feature-splash"))
- implementation(project(":ui:feature-flowsummator"))
- implementation(project(":ui:feature-movie"))
- implementation(project(":ui:feature-demo-dialog"))
- // Templates
+ implementation(projects.ui.featureDemo)
+ implementation(projects.ui.featureCamerarecording)
+ implementation(projects.ui.featureBottomnavigation)
+ implementation(projects.ui.featureSetting)
+ implementation(projects.ui.featureSplash)
+ implementation(projects.ui.featureFlowsummator)
+ implementation(projects.ui.featureMovie)
+ implementation(projects.ui.featureDemoDialog)
+ // Template
implementation(libs.bundles.kotlin.all)
implementation(libs.bundles.androidx.module)
}
diff --git a/infrastructure/di/src/main/java/com/velord/di/UseCaseModule.kt b/infrastructure/di/src/main/java/com/velord/di/UseCaseModule.kt
deleted file mode 100644
index 0e2c22bd..00000000
--- a/infrastructure/di/src/main/java/com/velord/di/UseCaseModule.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.velord.di
-
-import com.velord.usecase.movie.GetAllMovieUC
-import com.velord.usecase.movie.GetFavoriteMovieUC
-import com.velord.usecase.movie.GetMovieSortOptionUC
-import com.velord.usecase.movie.LoadNewPageMovieUC
-import com.velord.usecase.movie.RefreshMovieUC
-import com.velord.usecase.movie.SetMovieSortOptionUC
-import com.velord.usecase.movie.UpdateMovieLikeUC
-import com.velord.usecase.setting.GetThemeConfigUC
-import com.velord.usecase.setting.SwitchAbideToOsThemeConfigUC
-import com.velord.usecase.setting.SwitchDarkThemeConfigUC
-import com.velord.usecase.setting.SwitchDynamicColorThemeConfigUC
-import org.koin.dsl.module
-
-val useCaseModule = module {
- factory { GetThemeConfigUC(get()) }
- factory { SwitchDarkThemeConfigUC(get()) }
- factory { SwitchAbideToOsThemeConfigUC(get()) }
- factory { SwitchDynamicColorThemeConfigUC(get()) }
- factory { GetAllMovieUC(get(), get()) }
- factory { GetFavoriteMovieUC(get(), get()) }
- factory { GetMovieSortOptionUC(get()) }
- factory { SetMovieSortOptionUC(get()) }
- factory { UpdateMovieLikeUC(get()) }
- factory { LoadNewPageMovieUC(get()) }
- factory { RefreshMovieUC(get()) }
-}
diff --git a/infrastructure/di/src/main/java/com/velord/di/Init.kt b/infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/Init.kt
similarity index 66%
rename from infrastructure/di/src/main/java/com/velord/di/Init.kt
rename to infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/Init.kt
index 688c39ea..72c18e9c 100644
--- a/infrastructure/di/src/main/java/com/velord/di/Init.kt
+++ b/infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/Init.kt
@@ -1,20 +1,17 @@
-package com.velord.di
+package com.velord.infrastructure.di
import android.app.Application
-import com.velord.backend.ktor.httpModule
-import com.velord.bottomnavigation.BottomNavigationModule
-import com.velord.db.databaseModule
-import com.velord.gateway.GatewayModule
-import com.velord.os.OsModule
+import com.velord.data.backend.ktor.httpModule
+import com.velord.data.db.databaseModule
+import com.velord.data.gateway.GatewayModule
+import com.velord.ui.feature.bottomnavigation.BottomNavigationModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.ksp.generated.module
-fun Application.startKoin(
- vararg moduleToInstall: Module
-) {
+fun Application.startKoin(vararg moduleToInstall: Module) {
val app = this
startKoin {
androidLogger()
@@ -29,6 +26,6 @@ fun Application.startKoin(
// Load Annotation-based modules
modules(BottomNavigationModule().module)
modules(GatewayModule().module)
- modules(OsModule().module)
+ //modules(OsModule().module)
}
}
diff --git a/infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/UseCaseModule.kt b/infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/UseCaseModule.kt
new file mode 100644
index 00000000..054e6f86
--- /dev/null
+++ b/infrastructure/di/src/main/kotlin/com/velord/infrastructure/di/UseCaseModule.kt
@@ -0,0 +1,70 @@
+package com.velord.infrastructure.di
+
+import com.velord.data.gateway.camera.CameraGateway
+import com.velord.data.gateway.movie.MovieByGateway
+import com.velord.data.gateway.movie.MovieFavoriteGateway
+import com.velord.data.gateway.movie.MoviePaginationGateway
+import com.velord.data.gateway.movie.MovieSortGateway
+import com.velord.data.gateway.setting.GetThemeConfigGateway
+import com.velord.data.gateway.setting.SwitchThemeConfigGateway
+import com.velord.data.gateway.toast.ToastGateway
+import com.velord.usecase.camera.StartRecordingUC
+import com.velord.usecase.event.GetToastConfigFlowUC
+import com.velord.usecase.event.ShowToastUC
+import com.velord.usecase.movie.GetAllMovieUC
+import com.velord.usecase.movie.GetFavoriteMovieUC
+import com.velord.usecase.movie.GetMovieSortOptionUC
+import com.velord.usecase.movie.LoadNewPageMovieUC
+import com.velord.usecase.movie.RefreshMovieUC
+import com.velord.usecase.movie.SetMovieSortOptionUC
+import com.velord.usecase.movie.UpdateMovieLikeUC
+import com.velord.usecase.setting.GetThemeConfigUC
+import com.velord.usecase.setting.SwitchAbideToOsThemeConfigUC
+import com.velord.usecase.setting.SwitchDarkThemeConfigUC
+import com.velord.usecase.setting.SwitchDynamicColorThemeConfigUC
+import org.koin.dsl.module
+
+val useCaseModule = module {
+ single {
+ GetToastConfigFlowUC(get()::getFlow)
+ }
+ single {
+ ShowToastUC(get()::show)
+ }
+ single {
+ GetThemeConfigUC(get()::getFlow)
+ }
+ single {
+ SwitchDarkThemeConfigUC(get()::switchDarkTheme)
+ }
+ single {
+ SwitchAbideToOsThemeConfigUC(get()::switchAbideToOs)
+ }
+ single {
+ SwitchDynamicColorThemeConfigUC(get()::switchDynamicColor)
+ }
+ single {
+ GetAllMovieUC(get()::getBySort)
+ }
+ single {
+ GetFavoriteMovieUC(get()::getByFavorite)
+ }
+ single {
+ GetMovieSortOptionUC(get()::getFlow)
+ }
+ single {
+ SetMovieSortOptionUC(get()::update)
+ }
+ single {
+ UpdateMovieLikeUC(get()::update)
+ }
+ single {
+ LoadNewPageMovieUC(get()::load)
+ }
+ single