Compare commits

..

1 Commits

Author SHA1 Message Date
3e24cf1768 launchInCurrentThread 2022-12-27 09:28:37 +06:00
369 changed files with 4145 additions and 13050 deletions

View File

@@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
java-version: 11
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
java-version: 11
- name: Build
run: ./gradlew build && ./gradlew dokkaHtml
- name: Publish KDocs

3
.gitignore vendored
View File

@@ -14,6 +14,3 @@ local.properties
kotlin-js-store
publishing.sh
local.*
local/

View File

@@ -1,666 +1,9 @@
# Changelog
## 0.20.51
* `Versions`:
* `Android Fragment`: `1.7.0` -> `1.7.1`
* `Pagination`:
* Add `Pagination.nextPageIfTrue` and `Pagination.thisPageIftrue` extensions to get the page according to condition
pass status
* Add `PaginationResult.nextPageIfNotEmptyOrLastPage` and `PaginationResult.thisPageIfNotEmptyOrLastPage`
* Change all `doForAll` and `getAll` extensions fo pagination to work basing on `nextPageIfNotEmptyOrLastPage` and
`thisPageIfNotEmptyOrLastPage`
## 0.20.50
* `Versions`:
* `Coroutines`: `1.8.0` -> `1.8.1`
* `KSLog`: `1.3.3` -> `1.3.4`
* `Exposed`: `0.50.0` -> `0.50.1`
* `Ktor`: `2.3.10` -> `2.3.11`
* A lot of inline functions became common functions due to inline with only noinline callbacks in arguments leads to
low performance
* `Coroutines`:
* `SmartMutex`, `SmartSemaphore` and `SmartRWLocker` as their user changed their state flow to `SpecialMutableStateFlow`
## 0.20.49
* `Repos`:
* `Common`:
* All `Repo`s get `diff` extensions
* `KeyValueRepo` and `KeyValuesRepo` get `applyDiff` extension
* Add new extensions `on*` flows for crud repos
## 0.20.48
* `Versions`:
* `Android Core KTX`: `1.13.0` -> `1.13.1`
* `AndroidX Fragment`: `1.6.2` -> `1.7.0`
## 0.20.47
* `Versions`:
* `Exposed`: `0.49.0` -> `0.50.0`
## 0.20.46
* `Common`:
* Now this repo depends on `klock`
* Add new object-serializer `DateTimeSerializer` for `klock` serializer
## 0.20.45
* `Versions`:
* `Android Core KTX`: `1.12.0` -> `1.13.0`
## 0.20.44
* `Versions`:
* `Compose`: `1.6.1` -> `1.6.2`
* `Koin`: `3.5.4` -> `3.5.6`
## 0.20.43
* `Versions`:
* `Ktor`: `2.3.9` -> `2.3.10`
* `Koin`: `3.5.3` -> `3.5.4`
## 0.20.42
* `Repos`:
* `Generator`:
* Improvements
## 0.20.41
* `Repos`:
* `Exposed`:
* `AbstractExposedKeyValueRepo`, `ExposedKeyValueRepo`, `AbstractExposedKeyValuesRepo`, `ExposedKeyValuesRepo` got opportunity to setup some part of their flows
## 0.20.40
* `Versions`:
* `KSLog`: `1.3.2` -> `1.3.3`
* `Exposed`: `0.48.0` -> `0.49.0`
* `UUID`: `0.8.2` -> `0.8.4`
## 0.20.39
* `Versions`:
* `Kotlin`: `1.9.22` -> `1.9.23`
* `Korlibs`: `5.3.2` -> `5.4.0`
* `Okio`: `3.8.0` -> `3.9.0`
* `Compose`: `1.6.0` -> `1.6.1`
* `ComposeMaterial3`: `1.2.0` -> `1.2.1`
## 0.20.38
* `Versions`:
* `Ktor`: `2.3.8` -> `2.3.9`
## 0.20.37
* `Versions`:
* `Compose`: `1.5.12` -> `1.6.0`
* `Exposed`: `0.47.0` -> `0.48.0`
## 0.20.36
* `Versions`:
* `Serialization`: `1.6.2` -> `1.6.3`
* `Korlibs`: `5.3.1` -> `5.3.2`
* `Repos`:
* `Cache`:
* Improve work and functionality of `actualizeAll` and subsequent functions
* All internal repos `invalidate`/`actualizeAll` now use common `actualizeAll` functions
## 0.20.35
* `Versions`:
* `Coroutines`: `1.7.3` -> `1.8.0`
* `Material3`: `1.1.2` -> `1.2.0`
## 0.20.34
* `Repos`:
* `Common`:
* Improve default `set` realization of `KeyValuesRepo`
## 0.20.33
* `Colors`
* `Common`:
* Add opportunity to use `HEXAColor` with `ahex` colors
## 0.20.32
* `Versions`:
* `Okio`: `3.7.0` -> `3.8.0`
* `Resources`:
* Make `StringResource` serializable
* Add several variants of builder usages
## 0.20.31
* `Versions`:
* `Ktor`: `2.3.7` -> `2.3.8`
## 0.20.30
* `Versions`:
* `Exposed`: `0.46.0` -> `0.47.0`
## 0.20.29
* `Versions`:
* `Kotlin`: `1.9.21` -> `1.9.22`
* `Compose`: `1.5.11` -> `1.5.12`
* `Korlibs`: `5.3.0` -> `5.3.1`
## 0.20.28
* `Versions`:
* `Kotlin`: `1.9.22` -> `1.9.21` (downgrade)
* `Compose`: `1.6.0-dev13691` -> `1.5.11` (downgrade)
## 0.20.27
* `Versions`:
* `Kotlin`: `1.9.21` -> `1.9.22`
* `Compose`: `1.5.11` -> `1.6.0-dev13691`
## 0.20.26
* `Versions`:
* `Exposed`: `0.45.0` -> `0.46.0`. **This update brinds new api deprecations in exposed**
* `Resources`:
* Add opportunity to get default translation by passing `null` as `IetfLang` argument
* Add several useful extensions to get translations in `JS` target
## 0.20.25
* `Colors`:
* `Common`:
* Module inited
## 0.20.24
**Since this version depdendencies of klock and krypto replaced with `com.soywiz.korge:korlibs-time` and `com.soywiz.korge:korlibs-crypto`**
* `Versions`:
* `Klock` (since now `KorlibsTime`): `4.0.10` -> `5.3.0`
* `Krypto` (since now `KorlibsCrypto`): `4.0.10` -> `5.3.0`
* `Serialization`:
* `Mapper`:
* `Mapper` pass decoder into callback of deserialization strategy
* `Mapper` pass encoder into callback of serialization strategy
## 0.20.23
* `Versions`:
* `Koin`: `3.5.0` -> `3.5.3`
* `Okio`: `3.6.0` -> `3.7.0`
* `LanguageCodes`:
* Fixes in intermediate language codes (like `Chinese.Hans`)
* Rename `IetfLanguageCode` to `IetfLang`
* Rename all subsequent functions (including serializer)
* New lazy properties `knownLanguageCodesMap`, `knownLanguageCodesMapByLowerCasedKeys` and several others
## 0.20.22
* `Common`:
* Add opportunity to create own `Diff` with base constructor
## 0.20.21
* `Resources`:
* Inited
## 0.20.20
* `Repos`:
* `Exposed`:
* Add opportunity for setup flows in `AbstractExposedCRUDRepo`
## 0.20.19
* `Versions`:
* `Ktor`: `2.3.6` -> `2.3.7`
## 0.20.18
* `Coroutines`:
* `SpecialMutableStateFlow` now extends `MutableStateFlow`
* `Compose`:
* Deprecate `FlowState` due to its complexity in fixes
## 0.20.17
* `Versions`:
* `Serialization`: `1.6.1` -> `1.6.2`
## 0.20.16
* `Versions`:
* `Exposed`: `0.44.1` -> `0.45.0`
* `Coroutines`:
* Add `SpecialMutableStateFlow`
* `Compose`:
* Add `FlowState`
## 0.20.15
* `Versions`:
* `Kotlin`: `1.9.20` -> `1.9.21`
* `KSLog`: `1.3.0` -> `1.3.1`
* `Compose`: `1.5.10` -> `1.5.11`
## 0.20.14
* `Versions`:
* `Serialization`: `1.6.0` -> `1.6.1`
* `KSLog`: `1.2.4` -> `1.3.0`
## 0.20.13
* `Versions`:
* `Ktor`: `2.3.5` -> `2.3.6`
* `UUID`: `0.8.1` -> `0.8.2`
## 0.20.12
**It is experimental migration onto new gradle version. Be careful in use of this version**
**This update have JDK 17 in `compatibility` and `target` versions**
## 0.20.11
* `Versions`:
* `Kotlin`: `1.9.20-RC2` -> `1.9.20`
* `Exposed`: `0.44.0` -> `0.44.1`
* `Compose`: `1.5.10-rc02` -> `1.5.10`
* `Coroutines`:
* `SmartRWLocker` now will wait first unlock of write mutex for acquiring read
## 0.20.10
* `Versions`:
* `Kotlin`: `1.9.20-RC` -> `1.9.20-RC1`
* `KSLog`: `1.2.1` -> `1.2.2`
* `Compose`: `1.5.10-rc01` -> `1.5.10-rc02`
* `RecyclerView`: `1.3.1` -> `1.3.2`
## 0.20.9
* Most of common modules now supports `linuxArm64` target
## 0.20.8
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION**
* `Versions`:
* `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC`
* `Compose`: `1.5.10-beta02` -> `1.5.10-rc01`
## 0.20.7
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION**
* `Versions`:
* `Kotlin`: `1.9.10` -> `1.9.20-Beta2`
* `Compose`: `1.5.1` -> `1.5.10-beta02`
* `Exposed`: `0.43.0` -> `0.44.0`
* `Ktor`: `2.3.4` -> `2.3.5`
* `Koin`: `3.4.3` -> `3.5.0`
* `Okio`: `3.5.0` -> `3.6.0`
* `Android Core`: `1.10.1` -> `1.12.0`
* `Android Compose Material`: `1.1.1` -> `1.1.2`
## 0.20.6
* `Repos`:
* `Exposed`
* Fixes in exposed key-values repos
## 0.20.5
* `Coroutines`:
* Fixes in `SmartRWLocker`
## 0.20.4
* `Versions`:
* `Kotlin`: `1.9.0` -> `1.9.10`
* `KSLog`: `1.2.0` -> `1.2.1`
* `Compose`: `1.5.0` -> `1.5.1`
* `UUID`: `0.8.0` -> `0.8.1`
## 0.20.3
* `Versions`:
* `Compose`: `1.4.3` -> `1.5.0`
* `Exposed`: `0.42.1` -> `0.43.0`
* `Ktor`: `2.3.3` -> `2.3.4`
* `Repos`:
* `Cache`:
* Fixes in locks of caches
## 0.20.2
* All main repos uses `SmartRWLocker`
* `Versions`:
* `Serialization`: `1.5.1` -> `1.6.0`
* `Exposed`: `0.42.0` -> `0.42.1`
* `Korlibs`: `4.0.9` -> `4.0.10`
* `Androis SDK`: `33` -> `34`
## 0.20.1
* `SmallTextField`:
* Module is initialized
* `Pickers`:
* Module is initialized
* `Coroutines`:
* Add `SmartSemaphore`
* Add `SmartRWLocker`
## 0.20.0
* `Versions`:
* `Kotlin`: `1.8.22` -> `1.9.0`
* `KSLog`: `1.1.1` -> `1.2.0`
* `Exposed`: `0.41.1` -> `0.42.0`
* `UUID`: `0.7.1` -> `0.8.0`
* `Korlibs`: `4.0.3` -> `4.0.9`
* `Ktor`: `2.3.2` -> `2.3.3`
* `Okio`: `3.4.0` -> `3.5.0`
## 0.19.9
* `Versions`:
* `Koin`: `3.4.2` -> `3.4.3`
* `Startup`:
* Now it is possible to start application in synchronous way
## 0.19.8
* `Versions`:
* `Coroutines`: `1.7.2` -> `1.7.3`
* `Kotlin`: `1.8.20` -> `1.8.22`
* `Compose`: `1.4.1` -> `1.4.3`
* `Okio`: `3.3.0` -> `3.4.0`
* `RecyclerView`: `1.3.0` -> `1.3.1`
* `Fragment`: `1.6.0` -> `1.6.1`
* `Repos`:
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
* `Cache`:
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
will be automatically invalidated on start of their work
## 0.19.7
* `Versions`:
* `Coroutines`: `1.7.1` -> `1.7.2`
## 0.19.6
* `Versions`:
* `Coroutines`: `1.6.4` -> `1.7.1`
* `Ktor`: `2.3.1` -> `2.3.2`
* `Compose`: `1.4.0` -> `1.4.1`
## 0.19.5
* `Repos`:
* `Generator`:
* Fixes in new type generation
## 0.19.4
* `Versions`:
* `Koin`: `3.4.1` -> `3.4.2`
* `Android Fragments`: `1.5.7` -> `1.6.0`
* `Koin`
* `Generator`
* Fixes in new generic generator part
## 0.19.3
* `Koin`
* `Generator`
* New getter methods now available with opportunity to use parameters
* Old notation `*Single` and `*Factory` is deprecated since this release. With old
will be generated new `single*` and `factory*` notations for new generations
* Add opportunity to use generic-oriented koin definitions
## 0.19.2
* `Versions`:
* `Ktor`: `2.3.0` -> `2.3.1`
* `Koin`: `3.4.0` -> `3.4.1`
* `Uuid`: `0.7.0` -> `0.7.1`
## 0.19.1
* `Versions`:
* `Korlibs`: `4.0.1` -> `4.0.3`
* `Kotlin Poet`: `1.13.2` -> `1.14.0`
## 0.19.0
* `Versions`:
* `Korlibs`: `3.4.0` -> `4.0.1`
## 0.18.4
* `Koin`:
* New extension `lazyInject`
## 0.18.3
* `Versions`:
* `Serialization`: `1.5.0` -> `1.5.1`
* `Android Cor Ktx`: `1.10.0` -> `1.10.1`
## 0.18.2
* `Startup`:
* Now internal `Json` is fully customizable
## 0.18.1
* `Common`:
* Add `MapDiff`
* `Coroutines`:
* Add `SmartMutex`
## 0.18.0
**ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED**
* `Versions`:
* `Android Fragments`: `1.5.6` -> `1.5.7`
* `Ktor`:
* `Server`:
* Now it is possible to take query parameters as list
* `Repos`:
* `Common`:
* New `WriteKeyValuesRepo.removeWithValue`
* `Cache`:
* Rename full caching factories functions to `fullyCached`
## 0.17.8
* `Versions`:
* `Ktor`: `2.2.4` -> `2.3.0`
## 0.17.7
* `Versions`:
* `Android CoreKtx`: `1.9.0` -> `1.10.0`
* `Startup`:
* Add support of `linuxX64` and `mingwX64` platforms
## 0.17.6
* `Versions`:
* `Kotlin`: `1.8.10` -> `1.8.20`
* `KSLog`: `1.0.0` -> `1.1.1`
* `Compose`: `1.3.1` -> `1.4.0`
* `Koin`: `3.3.2` -> `3.4.0`
* `RecyclerView`: `1.2.1` -> `1.3.0`
* `Fragment`: `1.5.5` -> `1.5.6`
* Experimentally (`!!!`) add `linuxX64` and `mingwX64` targets
## 0.17.5
* `Common`:
* Conversations of number primitives with bounds care
* `Repos`:
* `Common`:
* By default, `getAll` for repos will take all the size of repo as page size
* New extension for all built-in repos `maxPagePagination`
* All the repos got `getAll` functions
## 0.17.4
* `Serialization`:
* `Mapper`:
* Module inited
* `Versions`:
* `Compose`: `1.3.1-rc02` -> `1.3.1`
## 0.17.3
* `Common`:
* Add `fixed` extensions for `Float` and `Double`
* New function `emptyDiff`
* Now you may pass custom `comparisonFun` to all `diff` functions
## 0.17.2
* `FSM`:
* `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default
## 0.17.1
* **Hotfix** for absence of jvm dependencies in android modules
* `Versions`:
* `Ktor`: `2.2.3` -> `2.2.4`
## 0.17.0
* `Versions`:
* `Kotlin`: `1.7.20` -> `1.8.10`
* `Serialization`: `1.4.1` -> `1.5.0`
* `KSLog`: `0.5.4` -> `1.0.0`
* `AppCompat`: `1.6.0` -> `1.6.1`
## 0.16.13
* `Repos`:
* `Generator`:
* Module has been created
## 0.16.12
* `Repos`:
* `Exposed`:
* `CommonExposedRepo.selectByIds` uses `foldRight` by default instead of raw foreach
* `Koin`:
* `Generator`:
* Module has been created
## 0.16.11
* `LanguageCodes`:
* In android and JVM targets now available `toJavaLocale` and from Java `Locale` conversations from/to
`IetfLanguageCode`
## 0.16.10
* `Repos`:
* `Cache`:
* New transformer type: `ReadCRUDFromKeyValueRepo`
* New transformer type: `ReadKeyValueFromCRUDRepo`
* `Pagination`:
* New `paginate` extensions with `reversed` support for `List`/`Set`
## 0.16.9
* `Versions`:
* `Koin`: `3.2.2` -> `3.3.2`
* `AppCompat`: `1.5.1` -> `1.6.0`
* `Ktor`:
* `Client`
* `HttpResponse.bodyOrNull` now retrieve callback to check if body should be received or null
* New extension `HttpResponse.bodyOrNullOnNoContent`
## 0.16.8
* `Versions`:
* `Ktor`: `2.2.2` -> `2.2.3`
* `Ktor`:
* `Client`
* Fixes in `HttpClient.uniUpload`
* `Server`
* Fixes in `PartData.FileItem.download`
* `Repos`:
* `Cache`:
* New type of caches: `FallbackCacheRepo`
* Fixes in `Write*` variants of cached repos
* New type `ActionWrapper`
* New `AutoRecache*` classes for all types of repos as `FallbackCacheRepo`s
* `Common`:
* New transformations for key-value and key-values vice-verse
* Fixes in `FileReadKeyValueRepo`
## 0.16.7
* `Common`:
* New extensions `ifTrue`/`ifFalse`/`alsoIfTrue`/`alsoIfFalse`/`letIfTrue`/`letIfFalse`
* `Diff` now is serializable
* Add `IndexedValue` serializer
* `repeatOnFailure` extending: now you may pass any lambda to check if continue to try/do something
* `Compose`:
* New extension `MutableState.asState`
* `Coroutines`:
* `Compose`:
* All the `Flow` conversations to compose `State`/`MutableState`/`SnapshotStateList`/`List` got several new
parameters
* `Flow.toMutableState` now is deprecated in favor to `asMutableComposeState`
* `Repos`:
* `Cache`:
* New type `FullCacheRepo`
* New type `CommonCacheRepo`
* `CacheRepo` got `invalidate` method. It will fully reload `FullCacheRepo` and just clear `CommonCacheRepo`
* New extensions `KVCache.actualizeAll`
## 0.16.6
* `Startup`:
* `Launcher`:
* Improvements in `StartLauncherPlugin#start` methods
* Add opportunity to pass second argument on `JVM` platform as log level
* `Repos`:
* `Ktor`:
* `Client`:
* All clients repos got opportunity to customize their flows
* `Exposed`:
* Extensions `eqOrIsNull` and `neqOrIsNotNull` for `Column`
## 0.16.5
* `Versions`:
* `Ktor`: `2.2.1` -> `2.2.2`
## 0.16.4
* `Coroutines`:
* Create `launchInCurrentThread`
## 0.16.3
* `Coroutines`:
* Create `launchInCurrentThread`
* `Startup`:
* `Launcher`:
* All starting API have been moved into `StartLauncherPlugin` and do not require serialize/deserialize cycle for now

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>

