From ae1984d41984bde5df1e5647d314f281f9ca4ef0 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 15:46:21 +0100 Subject: [PATCH 01/26] feat: Add AsyncCache plugin, Deinit plugin, and onStop() callback for store --- .../pro/respawn/flowmvi/dsl/PipelineDsl.kt | 24 +++++++++++++++ .../flowmvi/plugins/AsyncCachePlugin.kt | 29 +++++++++++++++++++ .../respawn/flowmvi/plugins/DeinitPlugin.kt | 27 +++++++++++++++++ .../pro/respawn/flowmvi/plugins/InitPlugin.kt | 14 +++++++++ 4 files changed, 94 insertions(+) create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt index a4a1564e..ae027404 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt @@ -1,10 +1,16 @@ package pro.respawn.flowmvi.dsl +import kotlinx.coroutines.CompletionHandler +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.job import pro.respawn.flowmvi.api.DelicateStoreApi +import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.PipelineContext +import pro.respawn.flowmvi.api.StorePlugin import kotlin.coroutines.coroutineContext /** @@ -14,3 +20,21 @@ import kotlin.coroutines.coroutineContext @DelicateStoreApi public suspend inline fun pipelineContext(): PipelineContext? = coroutineContext[PipelineContext] as? PipelineContext? + +/** + * Invoke [handler] when the store stops. + * + * The biggest difference from [StorePlugin.onStop] is that this handler is attached to the pipeline's job and thus: + * * there's **no guarantee** on the order of invocations between **all** registered handlers and **any plugins** + * * there's no guarantee as to which **thread or coroutine context** this will be invoked on. + * * Implementation of [handler] must be fast, non-blocking, and thread-safe. This handler can be invoked concurrently with the surrounding code. + * + * As a rule, this can be useful for cleanup work that you can't put into a [StorePlugin.onStop] callback and that does + * not reference any other plugins. + * + * @see Job.invokeOnCompletion + */ +@FlowMVIDSL +public fun PipelineContext<*, *, *>.onStop( + handler: CompletionHandler +): DisposableHandle = coroutineContext.job.invokeOnCompletion(handler) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt new file mode 100644 index 00000000..a77155d1 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt @@ -0,0 +1,29 @@ +package pro.respawn.flowmvi.plugins + +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.PipelineContext +import pro.respawn.flowmvi.dsl.StoreBuilder +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +public suspend operator fun Deferred.invoke(): T = await() + +@FlowMVIDSL +public inline fun asyncCached( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.UNDISPATCHED, + crossinline init: suspend PipelineContext.() -> T, +): CachedValue, S, I, A> = cached { async(context, start) { init() } } + +@FlowMVIDSL +public inline fun StoreBuilder.asyncCache( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.UNDISPATCHED, + crossinline init: suspend PipelineContext.() -> T, +): CachedValue, S, I, A> = asyncCached(context, start, init).also { install(cachePlugin(it)) } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt new file mode 100644 index 00000000..b8acdeaf --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt @@ -0,0 +1,27 @@ +package pro.respawn.flowmvi.plugins + +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.dsl.plugin +import pro.respawn.flowmvi.api.StorePlugin + +/** + * Alias for [StorePlugin.onStop] callback or `plugin { onStop { block() } }` + * + * See the function documentation for more info. + */ +@FlowMVIDSL +public inline fun deinitPlugin( + crossinline block: (e: Exception?) -> Unit +): StorePlugin = plugin { onStop { block(it) } } + +/** + * Install a new [deinitPlugin]. + */ +@FlowMVIDSL +public inline fun StoreBuilder.deinit( + crossinline block: (e: Exception?) -> Unit +): Unit = install(deinitPlugin(block)) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/InitPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/InitPlugin.kt index 4ae2c201..0ccbc6ba 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/InitPlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/InitPlugin.kt @@ -1,5 +1,6 @@ package pro.respawn.flowmvi.plugins +import kotlinx.coroutines.launch import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent @@ -8,6 +9,8 @@ import pro.respawn.flowmvi.api.PipelineContext import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.StoreBuilder import pro.respawn.flowmvi.dsl.plugin +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * Installs a plugin that invokes [block] when [pro.respawn.flowmvi.api.Store.start] is called. @@ -26,3 +29,14 @@ public fun StoreBuilder.in public fun initPlugin( @BuilderInference block: suspend PipelineContext.() -> Unit, ): StorePlugin = plugin { onStart(block) } + +/** + * [initPlugin] overload that launches a new coroutine instead of preventing store startup sequence. + * + * The [block] is executed on every startup of the store in a separate coroutine. + */ +@FlowMVIDSL +public inline fun StoreBuilder.asyncInit( + context: CoroutineContext = EmptyCoroutineContext, + crossinline block: suspend PipelineContext.() -> Unit +): Unit = init { launch(context) { block() } } From a5c28ce1c2a0ed2e2902c3208b35c426a59f91f1 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 15:56:17 +0100 Subject: [PATCH 02/26] breaking: reverse the order of onStop invocations. --- .../flowmvi/plugins/CompositePlugin.kt | 2 +- .../flowmvi/test/store/DeinitOrderTest.kt | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/DeinitOrderTest.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt index 1402742a..630443c5 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt @@ -31,7 +31,7 @@ public fun compositePlugin( onSubscribe = { subs: Int -> plugins.chain { onSubscribe(subs) } }, onUnsubscribe = { subs: Int -> plugins.chain { onUnsubscribe(subs) } }, onStart = { plugins.chain { onStart() } }, - onStop = { plugins.chain { onStop(it) } } + onStop = { plugins.asReversed().chain { onStop(it) } } ) private inline fun List>.chain( diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/DeinitOrderTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/DeinitOrderTest.kt new file mode 100644 index 00000000..4dc430af --- /dev/null +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/DeinitOrderTest.kt @@ -0,0 +1,33 @@ +package pro.respawn.flowmvi.test.store + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import pro.respawn.flowmvi.plugins.cache +import pro.respawn.flowmvi.plugins.deinit +import pro.respawn.flowmvi.test.subscribeAndTest +import pro.respawn.flowmvi.util.testStore + +class DeinitOrderTest : FreeSpec({ + "given store with cache plugin" - { + val store = testStore { + var deinits = 0 + val firstValue by cache { 0 } + deinit { + deinits shouldBe 1 + ++deinits + firstValue shouldBe 0 + } + val secondValue by cache { 1 } + deinit { + deinits shouldBe 0 + secondValue shouldBe 1 + ++deinits + } + } + "then onStop can access plugins declared above" { + store.subscribeAndTest { + // nothing to do in the body + } + } + } +}) From c3b5a961e4f05370c603445c925b0e260a939c4e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 15:58:00 +0100 Subject: [PATCH 03/26] chore: update changelog label parser --- .github/changelog_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/changelog_config.json b/.github/changelog_config.json index c53db87d..2d8f82d3 100644 --- a/.github/changelog_config.json +++ b/.github/changelog_config.json @@ -50,7 +50,7 @@ ], "label_extractor" : [ { - "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", + "pattern" : "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|feat!|breaking|api){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", "target" : "$1" } ] From 3622b66c19a09936ca8500a80942314e7d182c9f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 16:19:34 +0100 Subject: [PATCH 04/26] chore: update app deps --- .../src/main/kotlin/ConfigureMultiplatform.kt | 4 +- gradle/libs.versions.toml | 41 ++++++++++--------- sample/libs.versions.toml | 6 +-- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index 36394c66..75e12aac 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -4,11 +4,11 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.getting import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -@OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) +@OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class) fun Project.configureMultiplatform( ext: KotlinMultiplatformExtension, jvm: Boolean = true, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 534e8b9a..7bec2418 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,32 +1,33 @@ [versions] -activity = "1.9.2" -compose = "1.7.0-beta01" +activity = "1.9.3" +compose = "1.7.0" composeDetektPlugin = "1.4.0" core-ktx = "1.13.1" -coroutines = "1.9.0-RC.2" +coroutines = "1.9.0" datetime = "0.6.1" -dependencyAnalysisPlugin = "2.0.1" +dependencyAnalysisPlugin = "2.4.2" detekt = "1.23.7" -dokka = "1.9.20" -essenty = "2.2.0-beta01" -fragment = "1.8.3" -gradleAndroid = "8.6.0" +dokka = "2.0.0-Beta" +essenty = "2.2.1" +fragment = "1.8.5" +gradleAndroid = "8.7.2" gradleDoctorPlugin = "0.10.0" -intellij-ide-plugin = "2.0.1" +intellij-ide-plugin = "2.1.0" junit = "4.13.2" -kotest = "5.9.1" +kotest = "6.0.0.M1" # @pin -kotlin = "2.0.20" +kotlin = "2.0.21" kotlin-collections = "0.3.8" -kotlin-io = "0.5.3" -kotlinx-atomicfu = "0.25.0" -ktor = "3.0.0-rc-1" -lifecycle = "2.8.2" -maven-publish-plugin = "0.29.0" -serialization = "1.7.2" -turbine = "1.1.0" +kotlin-io = "0.5.4" +kotlinx-atomicfu = "0.26.0" +ktor = "3.0.1" +lifecycle = "2.8.3" +androidx-lifecycle = "2.8.7" +maven-publish-plugin = "0.30.0" +serialization = "1.7.3" +turbine = "1.2.0" uuid = "0.8.4" -versionCatalogUpdatePlugin = "0.8.4" +versionCatalogUpdatePlugin = "0.8.5" [libraries] android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradleAndroid" } @@ -82,7 +83,7 @@ ktor-server-partialcontent = { module = "io.ktor:ktor-server-partial-content", v ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } lifecycle-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" } -lifecycle-savedstate = "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.5" +lifecycle-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } diff --git a/sample/libs.versions.toml b/sample/libs.versions.toml index bc37013a..93ffc4c1 100644 --- a/sample/libs.versions.toml +++ b/sample/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -decompose = "3.2.0-beta01" +decompose = "3.2.0-beta03" apiresult = "2.0.0" -koin = "4.0.0-RC2" +koin = "4.0.0" kmputils = "1.4.4" material = "1.12.0" okio = "3.9.0" splashscreen = "1.1.0-rc01" -xml-constraintlayout = "2.2.0-beta01" +xml-constraintlayout = "2.2.0" codehighlights = "0.9.0" [libraries] From 196f588cccc3ae4440c30e3f6b3dbef9462058e4 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 16:19:56 +0100 Subject: [PATCH 05/26] fix: disable dependency analysis plugin bc of Guava incompatibility --- build.gradle.kts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 14469163..48336aa0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { alias(libs.plugins.version.catalog.update) alias(libs.plugins.dokka) alias(libs.plugins.atomicfu) - alias(libs.plugins.dependencyAnalysis) + // alias(libs.plugins.dependencyAnalysis) alias(libs.plugins.serialization) apply false alias(libs.plugins.compose) apply false alias(libs.plugins.maven.publish) apply false @@ -98,12 +98,12 @@ doctor { ensureJavaHomeMatches.set(false) } } - -dependencyAnalysis { - structure { - ignoreKtx(true) - } -} +// +// dependencyAnalysis { +// structure { +// ignoreKtx(true) +// } +// } dependencies { detektPlugins(rootProject.libs.detekt.formatting) @@ -127,7 +127,6 @@ atomicfu { dependenciesVersion = libs.versions.kotlinx.atomicfu.get() transformJvm = true jvmVariant = "VH" - transformJs = true } tasks { From fd094c355588eab55969cd6ec6c8930b9f2b9a3c Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 16:20:33 +0100 Subject: [PATCH 06/26] chore: update gradle and new gradle properties --- gradle.properties | 12 +++++++++++- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2b7fad35..506ca100 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,9 +22,19 @@ android.disableResourceValidation=true org.gradle.daemon=true android.nonFinalResIds=true kotlinx.atomicfu.enableJvmIrTransformation=true -org.jetbrains.compose.experimental.macos.enabled=true android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.wasm.enabled=true +# Do not garbage collect on timeout on native when appExtensions are used and app is in bacground +kotlin.native.binary.appStateTracking=enabled +# Lift main thread suspending function invocation restriction +kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none +# Native incremental compilation +kotlin.incremental.native=true +android.experimental.additionalArtifactsInModel=true +kotlin.apple.xcodeCompatibility.nowarn=true +# Enable new k/n GC +kotlin.native.binary.gc=cms +org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers release=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e..df97d72b 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-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From e7ad54d0adfcb513d9a30576043df245dc6df8c9 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 16:26:27 +0100 Subject: [PATCH 07/26] chore: bump target SDK to 35 on Android --- buildSrc/src/main/kotlin/Config.kt | 4 ++-- .../commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 90ccb4ed..22ad89a4 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -19,7 +19,7 @@ object Config { const val majorRelease = 3 const val minorRelease = 1 const val patch = 0 - const val postfix = "-beta01" // include dash (-) + const val postfix = "-beta02" // include dash (-) const val majorVersionName = "$majorRelease.$minorRelease.$patch" const val versionName = "$majorVersionName$postfix" const val url = "https://github.com/respawn-app/FlowMVI" @@ -76,7 +76,7 @@ object Config { val jvmTarget = JvmTarget.JVM_11 val idePluginJvmTarget = JvmTarget.JVM_17 val javaVersion = JavaVersion.VERSION_11 - const val compileSdk = 34 + const val compileSdk = 35 const val targetSdk = compileSdk const val minSdk = 21 const val appMinSdk = 26 diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt index ae027404..b7150987 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PipelineDsl.kt @@ -25,8 +25,9 @@ public suspend inline fun pipelineC * Invoke [handler] when the store stops. * * The biggest difference from [StorePlugin.onStop] is that this handler is attached to the pipeline's job and thus: - * * there's **no guarantee** on the order of invocations between **all** registered handlers and **any plugins** - * * there's no guarantee as to which **thread or coroutine context** this will be invoked on. + * * This handler must not throw, or the app will crash. + * * There's **no guarantee** on the order of invocations between **all** registered handlers and **any plugins** + * * There's no guarantee as to which **thread or coroutine context** this will be invoked on. * * Implementation of [handler] must be fast, non-blocking, and thread-safe. This handler can be invoked concurrently with the surrounding code. * * As a rule, this can be useful for cleanup work that you can't put into a [StorePlugin.onStop] callback and that does From c4a6eeb55f9189b9e037e68855d6f5bb137cfd8c Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 16:49:31 +0100 Subject: [PATCH 08/26] fix: fix deprecations and build errors --- core/src/jvmTest/kotlin/pro/respawn/flowmvi/CoreTestConfig.kt | 1 - .../kotlin/pro/respawn/flowmvi/debugger/plugin/HttpClient.kt | 3 +-- gradle.properties | 1 + gradle/libs.versions.toml | 1 + sample/libs.versions.toml | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/CoreTestConfig.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/CoreTestConfig.kt index e01724c7..534ccf2d 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/CoreTestConfig.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/CoreTestConfig.kt @@ -10,7 +10,6 @@ import kotlin.time.Duration.Companion.seconds class CoreTestConfig : AbstractProjectConfig() { override val coroutineTestScope = true - override var testCoroutineDispatcher = true override val includeTestScopePrefixes: Boolean = true override val failOnEmptyTestSuite: Boolean = true override val isolationMode: IsolationMode = IsolationMode.SingleInstance diff --git a/debugger/debugger-plugin/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/plugin/HttpClient.kt b/debugger/debugger-plugin/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/plugin/HttpClient.kt index 2cda8b0a..fd9a09d8 100644 --- a/debugger/debugger-plugin/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/plugin/HttpClient.kt +++ b/debugger/debugger-plugin/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/plugin/HttpClient.kt @@ -31,7 +31,7 @@ internal fun HttpClient( pingInterval: Long = 5000L ) = HttpClient(CIO) { install(WebSockets) { - this.pingInterval = pingInterval + pingIntervalMillis = pingInterval contentConverter = KotlinxWebsocketSerializationConverter(json) } install(Logging) { @@ -57,7 +57,6 @@ internal fun HttpClient( identity(0.5f) } addDefaultResponseValidation() - developmentMode = true expectSuccess = true followRedirects = true engine { diff --git a/gradle.properties b/gradle.properties index 506ca100..503e8b1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,6 +26,7 @@ android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.wasm.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true # Do not garbage collect on timeout on native when appExtensions are used and app is in bacground kotlin.native.binary.appStateTracking=enabled # Lift main thread suspending function invocation restriction diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bec2418..9bf0f2b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,7 @@ detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } dokka-android = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } essenty-instancekeeper = { module = "com.arkivanov.essenty:instance-keeper", version.ref = "essenty" } essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" } essenty-lifecycle-coroutines = { module = "com.arkivanov.essenty:lifecycle-coroutines", version.ref = "essenty" } diff --git a/sample/libs.versions.toml b/sample/libs.versions.toml index 93ffc4c1..d4e1d911 100644 --- a/sample/libs.versions.toml +++ b/sample/libs.versions.toml @@ -7,7 +7,7 @@ material = "1.12.0" okio = "3.9.0" splashscreen = "1.1.0-rc01" xml-constraintlayout = "2.2.0" -codehighlights = "0.9.0" +codehighlights = "1.0.0" [libraries] decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } From a3647628a3b05498fb3aad8c08f045e4bedd3f7d Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 17:43:30 +0100 Subject: [PATCH 09/26] feat: migrate to dokka V2 plugin --- .github/workflows/docs.yml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 3 +- .idea/runConfigurations.xml | 13 +++++++ android/build.gradle.kts | 1 + build.gradle.kts | 8 ----- buildSrc/build.gradle.kts | 1 + buildSrc/src/main/kotlin/Config.kt | 1 + .../main/kotlin/dokkaDocumentation.gradle.kts | 35 +++++++++++++++++++ compose/build.gradle.kts | 1 + core/build.gradle.kts | 1 + debugger/debugger-client/build.gradle.kts | 1 + debugger/debugger-common/build.gradle.kts | 1 + debugger/debugger-plugin/build.gradle.kts | 1 + essenty/build.gradle.kts | 1 + essenty/essenty-compose/build.gradle.kts | 1 + savedstate/build.gradle.kts | 1 + test/build.gradle.kts | 1 + 17 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 .idea/runConfigurations.xml create mode 100644 buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d6e74d72..424463d0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -52,7 +52,7 @@ jobs: run: cp ./README.md ./docs/README.md - name: Generate docs - run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache + run: ./gradlew :dokkaGenerateHtml - name: Move docs to the parent docs dir run: cp -r ./build/dokka/htmlMultiModule/ ./docs/javadocs/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 28f93916..84353676 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,7 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..931b96c3 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 057f9294..3a289883 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("pro.respawn.android-library") alias(libs.plugins.maven.publish) + dokkaDocumentation } android { diff --git a/build.gradle.kts b/build.gradle.kts index 48336aa0..a76212b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,6 @@ plugins { alias(libs.plugins.detekt) alias(libs.plugins.gradleDoctor) alias(libs.plugins.version.catalog.update) - alias(libs.plugins.dokka) alias(libs.plugins.atomicfu) // alias(libs.plugins.dependencyAnalysis) alias(libs.plugins.serialization) apply false @@ -81,13 +80,6 @@ subprojects { filter { isFailOnNoMatchingTests = true } } } - // TODO: Migrate to applying dokka plugin per-project in conventions - if (name in setOf("sample", "debugger", "server")) return@subprojects - apply(plugin = rootProject.libs.plugins.dokka.id) - - dependencies { - dokkaPlugin(rootProject.libs.dokka.android) - } } doctor { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a88ee7a5..4909b970 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -6,4 +6,5 @@ plugins { dependencies { implementation(libs.android.gradle) implementation(libs.kotlin.gradle) + implementation(libs.dokka.gradle) } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 22ad89a4..960253b0 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -28,6 +28,7 @@ object Config { const val licenseName = "The Apache Software License, Version 2.0" const val licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" const val scmUrl = "https://github.com/respawn-app/FlowMVI.git" + const val docsUrl = "https://opensource.respawn.pro/FlowMVI/#/" const val description = """A Kotlin Multiplatform MVI library based on coroutines with a powerful plugin system""" const val supportEmail = "hello@respawn.pro" const val vendorName = "Respawn Open Source Team" diff --git a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts new file mode 100644 index 00000000..d7e896b1 --- /dev/null +++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("org.jetbrains.dokka") + // id("org.jetbrains.dokka-javadoc") +} + +val libs by versionCatalog + +dokka { + dokkaGeneratorIsolation = ClassLoaderIsolation() + moduleName = project.name + moduleVersion = project.version.toString() + pluginsConfiguration.html { + footerMessage = "© ${Config.vendorName}" + customAssets.from(rootDir.resolve("docs/images/icon-512-maskable.png")) + homepageLink = Config.url + } + dokkaPublications.configureEach { + suppressInheritedMembers = false + suppressObviousFunctions = true + } + dokkaSourceSets.configureEach { + reportUndocumented = true + enableJdkDocumentationLink = true + enableAndroidDocumentationLink = true + enableKotlinStdLibDocumentationLink = true + skipEmptyPackages = true + skipDeprecated = true + jdkVersion = Config.javaVersion.majorVersion.toInt() + } + // remoteUrl = Config.docsUrl +} + +dependencies { + dokkaPlugin(libs.requireLib("dokka-android")) +} diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 8075e011..1728ec56 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.maven.publish) + dokkaDocumentation } android { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index fcde7cdf..c1756f96 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -6,6 +6,7 @@ plugins { // TODO: https://github.com/kotest/kotest/issues/3598 // alias(libs.plugins.kotest) + dokkaDocumentation } android { diff --git a/debugger/debugger-client/build.gradle.kts b/debugger/debugger-client/build.gradle.kts index c31af253..a6ae76a6 100644 --- a/debugger/debugger-client/build.gradle.kts +++ b/debugger/debugger-client/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.library") alias(libs.plugins.serialization) alias(libs.plugins.maven.publish) + dokkaDocumentation } kotlin { configureMultiplatform( diff --git a/debugger/debugger-common/build.gradle.kts b/debugger/debugger-common/build.gradle.kts index 25c7b06e..aac43aa4 100644 --- a/debugger/debugger-common/build.gradle.kts +++ b/debugger/debugger-common/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.library") alias(libs.plugins.serialization) alias(libs.plugins.maven.publish) + dokkaDocumentation } kotlin { diff --git a/debugger/debugger-plugin/build.gradle.kts b/debugger/debugger-plugin/build.gradle.kts index d8f28104..33b5f083 100644 --- a/debugger/debugger-plugin/build.gradle.kts +++ b/debugger/debugger-plugin/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.library") alias(libs.plugins.serialization) alias(libs.plugins.maven.publish) + dokkaDocumentation } kotlin { diff --git a/essenty/build.gradle.kts b/essenty/build.gradle.kts index abc5fab4..ab9f9471 100644 --- a/essenty/build.gradle.kts +++ b/essenty/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("multiplatform") id("com.android.library") alias(libs.plugins.maven.publish) + dokkaDocumentation } kotlin { diff --git a/essenty/essenty-compose/build.gradle.kts b/essenty/essenty-compose/build.gradle.kts index 8ac6f74b..b5e68e64 100644 --- a/essenty/essenty-compose/build.gradle.kts +++ b/essenty/essenty-compose/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.maven.publish) + dokkaDocumentation } android { diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index 59b87c15..353a9af0 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.serialization) id("com.android.library") alias(libs.plugins.maven.publish) + dokkaDocumentation } kotlin { diff --git a/test/build.gradle.kts b/test/build.gradle.kts index 68f380ac..4db88bad 100644 --- a/test/build.gradle.kts +++ b/test/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("pro.respawn.shared-library") alias(libs.plugins.maven.publish) + dokkaDocumentation } android { namespace = "${Config.namespace}.test" From ce9a3074700d94d7371be6ff9548102e9a0c37f8 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 18:01:15 +0100 Subject: [PATCH 10/26] feat: enable wasmWasi target for :core module --- buildSrc/src/main/kotlin/ConfigureMultiplatform.kt | 6 ++++-- buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts | 5 ++++- compose/build.gradle.kts | 1 + debugger/debugger-client/build.gradle.kts | 1 + debugger/debugger-common/build.gradle.kts | 2 +- debugger/debugger-plugin/build.gradle.kts | 1 + essenty/build.gradle.kts | 3 ++- essenty/essenty-compose/build.gradle.kts | 1 + gradle.properties | 3 ++- savedstate/build.gradle.kts | 2 +- 10 files changed, 18 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index 75e12aac..748c137d 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -21,7 +21,7 @@ fun Project.configureMultiplatform( watchOs: Boolean = true, windows: Boolean = true, wasmJs: Boolean = true, - wasmWasi: Boolean = false, // TODO: Coroutines do not support wasmWasi yet + wasmWasi: Boolean = true, configure: KotlinHierarchyBuilder.Root.() -> Unit = {}, ) = ext.apply { val libs by versionCatalog @@ -49,7 +49,9 @@ fun Project.configureMultiplatform( binaries.library() } - if (wasmWasi) wasmWasi() + if (wasmWasi) wasmWasi { + nodejs() + } if (android) androidTarget { publishLibraryVariants("release") diff --git a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts index d7e896b1..22f04595 100644 --- a/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts +++ b/buildSrc/src/main/kotlin/dokkaDocumentation.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier + plugins { id("org.jetbrains.dokka") // id("org.jetbrains.dokka-javadoc") @@ -19,13 +21,14 @@ dokka { suppressObviousFunctions = true } dokkaSourceSets.configureEach { - reportUndocumented = true + reportUndocumented = false enableJdkDocumentationLink = true enableAndroidDocumentationLink = true enableKotlinStdLibDocumentationLink = true skipEmptyPackages = true skipDeprecated = true jdkVersion = Config.javaVersion.majorVersion.toInt() + documentedVisibilities(VisibilityModifier.Public) } // remoteUrl = Config.docsUrl } diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 1728ec56..ecf5beb9 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { linux = false, js = true, wasmJs = true, + wasmWasi = false, windows = false, ) sourceSets { diff --git a/debugger/debugger-client/build.gradle.kts b/debugger/debugger-client/build.gradle.kts index a6ae76a6..19d7b6fd 100644 --- a/debugger/debugger-client/build.gradle.kts +++ b/debugger/debugger-client/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { // not supported by all needed ktor artifacts? watchOs = false, wasmJs = false, + wasmWasi = false, ) } android { diff --git a/debugger/debugger-common/build.gradle.kts b/debugger/debugger-common/build.gradle.kts index aac43aa4..8dc2e24a 100644 --- a/debugger/debugger-common/build.gradle.kts +++ b/debugger/debugger-common/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } kotlin { - configureMultiplatform(this) + configureMultiplatform(this, wasmWasi = false) } android { diff --git a/debugger/debugger-plugin/build.gradle.kts b/debugger/debugger-plugin/build.gradle.kts index 33b5f083..04ad0f78 100644 --- a/debugger/debugger-plugin/build.gradle.kts +++ b/debugger/debugger-plugin/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { js = false, wasmJs = false, windows = false, + wasmWasi = false, ) } diff --git a/essenty/build.gradle.kts b/essenty/build.gradle.kts index ab9f9471..4ed9a80d 100644 --- a/essenty/build.gradle.kts +++ b/essenty/build.gradle.kts @@ -14,7 +14,8 @@ kotlin { tvOs = false, watchOs = false, linux = false, - windows = false + windows = false, + wasmWasi = false, ) } diff --git a/essenty/essenty-compose/build.gradle.kts b/essenty/essenty-compose/build.gradle.kts index b5e68e64..a1960514 100644 --- a/essenty/essenty-compose/build.gradle.kts +++ b/essenty/essenty-compose/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { watchOs = false, linux = false, windows = false, + wasmWasi = false, ) sourceSets { androidMain.dependencies { diff --git a/gradle.properties b/gradle.properties index 503e8b1c..593c8dd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,5 +37,6 @@ android.experimental.additionalArtifactsInModel=true kotlin.apple.xcodeCompatibility.nowarn=true # Enable new k/n GC kotlin.native.binary.gc=cms -org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true release=true diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index 353a9af0..dd0476a6 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -10,7 +10,7 @@ plugins { kotlin { @OptIn(ExperimentalKotlinGradlePluginApi::class) - configureMultiplatform(this) { + configureMultiplatform(this, wasmWasi = false) { common { group("nonBrowser") { withJvm() From df199d431bd1eed90668f49b8560211e8bf1784a Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Mon, 4 Nov 2024 21:30:21 +0100 Subject: [PATCH 11/26] feat: implement new StoreLifecycle API --- .../kotlin/pro/respawn/flowmvi/StoreImpl.kt | 22 +++---- .../pro/respawn/flowmvi/api/ImmutableStore.kt | 5 +- .../respawn/flowmvi/api/PipelineContext.kt | 15 ++--- .../kotlin/pro/respawn/flowmvi/api/Store.kt | 18 ++++-- .../api/lifecycle/ImmutableStoreLifecycle.kt | 28 +++++++++ .../flowmvi/api/lifecycle/StoreLifecycle.kt | 14 +++++ .../respawn/flowmvi/modules/PipelineModule.kt | 59 ++++++++++--------- .../flowmvi/modules/StoreLifecycleModule.kt | 52 ++++++++++++++++ .../test/store/ActionShareBehaviorTest.kt | 2 +- .../flowmvi/test/store/StoreExceptionsTest.kt | 12 ++-- .../flowmvi/test/store/StoreLaunchTest.kt | 10 ++-- .../sample/features/simple/SimpleScreen.kt | 2 +- .../pro/respawn/flowmvi/test/TestDsl.kt | 21 ++++--- .../flowmvi/test/TestStoreLifecycle.kt | 28 +++++++++ .../test/plugin/TestPipelineContext.kt | 33 ++++++++--- 15 files changed, 235 insertions(+), 86 deletions(-) create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/ImmutableStoreLifecycle.kt create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/StoreLifecycle.kt create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StoreLifecycleModule.kt create mode 100644 test/src/commonMain/kotlin/pro/respawn/flowmvi/test/TestStoreLifecycle.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt index c534170d..06df3d12 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt @@ -1,11 +1,9 @@ package pro.respawn.flowmvi -import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.job import kotlinx.coroutines.launch import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent @@ -20,6 +18,7 @@ import pro.respawn.flowmvi.exceptions.UnhandledIntentException import pro.respawn.flowmvi.modules.ActionModule import pro.respawn.flowmvi.modules.IntentModule import pro.respawn.flowmvi.modules.RecoverModule +import pro.respawn.flowmvi.modules.RestartableLifecycle import pro.respawn.flowmvi.modules.StateModule import pro.respawn.flowmvi.modules.SubscriptionModule import pro.respawn.flowmvi.modules.actionModule @@ -27,6 +26,7 @@ import pro.respawn.flowmvi.modules.intentModule import pro.respawn.flowmvi.modules.launchPipeline import pro.respawn.flowmvi.modules.observeSubscribers import pro.respawn.flowmvi.modules.recoverModule +import pro.respawn.flowmvi.modules.restartableLifecycle import pro.respawn.flowmvi.modules.stateModule import pro.respawn.flowmvi.modules.subscriptionModule @@ -40,6 +40,7 @@ internal class StoreImpl( actions: ActionModule = actionModule(config.actionShareBehavior), ) : Store, Provider, + RestartableLifecycle by restartableLifecycle(), StorePlugin by plugin, RecoverModule by recover, SubscriptionModule by subs, @@ -47,10 +48,9 @@ internal class StoreImpl( IntentModule by intents, ActionModule by actions { - private val launchJob = atomic(null) override val name by config::name - override fun start(scope: CoroutineScope): Job = launchPipeline( + override fun start(scope: CoroutineScope) = launchPipeline( parent = scope, config = config, onAction = { action -> onAction(action)?.let { this@StoreImpl.action(it) } }, @@ -58,11 +58,11 @@ internal class StoreImpl( this@StoreImpl.updateState { onState(this, transform()) ?: this } }, onStop = { - checkNotNull(launchJob.getAndSet(null)) { "Store is stopped but was not started before" } + close() // makes sure to also clear the reference from RestartableLifecycle onStop(it) }, - onStart = { - check(launchJob.getAndSet(coroutineContext.job) == null) { "Store is already started" } + onStart = { lifecycle -> + beginStartup(lifecycle) launch intents@{ coroutineScope { // run onStart plugins first to not let subscribers appear before the store is started fully @@ -78,6 +78,7 @@ internal class StoreImpl( catch { if (onIntent(it) != null && config.debuggable) throw UnhandledIntentException() } } } + lifecycle.completeStartup() } } } @@ -86,18 +87,13 @@ internal class StoreImpl( override fun CoroutineScope.subscribe( block: suspend Provider.() -> Unit ): Job = launch { - if (launchJob.value?.isActive != true && !config.allowIdleSubscriptions) throw SubscribeBeforeStartException() + if (!isActive && !config.allowIdleSubscriptions) throw SubscribeBeforeStartException() launch { awaitUnsubscription() } block(this@StoreImpl) if (config.debuggable) throw NonSuspendingSubscriberException() cancel() } - override fun close() { - // completion handler will cleanup the pipeline later - launchJob.value?.cancel() - } - override fun hashCode() = name?.hashCode() ?: super.hashCode() override fun toString(): String = name ?: super.toString() override fun equals(other: Any?): Boolean { diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt index 8e75b307..a762b3ea 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt @@ -3,12 +3,13 @@ package pro.respawn.flowmvi.api import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import pro.respawn.flowmvi.api.lifecycle.ImmutableStoreLifecycle /** * A [Store] that does not allow sending intents. * @see Store */ -public interface ImmutableStore { +public interface ImmutableStore : ImmutableStoreLifecycle { /** * The name of the store. Used for debugging purposes and when storing multiple stores in a collection. @@ -24,7 +25,7 @@ public interface ImmutableStore : StateReceiver, ActionReceiver, CoroutineScope, + StoreLifecycle, CoroutineContext.Element { /** @@ -37,22 +37,17 @@ public interface PipelineContext : */ public val config: StoreConfiguration - /** - * Same as [cancel], but for resolving the ambiguity between context.cancel() and scope.cancel() - */ - public fun close(): Unit = coroutineContext.job.cancel() - /** * An alias for [Flow.collect] that does not override the context, amending it instead. * Use as a safer alternative to [Flow.flowOn] and then [Flow.collect] */ - public suspend fun Flow.consume(context: CoroutineContext = EmptyCoroutineContext): Unit = - flowOn(this@PipelineContext + context).collect() + public suspend fun Flow.consume( + context: CoroutineContext = EmptyCoroutineContext + ): Unit = flowOn(this@PipelineContext + context).collect() /** * A key of the [PipelineContext] in the parent coroutine context. */ - @DelicateStoreApi public companion object : CoroutineContext.Key> @DelicateStoreApi diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/Store.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/Store.kt index d38a4faa..86c8b42b 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/Store.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/Store.kt @@ -1,13 +1,23 @@ package pro.respawn.flowmvi.api +import kotlinx.coroutines.CoroutineScope +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle + /** * A central business logic unit for handling [MVIIntent]s, [MVIAction]s, and [MVIState]s. * Usually not subclassed but used with a corresponding builder (see [pro.respawn.flowmvi.dsl.store]). - * A store functions independently of any subscribers, has its own lifecycle, can be stopped and relaunched at will. - * The store can be mutated only through [MVIIntent]. - * Store is an [IntentReceiver] and can be [close]d to stop it. + * + * * A Store functions independently of any subscribers, + * * Store has its own [StoreLifecycle], can be stopped and relaunched at will via [StoreLifecycle] returned + * from [start] or as this [StoreLifecycle] reference. + * * The store can be mutated only through [MVIIntent]. + * * Store is an [IntentReceiver] */ public interface Store : ImmutableStore, IntentReceiver, - AutoCloseable + StoreLifecycle { + + // mutable return type + override fun start(scope: CoroutineScope): StoreLifecycle +} diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/ImmutableStoreLifecycle.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/ImmutableStoreLifecycle.kt new file mode 100644 index 00000000..e2ad4549 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/ImmutableStoreLifecycle.kt @@ -0,0 +1,28 @@ +package pro.respawn.flowmvi.api.lifecycle + +import pro.respawn.flowmvi.api.Store + +public interface ImmutableStoreLifecycle { + + /** + * Wait while the [Store] is started. If it is already started, returns immediately. + */ + public suspend fun awaitStartup() + + /** + * Suspend until the store is closed + */ + public suspend fun awaitUntilClosed() + + /** + * Whether the [Store] is active (store is started or **being started**). + */ + public val isActive: Boolean + + /** + * Whether the [Store] has started fully. + * + * Unlike [isActive], returns `false` until store is fully started. + */ + public val isStarted: Boolean +} diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/StoreLifecycle.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/StoreLifecycle.kt new file mode 100644 index 00000000..b3dd7a85 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/lifecycle/StoreLifecycle.kt @@ -0,0 +1,14 @@ +package pro.respawn.flowmvi.api.lifecycle + +import pro.respawn.flowmvi.api.Store + +/** + * [ImmutableStoreLifecycle] that is also [AutoCloseable], which can be closed on demand. + */ +public interface StoreLifecycle : ImmutableStoreLifecycle, AutoCloseable { + + /** + * Await while the [Store] is fully closed. + */ + public suspend fun closeAndWait() +} diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt index 3586a8bf..bd4aa8a0 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.job import kotlinx.coroutines.launch import pro.respawn.flowmvi.api.ActionReceiver import pro.respawn.flowmvi.api.DelicateStoreApi @@ -18,7 +17,7 @@ import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.PipelineContext import pro.respawn.flowmvi.api.StateReceiver import pro.respawn.flowmvi.api.StoreConfiguration -import kotlin.coroutines.CoroutineContext +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle /** * Coroutine context consists of the following: job, name, handler, dispatcher. @@ -31,23 +30,15 @@ import kotlin.coroutines.CoroutineContext * * using this pipeline instance as the context element */ @OptIn(DelicateStoreApi::class) -@Suppress("Indentation") internal inline fun T.launchPipeline( parent: CoroutineScope, config: StoreConfiguration, crossinline onStop: (e: Exception?) -> Unit, crossinline onAction: suspend PipelineContext.(action: A) -> Unit, crossinline onTransformState: suspend PipelineContext.(transform: suspend S.() -> S) -> Unit, - onStart: PipelineContext.() -> Unit, -): Job where T : IntentReceiver, T : StateReceiver, T : RecoverModule = object : - IntentReceiver by this, - StateReceiver by this, - PipelineContext, - ActionReceiver { - - override val config get() = config - override val key = PipelineContext // recoverable should be separate. - private val job = SupervisorJob(parent.coroutineContext[Job]).apply { + onStart: PipelineContext.(lifecycle: StoreLifecycleModule) -> Unit, +): StoreLifecycle where T : IntentReceiver, T : StateReceiver, T : RecoverModule { + val job = SupervisorJob(parent.coroutineContext[Job]).apply { invokeOnCompletion { when (it) { null, is CancellationException -> onStop(null) @@ -56,23 +47,33 @@ internal inline fun T.launchPipe } } } - private val handler = PipelineExceptionHandler() - private val pipelineName = CoroutineName(toString()) - override val coroutineContext: CoroutineContext = parent.coroutineContext + - config.coroutineContext + - pipelineName + - job + - handler + - this + return object : + IntentReceiver by this, + StateReceiver by this, + PipelineContext, + StoreLifecycleModule by storeLifecycle(job), + ActionReceiver { + + override val config get() = config + override val key = PipelineContext // recoverable should be separate from this key + private val handler = PipelineExceptionHandler() + private val pipelineName = CoroutineName(toString()) - override fun toString(): String = "${config.name.orEmpty()}PipelineContext" + override val coroutineContext = parent.coroutineContext + + config.coroutineContext + + pipelineName + + job + + handler + + this - override suspend fun updateState(transform: suspend S.() -> S) = catch { onTransformState(transform) } - override suspend fun action(action: A) = catch { onAction(action) } - override fun send(action: A) { - launch { action(action) } + override fun toString(): String = "${config.name.orEmpty()}PipelineContext" + + override suspend fun updateState(transform: suspend S.() -> S) = catch { onTransformState(transform) } + override suspend fun action(action: A) = catch { onAction(action) } + override fun send(action: A) { + launch { action(action) } + } + }.apply { + onStart(this) } -}.run { - onStart() - coroutineContext.job } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StoreLifecycleModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StoreLifecycleModule.kt new file mode 100644 index 00000000..21b0e7c6 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StoreLifecycleModule.kt @@ -0,0 +1,52 @@ +package pro.respawn.flowmvi.modules + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle + +internal interface StoreLifecycleModule : StoreLifecycle { + + fun completeStartup(): Boolean +} + +internal interface RestartableLifecycle : StoreLifecycle { + + fun beginStartup(lifecycle: StoreLifecycle) +} + +internal fun storeLifecycle(parent: Job) = object : StoreLifecycleModule { + private val marker = CompletableDeferred(parent) + override val isActive get() = parent.isActive + override val isStarted get() = isActive && marker.isCompleted + + override fun completeStartup() = marker.complete(Unit) + override suspend fun awaitStartup() = marker.await() + override suspend fun awaitUntilClosed() = parent.join() + override suspend fun closeAndWait() = parent.cancelAndJoin() + override fun close() = parent.cancel() +} + +internal fun restartableLifecycle() = object : RestartableLifecycle { + private val delegate = MutableStateFlow(null) + override val isActive: Boolean get() = delegate.value?.isActive == true + override val isStarted: Boolean get() = delegate.value?.isStarted == true + + override suspend fun closeAndWait() = delegate.filterNotNull().first().closeAndWait() + override suspend fun awaitStartup() = delegate.filterNotNull().first().awaitStartup() + override suspend fun awaitUntilClosed() = delegate.filterNotNull().first().awaitUntilClosed() + + override fun beginStartup(lifecycle: StoreLifecycle) = delegate.update { + check(it == null) { "Store is already started" } + lifecycle + } + + override fun close() = delegate.update { + it?.close() + null + } +} diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/ActionShareBehaviorTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/ActionShareBehaviorTest.kt index 63547e46..8d773add 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/ActionShareBehaviorTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/ActionShareBehaviorTest.kt @@ -48,7 +48,7 @@ class ActionShareBehaviorTest : FreeSpec({ intent { action(TestAction.Some) } }.join() } - job.join() + job.closeAndWait() } } } diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreExceptionsTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreExceptionsTest.kt index b5be6272..2791fdcb 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreExceptionsTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreExceptionsTest.kt @@ -41,8 +41,8 @@ class StoreExceptionsTest : FreeSpec({ } } "then store is not closed when thrown" { - store.test { job -> - job.isActive shouldBe true + store.test { + isActive shouldBe true idle() plugin.starts shouldBe 1 plugin.exceptions.shouldContainExactly(e) @@ -50,10 +50,10 @@ class StoreExceptionsTest : FreeSpec({ } "then exceptions in processing scope do not cancel the pipeline" { - store.test { job -> + store.test { intent { } idle() - job.isActive shouldBe true + isActive shouldBe true plugin.intents.shouldBeSingleton() } } @@ -80,7 +80,7 @@ class StoreExceptionsTest : FreeSpec({ val store = testStore(plugin) { recover { null } } - store.test { job -> + store.test { intent { launch a@{ println("job 1 started") @@ -96,7 +96,7 @@ class StoreExceptionsTest : FreeSpec({ } } idle() - job.isActive shouldBe true + isActive shouldBe true } idle() plugin.intents.shouldBeSingleton() diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt index 0ebb626f..8d5d3f49 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt @@ -37,14 +37,14 @@ class StoreLaunchTest : FreeSpec({ store.close() idle() job.isActive shouldBe false - job.join() + job.awaitUntilClosed() } } "then can be launched twice" { coroutineScope { - store.start(this).cancelAndJoin() + store.start(this).closeAndWait() idle() - store.start(this).cancelAndJoin() + store.start(this).closeAndWait() } } "then cannot be launched when already launched" { @@ -52,8 +52,8 @@ class StoreLaunchTest : FreeSpec({ supervisorScope { val job1 = store.start(this) val job2 = store.start(this) - job1.cancelAndJoin() - job2.cancelAndJoin() + job1.closeAndWait() + job2.closeAndWait() } } } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/simple/SimpleScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/simple/SimpleScreen.kt index 7033ef43..8162b29d 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/simple/SimpleScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/simple/SimpleScreen.kt @@ -66,7 +66,7 @@ private const val Description = """ fun SimpleScreen( navigator: Navigator, ) = with(simpleStore) { - LaunchedEffect(Unit) { start(this).join() } + LaunchedEffect(Unit) { start(this).awaitUntilClosed() } val state by subscribe(DefaultLifecycle) diff --git a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/TestDsl.kt b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/TestDsl.kt index 39229de1..7a7b76d5 100644 --- a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/TestDsl.kt +++ b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/TestDsl.kt @@ -1,8 +1,6 @@ package pro.respawn.flowmvi.test import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -10,16 +8,18 @@ import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.Store +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle +import kotlin.test.assertTrue /** * Call [Store.start] and then execute [block], cancelling the store afterwards */ public suspend inline fun Store.test( - crossinline block: suspend Store.(job: Job) -> Unit -): Job = coroutineScope { - val job = start(this) - block(job) - job.apply { cancelAndJoin() } + crossinline block: suspend Store.() -> Unit +): Unit = coroutineScope { + start(this) + block() + closeAndWait() } /** @@ -28,7 +28,7 @@ public suspend inline fun Store Store.subscribeAndTest( crossinline block: suspend StoreTestScope.() -> Unit, -): Job = test { +): Unit = test { coroutineScope { subscribe { StoreTestScope(this, this@subscribeAndTest).run { block() } @@ -41,3 +41,8 @@ public suspend inline fun Store(parent) + override val isActive: Boolean get() = closed.isActive + override val isStarted: Boolean get() = closed.isActive + + override suspend fun awaitStartup(): Unit = Unit + override suspend fun awaitUntilClosed() { + closed.await() + } + + override fun close() { + closed.complete(Unit) + } + + override suspend fun closeAndWait() { + closed.complete(Unit) + closed.await() + } +} diff --git a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt index ddc35dab..4f727346 100644 --- a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt +++ b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt @@ -3,19 +3,28 @@ package pro.respawn.flowmvi.test.plugin import kotlinx.atomicfu.atomic +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.job import kotlinx.coroutines.launch import pro.respawn.flowmvi.api.DelicateStoreApi +import pro.respawn.flowmvi.api.ExperimentalStoreApi import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.PipelineContext import pro.respawn.flowmvi.api.StoreConfiguration import pro.respawn.flowmvi.api.StorePlugin +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle +import pro.respawn.flowmvi.test.TestStoreLifecycle +import pro.respawn.flowmvi.test.ensureStarted +@OptIn(ExperimentalStoreApi::class) internal class TestPipelineContext @PublishedApi internal constructor( override val config: StoreConfiguration, val plugin: StorePlugin, -) : PipelineContext { +) : PipelineContext, StoreLifecycle by TestStoreLifecycle(config.coroutineContext[Job]) { override val coroutineContext by config::coroutineContext @@ -24,27 +33,37 @@ internal class TestPipelineContext @ @DelicateStoreApi override fun send(action: A) { + ensureStarted() launch { with(plugin) { onAction(action) } } } override suspend fun action(action: A) { + ensureStarted() with(plugin) { onAction(action) } } - override suspend fun emit(intent: I): Unit = with(plugin) { onIntent(intent) } + override suspend fun emit(intent: I): Unit = with(plugin) { + ensureStarted() + onIntent(intent) + } override fun intent(intent: I) { + ensureStarted() launch { emit(intent) } } - override suspend fun updateState(transform: suspend S.() -> S) { - with(plugin) { - onState(state, state.transform())?.also { state = it } - } + override suspend fun updateState(transform: suspend S.() -> S) = with(plugin) { + ensureStarted() + onState(state, state.transform())?.also { state = it } + Unit } - override suspend fun withState(block: suspend S.() -> Unit): Unit = block(state) + override suspend fun withState(block: suspend S.() -> Unit) { + ensureStarted() + block(state) + } override fun updateStateImmediate(block: S.() -> S) { + ensureStarted() state = block(state) } } From afc97ad1356469fab9c0073e244f0d09969b447e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 14:39:58 +0100 Subject: [PATCH 12/26] feat: [wip] set up some basic code for ide plugin --- .gitignore | 1 + debugger/app/build.gradle.kts | 11 --- debugger/ideplugin/build.gradle.kts | 55 +++++++++++---- .../flowmvi/ideplugin/PluginToolWindow.kt | 52 ++++++++++++++ .../src/main/resources/META-INF/plugin.xml | 15 +++- .../main/resources/META-INF/pluginIcon.svg | 68 ++++++++++++++----- .../ideplugin/src/main/resources/icon.svg | 52 ++++++++++++++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 10 ++- 9 files changed, 218 insertions(+), 48 deletions(-) create mode 100644 debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt create mode 100644 debugger/ideplugin/src/main/resources/icon.svg diff --git a/.gitignore b/.gitignore index 4290397d..f764e7c8 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,4 @@ hs_err_pid* **/plugin_encrypted_key.pem **/plugin_rsa_private_key.pem **/.cache +/.intellijPlatform/ diff --git a/debugger/app/build.gradle.kts b/debugger/app/build.gradle.kts index 1656093e..2862a76d 100644 --- a/debugger/app/build.gradle.kts +++ b/debugger/app/build.gradle.kts @@ -4,7 +4,6 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) - alias(libs.plugins.serialization) } kotlin { @@ -23,21 +22,11 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) - implementation(applibs.apiresult) implementation(applibs.decompose) implementation(applibs.decompose.compose) - implementation(applibs.bundles.kmputils) implementation(applibs.bundles.koin) - implementation(libs.bundles.serialization) - implementation(libs.kotlin.datetime) - implementation(libs.uuid) - implementation(libs.kotlin.io) - - implementation(projects.core) implementation(projects.debugger.server) - implementation(projects.debugger.debuggerCommon) - implementation(projects.compose) } desktopMain.apply { dependencies { diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index 094c93a2..91e7cd76 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UnstableApiUsage") + import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -10,6 +12,13 @@ plugins { val props by localProperties() repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() intellijPlatform { defaultRepositories() @@ -22,13 +31,8 @@ configurations.all { intellijPlatform { projectName = Config.name -// needed when plugin provides custom settings exposed to the UI + // needed when plugin provides custom settings exposed to the UI buildSearchableOptions = false - pluginVerification { - ides { - recommended() - } - } signing { certificateChainFile = File("plugin_certificate_chain.crt") privateKey = props["plugin.publishing.privatekey"]?.toString() @@ -40,7 +44,7 @@ intellijPlatform { pluginConfiguration { ideaVersion { sinceBuild = "241" - untilBuild = "242.*" + untilBuild = provider { null } } vendor { name = Config.vendorName @@ -54,27 +58,42 @@ intellijPlatform { } } +kotlin { + compilerOptions { + jvmTarget.set(Config.idePluginJvmTarget) + } +} + tasks { withType { sourceCompatibility = Config.idePluginJvmTarget.target targetCompatibility = Config.idePluginJvmTarget.target } - withType { - kotlinOptions.jvmTarget = Config.idePluginJvmTarget.target - } } dependencies { compileOnly(compose.desktop.currentOs) + compileOnly(libs.kotlin.stdlib) implementation(compose.desktop.common) + implementation(compose.desktop.linux_arm64) + implementation(compose.desktop.linux_x64) + implementation(compose.desktop.macos_arm64) + implementation(compose.desktop.macos_x64) + implementation(compose.desktop.windows_x64) + implementation(projects.core) + implementation(projects.debugger.server) + + implementation(applibs.decompose) + implementation(applibs.decompose.compose) + implementation(applibs.bundles.koin) + intellijPlatform { - // bundledPlugin("org.jetbrains.kotlin") - // props["plugin.local.ide.path"]?.toString()?.let(::local) - // intellijIdeaCommunity("2024.2.1") + intellijIdeaCommunity(libs.versions.intellij.idea) pluginVerifier() zipSigner() instrumentationTools() + bundledPlugin(libs.kotlin.stdlib.map(Dependency::getGroup)) } } @@ -82,9 +101,17 @@ tasks { // workaround for https://youtrack.jetbrains.com/issue/IDEA-285839/Classpath-clash-when-using-coroutines-in-an-unbundled-IntelliJ-plugin buildPlugin { exclude { "coroutines" in it.name } - archiveFileName = "valkyrie-$version.zip" + archiveFileName = "flowmvi-$version.zip" } prepareSandbox { exclude { "coroutines" in it.name } } } +// +// configurations.configureEach { +// resolutionStrategy.eachDependency { +// if (requested.group == libs.kotlin.stdlib.get().group) { +// useVersion(libs.versions.kotlin.asProvider().get()) +// } +// } +// } diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt new file mode 100644 index 00000000..ffe01353 --- /dev/null +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt @@ -0,0 +1,52 @@ +package pro.respawn.flowmvi.ideplugin + +import androidx.compose.runtime.Composable +import androidx.compose.ui.awt.ComposePanel +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import pro.respawn.flowmvi.debugger.server.di.koin +import pro.respawn.flowmvi.debugger.server.navigation.AppContent +import pro.respawn.flowmvi.debugger.server.navigation.component.RootComponent + +class PluginToolWindow : + ToolWindowFactory, + DumbAware { + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + System.setProperty("compose.swing.render.on.graphics", "true") + koin.createEagerInstances() + toolWindow.apply { + // setTitleActions(listOf()) + addComposePanel { + val lifecycle = LifecycleRegistry() + + // STOPSHIP: TODO + // LifecycleController() + + val component = RootComponent( + context = DefaultComponentContext( + lifecycle = lifecycle, + ) + ) + AppContent(component) + } + } + } +} + +private fun ToolWindow.addComposePanel( + height: Int = 800, + width: Int = 800, + y: Int = 0, + x: Int = 0, + displayName: String = "", + isLockable: Boolean = true, + content: @Composable ComposePanel.() -> Unit, +) = ComposePanel().apply { + setBounds(x = x, y = y, width = width, height = height) + setContent { content() } +}.also { contentManager.addContent(contentManager.factory.createContent(it, displayName, isLockable)) } diff --git a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml index aba4a3c8..8d4b215f 100644 --- a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml +++ b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml @@ -1,5 +1,14 @@ - - com.intellij.modules.platform - org.jetbrains.compose.intellij.platform + com.intellij.modules.platform + + org.jetbrains.kotlin + + + + + + + + diff --git a/debugger/ideplugin/src/main/resources/META-INF/pluginIcon.svg b/debugger/ideplugin/src/main/resources/META-INF/pluginIcon.svg index e5b665ee..2e6c46c0 100644 --- a/debugger/ideplugin/src/main/resources/META-INF/pluginIcon.svg +++ b/debugger/ideplugin/src/main/resources/META-INF/pluginIcon.svg @@ -1,23 +1,55 @@ - - + + + + + - - - - + + + + + + + + + + diff --git a/debugger/ideplugin/src/main/resources/icon.svg b/debugger/ideplugin/src/main/resources/icon.svg new file mode 100644 index 00000000..93a0eb41 --- /dev/null +++ b/debugger/ideplugin/src/main/resources/icon.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9bf0f2b2..5a422009 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ fragment = "1.8.5" gradleAndroid = "8.7.2" gradleDoctorPlugin = "0.10.0" intellij-ide-plugin = "2.1.0" +intellij-idea = "2024.1" junit = "4.13.2" kotest = "6.0.0.M1" # @pin @@ -50,6 +51,7 @@ kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = kotest-junit = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } kotlin-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomicfu" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-collections" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2a8c659f..b6fdc777 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,13 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - google() + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } gradlePluginPortal() mavenCentral() } @@ -39,4 +45,4 @@ include(":debugger:debugger-client") include(":debugger:debugger-plugin") include(":debugger:server") include(":debugger:debugger-common") -// include(":debugger:ideplugin") +include(":debugger:ideplugin") From a8c85d5430696b1bf07d20e63550cfb2d30ddd98 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 16:08:01 +0100 Subject: [PATCH 13/26] fix: fix debugger server cache folder name --- .../server/arch/configuration/DefaultStoreConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt index b6f32792..ba16fcde 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt @@ -26,7 +26,7 @@ internal class DefaultStoreConfiguration( fileName: String, ) = CompressedFileSaver( // TODO: Abstract away - path = File("states").apply { mkdirs() }.resolve("$fileName.json").absolutePath, + path = File(".cache").apply { mkdirs() }.resolve("$fileName.json.gz").absolutePath, recover = NullRecover ).let { JsonSaver(json, serializer, it) } From 5786d177d490302af86239be3cf1936810aac0d8 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 16:15:47 +0100 Subject: [PATCH 14/26] fix: workarounds for intellij platform bugs --- debugger/ideplugin/build.gradle.kts | 31 +++++++++++------------------ debugger/server/build.gradle.kts | 10 +++++++++- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index 91e7cd76..89d1c7f1 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -1,7 +1,5 @@ @file:Suppress("UnstableApiUsage") -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { kotlin("jvm") alias(libs.plugins.compose) @@ -25,10 +23,6 @@ repositories { } } -configurations.all { - exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") -} - intellijPlatform { projectName = Config.name // needed when plugin provides custom settings exposed to the UI @@ -71,9 +65,21 @@ tasks { } } +// https://youtrack.jetbrains.com/issue/IJPL-1901 +configurations.implementation.configure { + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core-jvm") +} +configurations.api.configure { + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core-jvm") +} + dependencies { compileOnly(compose.desktop.currentOs) compileOnly(libs.kotlin.stdlib) + compileOnly(libs.kotlin.coroutines.core) + + // implementation(libs.kotlin.coroutines.swing) + implementation(compose.desktop.common) implementation(compose.desktop.linux_arm64) implementation(compose.desktop.linux_x64) @@ -98,20 +104,7 @@ dependencies { } tasks { - // workaround for https://youtrack.jetbrains.com/issue/IDEA-285839/Classpath-clash-when-using-coroutines-in-an-unbundled-IntelliJ-plugin buildPlugin { - exclude { "coroutines" in it.name } archiveFileName = "flowmvi-$version.zip" } - prepareSandbox { - exclude { "coroutines" in it.name } - } } -// -// configurations.configureEach { -// resolutionStrategy.eachDependency { -// if (requested.group == libs.kotlin.stdlib.get().group) { -// useVersion(libs.versions.kotlin.asProvider().get()) -// } -// } -// } diff --git a/debugger/server/build.gradle.kts b/debugger/server/build.gradle.kts index ba37f376..038f889d 100644 --- a/debugger/server/build.gradle.kts +++ b/debugger/server/build.gradle.kts @@ -9,8 +9,17 @@ compose.resources { publicResClass = true } +tasks { + withType { + sourceCompatibility = Config.jvmTarget.target + targetCompatibility = Config.jvmTarget.target + } +} kotlin { jvm { + compilerOptions { + jvmTarget = Config.jvmTarget + } } sourceSets { @@ -44,7 +53,6 @@ kotlin { implementation(libs.kotlin.atomicfu) } jvmMain.dependencies { - implementation(libs.kotlin.coroutines.swing) implementation(compose.desktop.common) } } From 68b5e2465f061f4d1e11c900d56cf82d64ba3495 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 16:18:12 +0100 Subject: [PATCH 15/26] feat: set up dynamic color scheme for ide plugin --- .../pro/respawn/flowmvi/debugger/app/Main.kt | 3 +- .../respawn/flowmvi/ideplugin/PluginTheme.kt | 23 ++++++++ .../flowmvi/ideplugin/PluginToolWindow.kt | 23 ++++---- .../ideplugin/rememberIntelliJTheme.kt | 2 + .../debugger/server/navigation/AppContent.kt | 57 +++++++++---------- .../ui/screens/connect/ConnectScreen.kt | 21 ++++--- .../flowmvi/debugger/server/ui/theme/Theme.kt | 35 ++++++------ 7 files changed, 96 insertions(+), 68 deletions(-) create mode 100644 debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt diff --git a/debugger/app/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/app/Main.kt b/debugger/app/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/app/Main.kt index e0a16e5a..daa158fc 100644 --- a/debugger/app/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/app/Main.kt +++ b/debugger/app/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/app/Main.kt @@ -13,6 +13,7 @@ import org.jetbrains.compose.resources.painterResource import pro.respawn.flowmvi.debugger.server.di.koin import pro.respawn.flowmvi.debugger.server.navigation.AppContent import pro.respawn.flowmvi.debugger.server.navigation.component.RootComponent +import pro.respawn.flowmvi.debugger.server.ui.theme.RespawnTheme import pro.respawn.flowmvi.server.generated.resources.icon_nobg_32 import pro.respawn.flowmvi.server.generated.resources.Res as UiR @@ -38,5 +39,5 @@ fun main() = application { icon = painterResource(UiR.drawable.icon_nobg_32), title = "FlowMVI Debugger", state = state, - ) { AppContent(component) } + ) { RespawnTheme { AppContent(component) } } } diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt new file mode 100644 index 00000000..357328e4 --- /dev/null +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt @@ -0,0 +1,23 @@ +package pro.respawn.flowmvi.ideplugin + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import pro.respawn.flowmvi.debugger.server.ui.theme.RespawnTheme +import pro.respawn.flowmvi.debugger.server.ui.theme.rememberColorScheme + +@Composable +fun PluginTheme(content: @Composable () -> Unit) { + val ideTheme = rememberIntelliJTheme() + val colorTheme = rememberColorScheme(dark = ideTheme.isDark) + val colors = remember(ideTheme, colorTheme) { + colorTheme.copy( + primary = ideTheme.primary, + background = ideTheme.background, + onBackground = ideTheme.onBackground + ) + } + RespawnTheme( + colors = colors, + content = content + ) +} diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt index ffe01353..a18875bb 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt @@ -19,20 +19,19 @@ class PluginToolWindow : override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { System.setProperty("compose.swing.render.on.graphics", "true") koin.createEagerInstances() - toolWindow.apply { - // setTitleActions(listOf()) - addComposePanel { - val lifecycle = LifecycleRegistry() + val lifecycle = LifecycleRegistry() - // STOPSHIP: TODO - // LifecycleController() + // STOPSHIP: TODO + // LifecycleController() - val component = RootComponent( - context = DefaultComponentContext( - lifecycle = lifecycle, - ) - ) - AppContent(component) + val component = RootComponent( + context = DefaultComponentContext( + lifecycle = lifecycle, + ) + ) + toolWindow.apply { + addComposePanel { + PluginTheme { AppContent(component) } } } } diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt index b2c65e31..59b0f70f 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt @@ -47,6 +47,8 @@ data class IntelliJTheme( val background: Color, val onBackground: Color, ) { + + val isDark = mode == Mode.DARK enum class Mode { LIGHT, DARK, diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/navigation/AppContent.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/navigation/AppContent.kt index 03479db9..75029f72 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/navigation/AppContent.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/navigation/AppContent.kt @@ -14,7 +14,6 @@ import pro.respawn.flowmvi.debugger.server.di.koin import pro.respawn.flowmvi.debugger.server.navigation.component.RootComponent import pro.respawn.flowmvi.debugger.server.navigation.destination.Destinations import pro.respawn.flowmvi.debugger.server.navigation.util.defaultNavAnimation -import pro.respawn.flowmvi.debugger.server.ui.theme.RespawnTheme import pro.respawn.flowmvi.debugger.server.ui.widgets.DynamicTwoPaneLayout import pro.respawn.kmmutils.compose.windowsize.isWideScreen @@ -22,33 +21,31 @@ import pro.respawn.kmmutils.compose.windowsize.isWideScreen fun AppContent( root: RootComponent ) = KoinContext(koin) { - RespawnTheme { - val navigator = rememberAppNavigator(isWideScreen, root) - val details by root.details.details.subscribeAsState() - DynamicTwoPaneLayout( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), - secondPaneVisible = details.child != null && isWideScreen, - firstPaneContent = { - Children( - stack = root.stack, - animation = defaultNavAnimation(root), - ) { child -> - Destinations( - component = child.instance, - destination = child.configuration, - navigator = navigator, - ) - } - }, - secondaryPaneContent = pane@{ - Crossfade(details.child) { value -> - Destinations( - component = value?.instance ?: return@Crossfade, - destination = value.configuration, - navigator = navigator, - ) - } - }, - ) - } + val navigator = rememberAppNavigator(isWideScreen, root) + val details by root.details.details.subscribeAsState() + DynamicTwoPaneLayout( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), + secondPaneVisible = details.child != null && isWideScreen, + firstPaneContent = { + Children( + stack = root.stack, + animation = defaultNavAnimation(root), + ) { child -> + Destinations( + component = child.instance, + destination = child.configuration, + navigator = navigator, + ) + } + }, + secondaryPaneContent = pane@{ + Crossfade(details.child) { value -> + Destinations( + component = value?.instance ?: return@Crossfade, + destination = value.configuration, + navigator = navigator, + ) + } + }, + ) } diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/screens/connect/ConnectScreen.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/screens/connect/ConnectScreen.kt index 6cd68967..426b5cd2 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/screens/connect/ConnectScreen.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/screens/connect/ConnectScreen.kt @@ -3,13 +3,15 @@ package pro.respawn.flowmvi.debugger.server.ui.screens.connect import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -58,14 +60,17 @@ private fun IntentReceiver.ConnectScreenContent( verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, ) { - Image( - painter = painterResource(Res.drawable.icon_nobg_32), - modifier = Modifier.padding(64.dp).size(120.dp), - contentDescription = null, - ) + Box(Modifier.weight(1f).aspectRatio(1f), contentAlignment = Alignment.Center) { + Image( + painter = painterResource(Res.drawable.icon_nobg_32), + modifier = Modifier.matchParentSize().padding(64.dp).sizeIn(maxWidth = 120.dp, maxHeight = 120.dp), + contentDescription = null, + ) + } RTextInput(host, onTextChange = { intent(HostChanged(it)) }, label = "Host") RTextInput(port, onTextChange = { intent(PortChanged(it)) }, label = "Port") - TextButton(onClick = { intent(StartServerClicked) }, enabled = canStart) { Text("Connect") } + Button(onClick = { intent(StartServerClicked) }, enabled = canStart) { Text("Connect") } + Box(Modifier.weight(0.5f, fill = false)) } } } diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Theme.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Theme.kt index 1ac636ad..1684c227 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Theme.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Theme.kt @@ -4,6 +4,7 @@ package pro.respawn.flowmvi.debugger.server.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Shapes import androidx.compose.material3.darkColorScheme @@ -49,13 +50,13 @@ private val LightColors = lightColorScheme( surfaceTint = md_theme_light_surfaceTint, outlineVariant = md_theme_light_outlineVariant, scrim = md_theme_light_scrim, -// surfaceBright = md_theme_light_surfaceBright, -// surfaceContainer = md_theme_light_surfaceContainer, -// surfaceContainerHigh = md_theme_light_surfaceContainerHigh, -// surfaceContainerHighest = md_theme_light_surfaceContainerHighest, -// surfaceContainerLow = md_theme_light_surfaceContainerLow, -// surfaceContainerLowest = md_theme_light_surfaceContainerLowest, -// surfaceDim = md_theme_light_surfaceDim, + surfaceBright = md_theme_light_surfaceBright, + surfaceContainer = md_theme_light_surfaceContainer, + surfaceContainerHigh = md_theme_light_surfaceContainerHigh, + surfaceContainerHighest = md_theme_light_surfaceContainerHighest, + surfaceContainerLow = md_theme_light_surfaceContainerLow, + surfaceContainerLowest = md_theme_light_surfaceContainerLowest, + surfaceDim = md_theme_light_surfaceDim, ) private val DarkColors = darkColorScheme( @@ -88,27 +89,27 @@ private val DarkColors = darkColorScheme( surfaceTint = md_theme_dark_surfaceTint, outlineVariant = md_theme_dark_outlineVariant, scrim = md_theme_dark_scrim, - // surfaceBright = md_theme_dark_surfaceBright, - // surfaceContainer = md_theme_dark_surfaceContainer, - // surfaceContainerHigh = md_theme_dark_surfaceContainerHigh, - // surfaceContainerHighest = md_theme_dark_surfaceContainerHighest, - // surfaceContainerLow = md_theme_dark_surfaceContainerLow, - // surfaceContainerLowest = md_theme_dark_surfaceContainerLowest, - // surfaceDim = md_theme_dark_surfaceDim, + surfaceBright = md_theme_dark_surfaceBright, + surfaceContainer = md_theme_dark_surfaceContainer, + surfaceContainerHigh = md_theme_dark_surfaceContainerHigh, + surfaceContainerHighest = md_theme_dark_surfaceContainerHighest, + surfaceContainerLow = md_theme_dark_surfaceContainerLow, + surfaceContainerLowest = md_theme_dark_surfaceContainerLowest, + surfaceDim = md_theme_dark_surfaceDim, ) @Composable -internal fun rememberColorScheme(dark: Boolean) = remember(dark) { if (dark) DarkColors else LightColors } +fun rememberColorScheme(dark: Boolean) = remember(dark) { if (dark) DarkColors else LightColors } /** * Respawn branded theme */ @Composable fun RespawnTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), + colors: ColorScheme = rememberColorScheme(dark = isSystemInDarkTheme()), content: @Composable () -> Unit, ) = MaterialTheme( - colorScheme = rememberColorScheme(dark = useDarkTheme), + colorScheme = colors, shapes = shapes, typography = AppTypography, content = content, From cc45cfe2289698711f3dd51bbf7fd5005df84527 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 16:22:34 +0100 Subject: [PATCH 16/26] feat: update theme colors --- .../flowmvi/debugger/server/ui/theme/Color.kt | 86 ++++++++++--------- .../respawn/flowmvi/sample/ui/theme/Color.kt | 86 ++++++++++--------- 2 files changed, 90 insertions(+), 82 deletions(-) diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Color.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Color.kt index 7e6b7554..7e07ae41 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Color.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/theme/Color.kt @@ -70,12 +70,13 @@ val rainbow = listOf( brown ) -internal val md_theme_light_primary = mint_darker +// region light +internal val md_theme_light_primary = Color(0xFF00D46A) internal val md_theme_light_onPrimary = Color(0xFFE2FFEB) internal val md_theme_light_primaryContainer = Color(0xFF51CF79) internal val md_theme_light_onPrimaryContainer = Color(0xFF00210B) -internal val md_theme_light_secondary = Color(0xFF00639A) -internal val md_theme_light_onSecondary = Color(0xFFE6F6FF) +internal val md_theme_light_secondary = Color(0xFF0097E9) +internal val md_theme_light_onSecondary = Color(0xFFCEE5FF) internal val md_theme_light_secondaryContainer = Color(0xFFCEE5FF) internal val md_theme_light_onSecondaryContainer = Color(0xFF001D32) internal val md_theme_light_tertiary = soft_yellow_darker @@ -86,62 +87,65 @@ internal val md_theme_light_error = bright_red internal val md_theme_light_onError = Color(0xFFFFFFFF) internal val md_theme_light_errorContainer = Color(0xFFFFDAD7) internal val md_theme_light_onErrorContainer = Color(0xFF410006) -internal val md_theme_light_background = Color(0xFFF5FFF3) +internal val md_theme_light_background = Color(0xFFFFFFFF) internal val md_theme_light_onBackground = Color(0xFF00210E) -internal val md_theme_light_surface = Color(0xFFF5FFF3) -internal val md_theme_light_surfaceBright = Color(0xFFC3FFB7) -internal val md_theme_light_surfaceDim = Color(0xFFC8D3C7) +internal val md_theme_light_surface = Color(0xFFF7F7F7) +internal val md_theme_light_surfaceBright = Color(0xFFFFFFFF) +internal val md_theme_light_surfaceDim = Color(0xFFF0F0F0) internal val md_theme_light_onSurface = Color(0xFF00210E) -internal val md_theme_light_surfaceVariant = Color(0xFFE3ECDF) +internal val md_theme_light_surfaceVariant = Color(0xFFF6F6F6) internal val md_theme_light_onSurfaceVariant = Color(0xFF414941) internal val md_theme_light_outline = Color(0xFF717970) -internal val md_theme_light_inverseOnSurface = Color(0xFFE0FFE7) +internal val md_theme_light_inverseOnSurface = Color(0xFFE2FFEB) internal val md_theme_light_inverseSurface = Color(0xFF212E27) -internal val md_theme_light_inversePrimary = mint_lighter +internal val md_theme_light_inversePrimary = Color(0xFF00D46A) internal val md_theme_light_shadow = Color(0xFF000000) -internal val md_theme_light_surfaceTint = Color(0xFF006D33) -internal val md_theme_light_outlineVariant = Color(0xFFC1C9BE) +internal val md_theme_light_surfaceTint = Color(0xFF00D46A) +internal val md_theme_light_outlineVariant = Color(0xFFD1D9CF) internal val md_theme_light_scrim = Color(0xFFFFFFFF) -internal val md_theme_light_surfaceContainerLowest = Color(0xFFCBCECA) -internal val md_theme_light_surfaceContainerLow = Color(0xFFCED5CB) -internal val md_theme_light_surfaceContainer = Color(0xFFD7E4D4) -internal val md_theme_light_surfaceContainerHigh = Color(0xFFE2F0DF) -internal val md_theme_light_surfaceContainerHighest = Color(0xFFDDFFD6) +internal val md_theme_light_surfaceContainerLowest = Color(0xFFF6F6F6) +internal val md_theme_light_surfaceContainerLow = Color(0xFFF7F7F7) +internal val md_theme_light_surfaceContainer = Color(0xFFF5F5F5) +internal val md_theme_light_surfaceContainerHigh = Color(0xFFF5F5F5) +internal val md_theme_light_surfaceContainerHighest = Color(0xFFF5F5F5) -internal val md_theme_dark_primary = mint -internal val md_theme_dark_onPrimary = Color(0xFF003918) +//endregion + +// region dark +internal val md_theme_dark_primary = Color(0xFF00D46A) +internal val md_theme_dark_onPrimary = Color(0xFF001D0C) internal val md_theme_dark_primaryContainer = Color(0xFF005225) -internal val md_theme_dark_onPrimaryContainer = Color(0xFF63FF94) -internal val md_theme_dark_secondary = Color(0xFF96CCFF) +internal val md_theme_dark_onPrimaryContainer = Color(0xFFA0FFBE) +internal val md_theme_dark_secondary = Color(0xFF0097E9) internal val md_theme_dark_onSecondary = Color(0xFF003353) internal val md_theme_dark_secondaryContainer = Color(0xFF004A75) internal val md_theme_dark_onSecondaryContainer = Color(0xFFCEE5FF) internal val md_theme_dark_tertiary = soft_yellow -internal val md_theme_dark_onTertiary = Color(0xFF363100) -internal val md_theme_dark_tertiaryContainer = Color(0xFF4F4800) -internal val md_theme_dark_onTertiaryContainer = Color(0xFFF9E534) +internal val md_theme_dark_onTertiary = Color(0xFF1D1B07) +internal val md_theme_dark_tertiaryContainer = Color(0xFF1D1B07) +internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFF38D) internal val md_theme_dark_error = bright_red internal val md_theme_dark_errorContainer = Color(0xFF93000A) internal val md_theme_dark_onError = Color(0xFFFFDCDC) internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -internal val md_theme_dark_background = Color(0xFF0F110F) +internal val md_theme_dark_background = Color(0xFF0D0F0D) internal val md_theme_dark_onBackground = Color(0xFFE2E3DE) internal val md_theme_dark_surface = Color(0xFF161916) internal val md_theme_dark_onSurface = Color(0xFFE2E3DE) -internal val md_theme_dark_surfaceVariant = Color(0xFF202420) -internal val md_theme_dark_onSurfaceVariant = Color(0xFFC1C9BE) -internal val md_theme_dark_outline = Color(0xFF8B9389) -internal val md_theme_dark_inverseOnSurface = Color(0xFF00210E) -internal val md_theme_dark_inverseSurface = Color(0xFF99F7B5) -internal val md_theme_dark_inversePrimary = mint_darker -internal val md_theme_dark_shadow = Color(0xFF000000) -internal val md_theme_dark_surfaceTint = Color(0xFF41E484) -internal val md_theme_dark_outlineVariant = Color(0xFF414941) +internal val md_theme_dark_surfaceVariant = Color(0xFF151714) +internal val md_theme_dark_onSurfaceVariant = Color(0xFF787878) +internal val md_theme_dark_outline = Color(0xFF4E4E4E) +internal val md_theme_dark_inverseOnSurface = Color(0xFF1E2722) +internal val md_theme_dark_inverseSurface = Color(0xFFEDFFF3) +internal val md_theme_dark_inversePrimary = Color(0xFF00D46A) +internal val md_theme_dark_shadow = Color(0xFFFFFFFF) +internal val md_theme_dark_surfaceTint = Color(0xFFD6D6D6) +internal val md_theme_dark_outlineVariant = Color(0xFF434343) internal val md_theme_dark_scrim = Color(0xFF000000) -internal val md_theme_dark_surfaceContainerLowest = Color(0xFF0D0E0D) -internal val md_theme_dark_surfaceContainerLow = Color(0xFF111311) -internal val md_theme_dark_surfaceContainer = Color(0xFF141614) -internal val md_theme_dark_surfaceContainerHigh = Color(0xFF222522) -internal val md_theme_dark_surfaceContainerHighest = Color(0xFF262C26) -internal val md_theme_dark_surfaceBright = Color(0xFF2F3A2F) -internal val md_theme_dark_surfaceDim = Color(0xFF0D0F0D) +internal val md_theme_dark_surfaceContainerLowest = Color(0xFF151714) +internal val md_theme_dark_surfaceContainerLow = Color(0xFF1A1C19) +internal val md_theme_dark_surfaceContainer = Color(0xFF0E100D) +internal val md_theme_dark_surfaceContainerHigh = Color(0xFF0E100D) +internal val md_theme_dark_surfaceContainerHighest = Color(0xFF0E100D) +internal val md_theme_dark_surfaceBright = Color(0xFF000000) +internal val md_theme_dark_surfaceDim = Color(0xFF0F110F) diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/theme/Color.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/theme/Color.kt index 06c92988..110be774 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/theme/Color.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/theme/Color.kt @@ -69,12 +69,13 @@ val rainbow = listOf( brown ) -internal val md_theme_light_primary = mint_darker +// region light +internal val md_theme_light_primary = Color(0xFF00D46A) internal val md_theme_light_onPrimary = Color(0xFFE2FFEB) internal val md_theme_light_primaryContainer = Color(0xFF51CF79) internal val md_theme_light_onPrimaryContainer = Color(0xFF00210B) -internal val md_theme_light_secondary = Color(0xFF00639A) -internal val md_theme_light_onSecondary = Color(0xFFE6F6FF) +internal val md_theme_light_secondary = Color(0xFF0097E9) +internal val md_theme_light_onSecondary = Color(0xFFCEE5FF) internal val md_theme_light_secondaryContainer = Color(0xFFCEE5FF) internal val md_theme_light_onSecondaryContainer = Color(0xFF001D32) internal val md_theme_light_tertiary = soft_yellow_darker @@ -85,62 +86,65 @@ internal val md_theme_light_error = bright_red internal val md_theme_light_onError = Color(0xFFFFFFFF) internal val md_theme_light_errorContainer = Color(0xFFFFDAD7) internal val md_theme_light_onErrorContainer = Color(0xFF410006) -internal val md_theme_light_background = Color(0xFFF0F7EF) +internal val md_theme_light_background = Color(0xFFFFFFFF) internal val md_theme_light_onBackground = Color(0xFF00210E) -internal val md_theme_light_surface = Color(0xFFE8FFE4) -internal val md_theme_light_surfaceBright = Color(0xFFC3FFB7) -internal val md_theme_light_surfaceDim = Color(0xFFC8D3C7) +internal val md_theme_light_surface = Color(0xFFF7F7F7) +internal val md_theme_light_surfaceBright = Color(0xFFFFFFFF) +internal val md_theme_light_surfaceDim = Color(0xFFF0F0F0) internal val md_theme_light_onSurface = Color(0xFF00210E) -internal val md_theme_light_surfaceVariant = Color(0xFFE3ECDF) +internal val md_theme_light_surfaceVariant = Color(0xFFF6F6F6) internal val md_theme_light_onSurfaceVariant = Color(0xFF414941) internal val md_theme_light_outline = Color(0xFF717970) -internal val md_theme_light_inverseOnSurface = Color(0xFFE0FFE7) +internal val md_theme_light_inverseOnSurface = Color(0xFFE2FFEB) internal val md_theme_light_inverseSurface = Color(0xFF212E27) -internal val md_theme_light_inversePrimary = mint_lighter +internal val md_theme_light_inversePrimary = Color(0xFF00D46A) internal val md_theme_light_shadow = Color(0xFF000000) -internal val md_theme_light_surfaceTint = Color(0xFF006D33) -internal val md_theme_light_outlineVariant = Color(0xFFC1C9BE) +internal val md_theme_light_surfaceTint = Color(0xFF00D46A) +internal val md_theme_light_outlineVariant = Color(0xFFD1D9CF) internal val md_theme_light_scrim = Color(0xFFFFFFFF) -internal val md_theme_light_surfaceContainerLowest = Color(0xFFF7FFF5) -internal val md_theme_light_surfaceContainerLow = Color(0xFFEAF5E5) -internal val md_theme_light_surfaceContainer = Color(0xFFE4F3E0) -internal val md_theme_light_surfaceContainerHigh = Color(0xFFD1E2CE) -internal val md_theme_light_surfaceContainerHighest = Color(0xFFD2E7CE) +internal val md_theme_light_surfaceContainerLowest = Color(0xFFF6F6F6) +internal val md_theme_light_surfaceContainerLow = Color(0xFFF7F7F7) +internal val md_theme_light_surfaceContainer = Color(0xFFF5F5F5) +internal val md_theme_light_surfaceContainerHigh = Color(0xFFF5F5F5) +internal val md_theme_light_surfaceContainerHighest = Color(0xFFF5F5F5) -internal val md_theme_dark_primary = mint -internal val md_theme_dark_onPrimary = Color(0xFF003918) +//endregion + +// region dark +internal val md_theme_dark_primary = Color(0xFF00D46A) +internal val md_theme_dark_onPrimary = Color(0xFF001D0C) internal val md_theme_dark_primaryContainer = Color(0xFF005225) -internal val md_theme_dark_onPrimaryContainer = Color(0xFF63FF94) -internal val md_theme_dark_secondary = Color(0xFF96CCFF) +internal val md_theme_dark_onPrimaryContainer = Color(0xFFA0FFBE) +internal val md_theme_dark_secondary = Color(0xFF0097E9) internal val md_theme_dark_onSecondary = Color(0xFF003353) internal val md_theme_dark_secondaryContainer = Color(0xFF004A75) internal val md_theme_dark_onSecondaryContainer = Color(0xFFCEE5FF) internal val md_theme_dark_tertiary = soft_yellow -internal val md_theme_dark_onTertiary = Color(0xFF363100) -internal val md_theme_dark_tertiaryContainer = Color(0xFF4F4800) -internal val md_theme_dark_onTertiaryContainer = Color(0xFFF9E534) +internal val md_theme_dark_onTertiary = Color(0xFF1D1B07) +internal val md_theme_dark_tertiaryContainer = Color(0xFF1D1B07) +internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFF38D) internal val md_theme_dark_error = bright_red internal val md_theme_dark_errorContainer = Color(0xFF93000A) internal val md_theme_dark_onError = Color(0xFFFFDCDC) internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -internal val md_theme_dark_background = Color(0xFF0F110F) +internal val md_theme_dark_background = Color(0xFF0D0F0D) internal val md_theme_dark_onBackground = Color(0xFFE2E3DE) internal val md_theme_dark_surface = Color(0xFF161916) internal val md_theme_dark_onSurface = Color(0xFFE2E3DE) -internal val md_theme_dark_surfaceVariant = Color(0xFF202420) -internal val md_theme_dark_onSurfaceVariant = Color(0xFFC1C9BE) -internal val md_theme_dark_outline = Color(0xFF8B9389) -internal val md_theme_dark_inverseOnSurface = Color(0xFF00210E) -internal val md_theme_dark_inverseSurface = Color(0xFF99F7B5) -internal val md_theme_dark_inversePrimary = mint_darker -internal val md_theme_dark_shadow = Color(0xFF000000) -internal val md_theme_dark_surfaceTint = Color(0xFF41E484) -internal val md_theme_dark_outlineVariant = Color(0xFF414941) +internal val md_theme_dark_surfaceVariant = Color(0xFF151714) +internal val md_theme_dark_onSurfaceVariant = Color(0xFF787878) +internal val md_theme_dark_outline = Color(0xFF4E4E4E) +internal val md_theme_dark_inverseOnSurface = Color(0xFF1E2722) +internal val md_theme_dark_inverseSurface = Color(0xFFEDFFF3) +internal val md_theme_dark_inversePrimary = Color(0xFF00D46A) +internal val md_theme_dark_shadow = Color(0xFFFFFFFF) +internal val md_theme_dark_surfaceTint = Color(0xFFD6D6D6) +internal val md_theme_dark_outlineVariant = Color(0xFF434343) internal val md_theme_dark_scrim = Color(0xFF000000) -internal val md_theme_dark_surfaceContainerLowest = Color(0xFF05B605) -internal val md_theme_dark_surfaceContainerLow = Color(0xFF121412) -internal val md_theme_dark_surfaceContainer = Color(0xFF141614) -internal val md_theme_dark_surfaceContainerHigh = Color(0xFF222522) -internal val md_theme_dark_surfaceContainerHighest = Color(0xFF262C26) -internal val md_theme_dark_surfaceBright = Color(0xFF2F3A2F) -internal val md_theme_dark_surfaceDim = Color(0xFF0D0F0D) +internal val md_theme_dark_surfaceContainerLowest = Color(0xFF151714) +internal val md_theme_dark_surfaceContainerLow = Color(0xFF1A1C19) +internal val md_theme_dark_surfaceContainer = Color(0xFF0E100D) +internal val md_theme_dark_surfaceContainerHigh = Color(0xFF0E100D) +internal val md_theme_dark_surfaceContainerHighest = Color(0xFF0E100D) +internal val md_theme_dark_surfaceBright = Color(0xFF000000) +internal val md_theme_dark_surfaceDim = Color(0xFF0F110F) From 71c616fc35ce9434262baafa81b47279ddd96117 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 17:53:48 +0100 Subject: [PATCH 17/26] feat: implement tool window lifecycle for ide plugin --- debugger/ideplugin/build.gradle.kts | 1 + .../ideplugin/GlobalToolWindowListener.kt | 46 +++++++++++++++++++ .../flowmvi/ideplugin/PluginToolWindow.kt | 15 +++--- .../src/main/resources/META-INF/plugin.xml | 5 ++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index 89d1c7f1..c8267f26 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { implementation(compose.desktop.macos_arm64) implementation(compose.desktop.macos_x64) implementation(compose.desktop.windows_x64) + implementation(compose.material3) implementation(projects.core) implementation(projects.debugger.server) diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt new file mode 100644 index 00000000..5f6d18ab --- /dev/null +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt @@ -0,0 +1,46 @@ +package pro.respawn.flowmvi.ideplugin + +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.create +import com.arkivanov.essenty.lifecycle.destroy +import com.arkivanov.essenty.lifecycle.pause +import com.arkivanov.essenty.lifecycle.resume +import com.arkivanov.essenty.lifecycle.stop +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.ex.ToolWindowManagerListener +import com.intellij.openapi.wm.ex.ToolWindowManagerListener.ToolWindowManagerEventType.ActivateToolWindow +import com.intellij.openapi.wm.ex.ToolWindowManagerListener.ToolWindowManagerEventType.HideToolWindow +import com.intellij.openapi.wm.ex.ToolWindowManagerListener.ToolWindowManagerEventType.RegisterToolWindow +import com.intellij.openapi.wm.ex.ToolWindowManagerListener.ToolWindowManagerEventType.ShowToolWindow +import com.intellij.openapi.wm.ex.ToolWindowManagerListener.ToolWindowManagerEventType.UnregisterToolWindow + +@Suppress("UNUSED_PARAMETER") +class GlobalToolWindowListener(project: Project) : ToolWindowManagerListener { + + override fun stateChanged( + toolWindowManager: ToolWindowManager, + changeType: ToolWindowManagerListener.ToolWindowManagerEventType + ) { + if (PluginToolWindow.Id !in toolWindowManager.toolWindowIds) return + val toolWindow = toolWindowManager.getToolWindow(PluginToolWindow.Id) ?: return + println("Tool window: $changeType, ${toolWindow.id}") + when (changeType) { + RegisterToolWindow -> lifecycle.create() + UnregisterToolWindow -> lifecycle.destroy() + HideToolWindow -> lifecycle.stop() + ActivateToolWindow, ShowToolWindow -> when { + !toolWindow.isVisible -> lifecycle.stop() + toolWindow.isActive -> lifecycle.resume() + !toolWindow.isActive -> lifecycle.pause() + else -> Unit + } + else -> Unit + } + } + + companion object { + + val lifecycle by lazy { LifecycleRegistry() } + } +} diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt index a18875bb..02432300 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginToolWindow.kt @@ -3,7 +3,6 @@ package pro.respawn.flowmvi.ideplugin import androidx.compose.runtime.Composable import androidx.compose.ui.awt.ComposePanel import com.arkivanov.decompose.DefaultComponentContext -import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow @@ -19,22 +18,24 @@ class PluginToolWindow : override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { System.setProperty("compose.swing.render.on.graphics", "true") koin.createEagerInstances() - val lifecycle = LifecycleRegistry() - - // STOPSHIP: TODO - // LifecycleController() val component = RootComponent( context = DefaultComponentContext( - lifecycle = lifecycle, + lifecycle = GlobalToolWindowListener.lifecycle ) ) toolWindow.apply { - addComposePanel { + addComposePanel(displayName = Id) { PluginTheme { AppContent(component) } } } } + + companion object { + + // value derived from the plugin.xml and MUST be kept in sync + const val Id = "FlowMVI" + } } private fun ToolWindow.addComposePanel( diff --git a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml index 8d4b215f..7d269849 100644 --- a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml +++ b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml @@ -11,4 +11,9 @@ + + + + From 4b96a6467d65bdfa9bc963eaedb2d208ed69217f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 17:54:05 +0100 Subject: [PATCH 18/26] feat: use more theme colors from ide for plugin --- .../ideplugin/GlobalToolWindowListener.kt | 1 - .../respawn/flowmvi/ideplugin/PluginTheme.kt | 12 +++++- .../ideplugin/rememberIntelliJTheme.kt | 39 +++++++++++-------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt index 5f6d18ab..6e29d134 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/GlobalToolWindowListener.kt @@ -24,7 +24,6 @@ class GlobalToolWindowListener(project: Project) : ToolWindowManagerListener { ) { if (PluginToolWindow.Id !in toolWindowManager.toolWindowIds) return val toolWindow = toolWindowManager.getToolWindow(PluginToolWindow.Id) ?: return - println("Tool window: $changeType, ${toolWindow.id}") when (changeType) { RegisterToolWindow -> lifecycle.create() UnregisterToolWindow -> lifecycle.destroy() diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt index 357328e4..4d448cd6 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/PluginTheme.kt @@ -13,7 +13,17 @@ fun PluginTheme(content: @Composable () -> Unit) { colorTheme.copy( primary = ideTheme.primary, background = ideTheme.background, - onBackground = ideTheme.onBackground + onBackground = ideTheme.onBackground, + surface = ideTheme.surface, + onSurface = ideTheme.onSurface, + surfaceContainer = ideTheme.surface, + surfaceContainerLowest = ideTheme.surface, + surfaceContainerLow = ideTheme.surface, + surfaceContainerHigh = ideTheme.surface, + surfaceContainerHighest = ideTheme.surface, + surfaceVariant = ideTheme.surface, + onSurfaceVariant = ideTheme.onSurface, + onPrimary = ideTheme.onPrimary, ) } RespawnTheme( diff --git a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt index 59b0f70f..46564b7b 100644 --- a/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt +++ b/debugger/ideplugin/src/main/kotlin/pro/respawn/flowmvi/ideplugin/rememberIntelliJTheme.kt @@ -46,6 +46,9 @@ data class IntelliJTheme( val primary: Color, val background: Color, val onBackground: Color, + val surface: Color, + val onSurface: Color, + val onPrimary: Color, ) { val isDark = mode == Mode.DARK @@ -69,18 +72,15 @@ private class IntelliJThemeExtractorImpl : IntelliJThemeExtractor { theme = buildIntelliJTheme() } - private fun buildIntelliJTheme(): IntelliJTheme { - val theme = getCurrentTheme() - val primary = getColor(PRIMARY) - val background = getColor(BACKGROUND_KEY) - val onBackground = getColor(ON_BACKGROUND_KEY) - return IntelliJTheme( - mode = theme, - primary = primary, - background = background, - onBackground = onBackground, - ) - } + private fun buildIntelliJTheme() = IntelliJTheme( + mode = getCurrentTheme(), + primary = getColor(ColorKey.LinkFg), + onPrimary = getColor(ColorKey.ButtonBg), + background = getColor(ColorKey.PanelBg), + onBackground = getColor(ColorKey.PanelFg), + surface = getColor(ColorKey.EditorBg), + onSurface = getColor(ColorKey.EditorFg), + ) @Suppress("UnstableApiUsage") private fun getCurrentTheme(): IntelliJTheme.Mode { @@ -92,12 +92,19 @@ private class IntelliJThemeExtractorImpl : IntelliJThemeExtractor { } } - private fun getColor(key: String): Color = UIManager.getColor(key).toComposeColor() + private fun getColor(key: ColorKey): Color = requireNotNull(UIManager.getColor(key.key)?.toComposeColor()) { + "Color not found: ${key.key}" + } companion object { - private const val PRIMARY = "Link.activeForeground" - private const val BACKGROUND_KEY = "Panel.background" - private const val ON_BACKGROUND_KEY = "Panel.foreground" + enum class ColorKey(val key: String) { + ButtonBg("Button.background"), + LinkFg("Link.activeForeground"), + PanelBg("Panel.background"), + PanelFg("Panel.foreground"), + EditorBg("EditorPane.background"), + EditorFg("EditorPane.foreground"), + } private fun AwtColor.toComposeColor(): Color = Color(red, green, blue, alpha) } From 17d9633b23242227ae159b4cd42ca0d0b9e9ecfe Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 18:35:25 +0100 Subject: [PATCH 19/26] fix: fix panel icon size --- .../src/main/resources/META-INF/plugin.xml | 2 +- .../src/main/resources/ic_flowmvi_13.svg | 19 +++++++ .../ideplugin/src/main/resources/icon.svg | 52 ------------------- 3 files changed, 20 insertions(+), 53 deletions(-) create mode 100644 debugger/ideplugin/src/main/resources/ic_flowmvi_13.svg delete mode 100644 debugger/ideplugin/src/main/resources/icon.svg diff --git a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml index 7d269849..f17a8eb4 100644 --- a/debugger/ideplugin/src/main/resources/META-INF/plugin.xml +++ b/debugger/ideplugin/src/main/resources/META-INF/plugin.xml @@ -5,7 +5,7 @@ + factoryClass="pro.respawn.flowmvi.ideplugin.PluginToolWindow" icon="/ic_flowmvi_13.svg" /> diff --git a/debugger/ideplugin/src/main/resources/ic_flowmvi_13.svg b/debugger/ideplugin/src/main/resources/ic_flowmvi_13.svg new file mode 100644 index 00000000..d37a7746 --- /dev/null +++ b/debugger/ideplugin/src/main/resources/ic_flowmvi_13.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/debugger/ideplugin/src/main/resources/icon.svg b/debugger/ideplugin/src/main/resources/icon.svg deleted file mode 100644 index 93a0eb41..00000000 --- a/debugger/ideplugin/src/main/resources/icon.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - From 57a737b25c4dbf8db00b7f9f17421ce3e81e4c4e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 19:23:06 +0100 Subject: [PATCH 20/26] fix: don't start server if it is already running --- .../flowmvi/debugger/server/DebugServer.kt | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServer.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServer.kt index a37ffcb9..143bd697 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServer.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServer.kt @@ -34,37 +34,40 @@ internal object DebugServer : Container private var server: EmbeddedServer<*, *>? by atomic(null) private val logger = PlatformStoreLogger - fun start(host: String, port: Int) = embeddedServer(Netty, port = port, host = host) { - configureDebugServer() - // store will be started / closed along with the server - store.start(this) - store.intent(ServerStarted) - routing { - get("/") { call.respondText("FlowMVI Debugger Online", null) } - webSocket("/{id}") { - val storeId = call.parameters.getOrFail("id").asUUID - with(store) { - try { - subscribe { - actions - .filterIsInstance() - .filter { it.client == storeId } - .collect { sendSerialized(it.event) } + fun start(host: String, port: Int) { + if (store.isActive) return + embeddedServer(Netty, port = port, host = host) { + configureDebugServer() + // store will be started / closed along with the server + store.start(this) + store.intent(ServerStarted) + routing { + get("/") { call.respondText("FlowMVI Debugger Online", null) } + webSocket("/{id}") { + val storeId = call.parameters.getOrFail("id").asUUID + with(store) { + try { + subscribe { + actions + .filterIsInstance() + .filter { it.client == storeId } + .collect { sendSerialized(it.event) } + } + while (true) { + val event = receiveDeserialized() + intent(EventReceived(event, storeId)) + } + } finally { + logger(StoreLogLevel.Debug) { "Store $storeId disconnected" } + intent(EventReceived(StoreDisconnected(storeId), storeId)) } - while (true) { - val event = receiveDeserialized() - intent(EventReceived(event, storeId)) - } - } finally { - logger(StoreLogLevel.Debug) { "Store $storeId disconnected" } - intent(EventReceived(StoreDisconnected(storeId), storeId)) } } } } + .also { server = it } + .start() } - .also { server = it } - .start() suspend fun stop() = withContext(Dispatchers.IO) { store.intent(StopRequested) From a3f7018c7861e2d35da0bfbedbcabe043dc2ec77 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 19:23:52 +0100 Subject: [PATCH 21/26] feat: #98 add link to report issues with debugger to GH --- .idea/runConfigurations.xml | 4 +++ .idea/runConfigurations/Plugin___run_ide.xml | 24 ++++++++++++++ .../Plugin___sign__for_release_.xml | 24 ++++++++++++++ .idea/runConfigurations/Plugin___verify.xml | 25 ++++++++++++++ buildSrc/src/main/kotlin/Util.kt | 2 ++ debugger/server/build.gradle.kts | 33 +++++++++++++++++++ .../server/arch/configuration/BuildFlags.kt | 6 ++-- .../DefaultStoreConfiguration.kt | 1 + .../debugger/server/ui/widgets/RErrorView.kt | 19 +++++++++-- 9 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 .idea/runConfigurations/Plugin___run_ide.xml create mode 100644 .idea/runConfigurations/Plugin___sign__for_release_.xml create mode 100644 .idea/runConfigurations/Plugin___verify.xml diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml index 931b96c3..16660f1d 100644 --- a/.idea/runConfigurations.xml +++ b/.idea/runConfigurations.xml @@ -5,8 +5,12 @@ diff --git a/.idea/runConfigurations/Plugin___run_ide.xml b/.idea/runConfigurations/Plugin___run_ide.xml new file mode 100644 index 00000000..66d166dc --- /dev/null +++ b/.idea/runConfigurations/Plugin___run_ide.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Plugin___sign__for_release_.xml b/.idea/runConfigurations/Plugin___sign__for_release_.xml new file mode 100644 index 00000000..2a488000 --- /dev/null +++ b/.idea/runConfigurations/Plugin___sign__for_release_.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Plugin___verify.xml b/.idea/runConfigurations/Plugin___verify.xml new file mode 100644 index 00000000..3d3c618b --- /dev/null +++ b/.idea/runConfigurations/Plugin___verify.xml @@ -0,0 +1,25 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Util.kt b/buildSrc/src/main/kotlin/Util.kt index 63799467..08154cc2 100644 --- a/buildSrc/src/main/kotlin/Util.kt +++ b/buildSrc/src/main/kotlin/Util.kt @@ -71,3 +71,5 @@ fun Config.version(isRelease: Boolean) = buildString { append(versionName) if (!isRelease) append("-SNAPSHOT") } + +fun Project.namespaceByPath() = "${Config.namespace}.${path.replace(":", ".").removePrefix(".")}" diff --git a/debugger/server/build.gradle.kts b/debugger/server/build.gradle.kts index 038f889d..1f08a5ad 100644 --- a/debugger/server/build.gradle.kts +++ b/debugger/server/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + plugins { id(libs.plugins.kotlinMultiplatform.id) alias(libs.plugins.compose) @@ -5,6 +7,32 @@ plugins { alias(libs.plugins.serialization) } +val parentNamespace = namespaceByPath() + +// must be earlier than other config or build tasks +val generateBuildConfig by tasks.registering(Sync::class) { + from( + resources.text.fromString( + """ + package $parentNamespace + + object BuildFlags { + const val VersionCode = ${Config.versionCode} + const val VersionName = "${Config.versionName}" + const val SupportEmail = "${Config.supportEmail}" + const val ProjectUrl = "${Config.url}" + + } + """.trimIndent() + ) + ) { + rename { "BuildFlags.kt" } + into(parentNamespace.replace(".", "/")) + } + // the target directory + into(layout.buildDirectory.dir("generated/kotlin/src/commonMain")) +} + compose.resources { publicResClass = true } @@ -14,15 +42,20 @@ tasks { sourceCompatibility = Config.jvmTarget.target targetCompatibility = Config.jvmTarget.target } + } kotlin { jvm { + @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { jvmTarget = Config.jvmTarget } } sourceSets { + commonMain { + kotlin.srcDir(generateBuildConfig.map { it.destinationDir }) + } commonMain.dependencies { implementation(projects.core) implementation(projects.compose) diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/BuildFlags.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/BuildFlags.kt index 1a64ad97..9da75edc 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/BuildFlags.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/BuildFlags.kt @@ -1,5 +1,5 @@ package pro.respawn.flowmvi.debugger.server.arch.configuration -data object BuildFlags { - val debuggable = System.getenv("DEBUG")?.toBooleanStrictOrNull() ?: false -} +import pro.respawn.flowmvi.debugger.server.BuildFlags + +val BuildFlags.debuggable by lazy { System.getenv("DEBUG")?.toBooleanStrictOrNull() ?: false } diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt index ba16fcde..890982c2 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/arch/configuration/DefaultStoreConfiguration.kt @@ -8,6 +8,7 @@ import pro.respawn.flowmvi.api.ActionShareBehavior import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.debugger.server.BuildFlags import pro.respawn.flowmvi.dsl.StoreBuilder import pro.respawn.flowmvi.plugins.enableLogging import pro.respawn.flowmvi.savedstate.api.NullRecover diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/widgets/RErrorView.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/widgets/RErrorView.kt index 4101dff7..253596e4 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/widgets/RErrorView.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/ui/widgets/RErrorView.kt @@ -7,7 +7,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.sp +import pro.respawn.flowmvi.debugger.server.BuildFlags +import pro.respawn.kmmutils.compose.annotate @Composable fun RErrorView( @@ -20,8 +26,15 @@ fun RErrorView( ) { Text("An error has occurred", fontSize = 32.sp) SelectionContainer { - Text("Message: ${e.message}") - Text("stack trace: ${e.stackTraceToString()}") - // TODO: Report to github link + Column { + Text("Message: ${e.message}") + Text("stack trace: ${e.stackTraceToString()}", fontFamily = FontFamily.Monospace) + Text( + textDecoration = TextDecoration.Underline, + text = "Please report this to Github".annotate { + withLink(LinkAnnotation.Url(BuildFlags.ProjectUrl)) { append(it) } + } + ) + } } } From a61c124bfc8765f94c7a709cfc0add4ee2f989a0 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 19:37:13 +0100 Subject: [PATCH 22/26] chore: add code documentation for new APIs --- .../pro/respawn/flowmvi/api/ImmutableStore.kt | 14 ++++++++++++- .../api/lifecycle/ImmutableStoreLifecycle.kt | 9 ++++++++ .../respawn/flowmvi/modules/PipelineModule.kt | 10 ++++----- .../flowmvi/plugins/AsyncCachePlugin.kt | 21 +++++++++++++++++++ .../respawn/flowmvi/plugins/DeinitPlugin.kt | 2 +- .../flowmvi/test/store/StoreLaunchTest.kt | 1 - debugger/server/build.gradle.kts | 1 - .../test/plugin/TestPipelineContext.kt | 3 --- 8 files changed, 49 insertions(+), 12 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt index a762b3ea..c3f7a321 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import pro.respawn.flowmvi.api.lifecycle.ImmutableStoreLifecycle +import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle /** * A [Store] that does not allow sending intents. @@ -20,10 +21,21 @@ public interface ImmutableStore T.launchPipe private val pipelineName = CoroutineName(toString()) override val coroutineContext = parent.coroutineContext + - config.coroutineContext + - pipelineName + - job + - handler + - this + config.coroutineContext + + pipelineName + + job + + handler + + this override fun toString(): String = "${config.name.orEmpty()}PipelineContext" diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt index a77155d1..54ee6c0d 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AsyncCachePlugin.kt @@ -12,8 +12,19 @@ import pro.respawn.flowmvi.dsl.StoreBuilder import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +/** + * Alias for [Deferred.await] + */ public suspend operator fun Deferred.invoke(): T = await() +/** + * Create a new [CachedValue] but run the [init] in an asynchronous way and return a [Deferred] that can be used + * to await the value + * @return A [CachedValue] granting access to the value returned from [init] + * @see cached + * @see cachePlugin + * @see Deferred + */ @FlowMVIDSL public inline fun asyncCached( context: CoroutineContext = EmptyCoroutineContext, @@ -21,6 +32,16 @@ public inline fun asyncCached( crossinline init: suspend PipelineContext.() -> T, ): CachedValue, S, I, A> = cached { async(context, start) { init() } } +/** + * Install a new [cachePlugin] that will run the [init] in an asynchronous way and return a [Deferred] that can be used + * to await the value. + * + * @return A [CachedValue] granting access to the value returned from [init] + * @see cached + * @see cachePlugin + * @see asyncCached + * @see Deferred + */ @FlowMVIDSL public inline fun StoreBuilder.asyncCache( context: CoroutineContext = EmptyCoroutineContext, diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt index b8acdeaf..59ba7030 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/DeinitPlugin.kt @@ -4,9 +4,9 @@ import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.StoreBuilder import pro.respawn.flowmvi.dsl.plugin -import pro.respawn.flowmvi.api.StorePlugin /** * Alias for [StorePlugin.onStop] callback or `plugin { onStop { block() } }` diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt index 8d5d3f49..f55682ce 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreLaunchTest.kt @@ -7,7 +7,6 @@ import io.kotest.matchers.collections.shouldBeSingleton import io.kotest.matchers.shouldBe import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withTimeout diff --git a/debugger/server/build.gradle.kts b/debugger/server/build.gradle.kts index 1f08a5ad..cb28e9ac 100644 --- a/debugger/server/build.gradle.kts +++ b/debugger/server/build.gradle.kts @@ -42,7 +42,6 @@ tasks { sourceCompatibility = Config.jvmTarget.target targetCompatibility = Config.jvmTarget.target } - } kotlin { jvm { diff --git a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt index 4f727346..7dcc862d 100644 --- a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt +++ b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt @@ -4,9 +4,6 @@ package pro.respawn.flowmvi.test.plugin import kotlinx.atomicfu.atomic import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.job import kotlinx.coroutines.launch import pro.respawn.flowmvi.api.DelicateStoreApi import pro.respawn.flowmvi.api.ExperimentalStoreApi From 5a84103ea42b1fe24d01db30890e0e70623ae81e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 19:44:00 +0100 Subject: [PATCH 23/26] chore: fix code review --- .../commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt | 2 +- .../kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt | 8 ++++---- gradle/libs.versions.toml | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt index 06df3d12..ad936765 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/StoreImpl.kt @@ -52,7 +52,7 @@ internal class StoreImpl( override fun start(scope: CoroutineScope) = launchPipeline( parent = scope, - config = config, + storeConfig = config, onAction = { action -> onAction(action)?.let { this@StoreImpl.action(it) } }, onTransformState = { transform -> this@StoreImpl.updateState { onState(this, transform()) ?: this } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt index 49f89276..ad809c94 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt @@ -32,7 +32,7 @@ import pro.respawn.flowmvi.api.lifecycle.StoreLifecycle @OptIn(DelicateStoreApi::class) internal inline fun T.launchPipeline( parent: CoroutineScope, - config: StoreConfiguration, + storeConfig: StoreConfiguration, crossinline onStop: (e: Exception?) -> Unit, crossinline onAction: suspend PipelineContext.(action: A) -> Unit, crossinline onTransformState: suspend PipelineContext.(transform: suspend S.() -> S) -> Unit, @@ -54,19 +54,19 @@ internal inline fun T.launchPipe StoreLifecycleModule by storeLifecycle(job), ActionReceiver { - override val config get() = config + override val config = storeConfig override val key = PipelineContext // recoverable should be separate from this key private val handler = PipelineExceptionHandler() private val pipelineName = CoroutineName(toString()) override val coroutineContext = parent.coroutineContext + - config.coroutineContext + + storeConfig.coroutineContext + pipelineName + job + handler + this - override fun toString(): String = "${config.name.orEmpty()}PipelineContext" + override fun toString(): String = "${storeConfig.name.orEmpty()}PipelineContext" override suspend fun updateState(transform: suspend S.() -> S) = catch { onTransformState(transform) } override suspend fun action(action: A) = catch { onAction(action) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a422009..e3ada66a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,9 @@ kotlin-io = "0.5.4" kotlinx-atomicfu = "0.26.0" ktor = "3.0.1" lifecycle = "2.8.3" -androidx-lifecycle = "2.8.7" +# @pin +#noinspection GradleDependency - must match jb-lifecycle +androidx-lifecycle = "2.8.3" maven-publish-plugin = "0.30.0" serialization = "1.7.3" turbine = "1.2.0" From 41f4939a533d12548e3ba1fae7491c6c4d114e2e Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Tue, 5 Nov 2024 19:57:29 +0100 Subject: [PATCH 24/26] fix: implement expect-actuals for wasmWasi in :core --- .../kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt | 2 +- .../respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt | 6 ++++++ .../pro/respawn/flowmvi/util/ConcurrentHashMap.wasmWasi.kt | 3 +++ .../respawn/flowmvi/util/SynchronizedLinkedSet.wasmWasi.kt | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt create mode 100644 core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/ConcurrentHashMap.wasmWasi.kt create mode 100644 core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/SynchronizedLinkedSet.wasmWasi.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt index ad809c94..eea0033d 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/PipelineModule.kt @@ -60,7 +60,7 @@ internal inline fun T.launchPipe private val pipelineName = CoroutineName(toString()) override val coroutineContext = parent.coroutineContext + - storeConfig.coroutineContext + + storeConfig.coroutineContext + pipelineName + job + handler + diff --git a/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt new file mode 100644 index 00000000..a4044ec8 --- /dev/null +++ b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt @@ -0,0 +1,6 @@ +package pro.respawn.flowmvi.logging + +/** + * A [StoreLogger] instance for each supported platform + */ +public actual val PlatformStoreLogger: StoreLogger get() = ConsoleStoreLogger diff --git a/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/ConcurrentHashMap.wasmWasi.kt b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/ConcurrentHashMap.wasmWasi.kt new file mode 100644 index 00000000..1d943e8c --- /dev/null +++ b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/ConcurrentHashMap.wasmWasi.kt @@ -0,0 +1,3 @@ +package pro.respawn.flowmvi.util + +internal actual fun concurrentMutableMap(): MutableMap = SynchronizedHashMap() diff --git a/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/SynchronizedLinkedSet.wasmWasi.kt b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/SynchronizedLinkedSet.wasmWasi.kt new file mode 100644 index 00000000..7625232f --- /dev/null +++ b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/util/SynchronizedLinkedSet.wasmWasi.kt @@ -0,0 +1,3 @@ +package pro.respawn.flowmvi.util + +internal actual fun concurrentLinkedSet(): MutableSet = SynchronizedLinkedSet() From 4e61619a87e0a48a9e1cc0f7780491024f46c07f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Wed, 6 Nov 2024 11:31:19 +0100 Subject: [PATCH 25/26] feat: make android module multiplatform (however much that makes sense) --- android/build.gradle.kts | 38 +++++++++++++++---- .../flowmvi/android/view/SubscribeDsl.kt | 0 .../respawn/flowmvi/android/StoreViewModel.kt | 0 .../respawn/flowmvi/android/SubscribeDsl.kt | 1 + build.gradle.kts | 9 +++++ 5 files changed, 40 insertions(+), 8 deletions(-) rename android/src/{main => androidMain}/kotlin/pro/respawn/flowmvi/android/view/SubscribeDsl.kt (100%) rename android/src/{main => commonMain}/kotlin/pro/respawn/flowmvi/android/StoreViewModel.kt (100%) rename android/src/{main => commonMain}/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt (99%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 3a289883..80c5c53c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,18 +1,40 @@ plugins { - id("pro.respawn.android-library") + id(libs.plugins.kotlinMultiplatform.id) + id(libs.plugins.androidLibrary.id) alias(libs.plugins.maven.publish) dokkaDocumentation } +kotlin { + configureMultiplatform( + ext = this, + jvm = true, + android = true, + iOs = true, + macOs = true, + watchOs = false, + tvOs = false, + windows = false, + linux = true, + js = true, + wasmJs = true, + wasmWasi = false, + ) + + sourceSets.androidMain.dependencies { + api(libs.kotlin.coroutines.android) + api(libs.androidx.fragment) + api(libs.androidx.activity) + } +} + android { - namespace = "${Config.artifactId}.android" + configureAndroidLibrary(this) + namespace = "${Config.namespace}.android" } dependencies { - api(projects.core) - // api(libs.lifecycle.runtime) - // api(libs.lifecycle.viewmodel) - api(libs.kotlin.coroutines.android) - implementation(libs.androidx.fragment) - implementation(libs.androidx.activity) + commonMainApi(projects.core) + commonMainApi(libs.lifecycle.runtime) + commonMainApi(libs.lifecycle.viewmodel) } diff --git a/android/src/main/kotlin/pro/respawn/flowmvi/android/view/SubscribeDsl.kt b/android/src/androidMain/kotlin/pro/respawn/flowmvi/android/view/SubscribeDsl.kt similarity index 100% rename from android/src/main/kotlin/pro/respawn/flowmvi/android/view/SubscribeDsl.kt rename to android/src/androidMain/kotlin/pro/respawn/flowmvi/android/view/SubscribeDsl.kt diff --git a/android/src/main/kotlin/pro/respawn/flowmvi/android/StoreViewModel.kt b/android/src/commonMain/kotlin/pro/respawn/flowmvi/android/StoreViewModel.kt similarity index 100% rename from android/src/main/kotlin/pro/respawn/flowmvi/android/StoreViewModel.kt rename to android/src/commonMain/kotlin/pro/respawn/flowmvi/android/StoreViewModel.kt diff --git a/android/src/main/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt b/android/src/commonMain/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt similarity index 99% rename from android/src/main/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt rename to android/src/commonMain/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt index e2f332e8..11c08fdb 100644 --- a/android/src/main/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt +++ b/android/src/commonMain/kotlin/pro/respawn/flowmvi/android/SubscribeDsl.kt @@ -15,6 +15,7 @@ import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StateConsumer import pro.respawn.flowmvi.api.Store import pro.respawn.flowmvi.dsl.subscribe +import kotlin.jvm.JvmName /** * Subscribe to the [store] lifecycle-aware. diff --git a/build.gradle.kts b/build.gradle.kts index a76212b8..f3f42ae4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.MavenPublishBaseExtension import com.vanniktech.maven.publish.SonatypeHost import nl.littlerobots.vcu.plugin.versionCatalogUpdate @@ -45,6 +47,13 @@ subprojects { afterEvaluate { extensions.findByType()?.run { val isReleaseBuild = properties["release"]?.toString().toBoolean() + configure( + KotlinMultiplatform( + javadocJar = JavadocJar.Empty(), + sourcesJar = true, + androidVariantsToPublish = listOf("release"), + ) + ) publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, false) if (isReleaseBuild) signAllPublications() coordinates(Config.artifactId, name, Config.version(isReleaseBuild)) From 9496555df9eeff5bdac354f1c92283c6db6bd10d Mon Sep 17 00:00:00 2001 From: "Nek.12" Date: Wed, 6 Nov 2024 14:50:18 +0300 Subject: [PATCH 26/26] Update core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt index a4044ec8..ca53a443 100644 --- a/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt +++ b/core/src/wasmWasiMain/kotlin/pro/respawn/flowmvi/logging/PlatformStoreLogger.wasmWasi.kt @@ -1,6 +1,9 @@ package pro.respawn.flowmvi.logging /** - * A [StoreLogger] instance for each supported platform + * A [StoreLogger] instance for the WASM/WASI platform. + * + * Uses [ConsoleStoreLogger] to output logs to the JavaScript console + * when running in a browser environment. */ public actual val PlatformStoreLogger: StoreLogger get() = ConsoleStoreLogger