diff --git a/.github/workflows/packages_push.yml b/.github/workflows/packages_push.yml
index f42c2d5a8e0..9fb2a736b8e 100644
--- a/.github/workflows/packages_push.yml
+++ b/.github/workflows/packages_push.yml
@@ -22,7 +22,7 @@ jobs:
run: ./gradlew build
- name: Publish
continue-on-error: true
- run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
+ run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25e40580650..1a8003d5c9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
# Changelog
+## 0.9.12
+
+* `Common`:
+ * `JS`:
+ * New function `openLink`
+ * New function `selectFile`
+ * New function `triggerDownloadFile`
+ * `Compose`:
+ * Created :)
+ * `Common`:
+ * `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
+ * `JS`:
+ * `openLink` on top of `openLink` with `String` target from common
+* `Coroutines`:
+ * `Compose`:
+ * `Common`:
+ * New extension `Flow.toMutableState`
+ * New extension `StateFlow.toMutableState`
+ * `JS`:
+ * New function `selectFileOrThrow` on top of `selectFile` from `common`
+ * New function `selectFileOrNull` on top of `selectFile` from `common`
+
## 0.9.11
* `Versions`:
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..c2af68ad32d
--- /dev/null
+++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/SelectFile.kt
@@ -0,0 +1,30 @@
+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 {
+ type = "file"
+ onchange = {
+ runCatching {
+ files ?.get(0) ?: error("File must not be null")
+ }.onSuccess {
+ onFile(it)
+ }.onFailure {
+ onFailure(it)
+ }
+ }
+ inputSetup(this)
+ }
+ } 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..2e2ab2f95a9
--- /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 selectFileOrThrow(
+ inputSetup: (HTMLInputElement) -> Unit = {}
+): MPPFile {
+ val result = CompletableDeferred()
+
+ selectFile(
+ inputSetup,
+ {
+ result.completeExceptionally(it)
+ }
+ ) {
+ result.complete(it)
+ }
+
+ return result.await()
+}
+
+suspend fun selectFileOrNull(
+ 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/gradle.properties b/gradle.properties
index bf5149f9b48..624e13335ff 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
-version=0.9.11
-android_code_version=101
+version=0.9.12
+android_code_version=102
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",