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",