diff --git a/app/src/main/java/com/fluxa/app/AppApplication.kt b/app/src/main/java/com/fluxa/app/AppApplication.kt index be99b7b..898c5ed 100644 --- a/app/src/main/java/com/fluxa/app/AppApplication.kt +++ b/app/src/main/java/com/fluxa/app/AppApplication.kt @@ -20,6 +20,7 @@ import okio.Path.Companion.toOkioPath import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.app import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.initialize import com.fluxa.app.ui.catalog.EpisodeReleaseWorker import com.fluxa.app.plugins.PluginAutoUpdateWorker import com.fluxa.app.ui.catalog.TrailerResolver diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/CalendarWidgetProvider.kt b/app/src/main/java/com/fluxa/app/ui/catalog/CalendarWidgetProvider.kt index 26470db..50345f3 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/CalendarWidgetProvider.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/CalendarWidgetProvider.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.RemoteViews import com.fluxa.app.R import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.ui.MainActivity import org.json.JSONArray import org.json.JSONObject diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/DetailComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/DetailComponents.kt index 81abe81..6c12388 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/DetailComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/DetailComponents.kt @@ -2,6 +2,7 @@ package com.fluxa.app.ui.catalog import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.common.ReleaseDateUtils import com.fluxa.app.data.local.* import com.fluxa.app.data.remote.* diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TvCalendarScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TvCalendarScreen.kt index c5e175e..e704e03 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TvCalendarScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TvCalendarScreen.kt @@ -1,6 +1,7 @@ package com.fluxa.app.ui.catalog import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.remote.Meta diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/calendar/MobileCalendarScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/calendar/MobileCalendarScreen.kt index 8245116..ca8dfbe 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/calendar/MobileCalendarScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/calendar/MobileCalendarScreen.kt @@ -2,6 +2,7 @@ package com.fluxa.app.ui.catalog import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.data.local.* import com.fluxa.app.data.remote.* import com.fluxa.app.data.repository.* diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/settings/MobileSettingsComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/settings/MobileSettingsComponents.kt index cef3de7..66a2bfd 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/settings/MobileSettingsComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/settings/MobileSettingsComponents.kt @@ -2,6 +2,7 @@ package com.fluxa.app.ui.catalog import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.data.local.* import com.fluxa.app.data.remote.* import com.fluxa.app.data.repository.* diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/source/SourceSelectionScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/source/SourceSelectionScreen.kt index 5c48505..38aaa39 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/source/SourceSelectionScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/source/SourceSelectionScreen.kt @@ -1,6 +1,7 @@ package com.fluxa.app.ui.catalog import com.fluxa.app.common.AppStrings +import com.fluxa.app.common.locale import com.fluxa.app.data.local.* import com.fluxa.app.data.remote.* import com.fluxa.app.data.repository.* diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 9198845..b7c2d89 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -29,5 +29,9 @@ gradlePlugin { id = "fluxa.android.hilt" implementationClass = "FluxaAndroidHiltPlugin" } + register("fluxaKmpLibrary") { + id = "fluxa.kmp.library" + implementationClass = "FluxaKmpLibraryPlugin" + } } } diff --git a/build-logic/src/main/kotlin/FluxaKmpLibraryPlugin.kt b/build-logic/src/main/kotlin/FluxaKmpLibraryPlugin.kt new file mode 100644 index 0000000..49f691e --- /dev/null +++ b/build-logic/src/main/kotlin/FluxaKmpLibraryPlugin.kt @@ -0,0 +1,41 @@ +import com.android.build.gradle.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class FluxaKmpLibraryPlugin : Plugin { + override fun apply(target: Project) = with(target) { + pluginManager.apply("com.android.library") + pluginManager.apply("org.jetbrains.kotlin.multiplatform") + + extensions.configure { + compileSdk = 36 + defaultConfig { minSdk = 30 } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } + + extensions.configure { + androidTarget { + compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } + } + iosArm64() + iosSimulatorArm64() + + targets.configureEach { + compilations.configureEach { + compileTaskProvider.configure { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + } + } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1e03a02..1f3e05d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,12 +1,21 @@ plugins { - alias(libs.plugins.fluxa.android.library) + alias(libs.plugins.fluxa.kmp.library) } android { namespace = "com.fluxa.app.core" } -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.bundles.coroutines) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.datetime) + } + androidMain.dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.kotlinx.coroutines.android) + } + } } diff --git a/core/src/main/assets/i18n/en-US.json b/core/src/androidMain/assets/i18n/en-US.json similarity index 100% rename from core/src/main/assets/i18n/en-US.json rename to core/src/androidMain/assets/i18n/en-US.json diff --git a/core/src/main/assets/i18n/tr-TR.json b/core/src/androidMain/assets/i18n/tr-TR.json similarity index 100% rename from core/src/main/assets/i18n/tr-TR.json rename to core/src/androidMain/assets/i18n/tr-TR.json diff --git a/core/src/androidMain/kotlin/com/fluxa/app/common/AppStrings.android.kt b/core/src/androidMain/kotlin/com/fluxa/app/common/AppStrings.android.kt new file mode 100644 index 0000000..97b1e90 --- /dev/null +++ b/core/src/androidMain/kotlin/com/fluxa/app/common/AppStrings.android.kt @@ -0,0 +1,28 @@ +package com.fluxa.app.common + +import android.content.Context +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap + +private var appContext: Context? = null + +fun AppStrings.Companion.initialize(context: Context) { + appContext = context.applicationContext +} + +fun AppStrings.Companion.locale(language: String?): Locale { + val tag = language + ?.substringBefore('_') + ?.takeIf { it.isNotBlank() } + ?: return Locale.US + return Locale.forLanguageTag(tag) +} + +internal actual fun readI18nAssetText(fileName: String): String? { + val context = appContext ?: return null + return runCatching { + context.assets.open("i18n/$fileName").bufferedReader().use { it.readText() } + }.getOrNull() +} + +internal actual fun createStringsCache(): MutableMap = ConcurrentHashMap() diff --git a/core/src/androidMain/kotlin/com/fluxa/app/common/PlatformClock.android.kt b/core/src/androidMain/kotlin/com/fluxa/app/common/PlatformClock.android.kt new file mode 100644 index 0000000..962d35f --- /dev/null +++ b/core/src/androidMain/kotlin/com/fluxa/app/common/PlatformClock.android.kt @@ -0,0 +1,11 @@ +package com.fluxa.app.common + +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +actual fun epochMillisNow(): Long = System.currentTimeMillis() + +internal actual class SimpleLock actual constructor() { + private val lock = ReentrantLock() + actual fun withLock(block: () -> T): T = lock.withLock(block) +} diff --git a/core/src/main/java/com/fluxa/app/common/AppStrings.kt b/core/src/commonMain/kotlin/com/fluxa/app/common/AppStrings.kt similarity index 75% rename from core/src/main/java/com/fluxa/app/common/AppStrings.kt rename to core/src/commonMain/kotlin/com/fluxa/app/common/AppStrings.kt index 6761ff7..b9cdf18 100644 --- a/core/src/main/java/com/fluxa/app/common/AppStrings.kt +++ b/core/src/commonMain/kotlin/com/fluxa/app/common/AppStrings.kt @@ -1,9 +1,12 @@ package com.fluxa.app.common -import android.content.Context -import org.json.JSONObject -import java.util.Locale -import java.util.concurrent.ConcurrentHashMap +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +internal expect fun readI18nAssetText(fileName: String): String? +internal expect fun createStringsCache(): MutableMap class AppStrings private constructor( private val values: Map, @@ -12,21 +15,15 @@ class AppStrings private constructor( fun get(key: String): String = values[key] ?: fallback[key] ?: key companion object { - private val cache = ConcurrentHashMap() - private var appContext: Context? = null + private val cache = createStringsCache() private val RUNTIME_UNIT_REGEX = Regex("""\b(min|m|dk)\.?\b""", RegexOption.IGNORE_CASE) private val WHITESPACE_REGEX = Regex("""\s+""") private val ISO_DURATION_REGEX = Regex("""^PT(?:(\d+)H)?(?:(\d+)M)?$""", RegexOption.IGNORE_CASE) private val HOURS_REGEX = Regex("""(\d+)\s*(?:h|hr|hour|hours|sa|saat)""", RegexOption.IGNORE_CASE) private val MINUTES_REGEX = Regex("""(\d+)\s*(?:m|min|minute|minutes|dk)""", RegexOption.IGNORE_CASE) - fun initialize(context: Context) { - appContext = context.applicationContext - } - fun t(language: String?, key: String): String { - val context = appContext ?: return key - return load(context, language).get(key) + return load(language).get(key) } fun format(language: String?, key: String, vararg args: Any?): String { @@ -39,14 +36,6 @@ class AppStrings private constructor( return t(language, key).split("|").map { it.trim() }.filter { it.isNotEmpty() } } - fun locale(language: String?): Locale { - val tag = language - ?.substringBefore('_') - ?.takeIf { it.isNotBlank() } - ?: return Locale.US - return Locale.forLanguageTag(tag) - } - fun runtimeMinutes(language: String?, minutes: Int): String { val hours = minutes / 60 val remainder = minutes % 60 @@ -102,11 +91,11 @@ class AppStrings private constructor( return normalized.toIntOrNull()?.takeIf { it > 0 } } - fun load(context: Context, language: String?): AppStrings { + private fun load(language: String?): AppStrings { val fileName = languageFileName(language) return cache.getOrPut(fileName) { - val fallback = readAsset(context, "i18n/en-US.json") - val values = if (fileName == "en-US.json") fallback else readAsset(context, "i18n/$fileName") + val fallback = readAsset("en-US.json") + val values = if (fileName == "en-US.json") fallback else readAsset(fileName) AppStrings(values, fallback) } } @@ -116,7 +105,7 @@ class AppStrings private constructor( ?.trim() .orEmpty() val languageCode = normalized.removeSuffix(".json") - return when (languageCode.lowercase(Locale.ROOT)) { + return when (languageCode.lowercase()) { "" -> "en-US.json" "en", "en-us" -> "en-US.json" "tr", "tr-tr" -> "tr-TR.json" @@ -124,11 +113,12 @@ class AppStrings private constructor( } } - private fun readAsset(context: Context, path: String): Map { + private fun readAsset(fileName: String): Map { + val json = readI18nAssetText(fileName) ?: return emptyMap() return runCatching { - val json = context.assets.open(path).bufferedReader().use { it.readText() } - val obj = JSONObject(json) - obj.keys().asSequence().associateWith { key -> obj.optString(key, key) } + Json.parseToJsonElement(json).jsonObject + .entries + .associate { (key, value) -> key to (value.jsonPrimitive.contentOrNull ?: key) } }.getOrDefault(emptyMap()) } } diff --git a/core/src/main/java/com/fluxa/app/common/Constants.kt b/core/src/commonMain/kotlin/com/fluxa/app/common/Constants.kt similarity index 100% rename from core/src/main/java/com/fluxa/app/common/Constants.kt rename to core/src/commonMain/kotlin/com/fluxa/app/common/Constants.kt diff --git a/core/src/commonMain/kotlin/com/fluxa/app/common/PlatformClock.kt b/core/src/commonMain/kotlin/com/fluxa/app/common/PlatformClock.kt new file mode 100644 index 0000000..1f47d48 --- /dev/null +++ b/core/src/commonMain/kotlin/com/fluxa/app/common/PlatformClock.kt @@ -0,0 +1,7 @@ +package com.fluxa.app.common + +expect fun epochMillisNow(): Long + +internal expect class SimpleLock() { + fun withLock(block: () -> T): T +} diff --git a/core/src/main/java/com/fluxa/app/common/ReleaseDateUtils.kt b/core/src/commonMain/kotlin/com/fluxa/app/common/ReleaseDateUtils.kt similarity index 64% rename from core/src/main/java/com/fluxa/app/common/ReleaseDateUtils.kt rename to core/src/commonMain/kotlin/com/fluxa/app/common/ReleaseDateUtils.kt index 3fc42af..4ffa1f5 100644 --- a/core/src/main/java/com/fluxa/app/common/ReleaseDateUtils.kt +++ b/core/src/commonMain/kotlin/com/fluxa/app/common/ReleaseDateUtils.kt @@ -1,16 +1,22 @@ package com.fluxa.app.common -import java.time.Instant -import java.time.LocalDate -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.temporal.ChronoUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.daysUntil +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +@OptIn(ExperimentalTime::class) object ReleaseDateUtils { private val isoDateRegex = Regex("""\d{4}-\d{2}-\d{2}""") private val offsetSuffixRegex = Regex(""".*[+-]\d{2}:?\d{2}$""") - fun todayIso(): String = LocalDate.now(ZoneId.systemDefault()).toString() + private fun todayDate(): LocalDate = + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + + fun todayIso(): String = todayDate().toString() fun isoDate(value: String?): String? { if (value.isNullOrBlank()) return null @@ -21,16 +27,16 @@ object ReleaseDateUtils { return runCatching { val instant = when { trimmed.endsWith("Z", ignoreCase = true) -> Instant.parse(trimmed) - offsetSuffixRegex.matches(trimmed) -> OffsetDateTime.parse(trimmed).toInstant() + offsetSuffixRegex.matches(trimmed) -> Instant.parse(trimmed) else -> Instant.parse("${trimmed.substringBefore(".")}Z") } - instant.atZone(ZoneId.systemDefault()).toLocalDate().toString() + instant.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString() }.getOrNull() ?: trimmed.take(10).takeIf { it.matches(isoDateRegex) } } fun daysSince(value: String?): Long? { val date = isoDate(value)?.let(LocalDate::parse) ?: return null - return ChronoUnit.DAYS.between(date, LocalDate.now(ZoneId.systemDefault())) + return date.daysUntil(todayDate()).toLong() } fun isUpcoming(value: String?): Boolean { diff --git a/core/src/main/java/com/fluxa/app/common/TtlMemoryCache.kt b/core/src/commonMain/kotlin/com/fluxa/app/common/TtlMemoryCache.kt similarity index 65% rename from core/src/main/java/com/fluxa/app/common/TtlMemoryCache.kt rename to core/src/commonMain/kotlin/com/fluxa/app/common/TtlMemoryCache.kt index 725e303..f724974 100644 --- a/core/src/main/java/com/fluxa/app/common/TtlMemoryCache.kt +++ b/core/src/commonMain/kotlin/com/fluxa/app/common/TtlMemoryCache.kt @@ -1,11 +1,9 @@ package com.fluxa.app.common -import java.util.concurrent.ConcurrentHashMap - class TtlMemoryCache( private val maxEntries: Int, private val ttlMillis: Long, - private val nowMillis: () -> Long = System::currentTimeMillis + private val nowMillis: () -> Long = ::epochMillisNow ) { private data class Entry( val value: T, @@ -13,18 +11,18 @@ class TtlMemoryCache( val insertionOrder: Long ) - private val entries = ConcurrentHashMap>() + private val lock = SimpleLock() + private val entries = mutableMapOf>() private var nextInsertionOrder = 0L - fun get(key: String): T? { - val entry = entries[key] ?: return null - return if (nowMillis() < entry.expiresAtMillis) entry.value else { + fun get(key: String): T? = lock.withLock { + val entry = entries[key] ?: return@withLock null + if (nowMillis() < entry.expiresAtMillis) entry.value else { entries.remove(key); null } } - @Synchronized - fun put(key: String, value: T) { + fun put(key: String, value: T) = lock.withLock { val now = nowMillis() trimExpired(now) entries[key] = Entry( @@ -35,23 +33,20 @@ class TtlMemoryCache( trimToMaxSize() } - @Synchronized - fun firstWithPrefix(prefix: String): T? { + fun firstWithPrefix(prefix: String): T? = lock.withLock { val now = nowMillis() trimExpired(now) - val key = entries.keys.asSequence().filter { it.startsWith(prefix) }.minOrNull() ?: return null - return entries[key]?.value + val key = entries.keys.asSequence().filter { it.startsWith(prefix) }.minOrNull() ?: return@withLock null + entries[key]?.value } - @Synchronized - fun invalidatePrefix(prefix: String) { + fun invalidatePrefix(prefix: String) = lock.withLock { entries.keys.removeAll { it.startsWith(prefix) } } - @Synchronized - fun size(): Int { + fun size(): Int = lock.withLock { trimExpired(nowMillis()) - return entries.size + entries.size } private fun trimExpired(now: Long) { diff --git a/core/src/iosMain/kotlin/com/fluxa/app/common/AppStrings.ios.kt b/core/src/iosMain/kotlin/com/fluxa/app/common/AppStrings.ios.kt new file mode 100644 index 0000000..5d4431c --- /dev/null +++ b/core/src/iosMain/kotlin/com/fluxa/app/common/AppStrings.ios.kt @@ -0,0 +1,20 @@ +package com.fluxa.app.common + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import platform.Foundation.NSBundle +import platform.Foundation.NSData +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataWithContentsOfFile + +@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +internal actual fun readI18nAssetText(fileName: String): String? { + val baseName = fileName.removeSuffix(".json") + val path = NSBundle.mainBundle.pathForResource(baseName, "json", "i18n") ?: return null + val data = NSData.dataWithContentsOfFile(path) ?: return null + return NSString.create(data, NSUTF8StringEncoding)?.toString() +} + +internal actual fun createStringsCache(): MutableMap = mutableMapOf() diff --git a/core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt b/core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt new file mode 100644 index 0000000..4050bf4 --- /dev/null +++ b/core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt @@ -0,0 +1,19 @@ +package com.fluxa.app.common + +import platform.Foundation.NSDate +import platform.Foundation.NSLock +import platform.Foundation.timeIntervalSince1970 + +actual fun epochMillisNow(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong() + +internal actual class SimpleLock actual constructor() { + private val lock = NSLock() + actual fun withLock(block: () -> T): T { + lock.lock() + try { + return block() + } finally { + lock.unlock() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe0514f..9e4ee8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,8 @@ androidxTestRunner = "1.6.2" androidxTestJunit = "1.2.1" jackson = "2.13.1" kotlinMetadataJvm = "2.4.0" +kotlinxSerialization = "1.11.0" +kotlinxDatetime = "0.8.0" [libraries] # Build-logic plugin artifacts (compileOnly in build-logic) @@ -116,6 +118,10 @@ zxing = { group = "com.google.zxing", name = "core", version.ref = kotlin-metadata-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-metadata-jvm", version.ref = "kotlinMetadataJvm" } +# Kotlinx serialization (KMP-portable JSON, replaces org.json in shared code) +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } + # Jackson jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson" } @@ -136,10 +142,13 @@ retrofit = ["retrofit", "retrofit-gson"] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform= { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization= { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } fluxa-android-library = { id = "fluxa.android.library" } fluxa-android-application = { id = "fluxa.android.application" } fluxa-android-compose = { id = "fluxa.android.compose" } fluxa-android-hilt = { id = "fluxa.android.hilt" } +fluxa-kmp-library = { id = "fluxa.kmp.library" }