mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
bad9a53fdb | |||
0bce7bd60a | |||
2f70a1cfb4 | |||
bfb6e738ee | |||
c7ad9aae07 | |||
fecd719239 | |||
18d6ac31b5 | |||
d8dbebfc7e | |||
16463d0eb9 | |||
837cac644d | |||
e83e0a8535 | |||
2e309c31a6 | |||
625db02651 |
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 0.22.5
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.7.0-beta02` -> `1.7.0-rc01`
|
||||
* `SQLite`: `3.46.1.2` -> `3.46.1.3`
|
||||
* `AndroidXFragment`: `1.8.3` -> `1.8.4`
|
||||
* `Common`:
|
||||
* Add extension `withReplacedAt`/`withReplaced` ([#489](https://github.com/InsanusMokrassar/MicroUtils/issues/489))
|
||||
* `Coroutines`:
|
||||
* Add extension `Flow.debouncedBy`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Add `KtorApplicationConfigurator.Routing.Static` as solution for [#488](https://github.com/InsanusMokrassar/MicroUtils/issues/488)
|
||||
|
||||
## 0.22.4
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.54.0` -> `0.55.0`
|
||||
* `SQLite`: `3.46.1.0` -> `3.46.1.2`
|
||||
|
||||
## 0.22.3
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.7.2` -> `1.7.3`
|
||||
* `Coroutines`: `1.8.1` -> `1.9.0`
|
||||
* `Compose`: `1.7.0-alpha03` -> `1.7.0-beta02`
|
||||
* `Koin`: `3.5.6` -> `4.0.0`
|
||||
* `Okio`: `3.9.0` -> `3.9.1`
|
||||
* `AndroidFragment`: `1.8.2` -> `1.8.3`
|
||||
* `androidx.compose.material3:material3` has been replaced with `org.jetbrains.compose.material3:material3`
|
||||
* `Common`:
|
||||
* `JS`:
|
||||
* Add several useful extensions
|
||||
* `Compose`:
|
||||
* `JS`:
|
||||
* Add several useful extensions
|
||||
|
||||
## 0.22.2
|
||||
|
||||
* `Versions`:
|
||||
|
@@ -12,7 +12,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.compose.material3
|
||||
api libs.jb.compose.material3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.w3c.dom.Element
|
||||
|
||||
operator fun <T : Element> AttrBuilderContext<T>?.plus(
|
||||
other: AttrBuilderContext<T>?
|
||||
) = when (this) {
|
||||
null -> other ?: {}
|
||||
else -> when (other) {
|
||||
null -> this ?: {}
|
||||
else -> {
|
||||
{
|
||||
invoke(this)
|
||||
other(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
import androidx.compose.runtime.DisposableEffectScope
|
||||
import org.jetbrains.compose.web.attributes.AttrsScope
|
||||
import org.jetbrains.compose.web.dom.ElementScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
/**
|
||||
* This function must be called in the context of your tag content. It works like default [AttrsScope.ref],
|
||||
* but able to be used several times. Uses [DisposableEffect] under the hood
|
||||
*/
|
||||
@Composable
|
||||
fun <T : Element> ElementScope<T>.ref(
|
||||
block: DisposableEffectScope.(T) -> DisposableEffectResult
|
||||
) {
|
||||
DisposableEffect(0) {
|
||||
block(scopeElement)
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
|
||||
fun tagClasses(vararg classnames: String): AttrBuilderContext<*> = {
|
||||
classes(*classnames)
|
||||
}
|
||||
|
||||
fun tagId(id: String): AttrBuilderContext<*> = {
|
||||
id(id)
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
|
||||
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)
|
||||
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WithReplacedTest {
|
||||
@Test
|
||||
fun testReplaced() {
|
||||
val data = 0 until 10
|
||||
val testData = Int.MAX_VALUE
|
||||
|
||||
for (i in 0 until data.last) {
|
||||
val withReplaced = data.withReplacedAt(i) {
|
||||
testData
|
||||
}
|
||||
val dataAsMutableList = data.toMutableList()
|
||||
dataAsMutableList[i] = testData
|
||||
assertEquals(withReplaced, dataAsMutableList.toList())
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun copyToClipboard(text: String): Boolean {
|
||||
return runCatching {
|
||||
window.navigator.clipboard.writeText(
|
||||
text
|
||||
)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.isSuccess
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.files.Blob
|
||||
import org.w3c.files.BlobPropertyBag
|
||||
import kotlin.js.json
|
||||
|
||||
external class ClipboardItem(data: dynamic)
|
||||
|
||||
inline fun Blob.convertToClipboardItem(): ClipboardItem {
|
||||
val itemData: dynamic = json(this.type to this)
|
||||
return ClipboardItem(itemData)
|
||||
}
|
||||
|
||||
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
|
||||
return runCatching {
|
||||
val response = window.fetch(imageUrl).await()
|
||||
val blob = response.blob().await()
|
||||
val data = arrayOf(
|
||||
Blob(
|
||||
arrayOf(blob),
|
||||
BlobPropertyBag("image/png")
|
||||
).convertToClipboardItem()
|
||||
).asDynamic()
|
||||
window.navigator.clipboard.write(data)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.isSuccess
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlin.time.Duration
|
||||
|
||||
@JvmInline
|
||||
private value class DebouncedByData<T>(
|
||||
val millisToData: Pair<Long, T>
|
||||
)
|
||||
|
||||
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
|
||||
val jobs = mutableMapOf<Any?, Job>()
|
||||
val mutex = Mutex()
|
||||
subscribe(this) {
|
||||
mutex.withLock {
|
||||
val marker = markerFactory(it)
|
||||
lateinit var job: Job
|
||||
job = async {
|
||||
delay(timeout(it))
|
||||
mutex.withLock {
|
||||
if (jobs[marker] === job) {
|
||||
this@channelFlow.send(it)
|
||||
jobs.remove(marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
jobs[marker] ?.cancel()
|
||||
jobs[marker] = job
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
|
||||
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)
|
42
coroutines/src/commonTest/kotlin/DebouncedByTests.kt
Normal file
42
coroutines/src/commonTest/kotlin/DebouncedByTests.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
import dev.inmo.micro_utils.coroutines.debouncedBy
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DebouncedByTests {
|
||||
@Test
|
||||
fun testThatParallelDebouncingWorksCorrectly() = runTest {
|
||||
val dataToMarkerFactories = listOf(
|
||||
1 to 0,
|
||||
2 to 1,
|
||||
3 to 2,
|
||||
4 to 0,
|
||||
5 to 1,
|
||||
6 to 2,
|
||||
7 to 0,
|
||||
8 to 1,
|
||||
9 to 2,
|
||||
)
|
||||
|
||||
val collected = mutableListOf<Int>()
|
||||
|
||||
dataToMarkerFactories.asFlow().debouncedBy(10L) {
|
||||
it.second
|
||||
}.collect {
|
||||
when (it.second) {
|
||||
0 -> assertEquals(7, it.first)
|
||||
1 -> assertEquals(8, it.first)
|
||||
2 -> assertEquals(9, it.first)
|
||||
else -> error("wtf")
|
||||
}
|
||||
collected.add(it.first)
|
||||
}
|
||||
|
||||
val expectedList = listOf(7, 8, 9)
|
||||
assertEquals(expectedList, collected)
|
||||
assertTrue { collected.containsAll(expectedList) }
|
||||
assertTrue { expectedList.containsAll(collected) }
|
||||
}
|
||||
}
|
@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.22.2
|
||||
android_code_version=268
|
||||
version=0.22.5
|
||||
android_code_version=271
|
||||
|
@@ -1,16 +1,16 @@
|
||||
[versions]
|
||||
|
||||
kt = "2.0.20"
|
||||
kt-serialization = "1.7.2"
|
||||
kt-coroutines = "1.8.1"
|
||||
kt-serialization = "1.7.3"
|
||||
kt-coroutines = "1.9.0"
|
||||
|
||||
kslog = "1.3.6"
|
||||
|
||||
jb-compose = "1.7.0-alpha03"
|
||||
jb-exposed = "0.54.0"
|
||||
jb-compose = "1.7.0-rc01"
|
||||
jb-exposed = "0.55.0"
|
||||
jb-dokka = "1.9.20"
|
||||
|
||||
sqlite = "3.46.1.0"
|
||||
sqlite = "3.46.1.3"
|
||||
|
||||
korlibs = "5.4.0"
|
||||
uuid = "0.8.4"
|
||||
@@ -19,11 +19,11 @@ ktor = "2.3.12"
|
||||
|
||||
gh-release = "2.5.2"
|
||||
|
||||
koin = "3.5.6"
|
||||
koin = "4.0.0"
|
||||
|
||||
okio = "3.9.0"
|
||||
okio = "3.9.1"
|
||||
|
||||
ksp = "2.0.20-1.0.24"
|
||||
ksp = "2.0.20-1.0.25"
|
||||
kotlin-poet = "1.18.1"
|
||||
|
||||
versions = "0.51.0"
|
||||
@@ -34,10 +34,10 @@ dexcount = "4.0.0"
|
||||
android-coreKtx = "1.13.1"
|
||||
android-recyclerView = "1.3.2"
|
||||
android-appCompat = "1.7.0"
|
||||
android-fragment = "1.8.2"
|
||||
android-fragment = "1.8.4"
|
||||
android-espresso = "3.6.1"
|
||||
android-test = "1.2.1"
|
||||
android-compose-material3 = "1.2.1"
|
||||
android-compose-material3 = "1.3.0"
|
||||
|
||||
android-props-minSdk = "21"
|
||||
android-props-compileSdk = "35"
|
||||
@@ -85,11 +85,11 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
|
||||
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
|
||||
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||
|
||||
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" }
|
||||
|
||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
|
||||
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
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
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -1,7 +1,101 @@
|
||||
package dev.inmo.micro_utils.ktor.server.configurators
|
||||
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.http.content.*
|
||||
import io.ktor.server.plugins.cachingheaders.*
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.sessions.*
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
|
||||
interface KtorApplicationConfigurator {
|
||||
@Serializable
|
||||
class Routing(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun Route.invoke() }
|
||||
private val rootInstaller = Element {
|
||||
elements.forEach {
|
||||
it.apply { invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun Application.configure() {
|
||||
pluginOrNull(io.ktor.server.routing.Routing) ?.apply {
|
||||
rootInstaller.apply { invoke() }
|
||||
} ?: install(io.ktor.server.routing.Routing) {
|
||||
rootInstaller.apply { invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pathToFolder Contains [Pair]s where firsts are paths in urls and seconds are folders file paths
|
||||
* @param pathToResource Contains [Pair]s where firsts are paths in urls and seconds are packages in resources
|
||||
*/
|
||||
class Static(
|
||||
private val pathToFolder: List<Pair<String, String>> = emptyList(),
|
||||
private val pathToResource: List<Pair<String, String>> = emptyList(),
|
||||
) : Element {
|
||||
override fun Route.invoke() {
|
||||
pathToFolder.forEach {
|
||||
staticFiles(
|
||||
it.first,
|
||||
File(it.second)
|
||||
)
|
||||
}
|
||||
pathToResource.forEach {
|
||||
staticResources(
|
||||
it.first,
|
||||
it.second
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StatusPages(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun StatusPagesConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(StatusPages) {
|
||||
elements.forEach {
|
||||
it.apply { invoke() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Sessions(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun SessionsConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(Sessions) {
|
||||
elements.forEach {
|
||||
it.apply { invoke() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CachingHeaders(
|
||||
private val elements: List<@Contextual Element>
|
||||
) : KtorApplicationConfigurator {
|
||||
fun interface Element { operator fun CachingHeadersConfig.invoke() }
|
||||
|
||||
override fun Application.configure() {
|
||||
install(CachingHeaders) {
|
||||
elements.forEach {
|
||||
it.apply { invoke() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.configure()
|
||||
}
|
||||
|
Reference in New Issue
Block a user