View File

@@ -1,18 +0,0 @@
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 {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
}
}
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,27 +0,0 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.*
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,
adjustTarget: ((Float) -> Float)?,
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
): AnimationResult<Float, AnimationVector1D> {
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
val adjustedTarget = adjustTarget?.invoke(targetValue)
return if (adjustedTarget != null) {
animateTo(
targetValue = adjustedTarget,
initialVelocity = initialVelocity,
block = block
)
} else {
animateDecay(
initialVelocity = initialVelocity,
animationSpec = animationSpec,
block = block,
)
}
}

View File

@@ -1,219 +0,0 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
}
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun NumberPicker(
number: Int,
modifier: Modifier = Modifier,
range: IntRange? = null,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
allowUseManualInput: Boolean = true,
onStateChanged: (Int) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 36.dp
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
if (range != null) {
val offsetRange = remember(number, range) {
val value = number
val first = -(range.last - value) * halvedNumbersColumnHeightPx
val last = -(range.first - value) * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val animatedStateValue = animatedStateValue(animatedOffset.value)
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
val inputFieldShown = if (allowUseManualInput) {
remember { mutableStateOf(false) }
} else {
null
}
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
}
},
onDragStopped = { velocity ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(animatedStateValue(endValue))
animatedOffset.snapTo(0f)
}
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = range == null || range.first < number
IconButton(
{
onStateChanged(number - 1)
inputFieldShown ?.value = false
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
contentAlignment = Alignment.Center
) {
val baseLabelModifier = Modifier.align(Alignment.Center)
ProvideTextStyle(textStyle) {
Text(
text = (animatedStateValue - 1).toString(),
modifier = baseLabelModifier
.offset(y = -halvedNumbersColumnHeight)
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
if (inputFieldShown ?.value == true) {
val currentValue = remember { mutableStateOf(number.toString()) }
val focusRequester = remember { FocusRequester() }
SmallTextField(
currentValue.value,
{
val asDigit = it.toIntOrNull()
when {
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
else -> { /* do nothing */ }
}
},
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
detectTapGestures {
if (!checkContains(it)) {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
}
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
keyboardActions = KeyboardActions {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
},
singleLine = true,
textStyle = textStyle
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
} else {
Text(
text = animatedStateValue.toString(),
modifier = baseLabelModifier
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
.clickable {
if (inputFieldShown ?.value == false) {
inputFieldShown.value = true
}
}
)
}
Text(
text = (animatedStateValue + 1).toString(),
modifier = baseLabelModifier
.offset(y = halvedNumbersColumnHeight)
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = range == null || range.last > number
IconButton(
{
onStateChanged(number + 1)
inputFieldShown ?.value = false
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@@ -1,153 +0,0 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.*
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun <T> SetPicker(
current: T,
dataList: List<T>,
modifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
dataToString: @Composable (T) -> String = { it.toString() },
onStateChanged: (T) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
textStyle.lineHeight.toDp()
}
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
val lastIndex = dataList.size - 1
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
val offsetRange = remember(index, lastIndex) {
val value = index
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
val last = value * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
val indexAnimatedOffset = if (animatedOffset.value > 0) {
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
} else {
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
},
onDragStopped = { velocity ->
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
animatedOffset.snapTo(0f)
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = index > 0
IconButton(
{
onStateChanged(dataList.elementAt(index - 1))
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
ProvideTextStyle(textStyle) {
dataList.forEachIndexed { i, t ->
val alpha = when {
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
val offset = when {
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> coercedAnimatedOffset
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
Text(
text = dataToString(t),
modifier = Modifier
.alpha(alpha)
.offset(y = with(LocalDensity.current) { offset.toDp() })
)
}
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = index < lastIndex
IconButton(
{
onStateChanged(dataList.elementAt(index + 1))
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>

View File

@@ -1,18 +0,0 @@
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 {
androidMain {
dependencies {
api libs.android.compose.material3
}
}
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,66 +0,0 @@
package dev.inmo.micro_utils.android.smalltextfield
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SmallTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
textColor: Color = textStyle.color.takeOrElse {
LocalContentColor.current
},
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
BasicTextField(
value = value,
modifier = modifier,
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle.copy(
color = textColor
),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
cursorBrush = SolidColor(
textStyle.color.takeOrElse {
LocalContentColor.current
}
)
)
}

View File

@@ -9,7 +9,6 @@ buildscript {
dependencies {
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
classpath libs.buildscript.kt.ksp
classpath libs.buildscript.jb.dokka
classpath libs.buildscript.gh.release
classpath libs.buildscript.android.gradle
@@ -17,17 +16,12 @@ buildscript {
}
}
plugins {
alias(libs.plugins.versions)
}
allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed
@@ -42,4 +36,3 @@ allprojects {
apply from: "./extensions.gradle"
apply from: "./github_release.gradle"
apply from: "./versions_plugin_setup.gradle"

View File

@@ -1,7 +0,0 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"

View File

@@ -1,174 +0,0 @@
package dev.inmo.micro_utils.colors.common
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
import kotlin.math.floor
/**
* Wrapper for RGBA colors. Receiving [UInt] in main constructor. Each part in main constructor
* configured with `0x00 - 0xff` range. Examples:
*
* * Red: `0xff0000ffu`
* * Red (0.5 capacity): `0xff000088u`
*
* Anyway it is recommended to use
*
* @param hexaUInt rgba [UInt] in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
*/
@Serializable
@JvmInline
value class HEXAColor (
val hexaUInt: UInt
) : Comparable<HEXAColor> {
/**
* @returns [hexaUInt] as a string with format `#FFEEBBAA` where FF - red, EE - green, BB - blue and AA - alpha
*/
val hexa: String
get() = "#${hexaUInt.toString(16).padStart(8, '0')}"
/**
* @returns [hexaUInt] as a string with format `#FFEEBB` where FF - red, EE - green and BB - blue
*/
val hex: String
get() = hexa.take(7)
/**
* @returns [hexaUInt] as a string with format `#AAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue
*/
val ahex: String
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
val rgba: String
get() = "rgba($r,$g,$b,${aOfOne.toString().take(5)})"
val rgb: String
get() = "rgb($r,$g,$b)"
val shortHex: String
get() = "#${r.shortPart()}${g.shortPart()}${b.shortPart()}"
val shortHexa: String
get() = "$shortHex${a.shortPart()}"
val rgbUInt: UInt
get() = (hexaUInt / 256u)
val rgbInt: Int
get() = rgbUInt.toInt()
val ahexUInt
get() = (a * 0x1000000).toUInt() + rgbUInt
val r: Int
get() = ((hexaUInt and 0xff000000u) / 0x1000000u).toInt()
val g: Int
get() = ((hexaUInt and 0x00ff0000u) / 0x10000u).toInt()
val b: Int
get() = ((hexaUInt and 0x0000ff00u) / 0x100u).toInt()
val a: Int
get() = ((hexaUInt and 0x000000ffu)).toInt()
val aOfOne: Float
get() = a.toFloat() / (0xff)
init {
require(hexaUInt in 0u ..0xffffffffu)
}
constructor(r: Int, g: Int, b: Int, a: Int) : this(
((r * 0x1000000).toLong() + g * 0x10000 + b * 0x100 + a).toUInt()
) {
require(r in 0 ..0xff)
require(g in 0 ..0xff)
require(b in 0 ..0xff)
require(a in 0 ..0xff)
}
constructor(r: Int, g: Int, b: Int, aOfOne: Float = 1f) : this(
r = r, g = g, b = b, a = (aOfOne * 0xff).toInt()
)
override fun toString(): String {
return hexa
}
override fun compareTo(other: HEXAColor): Int = (hexaUInt - other.hexaUInt).coerceIn(Int.MIN_VALUE.toUInt(), Int.MAX_VALUE.toLong().toUInt()).toInt()
fun copy(
r: Int = this.r,
g: Int = this.g,
b: Int = this.b,
aOfOne: Float = this.aOfOne
) = HEXAColor(r = r, g = g, b = b, aOfOne = aOfOne)
fun copy(
r: Int = this.r,
g: Int = this.g,
b: Int = this.b,
a: Int
) = HEXAColor(r = r, g = g, b = b, a = a)
companion object {
/**
* Parsing color from [color]
*
* Supported formats samples (on Red color based):
*
* * `#f00`
* * `#f00f`
* * `#ff0000`
* * `#ff0000ff`
* * `rgb(255, 0, 0)`
* * `rgba(255, 0, 0, 1)`
*/
fun parseStringColor(color: String): HEXAColor = when {
color.startsWith("#") -> color.removePrefix("#").let { color ->
when (color.length) {
3 -> color.map { "$it$it" }.joinToString(separator = "", postfix = "ff")
4 -> color.take(3).map { "$it$it" }.joinToString(separator = "", postfix = color.takeLast(1).let { "${it}0" })
6 -> "${color}ff"
8 -> color
else -> error("Malfurmed color string: $color. It is expected that color started with # will contains 3, 6 or 8 valuable parts")
}
}
color.startsWith("rgb(") -> color
.removePrefix("rgb(")
.removeSuffix(")")
.replace(Regex("\\s"), "")
.split(",")
.joinToString("", postfix = "ff") {
it.toInt().toString(16).padStart(2, '0')
}
color.startsWith("rgba(") -> color
.removePrefix("rgba(")
.removeSuffix(")")
.replace(Regex("\\s"), "")
.split(",").let {
it.take(3).map { it.toInt().toString(16).padStart(2, '0') } + (it.last().toFloat() * 0xff).toInt().toString(16).padStart(2, '0')
}
.joinToString("")
else -> color
}.lowercase().toUInt(16).let(::HEXAColor)
/**
* Creates [HEXAColor] from [uint] presume it is in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
*/
fun fromHexa(uint: UInt) = HEXAColor(uint)
/**
* Creates [HEXAColor] from [uint] presume it is in format `0xAAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue`
*/
fun fromAhex(uint: UInt) = HEXAColor(
a = ((uint and 0xff000000u) / 0x1000000u).toInt(),
r = ((uint and 0x00ff0000u) / 0x10000u).toInt(),
g = ((uint and 0x0000ff00u) / 0x100u).toInt(),
b = ((uint and 0x000000ffu)).toInt()
)
/**
* Parsing color from [color]
*
* Supported formats samples (on Red color based):
*
* * `#f00`
* * `#ff0000`
* * `#ff0000ff`
* * `rgb(255, 0, 0)`
* * `rgba(255, 0, 0, 1)`
*/
operator fun invoke(color: String) = parseStringColor(color)
private fun Int.shortPart(): String {
return (floor(toFloat() / 16)).toInt().toString(16)
}
}
}

View File

@@ -1,209 +0,0 @@
package dev.inmo.micro_utils.colors.common
import kotlin.math.floor
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class HexColorTests {
val alphaRgbaPrecision = 5
class TestColor(
val color: HEXAColor,
val shortHex: String,
val shortHexa: String,
val hex: String,
val hexa: String,
val ahex: String,
val ahexUInt: UInt,
val rgbUInt: UInt,
val rgb: String,
val rgba: String,
val r: Int,
val g: Int,
val b: Int,
val a: Int,
vararg val additionalRGBAVariants: String
)
val testColors: List<TestColor>
get() = listOf(
TestColor(
color = HEXAColor(hexaUInt = 0xff0000ffu),
shortHex = "#f00",
shortHexa = "#f00f",
hex = "#ff0000",
hexa = "#ff0000ff",
ahex = "#ffff0000",
ahexUInt = 0xffff0000u,
rgbUInt = 0xff0000u,
rgb = "rgb(255,0,0)",
rgba = "rgba(255,0,0,1.0)",
r = 0xff,
g = 0x00,
b = 0x00,
a = 0xff,
"rgba(255,0,0,1)",
),
TestColor(
color = HEXAColor(hexaUInt = 0x00ff00ffu),
shortHex = "#0f0",
shortHexa = "#0f0f",
hex = "#00ff00",
hexa = "#00ff00ff",
ahex = "#ff00ff00",
ahexUInt = 0xff00ff00u,
rgbUInt = 0x00ff00u,
rgb = "rgb(0,255,0)",
rgba = "rgba(0,255,0,1.0)",
r = 0x00,
g = 0xff,
b = 0x00,
a = 0xff,
"rgba(0,255,0,1)"
),
TestColor(
color = HEXAColor(0x0000ffffu),
shortHex = "#00f",
shortHexa = "#00ff",
hex = "#0000ff",
hexa = "#0000ffff",
ahex = "#ff0000ff",
ahexUInt = 0xff0000ffu,
rgbUInt = 0x0000ffu,
rgb = "rgb(0,0,255)",
rgba = "rgba(0,0,255,1.0)",
r = 0x00,
g = 0x00,
b = 0xff,
a = 0xff,
"rgba(0,0,255,1)"
),
TestColor(
color = HEXAColor(0xff000088u),
shortHex = "#f00",
shortHexa = "#f008",
hex = "#ff0000",
hexa = "#ff000088",
ahex = "#88ff0000",
ahexUInt = 0x88ff0000u,
rgbUInt = 0xff0000u,
rgb = "rgb(255,0,0)",
rgba = "rgba(255,0,0,0.533)",
r = 0xff,
g = 0x00,
b = 0x00,
a = 0x88,
),
TestColor(
color = HEXAColor(0x00ff0088u),
shortHex = "#0f0",
shortHexa = "#0f08",
hex = "#00ff00",
hexa = "#00ff0088",
ahex = "#8800ff00",
ahexUInt = 0x8800ff00u,
rgbUInt = 0x00ff00u,
rgb = "rgb(0,255,0)",
rgba = "rgba(0,255,0,0.533)",
r = 0x00,
g = 0xff,
b = 0x00,
a = 0x88,
),
TestColor(
color = HEXAColor(0x0000ff88u),
shortHex = "#00f",
shortHexa = "#00f8",
hex = "#0000ff",
hexa = "#0000ff88",
ahex = "#880000ff",
ahexUInt = 0x880000ffu,
rgbUInt = 0x0000ffu,
rgb = "rgb(0,0,255)",
rgba = "rgba(0,0,255,0.533)",
r = 0x00,
g = 0x00,
b = 0xff,
a = 0x88,
),
TestColor(
color = HEXAColor(0xff000022u),
shortHex = "#f00",
shortHexa = "#f002",
hex = "#ff0000",
hexa = "#ff000022",
ahex = "#22ff0000",
ahexUInt = 0x22ff0000u,
rgbUInt = 0xff0000u,
rgb = "rgb(255,0,0)",
rgba = "rgba(255,0,0,0.133)",
r = 0xff,
g = 0x00,
b = 0x00,
a = 0x22,
),
TestColor(
color = HEXAColor(0x00ff0022u),
shortHex = "#0f0",
shortHexa = "#0f02",
hex = "#00ff00",
hexa = "#00ff0022",
ahex = "#2200ff00",
ahexUInt = 0x2200ff00u,
rgbUInt = 0x00ff00u,
rgb = "rgb(0,255,0)",
rgba = "rgba(0,255,0,0.133)",
r = 0x00,
g = 0xff,
b = 0x00,
a = 0x22,
),
TestColor(
color = HEXAColor(0x0000ff22u),
shortHex = "#00f",
shortHexa = "#00f2",
hex = "#0000ff",
hexa = "#0000ff22",
ahex = "#220000ff",
ahexUInt = 0x220000ffu,
rgbUInt = 0x0000ffu,
rgb = "rgb(0,0,255)",
rgba = "rgba(0,0,255,0.133)",
r = 0x00,
g = 0x00,
b = 0xff,
a = 0x22,
),
)
@Test
fun baseTest() {
testColors.forEach {
assertEquals(it.hex, it.color.hex)
assertEquals(it.hexa, it.color.hexa)
assertEquals(it.ahex, it.color.ahex)
assertEquals(it.rgbUInt, it.color.rgbUInt)
assertEquals(it.ahexUInt, it.color.ahexUInt)
assertEquals(it.shortHex, it.color.shortHex)
assertEquals(it.shortHexa, it.color.shortHexa)
assertEquals(it.rgb, it.color.rgb)
assertTrue(it.rgba == it.color.rgba || it.color.rgba in it.additionalRGBAVariants)
assertEquals(it.r, it.color.r)
assertEquals(it.g, it.color.g)
assertEquals(it.b, it.color.b)
assertEquals(it.a, it.color.a)
assertEquals(it.color, HEXAColor.fromAhex(it.ahexUInt))
}
}
@Test
fun testHexParseColor() {
testColors.forEach {
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex))
assertEquals(it.color, HEXAColor.parseStringColor(it.hexa))
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.rgb))
assertTrue(it.color.hexaUInt.toInt() - HEXAColor.parseStringColor(it.rgba).hexaUInt.toInt() in -0x1 .. 0x1, )
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex))
assertEquals(it.color.copy(a = floor(it.color.a.toFloat() / 16).toInt() * 0x10), HEXAColor.parseStringColor(it.shortHexa))
}
}
}

View File

@@ -4,15 +4,10 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api libs.klock
}
}
jvmMain {
dependencies {
api project(":micro_utils.coroutines")
@@ -20,14 +15,9 @@ kotlin {
}
androidMain {
dependencies {
api project(":micro_utils.coroutines")
api libs.android.fragment
}
}
nativeMain {
dependencies {
api libs.okio
}
}
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,10 +0,0 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
/**
* Converts current [MutableState] to immutable [State] using [derivedStateOf]
*/
fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value }

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.common.compose"/>

View File

@@ -1,25 +0,0 @@
package dev.inmo.micro_utils.common
import korlibs.time.DateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Serializes [DateTime] as its raw [DateTime.unixMillis] and deserializes in the same way
*/
object DateTimeSerializer : KSerializer<DateTime> {
override val descriptor: SerialDescriptor
get() = Double.serializer().descriptor
override fun deserialize(decoder: Decoder): DateTime {
return DateTime(decoder.decodeDouble())
}
override fun serialize(encoder: Encoder, value: DateTime) {
encoder.encodeDouble(value.unixMillis)
}
}

View File

@@ -2,8 +2,6 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
private inline fun <T> getObject(
additional: MutableList<T>,
iterator: Iterator<T>
@@ -26,23 +24,14 @@ private inline fun <T> getObject(
*
* @see calculateDiff
*/
@Serializable
data class Diff<T> @Warning(warning) constructor(
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
data class Diff<T> internal constructor(
val removed: List<IndexedValue<T>>,
/**
* Old-New values pairs
*/
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
) {
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
companion object {
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
}
}
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
val added: List<IndexedValue<T>>
)
private inline fun <T> performChanges(
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
@@ -51,14 +40,14 @@ private inline fun <T> performChanges(
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
removedList: MutableList<IndexedValue<T>>,
addedList: MutableList<IndexedValue<T>>,
comparisonFun: (T?, T?) -> Boolean
strictComparison: Boolean
) {
var i = -1
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
i++
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
changedList.addAll(
potentialChanges.take(i).mapNotNull {
@@ -112,7 +101,7 @@ private inline fun <T> performChanges(
*/
fun <T> Iterable<T>.calculateDiff(
other: Iterable<T>,
comparisonFun: (T?, T?) -> Boolean
strictComparison: Boolean = false
): Diff<T> {
var i = -1
var j = -1
@@ -140,7 +129,7 @@ fun <T> Iterable<T>.calculateDiff(
}
when {
comparisonFun(oldObject, newObject) -> {
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
changedObjects.addAll(potentiallyChangedObjects.map {
@Suppress("UNCHECKED_CAST")
it as Pair<IndexedValue<T>, IndexedValue<T>>
@@ -151,49 +140,23 @@ fun <T> Iterable<T>.calculateDiff(
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
val previousOldsAdditionsSize = additionalInOld.size
val previousNewsAdditionsSize = additionalInNew.size
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
i -= (additionalInOld.size - previousOldsAdditionsSize)
j -= (additionalInNew.size - previousNewsAdditionsSize)
}
}
}
potentiallyChangedObjects.add(null to null)
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
}
/**
* Calculating [Diff] object
*
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
*/
fun <T> Iterable<T>.calculateDiff(
other: Iterable<T>,
strictComparison: Boolean = false
): Diff<T> = calculateDiff(
other,
comparisonFun = if (strictComparison) {
{ t1, t2 ->
t1 === t2
}
} else {
{ t1, t2 ->
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
}
}
)
inline fun <T> Iterable<T>.diff(
other: Iterable<T>,
strictComparison: Boolean = false
): Diff<T> = calculateDiff(other, strictComparison)
inline fun <T> Iterable<T>.diff(
other: Iterable<T>,
noinline comparisonFun: (T?, T?) -> Boolean
): Diff<T> = calculateDiff(other, comparisonFun)
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
/**
@@ -203,23 +166,6 @@ inline fun <T> Iterable<T>.calculateStrictDiff(
other: Iterable<T>
) = calculateDiff(other, strictComparison = true)
/**
* Applies [diff] to [this] [MutableList]
*/
fun <T> MutableList<T>.applyDiff(
diff: Diff<T>
) {
for (i in diff.removed.indices.sortedDescending()) {
removeAt(diff.removed[i].index)
}
diff.added.forEach { (i, t) ->
add(i, t)
}
diff.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}
/**
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
* mutable list
@@ -228,26 +174,13 @@ fun <T> MutableList<T>.applyDiff(
source: Iterable<T>,
strictComparison: Boolean = false
): Diff<T> = calculateDiff(source, strictComparison).also {
applyDiff(it)
for (i in it.removed.indices.sortedDescending()) {
removeAt(it.removed[i].index)
}
it.added.forEach { (i, t) ->
add(i, t)
}
it.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}
/**
* This method call [calculateDiff] and then apply differences to [this]
* mutable list
*/
fun <T> MutableList<T>.applyDiff(
source: Iterable<T>,
comparisonFun: (T?, T?) -> Boolean
): Diff<T> = calculateDiff(source, comparisonFun).also {
applyDiff(it)
}
/**
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
* [Diff.replaced] values will be reversed too
*/
fun <T> Diff<T>.reversed() = Diff(
removed = added,
replaced = replaced.map { it.second to it.first },
added = removed
)

View File

@@ -1,43 +0,0 @@
package dev.inmo.micro_utils.common
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) {
block()
} else {
null
}
}
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) {
null
} else {
block()
}
}
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block)
return this
}
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block)
return this
}
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) {
block()
} else {
null
}
}
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) {
null
} else {
block()
}
}

View File

@@ -1,30 +0,0 @@
package dev.inmo.micro_utils.common
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> {
private val originalSerializer = PairSerializer(Int.serializer(), subSerializer)
override val descriptor: SerialDescriptor
get() = originalSerializer.descriptor
override fun deserialize(decoder: Decoder): IndexedValue<T> {
val pair = originalSerializer.deserialize(decoder)
return IndexedValue(
pair.first,
pair.second
)
}
override fun serialize(encoder: Encoder, value: IndexedValue<T>) {
originalSerializer.serialize(
encoder,
Pair(value.index, value.value)
)
}
}

View File

@@ -1,135 +0,0 @@
package dev.inmo.micro_utils.common
/**
* Contains diff based on the comparison of objects with the same [K].
*
* @param removed Contains map with keys removed from parent map
* @param changed Contains map with keys values changed new map in comparison with old one
* @param added Contains map with new keys and values
*/
data class MapDiff<K, V> @Warning(warning) constructor(
val removed: Map<K, V>,
val changed: Map<K, Pair<V, V>>,
val added: Map<K, V>
) {
fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty()
inline fun isNotEmpty() = !isEmpty()
companion object {
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap())
}
}
private inline fun <K, V> createCompareFun(
strictComparison: Boolean
): (K, V, V) -> Boolean = if (strictComparison) {
{ _, first, second -> first === second }
} else {
{ _, first, second -> first == second }
}
/**
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
*
* @param compareFun Will be used to determine changed values
*/
fun <K, V> Map<K, V>.diff(
other: Map<K, V>,
compareFun: (K, V, V) -> Boolean
): MapDiff<K, V> {
val removed: Map<K, V> = (keys - other.keys).associateWith {
getValue(it)
}
val added: Map<K, V> = (other.keys - keys).associateWith {
other.getValue(it)
}
val changed = keys.intersect(other.keys).mapNotNull {
val old = getValue(it)
val new = other.getValue(it)
if (compareFun(it, old, new)) {
return@mapNotNull null
} else {
it to (old to new)
}
}.toMap()
return MapDiff(
removed,
changed,
added
)
}
/**
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
*
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
* `equals` will be used
*/
fun <K, V> Map<K, V>.diff(
other: Map<K, V>,
strictComparison: Boolean = false
): MapDiff<K, V> = diff(
other,
compareFun = createCompareFun(strictComparison)
)
/**
* Will apply [mapDiff] to [this] [MutableMap]
*/
fun <K, V> MutableMap<K, V>.applyDiff(
mapDiff: MapDiff<K, V>
) {
mapDiff.apply {
removed.keys.forEach { remove(it) }
changed.forEach { (k, oldNew) ->
put(k, oldNew.second)
}
added.forEach { (k, new) ->
put(k, new)
}
}
}
/**
* Will apply changes with [from] map into [this] one
*
* @param compareFun Will be used to determine changed values
*
* @return [MapDiff] applied to [this] [MutableMap]
*/
fun <K, V> MutableMap<K, V>.applyDiff(
from: Map<K, V>,
compareFun: (K, V, V) -> Boolean
): MapDiff<K, V> {
return diff(from, compareFun).also {
applyDiff(it)
}
}
/**
* Will apply changes with [from] map into [this] one
*
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
* `equals` will be used
*
* @return [MapDiff] applied to [this] [MutableMap]
*/
fun <K, V> MutableMap<K, V>.applyDiff(
from: Map<K, V>,
strictComparison: Boolean = false
): MapDiff<K, V> = applyDiff(
from,
compareFun = createCompareFun(strictComparison)
)
/**
* Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the
* [MapDiff.changed] values will be reversed too
*/
fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff(
removed = added,
changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first },
added = removed
)

View File

@@ -1,28 +0,0 @@
package dev.inmo.micro_utils.common
/**
* Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE]
*/
fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
/**
* Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
*/
fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort()
/**
* Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
*/
fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte()
/**
* Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
*/
fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
/**
* Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
*/
fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()
/**
* Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
*/
fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte()

View File

@@ -1,27 +1,5 @@
package dev.inmo.micro_utils.common
/**
* Executes the given [action] until getting of successful result specified number of [times].
*
* A zero-based index of current iteration is passed as a parameter to [action].
*/
inline fun <R> repeatOnFailure(
onFailure: (Throwable) -> Boolean,
action: () -> R
): Result<R> {
do {
runCatching {
action()
}.onFailure {
if (!onFailure(it)) {
return Result.failure(it)
}
}.onSuccess {
return Result.success(it)
}
} while (true)
}
/**
* Executes the given [action] until getting of successful result specified number of [times].
*
@@ -32,23 +10,12 @@ inline fun <R> repeatOnFailure(
onEachFailure: (Throwable) -> Unit = {},
action: (Int) -> R
): Optional<R> {
var i = 0
val result = repeatOnFailure(
{
onEachFailure(it)
if (i < times) {
i++
true
} else {
false
}
repeat(times) {
runCatching {
action(it)
}.onFailure(onEachFailure).onSuccess {
return Optional.presented(it)
}
) {
action(i)
}
return if (result.isSuccess) {
Optional.presented(result.getOrThrow())
} else {
Optional.absent()
}
return Optional.absent()
}

View File

@@ -1,6 +0,0 @@
package dev.inmo.micro_utils.common
val FixedSignsRange = 0 .. 100
expect fun Float.fixed(signs: Int): Float
expect fun Double.fixed(signs: Int): Double

View File

@@ -1,4 +0,0 @@
package dev.inmo.micro_utils.common
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()

View File

@@ -1,12 +0,0 @@
package dev.inmo.micro_utils.common
import java.math.BigDecimal
import java.math.RoundingMode
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
.toFloat();
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
.toDouble();

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.common"/>

View File

@@ -1,36 +0,0 @@
package dev.inmo.micro_utils.common
import okio.FileSystem
import okio.Path
import okio.use
actual typealias MPPFile = Path
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(toString())
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = FileSystem.SYSTEM.openReadOnly(this).use {
it.size()
}
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = {
FileSystem.SYSTEM.read(this) {
readByteArray()
}
}
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = {
bytesAllocatorSync()
}

View File

@@ -1,25 +0,0 @@
package dev.inmo.micro_utils.common
import kotlinx.cinterop.*
import platform.posix.snprintf
import platform.posix.sprintf
@OptIn(ExperimentalForeignApi::class)
actual fun Float.fixed(signs: Int): Float {
return memScoped {
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
sprintf(buff, "%.${signs}f", this@fixed)
buff.toKString().toFloat()
}
}
@OptIn(ExperimentalForeignApi::class)
actual fun Double.fixed(signs: Int): Double {
return memScoped {
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
sprintf(buff, "%.${signs}f", this@fixed)
buff.toKString().toDouble()
}
}

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
@@ -22,6 +22,7 @@ kotlin {
dependencies {
api libs.kt.coroutines.android
}
dependsOn(jvmMain)
}
}
}

View File

@@ -3,58 +3,24 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.inmo.micro_utils.common.applyDiff
import dev.inmo.micro_utils.coroutines.ExceptionHandler
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/**
* Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList]
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
scope: CoroutineScope
): SnapshotStateList<T> {
val state = mutableStateListOf<T>()
val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
{
withContext(useContextOnChange) {
state.applyDiff(it)
}
}
} ?: {
subscribeSafelyWithoutExceptions(scope) {
state.applyDiff(it)
}
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
return state
}
/**
* In fact, it is just classcast of [asMutableComposeListState] to [List]
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*
* @return Changing in time [List] which follow [Flow] values
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asComposeList(
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
): List<T> = asMutableComposeListState(scope, useContextOnChange, onException)
scope: CoroutineScope
): List<T> = asMutableComposeListState(scope)

View File

@@ -1,94 +1,35 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.compose.asState
import dev.inmo.micro_utils.coroutines.ExceptionHandler
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
import dev.inmo.micro_utils.coroutines.doInUI
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/**
* Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow]
*
* @param initial First value which will be passed to the result [MutableState]
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
fun <T> Flow<T>.asMutableComposeState(
initial: T,
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let {
{
withContext(useContextOnChange) {
state.value = it
}
}
} ?: {
state.value = it
}
subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
/**
* Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow].
* This conversation will pass its [StateFlow.value] as the first value
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asMutableComposeState(
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException)
scope: CoroutineScope
): MutableState<T> = asMutableComposeState(value, scope)
/**
* Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state
*
* @param initial First value which will be passed to the result [State]
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
fun <T> Flow<T>.asComposeState(
initial: T,
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
scope: CoroutineScope
): State<T> {
val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
return state.asState()
val state = asMutableComposeState(initial, scope)
return derivedStateOf { state.value }
}
/**
* Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value
*
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
*/
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asComposeState(
scope: CoroutineScope,
useContextOnChange: CoroutineContext? = Dispatchers.Main,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
): State<T> = asComposeState(value, scope, useContextOnChange, onException)
scope: CoroutineScope
): State<T> = asComposeState(value, scope)

View File

@@ -0,0 +1,23 @@
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 <T> Flow<T>.toMutableState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope
): MutableState<T> = toMutableState(value, scope)

View File

@@ -1,24 +0,0 @@
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import org.jetbrains.annotations.TestOnly
import kotlin.test.Test
class FlowStateTests {
@OptIn(ExperimentalTestApi::class)
@Test
@TestOnly
fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0)
setContent {
Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString())
}
onNodeWithText(0.toString()).assertExists()
onNodeWithText("Click").performClick()
onNodeWithText(1.toString()).assertExists()
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -16,8 +16,8 @@ class DoWithFirstBuilder<T>(
operator fun plus(block: suspend CoroutineScope.() -> T) {
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
}
fun add(block: suspend CoroutineScope.() -> T) = plus(block)
fun include(block: suspend CoroutineScope.() -> T) = plus(block)
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
fun build() = deferreds.toList()
}

View File

@@ -85,32 +85,32 @@ fun <T, M> Flow<T>.subscribeAsync(
return job
}
fun <T, M> Flow<T>.subscribeSafelyAsync(
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
block: suspend (T) -> Unit
noinline markerFactory: suspend (T) -> M,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
noinline block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
safely(onException) {
block(it)
}
}
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
block: suspend (T) -> Unit
noinline markerFactory: suspend (T) -> M,
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
safelyWithoutExceptions(onException) {
block(it)
}
}
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
block: suspend (T) -> Unit
noinline markerFactory: suspend (T) -> M,
noinline block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) {
safelyWithoutExceptions({ /* do nothing */}) {
block(it)

View File

@@ -51,7 +51,7 @@ class ContextSafelyExceptionHandler(
* @see ContextSafelyExceptionHandler
* @see ContextSafelyExceptionHandlerKey
*/
suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
/**
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
@@ -96,9 +96,9 @@ suspend fun <T> safelyWithContextExceptionHandler(
* @see safelyWithoutExceptions
* @see safelyWithContextExceptionHandler
*/
suspend fun <T> safely(
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
block: suspend CoroutineScope.() -> T
suspend inline fun <T> safely(
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
noinline block: suspend CoroutineScope.() -> T
): T {
return try {
supervisorScope(block)
@@ -108,26 +108,26 @@ suspend fun <T> safely(
}
}
suspend fun <T> runCatchingSafely(
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
block: suspend CoroutineScope.() -> T
suspend inline fun <T> runCatchingSafely(
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
noinline block: suspend CoroutineScope.() -> T
): Result<T> = runCatching {
safely(onException, block)
}
suspend fun <T, R> T.runCatchingSafely(
onException: ExceptionHandler<R> = defaultSafelyExceptionHandler,
block: suspend T.() -> R
suspend inline fun <T, R> T.runCatchingSafely(
noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler,
noinline block: suspend T.() -> R
): Result<R> = runCatching {
safely(onException) { block() }
}
suspend fun <T> safelyWithResult(
block: suspend CoroutineScope.() -> T
suspend inline fun <T> safelyWithResult(
noinline block: suspend CoroutineScope.() -> T
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
suspend fun <T, R> T.safelyWithResult(
block: suspend T.() -> R
suspend inline fun <T, R> T.safelyWithResult(
noinline block: suspend T.() -> R
): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block)
/**
@@ -147,21 +147,21 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
* result from exception (instead of throwing it, by default always returns null)
*/
suspend fun <T> safelyWithoutExceptions(
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
block: suspend CoroutineScope.() -> T
suspend inline fun <T> safelyWithoutExceptions(
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend CoroutineScope.() -> T
): T? = safely(onException, block)
suspend fun <T> runCatchingSafelyWithoutExceptions(
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
block: suspend CoroutineScope.() -> T
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend CoroutineScope.() -> T
): Result<T?> = runCatching {
safelyWithoutExceptions(onException, block)
}
fun CoroutineScope(
inline fun CoroutineScope(
context: CoroutineContext,
defaultExceptionsHandler: ExceptionHandler<Unit>
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
) = CoroutineScope(
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
)

View File

@@ -4,38 +4,38 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun CoroutineScope.launchSafely(
inline fun CoroutineScope.launchSafely(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
block: suspend CoroutineScope.() -> Unit
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
noinline block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
safely(onException, block)
}
fun CoroutineScope.launchSafelyWithoutExceptions(
inline fun CoroutineScope.launchSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
block: suspend CoroutineScope.() -> Unit
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend CoroutineScope.() -> Unit
) = launch(context, start) {
safelyWithoutExceptions(onException, block)
}
fun <T> CoroutineScope.asyncSafely(
inline fun <T> CoroutineScope.asyncSafely(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
block: suspend CoroutineScope.() -> T
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
noinline block: suspend CoroutineScope.() -> T
) = async(context, start) {
safely(onException, block)
}
fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
block: suspend CoroutineScope.() -> T
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
noinline block: suspend CoroutineScope.() -> T
) = async(context, start) {
safelyWithoutExceptions(onException, block)
}

View File

@@ -1,136 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the
* [SmartMutex] state.
*
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
* [Mutable] [SmartMutex]. Otherwise, [Immutable].
*
* Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its
* [Mutable] creator
*/
sealed interface SmartMutex {
val lockStateFlow: StateFlow<Boolean>
/**
* * True - locked
* * False - unlocked
*/
val isLocked: Boolean
get() = lockStateFlow.value
/**
* Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow]
*/
class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex
/**
* Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create
* [Immutable] variant of [this] instance with [immutable] factory
*
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/
class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex()
fun immutable() = Immutable(lockStateFlow)
/**
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
* wait for [isLocked] == false and then try to lock
*/
suspend fun lock() {
do {
waitUnlock()
val shouldContinue = internalChangesMutex.withLock {
if (_lockStateFlow.value) {
true
} else {
_lockStateFlow.value = true
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
}
/**
* Will try to lock this [SmartMutex] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryLock(): Boolean {
return if (!_lockStateFlow.value) {
internalChangesMutex.withLock {
if (!_lockStateFlow.value) {
_lockStateFlow.value = true
true
} else {
false
}
}
} else {
false
}
}
/**
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
* [SmartMutex] - false
*/
suspend fun unlock(): Boolean {
return if (_lockStateFlow.value) {
internalChangesMutex.withLock {
if (_lockStateFlow.value) {
_lockStateFlow.value = false
true
} else {
false
}
}
} else {
false
}
}
}
}
/**
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lock()
try {
return action()
} finally {
unlock()
}
}
/**
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
*
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again
*/
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it }

View File

@@ -1,105 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Composite mutex which works with next rules:
*
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
* * [releaseRead] will just free up one permit in [readSemaphore]
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
* * [unlockWrite] will just unlock [writeMutex]
*/
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
/**
* Do lock in [readSemaphore] inside of [writeMutex] locking
*/
suspend fun acquireRead() {
_writeMutex.waitUnlock()
_readSemaphore.acquire()
}
/**
* Release one read permit in [readSemaphore]
*/
suspend fun releaseRead(): Boolean {
return _readSemaphore.release()
}
/**
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
*/
suspend fun lockWrite() {
_writeMutex.lock()
_readSemaphore.acquire(readPermits)
}
/**
* Unlock [writeMutex]
*/
suspend fun unlockWrite(): Boolean {
return _writeMutex.unlock().also {
if (it) {
_readSemaphore.release(readPermits)
}
}
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquireRead()
try {
return action()
} finally {
releaseRead()
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
/**
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lockWrite()
try {
return action()
} finally {
unlockWrite()
}
}
/**
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
*
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()

View File

@@ -1,168 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
* [SmartSemaphore] state.
*
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
*
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
* [Mutable] creator
*/
sealed interface SmartSemaphore {
val permitsStateFlow: StateFlow<Int>
/**
* * True - locked
* * False - unlocked
*/
val freePermits: Int
get() = permitsStateFlow.value
/**
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
*/
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
/**
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
* [Immutable] variant of [this] instance with [immutable] factory
*
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
*/
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
*/
suspend fun acquire(permits: Int = 1) {
var acquiredPermits = 0
val checkedPermits = checkedPermits(permits)
try {
do {
val shouldContinue = internalChangesMutex.withLock {
val requiredPermits = checkedPermits - acquiredPermits
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true
acquiredPermits += acquiring
_freePermitsStateFlow.value -= acquiring
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
}
} while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) {
release(acquiredPermits)
throw e
}
}
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
* wait for [freePermits] == false and then try to lock
*/
suspend fun acquireByOne(permits: Int = 1) {
val checkedPermits = checkedPermits(permits)
do {
waitRelease(checkedPermits)
val shouldContinue = internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < checkedPermits) {
true
} else {
_freePermitsStateFlow.value -= checkedPermits
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
}
/**
* Will try to lock this [SmartSemaphore] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryAcquire(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < checkedPermits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < checkedPermits) {
_freePermitsStateFlow.value -= checkedPermits
true
} else {
false
}
}
} else {
false
}
}
/**
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false
*/
suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value < this.permits) {
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < this.permits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
true
} else {
false
}
}
} else {
false
}
}
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquire(permits)
try {
return action()
} finally {
release(permits)
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }

View File

@@ -1,70 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber
*/
open class SpecialMutableStateFlow<T>(
initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class)
private val syncObject = SynchronizedObject()
protected val sharingFlow: MutableSharedFlow<T> = MutableSharedFlow(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
@OptIn(InternalCoroutinesApi::class)
override var value: T = initialValue
set(value) {
synchronized(syncObject) {
if (field != value) {
field = value
sharingFlow.tryEmit(value)
}
}
}
override val replayCache: List<T>
get() = sharingFlow.replayCache
override val subscriptionCount: StateFlow<Int>
get() = sharingFlow.subscriptionCount
init {
sharingFlow.tryEmit(initialValue)
}
override fun compareAndSet(expect: T, update: T): Boolean {
if (expect == value) {
value = update
}
return expect == value
}
@ExperimentalCoroutinesApi
override fun resetReplayCache() = sharingFlow.resetReplayCache()
override fun tryEmit(value: T): Boolean {
return compareAndSet(this.value, value)
}
override suspend fun emit(value: T) {
compareAndSet(this.value, value)
}
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
}

View File

@@ -1,151 +0,0 @@
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SmartRWLockerTests {
@Test
fun compositeTest() {
val locker = SmartRWLocker()
val readAndWriteWorkers = 10
runTest {
var started = 0
var done = 0
val doneMutex = Mutex()
val readWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withReadAcquire {
doneMutex.withLock {
started++
}
delay(100L)
doneMutex.withLock {
done++
}
}
}
}
var doneWrites = 0
val writeWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withWriteLock {
assertTrue(done == readAndWriteWorkers || started == 0)
delay(10L)
doneWrites++
}
}
}
readWorkers.forEach { it.start() }
writeWorkers.forEach { it.start() }
readWorkers.joinAll()
writeWorkers.joinAll()
assertEquals(expected = readAndWriteWorkers, actual = done)
assertEquals(expected = readAndWriteWorkers, actual = doneWrites)
}
}
@Test
fun simpleWithWriteLockTest() {
val locker = SmartRWLocker()
runTest {
locker.withWriteLock {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
@Test
fun failureWithWriteLockTest() {
val locker = SmartRWLocker()
val exception = IllegalArgumentException()
try {
runTest {
val subscope = kotlinx.coroutines.CoroutineScope(this.coroutineContext)
var happenException: Throwable? = null
try {
locker.withWriteLock {
val checkFunction = fun (): Deferred<Unit> {
return subscope.async {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
throw exception
}
}
doInDefault {
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
checkFunction().await()
}
}
} catch (e: Exception) {
happenException = e
}
if (exception != happenException) {
assertEquals(exception, happenException ?.cause)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
} catch (e: Exception) {
assertEquals(exception, e)
}
}
@Test
fun simpleWithReadAcquireTest() {
val locker = SmartRWLocker()
runTest {
locker.withReadAcquire {
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
locker.withReadAcquire {
assertEquals(Int.MAX_VALUE - 2, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
@Test
fun simple2WithWriteLockTest() {
val locker = SmartRWLocker()
val unlockDelay = 1000L // 1 sec
var unlocked: Boolean = false
runTest {
launch {
locker.withReadAcquire {
delay(unlockDelay)
}
unlocked = true
}
locker.readSemaphore.permitsStateFlow.first { it == Int.MAX_VALUE - 1 }
assertEquals(false, unlocked)
locker.withWriteLock {
assertEquals(true, unlocked)
assertEquals(0, locker.readSemaphore.freePermits)
assertEquals(true, locker.writeMutex.isLocked)
}
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
assertEquals(false, locker.writeMutex.isLocked)
}
}
}

View File

@@ -1,33 +0,0 @@
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests {
@Test
fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
specialMutableStateFlow.value = 1
specialMutableStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value)
}
@Test
fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) {
when (it) {
1 -> specialMutableStateFlow.value = 2
2 -> subscriberJob.cancel()
}
}
specialMutableStateFlow.value = 1
subscriberJob.join()
assertEquals(2, specialMutableStateFlow.value)
}
}

View File

@@ -1,26 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import org.w3c.dom.Image
suspend fun preloadImage(src: String): Image {
val image = Image()
image.src = src
val job = Job()
image.addEventListener("load", {
runCatching { job.complete() }
})
runCatchingSafely {
job.join()
}.onFailure {
if (it is CancellationException) {
image.src = ""
}
}.getOrThrow()
return image
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.coroutines"/>

View File

@@ -4,14 +4,13 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api libs.krypto
}
}
jsMain {

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,8 +1,6 @@
package dev.inmo.micro_utils.crypto
import korlibs.crypto.md5
typealias MD5 = String
fun SourceBytes.md5(): MD5 = md5().hexLower
fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower
expect fun SourceBytes.md5(): MD5
fun SourceString.md5(): MD5 = encodeToByteArray().md5()

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.crypto
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.crypto
import java.math.BigInteger
import java.security.MessageDigest
/**
* @suppress
*/
actual fun SourceBytes.md5(): MD5 = BigInteger(
1,
MessageDigest.getInstance("MD5").digest(this)
).toString(16)

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.crypto"/>

View File

@@ -1,5 +1,30 @@
apply plugin: 'com.getkeepsafe.dexcount'
ext {
jvmKotlinFolderFile = {
String sep = File.separator
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
}
enableIncludingJvmCodeInAndroidPart = {
File jvmKotlinFolder = jvmKotlinFolderFile()
if (jvmKotlinFolder.exists()) {
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
}
}
disableIncludingJvmCodeInAndroidPart = {
File jvmKotlinFolder = jvmKotlinFolderFile()
String[] oldDirs = android.sourceSets.main.java.srcDirs
android.sourceSets.main.java.srcDirs = []
for (oldDir in oldDirs) {
if (oldDir != jvmKotlinFolder.path) {
android.sourceSets.main.java.srcDirs += oldDir
}
}
}
}
android {
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get()
@@ -9,7 +34,6 @@ android {
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
namespace "${project.group}.${project.name}"
}
buildTypes {
release {
@@ -27,7 +51,17 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
String sep = File.separator
main.java.srcDirs += "src${sep}main${sep}kotlin"
enableIncludingJvmCodeInAndroidPart()
}
}

View File

@@ -13,10 +13,10 @@ repositories {
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
// js(IR) {
// browser()
// nodejs()
// }
android {}
sourceSets {
@@ -26,44 +26,44 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
api it
}
}
}
}
jsMain {
dependencies {
implementation kotlin('stdlib')
// jsMain {
// dependencies {
// implementation kotlin('stdlib')
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
) {
api it
}
}
}
}
// project.parent.subprojects.forEach {
// if (
// it != project
// && it.hasProperty("kotlin")
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
// ) {
// api it
// }
// }
// }
// }
jvmMain {
dependencies {
implementation kotlin('stdlib')
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
) {
api it
}
@@ -76,10 +76,10 @@ kotlin {
project.parent.subprojects.forEach {
if (
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
it != project
&& it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
) {
api it
}
@@ -106,7 +106,7 @@ tasks.dokkaHtml {
skipDeprecated.set(true)
sourceLink {
localDirectory.set(file("../"))
localDirectory.set(file("./"))
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
remoteLineSuffix.set("#L")
}
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
sourceRoots.setFrom(findSourcesWithName("commonMain"))
}
named("jsMain") {
sourceRoots.setFrom(findSourcesWithName("jsMain"))
}
// named("jsMain") {
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
// }
named("jvmMain") {
sourceRoots.setFrom(findSourcesWithName("jvmMain"))

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.dokka"/>

View File

@@ -19,24 +19,16 @@ allprojects {
}
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
// String compilerPluginVersionFromProperties = (String) project.properties["compose.kotlinCompilerPluginVersion"]
// String compilerPluginVersionFromLibrariesVersions = libs.versions.compose.kotlin.get()
// composePluginKotlinVersion = compilerPluginVersionFromProperties
// if (compilerPluginVersionFromProperties == null) {
// composePluginKotlinVersion = compilerPluginVersionFromLibrariesVersions
// }
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.gradle"
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
mppJvmJsLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwLinuxArm64Project.gradle"
mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
mppJsAndJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsAndJavaProject.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
}
}

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -48,8 +48,8 @@ interface DefaultStatesManagerRepo<T : State> {
*/
open class DefaultStatesManager<T : State>(
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> false },
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> false }
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) : StatesManager<T> {
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.fsm.common"/>

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>

View File

@@ -18,13 +18,13 @@ if (new File(projectDir, "secret.gradle").exists()) {
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner = "InsanusMokrassar"
repo = "MicroUtils"
owner "InsanusMokrassar"
repo "MicroUtils"
tagName = "v${project.version}"
releaseName = "${project.version}"
targetCommitish = "${project.version}"
tagName "v${project.version}"
releaseName "${project.version}"
targetCommitish "${project.version}"
body = getCurrentVersionChangelog()
body getCurrentVersionChangelog()
}
}

View File

@@ -3,10 +3,9 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
#kotlin.experimental.tryK2=true
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
org.gradle.jvmargs=-Xmx2g
# JS NPM
@@ -15,5 +14,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.20.51
android_code_version=257
version=0.16.3
android_code_version=171

View File

@@ -1,45 +1,37 @@
[versions]
kt = "1.9.23"
kt-serialization = "1.6.3"
kt-coroutines = "1.8.1"
kt = "1.7.20"
kt-serialization = "1.4.1"
kt-coroutines = "1.6.4"
kslog = "1.3.4"
kslog = "0.5.4"
jb-compose = "1.6.2"
jb-exposed = "0.50.1"
jb-dokka = "1.9.20"
jb-compose = "1.2.2"
jb-exposed = "0.41.1"
jb-dokka = "1.7.20"
korlibs = "5.4.0"
uuid = "0.8.4"
klock = "3.4.0"
uuid = "0.6.0"
ktor = "2.3.11"
ktor = "2.2.1"
gh-release = "2.5.2"
gh-release = "2.4.1"
koin = "3.5.6"
koin = "3.2.2"
okio = "3.9.0"
android-gradle = "7.3.0"
dexcount = "3.1.0"
ksp = "1.9.23-1.0.20"
kotlin-poet = "1.16.0"
versions = "0.51.0"
android-gradle = "8.2.0"
dexcount = "4.0.0"
android-coreKtx = "1.13.1"
android-recyclerView = "1.3.2"
android-appCompat = "1.6.1"
android-fragment = "1.7.1"
android-espresso = "3.5.1"
android-test = "1.1.5"
android-compose-material3 = "1.2.1"
android-coreKtx = "1.9.0"
android-recyclerView = "1.2.1"
android-appCompat = "1.5.1"
android-fragment = "1.5.5"
android-espresso = "3.4.0"
android-test = "1.1.3"
android-props-minSdk = "21"
android-props-compileSdk = "34"
android-props-buildTools = "34.0.0"
android-props-compileSdk = "33"
android-props-buildTools = "33.0.0"
[libraries]
@@ -72,8 +64,7 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
kslog = { module = "dev.inmo:kslog", version.ref = "kslog" }
klock = { module = "com.soywiz.korge:korlibs-time", version.ref = "korlibs" }
krypto = { module = "com.soywiz.korge:korlibs-crypto", version.ref = "korlibs" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
@@ -84,7 +75,6 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
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" }
@@ -93,18 +83,9 @@ android-test-junit = { module = "androidx.test.ext:junit", version.ref = "androi
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
# ksp dependencies
kotlin-poet = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" }
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
# Buildscript
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-kt-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
@@ -113,5 +94,3 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
[plugins]
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }

Some files were not shown because too many files have changed in this diff Show More