improvements for kmp

This commit is contained in:
2025-10-04 02:14:37 +03:00
parent 715c4ba40b
commit ad2a102f1f
35 changed files with 262 additions and 215 deletions
+6 -6
View File
@@ -1,8 +1,8 @@
plugins {
alias(libs.plugins.multiplatform).apply(false)
alias(libs.plugins.compose.compiler).apply(false)
alias(libs.plugins.compose).apply(false)
alias(libs.plugins.compose.hot.reload).apply(false)
alias(libs.plugins.android.application).apply(false)
alias(libs.plugins.kotlinx.serialization).apply(false)
alias(libs.plugins.multiplatform) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.compose) apply false
alias(libs.plugins.compose.hot.reload) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlinx.serialization) apply false
}
+46 -67
View File
@@ -2,8 +2,6 @@ import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.*
plugins {
@@ -15,21 +13,6 @@ plugins {
alias(libs.plugins.kotlinx.serialization)
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-Xexpect-actual-classes"
)
}
}
compose.resources {
nameOfResClass = "R"
}
kotlin {
androidTarget {
compilerOptions {
@@ -39,53 +22,48 @@ kotlin {
jvm()
if (providers.gradleProperty("include_ios").get().toBoolean()) {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
if (providers.gradleProperty("include_wasm").get().toBoolean()) {
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
outputModuleName = "composeApp"
browser {
val rootDirPath = project.rootDir.path
val projectDirPath = project.projectDir.path
commonWebpackConfig {
outputFileName = "composeApp.js"
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
static = (static ?: mutableListOf()).apply {
// Serve sources to debug inside browser
add(rootDirPath)
add(projectDirPath)
}
}
}
}
binaries.executable()
}
js {
browser()
binaries.executable()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activityCompose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.ktor.client.okhttp)
implementation(libs.kstore.file)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.navigation.compose)
implementation(libs.coil)
implementation(libs.coil.network.ktor)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.core)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.json)
@@ -102,20 +80,12 @@ kotlin {
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlin.test)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
implementation(libs.kotlinx.coroutines.test)
}
androidMain.dependencies {
implementation(compose.uiTooling)
implementation(libs.androidx.activityCompose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.ktor.client.okhttp)
implementation(libs.kstore.file)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
@@ -124,18 +94,14 @@ kotlin {
implementation(libs.kstore.file)
}
findByName("iosMain")?.run {
dependencies {
implementation(libs.ktor.client.darwin)
implementation(libs.kstore.file)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
implementation(libs.kstore.file)
}
findByName("wasmJsMain")?.run {
dependencies {
implementation(libs.kstore.storage)
implementation(libs.ktor.client.js)
}
webMain.dependencies {
implementation(libs.kstore.storage)
implementation(libs.ktor.client.js)
}
}
}
@@ -211,6 +177,10 @@ dependencies {
debugImplementation(compose.uiTooling)
}
compose.resources {
nameOfResClass = "R"
}
compose.desktop {
application {
mainClass = "MainKt"
@@ -222,3 +192,12 @@ compose.desktop {
}
}
}
kotlin {
compilerOptions.freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-Xexpect-actual-classes"
)
}
@@ -0,0 +1,9 @@
package dev.meloda.overseerr
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
@@ -1,16 +1,5 @@
package dev.meloda.overseerr.common
import android.app.Application
import dev.meloda.overseerr.di.appModule
import org.koin.core.context.startKoin
class AppGlobal : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
modules(appModule)
}
}
}
class AppGlobal : Application()
@@ -8,30 +8,34 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.overseerr.datastore.SettingsController
import dev.meloda.overseerr.di.appModule
import dev.meloda.overseerr.screens.main.MainScreen
import dev.meloda.overseerr.theme.AppTheme
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import org.koin.compose.KoinApplication
import org.koin.compose.koinInject
var appDir: String = ""
@Composable
internal fun App() {
LaunchedEffect(true) {
Napier.base(DebugAntilog())
}
KoinApplication(application = { modules(appModule) }) {
LaunchedEffect(true) {
Napier.base(DebugAntilog())
}
val settingsController: SettingsController = koinInject()
val settings by settingsController.settings.collectAsStateWithLifecycle()
val settingsController: SettingsController = koinInject()
val settings by settingsController.settings.collectAsStateWithLifecycle()
LaunchedEffect(true) {
settingsController.loadAppSettings()
}
LaunchedEffect(true) {
settingsController.loadAppSettings()
}
AppTheme(themeMode = settings.themeMode) {
Surface(modifier = Modifier.fillMaxSize()) {
MainScreen()
AppTheme(themeMode = settings.themeMode) {
Surface(modifier = Modifier.fillMaxSize()) {
MainScreen()
}
}
}
}
@@ -0,0 +1,7 @@
package dev.meloda.overseerr
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
@@ -1,43 +1,35 @@
package dev.meloda.overseerr.datastore
import dev.meloda.overseerr.ext.setValue
import dev.meloda.overseerr.datastore.model.AppSettings
import dev.meloda.overseerr.datastore.model.ThemeMode
import dev.meloda.overseerr.ext.setValue
import io.github.xxfast.kstore.KStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
interface SettingsController {
val settings: StateFlow<AppSettings>
suspend fun saveAppSettings()
suspend fun updateAppSettings(update: (AppSettings) -> AppSettings)
suspend fun loadAppSettings(): AppSettings
class SettingsController(
private val store: KStore<AppSettings>,
) {
private val _settings = MutableStateFlow(AppSettings.EMPTY)
val settings: StateFlow<AppSettings> = _settings.asStateFlow()
fun updateThemeMode(newThemeMode: ThemeMode)
}
class SettingsControllerImpl(
private val store: KStore<AppSettings>
) : SettingsController {
override val settings = MutableStateFlow(AppSettings.EMPTY)
override suspend fun saveAppSettings() {
suspend fun saveAppSettings() {
store.set(settings.value)
}
override suspend fun updateAppSettings(update: (AppSettings) -> AppSettings) {
suspend fun updateAppSettings(update: (AppSettings) -> AppSettings) {
store.set(update(settings.value))
}
override suspend fun loadAppSettings(): AppSettings {
suspend fun loadAppSettings(): AppSettings {
val loadedSettings = store.get() ?: AppSettings.EMPTY
settings.setValue { loadedSettings }
_settings.setValue { loadedSettings }
return loadedSettings
}
override fun updateThemeMode(newThemeMode: ThemeMode) {
settings.setValue { old -> old.copy(themeMode = newThemeMode) }
fun updateThemeMode(newThemeMode: ThemeMode) {
_settings.setValue { old -> old.copy(themeMode = newThemeMode) }
}
}
@@ -1,13 +1,11 @@
package dev.meloda.overseerr.datastore.di
import dev.meloda.overseerr.datastore.SettingsController
import dev.meloda.overseerr.datastore.SettingsControllerImpl
import dev.meloda.overseerr.datastore.SettingsStoreProvider
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val dataStoreModule = module {
single { SettingsStoreProvider().provideStore() }
singleOf(::SettingsControllerImpl) bind SettingsController::class
singleOf(::SettingsController)
}
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
data class AppSettings(
val url: String? = null,
val plexToken: String? = null,
val themeMode: ThemeMode = ThemeMode.System,
val themeMode: ThemeMode = ThemeMode.System
) {
companion object {
val EMPTY: AppSettings = AppSettings()
@@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.overseerr.screens.requests.RequestsViewModel
import dev.meloda.overseerr.screens.requests.RequestsViewModelImpl
import dev.meloda.overseerr.screens.requests.model.RequestsScreenState
@@ -6,7 +6,6 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -1,12 +1,5 @@
import androidx.compose.ui.window.ComposeUIViewController
import dev.meloda.overseerr.App
import dev.meloda.overseerr.di.appModule
import org.koin.core.context.startKoin
import platform.UIKit.UIViewController
fun MainViewController(): UIViewController = ComposeUIViewController {
startKoin {
modules(appModule)
}
App()
}
fun MainViewController(): UIViewController = ComposeUIViewController { App() }
@@ -0,0 +1,9 @@
package dev.meloda.overseerr
import platform.UIKit.UIDevice
class IOSPlatform : Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
@@ -1,6 +1,7 @@
package dev.meloda.overseerr.settings.model
package dev.meloda.overseerr.datastore
import dev.meloda.overseerr.appDir
import dev.meloda.overseerr.datastore.model.AppSettings
import io.github.xxfast.kstore.KStore
import io.github.xxfast.kstore.file.storeOf
import kotlinx.io.files.Path
@@ -1,6 +0,0 @@
package dev.meloda.overseerr.model
internal actual class Platform actual constructor() {
actual val name: String
get() = "iOS"
}
@@ -0,0 +1,8 @@
package dev.meloda.overseerr.network
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.darwin.Darwin
actual class HttpClientEngineFactoryProvider actual constructor() {
actual fun get(): HttpClientEngineFactory<*> = Darwin
}
@@ -1,8 +0,0 @@
package dev.meloda.overseerr.network.model
import io.ktor.client.engine.*
import io.ktor.client.engine.darwin.*
actual class HttpClientEngineFactoryProvider actual constructor() {
actual fun get(): HttpClientEngineFactory<*> = Darwin
}
@@ -0,0 +1,7 @@
package dev.meloda.overseerr
class JsPlatform : Platform {
override val name: String = "Web with Kotlin/JS"
}
actual fun getPlatform(): Platform = JsPlatform()
@@ -0,0 +1,8 @@
package dev.meloda.overseerr.network
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.js.Js
actual class HttpClientEngineFactoryProvider actual constructor() {
actual fun get(): HttpClientEngineFactory<*> = Js
}
+25
View File
@@ -0,0 +1,25 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.browser.window
@Composable
actual fun ResizableWindow(content: @Composable () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(window.innerWidth.coerceIn(360..600).dp)
.height(window.innerHeight.coerceIn(minimumValue = 360, maximumValue = null).dp)
) {
content()
}
}
}
@@ -0,0 +1,7 @@
package dev.meloda.overseerr
class JVMPlatform : Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
}
actual fun getPlatform(): Platform = JVMPlatform()
+4 -6
View File
@@ -5,10 +5,8 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import dev.meloda.overseerr.App
import dev.meloda.overseerr.appDir
import dev.meloda.overseerr.di.appModule
import io.github.aakira.napier.Napier
import net.harawata.appdirs.AppDirsFactory
import org.koin.compose.KoinApplication
import java.awt.Dimension
import java.io.File
@@ -28,10 +26,10 @@ fun main() = application {
state = state,
onCloseRequest = ::exitApplication
) {
window.minimumSize = Dimension(320, 480)
KoinApplication(application = { modules(appModule) }) {
App()
LaunchedEffect(Unit) {
window.minimumSize = Dimension(320, 480)
}
App()
}
}
@@ -0,0 +1,7 @@
package dev.meloda.overseerr
class WasmPlatform : Platform {
override val name: String = "Web with Kotlin/Wasm"
}
actual fun getPlatform(): Platform = WasmPlatform()
@@ -1,7 +1,7 @@
package dev.meloda.overseerr.network
import io.ktor.client.engine.*
import io.ktor.client.engine.js.*
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.js.Js
actual class HttpClientEngineFactoryProvider actual constructor() {
actual fun get(): HttpClientEngineFactory<*> = Js
-34
View File
@@ -1,34 +0,0 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ComposeViewport
import dev.meloda.overseerr.App
import dev.meloda.overseerr.di.appModule
import kotlinx.browser.document
import kotlinx.browser.window
import org.koin.compose.KoinApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.body!!) {
KoinApplication(application = { modules(appModule) }) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(window.innerWidth.coerceIn(360..600).dp)
.height(window.innerHeight.coerceIn(minimumValue = 360, maximumValue = null).dp)
) {
App()
}
}
}
}
}
@@ -0,0 +1,25 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.browser.window
@Composable
actual fun ResizableWindow(content: @Composable () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(window.innerWidth.coerceIn(360..600).dp)
.height(window.innerHeight.coerceIn(minimumValue = 360, maximumValue = null).dp)
) {
content()
}
}
}
@@ -1,14 +1,12 @@
package dev.meloda.overseerr.theme
import androidx.compose.runtime.Composable
import androidx.navigation.ExperimentalBrowserHistoryApi
import androidx.navigation.NavController
@Composable
internal actual fun SystemAppearance(isDark: Boolean) {
}
@OptIn(ExperimentalBrowserHistoryApi::class)
@Composable
internal actual fun NavigationSettings(navController: NavController) {
}
+16
View File
@@ -0,0 +1,16 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import dev.meloda.overseerr.App
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport {
ResizableWindow {
App()
}
}
}
@Composable
expect fun ResizableWindow(content: @Composable () -> Unit)
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wasm App</title>
<title>Web App</title>
<link type="text/css" rel="stylesheet" href="styles.css">
<script type="application/javascript" src="composeApp.js"></script>
</head>
+21
View File
@@ -0,0 +1,21 @@
/*
* Temporary workaround for [KT-80582](https://youtrack.jetbrains.com/issue/KT-80582)
*
* This file should be safe to be removed once the ticket is closed and the project is updated to Kotlin version which solves that issue.
*/
config.watchOptions = config.watchOptions || {
ignored: ["**/*.kt", "**/node_modules"]
}
if (config.devServer) {
config.devServer.static = config.devServer.static.map(file => {
if (typeof file === "string") {
return {
directory: file,
watch: false,
}
} else {
return file
}
})
}
+4 -10
View File
@@ -1,22 +1,16 @@
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx4G
#Gradle
org.gradle.jvmargs=-Xmx4G
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.daemon=true
org.gradle.parallel=true
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx4G
kotlin.native.ignoreDisabledTargets=true
kotlin.native.enableKlibsCrossCompilation=true
#Android
android.useAndroidX=true
android.nonTransitiveRClass=true
include_wasm=true
include_ios=false
#Flip this to false when including the ios targets
org.gradle.unsafe.configuration-cache=true
+3 -1
View File
@@ -34,7 +34,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
@@ -52,6 +52,8 @@ kstore-storage = { module = "io.github.xxfast:kstore-storage", version.ref = "ks
appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
napier = { module = "io.github.aakira:napier", version.ref = "napier" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+13 -13
View File
@@ -4,32 +4,32 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
includeGroupByRegex("android.*")
mavenContent {
includeGroupAndSubgroups("androidx")
includeGroupAndSubgroups("com.android")
includeGroupAndSubgroups("com.google")
}
}
gradlePluginPortal()
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
includeGroupByRegex("android.*")
mavenContent {
includeGroupAndSubgroups("androidx")
includeGroupAndSubgroups("com.android")
includeGroupAndSubgroups("com.google")
}
}
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
include(":composeApp")