diff --git a/common/compose/build.gradle b/common/compose/build.gradle
new file mode 100644
index 00000000000..0b3570ad169
--- /dev/null
+++ b/common/compose/build.gradle
@@ -0,0 +1,18 @@
+plugins {
+ id "org.jetbrains.kotlin.multiplatform"
+ id "org.jetbrains.kotlin.plugin.serialization"
+ id "com.android.library"
+ alias(libs.plugins.jb.compose)
+}
+
+apply from: "$mppProjectWithSerializationAndComposePresetPath"
+
+kotlin {
+ sourceSets {
+ commonMain {
+ dependencies {
+ api project(":micro_utils.common")
+ }
+ }
+ }
+}
diff --git a/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/DefaultDisposableEffectResult.kt b/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/DefaultDisposableEffectResult.kt
new file mode 100644
index 00000000000..82cf96bff4b
--- /dev/null
+++ b/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/DefaultDisposableEffectResult.kt
@@ -0,0 +1,16 @@
+package dev.inmo.micro_utils.common.compose
+
+import androidx.compose.runtime.DisposableEffectResult
+
+class DefaultDisposableEffectResult(
+ private val onDispose: () -> Unit
+) : DisposableEffectResult {
+ override fun dispose() {
+ onDispose()
+ }
+
+ companion object {
+ val DoNothing = DefaultDisposableEffectResult {}
+ }
+}
+
diff --git a/common/compose/src/jsMain/kotlin/dev/inmo/micro_utils/common/compose/OpenLink.kt b/common/compose/src/jsMain/kotlin/dev/inmo/micro_utils/common/compose/OpenLink.kt
new file mode 100644
index 00000000000..41ed545c41d
--- /dev/null
+++ b/common/compose/src/jsMain/kotlin/dev/inmo/micro_utils/common/compose/OpenLink.kt
@@ -0,0 +1,10 @@
+package dev.inmo.micro_utils.common.compose
+
+import org.jetbrains.compose.web.attributes.ATarget
+
+fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
+ link,
+ mode.targetStr,
+ features
+)
+
diff --git a/common/compose/src/main/AndroidManifest.xml b/common/compose/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..65adc963cce
--- /dev/null
+++ b/common/compose/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/OpenLink.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/OpenLink.kt
new file mode 100644
index 00000000000..24315e95559
--- /dev/null
+++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/OpenLink.kt
@@ -0,0 +1,8 @@
+package dev.inmo.micro_utils.common
+
+import kotlinx.browser.window
+
+fun openLink(link: String, target: String = "_blank", features: String = "") {
+ window.open(link, target, features) ?.focus()
+}
+
diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/SelectFile.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/SelectFile.kt
new file mode 100644
index 00000000000..89c7bc105dd
--- /dev/null
+++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/SelectFile.kt
@@ -0,0 +1,29 @@
+package dev.inmo.micro_utils.common
+
+import kotlinx.browser.document
+import kotlinx.dom.createElement
+import org.w3c.dom.HTMLElement
+import org.w3c.dom.HTMLInputElement
+import org.w3c.files.get
+
+fun selectFile(
+ inputSetup: HTMLInputElement.() -> Unit = {},
+ onFailure: (Throwable) -> Unit = {},
+ onFile: (MPPFile) -> Unit
+) {
+ (document.createElement("input") {
+ (this as HTMLInputElement).apply {
+ inputSetup()
+ onchange = {
+ runCatching {
+ files ?.get(0) ?: error("File must not be null")
+ }.onSuccess {
+ onFile(it)
+ }.onFailure {
+ onFailure(it)
+ }
+ }
+ }
+ } as HTMLElement).click()
+}
+
diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/TriggerDownload.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/TriggerDownload.kt
new file mode 100644
index 00000000000..369050cf196
--- /dev/null
+++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/TriggerDownload.kt
@@ -0,0 +1,14 @@
+package dev.inmo.micro_utils.common
+
+import kotlinx.browser.document
+import org.w3c.dom.HTMLAnchorElement
+
+fun triggerDownloadFile(filename: String, fileLink: String) {
+ val hiddenElement = document.createElement("a") as HTMLAnchorElement
+
+ hiddenElement.href = fileLink
+ hiddenElement.target = "_blank"
+ hiddenElement.download = filename
+ hiddenElement.click()
+}
+
diff --git a/coroutines/build.gradle b/coroutines/build.gradle
index c614db7e76c..968d31e1521 100644
--- a/coroutines/build.gradle
+++ b/coroutines/build.gradle
@@ -13,6 +13,11 @@ kotlin {
api libs.kt.coroutines
}
}
+ jsMain {
+ dependencies {
+ api project(":micro_utils.common")
+ }
+ }
androidMain {
dependencies {
api libs.kt.coroutines.android
diff --git a/coroutines/compose/build.gradle b/coroutines/compose/build.gradle
index ca8aa5b5fce..c71171b88f3 100644
--- a/coroutines/compose/build.gradle
+++ b/coroutines/compose/build.gradle
@@ -12,6 +12,7 @@ kotlin {
commonMain {
dependencies {
api libs.kt.coroutines
+ api project(":micro_utils.coroutines")
}
}
}
diff --git a/coroutines/compose/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/compose/FlowToState.kt b/coroutines/compose/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/compose/FlowToState.kt
new file mode 100644
index 00000000000..22c1b7be83a
--- /dev/null
+++ b/coroutines/compose/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/compose/FlowToState.kt
@@ -0,0 +1,22 @@
+package dev.inmo.micro_utils.coroutines.compose
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+fun Flow.toMutableState(
+ initial: T,
+ scope: CoroutineScope
+): MutableState {
+ val state = mutableStateOf(initial)
+ subscribeSafelyWithoutExceptions(scope) { state.value = it }
+ return state
+}
+
+inline fun StateFlow.toMutableState(
+ scope: CoroutineScope
+): MutableState = toMutableState(value, scope)
+
diff --git a/coroutines/src/jsMain/kotlin/dev.inmo.micro_utils.coroutines/SelectFile.kt b/coroutines/src/jsMain/kotlin/dev.inmo.micro_utils.coroutines/SelectFile.kt
new file mode 100644
index 00000000000..525a51b83df
--- /dev/null
+++ b/coroutines/src/jsMain/kotlin/dev.inmo.micro_utils.coroutines/SelectFile.kt
@@ -0,0 +1,42 @@
+package dev.inmo.micro_utils.coroutines
+
+import dev.inmo.micro_utils.common.MPPFile
+import dev.inmo.micro_utils.common.selectFile
+import kotlinx.coroutines.CompletableDeferred
+import org.w3c.dom.HTMLInputElement
+
+suspend fun selectFile(
+ inputSetup: HTMLInputElement.() -> Unit = {}
+): MPPFile {
+ val result = CompletableDeferred()
+
+ selectFile(
+ inputSetup,
+ {
+ result.completeExceptionally(it)
+ }
+ ) {
+ result.complete(it)
+ }
+
+ return result.await()
+}
+
+suspend fun selectOptionalFile(
+ inputSetup: HTMLInputElement.() -> Unit = {},
+ onFailure: (Throwable) -> Unit = {}
+): MPPFile? {
+ val result = CompletableDeferred()
+
+ selectFile(
+ inputSetup,
+ {
+ result.complete(null)
+ onFailure(it)
+ }
+ ) {
+ result.complete(it)
+ }
+
+ return result.await()
+}
diff --git a/settings.gradle b/settings.gradle
index d22455ff9dd..d7471ac4ccc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -2,6 +2,7 @@ rootProject.name='micro_utils'
String[] includes = [
":common",
+ ":common:compose",
":matrix",
":crypto",
":selector:common",