mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-11-02 10:03:01 +00:00
Compare commits
142
Commits
0.18.2
...
22d7ac3e22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d7ac3e22 | ||
|
|
9b30c3a155 | ||
|
|
915bac64b1 | ||
|
|
9d2b50e55d | ||
|
|
bde100f63d | ||
|
|
05b035a13d | ||
|
|
eefb56bed7 | ||
|
|
fcc0dc4189 | ||
|
|
47ff20317f | ||
|
|
1558b9103d | ||
|
|
7a78742162 | ||
|
|
c01e240f66 | ||
|
|
fef4fcbac6 | ||
|
|
5ab18bce4b | ||
|
|
24aec7271a | ||
|
|
9b19a2cb95 | ||
|
|
efdd7b8a57 | ||
|
|
6df4cc9c3b | ||
|
|
b9d93db0f5 | ||
|
|
d7ee45ca64 | ||
|
|
d7c31b1b22 | ||
|
|
7d6794a358 | ||
|
|
473eb87346 | ||
|
|
8b18b07790 | ||
|
|
ab3c80a5ec | ||
|
|
075b93ecd6 | ||
|
|
f6d0f72e49 | ||
|
|
fcda3af862 | ||
|
|
d5c7a589b1 | ||
|
|
86e099ed25 | ||
|
|
ebd7befe73 | ||
|
|
b8c7e581a1 | ||
|
|
8281259179 | ||
|
|
d3e06b07df | ||
|
|
4967018418 | ||
|
|
537a3c38fa | ||
|
|
0124957833 | ||
|
|
f0420e2d61 | ||
|
|
7090566041 | ||
|
|
f0d5035cd0 | ||
|
|
0fb9b8dc30 | ||
|
|
eef6e81134 | ||
|
|
1593159a3f | ||
|
|
3da9eb9dbe | ||
|
|
f17613f3fb | ||
|
|
14337ccb46 | ||
|
|
1a3913b09c | ||
|
|
039aed2747 | ||
|
|
173991e3cb | ||
|
|
8b3f8cab01 | ||
|
|
2a20d24589 | ||
|
|
53c2d552ec | ||
|
|
af11c1a83d | ||
|
|
a65cf1481c | ||
|
|
0318716236 | ||
|
|
88eb4b3342 | ||
|
|
4810d1ef6a | ||
|
|
f2a9514d89 | ||
|
|
925ba6ac24 | ||
|
|
ef407268a2 | ||
|
|
cd6c4bbe38 | ||
|
|
c058e18408 | ||
|
|
6d3ca565ca | ||
|
|
236a7b4fd2 | ||
|
|
e1f387dbf7 | ||
|
|
3d113dd31e | ||
|
|
e0e57f0336 | ||
|
|
e775b58d41 | ||
|
|
5b070a8478 | ||
|
|
8a61193500 | ||
|
|
fad522b8fe | ||
|
|
0acac205af | ||
|
|
069d4c61b7 | ||
|
|
7c113f5700 | ||
|
|
dfdaf4225b | ||
|
|
bd39ab2467 | ||
|
|
6ce1eb3f2d | ||
|
|
ce7d4fe9a2 | ||
|
|
a65bb2f419 | ||
|
|
cbc868448b | ||
|
|
9c336a0b56 | ||
|
|
0f0d09399e | ||
|
|
e13a1162a9 | ||
|
|
57ebed903f | ||
|
|
4478193d8a | ||
|
|
ee948395e3 | ||
|
|
0616b051ae | ||
|
|
4d155d0505 | ||
|
|
a169e733d9 | ||
|
|
f081e237c8 | ||
|
|
f412d387fa | ||
|
|
67354b43e2 | ||
|
|
39135a4000 | ||
|
|
eaa014cebd | ||
|
|
856e657f81 | ||
|
|
3a609e5b66 | ||
|
|
d0022dd599 | ||
|
|
7ba6eed453 | ||
|
|
beeb6ecc0a | ||
|
|
7cdc17a714 | ||
|
|
4765a950a9 | ||
|
|
65e8137e08 | ||
|
|
ae546dd9ad | ||
|
|
8110c42be0 | ||
|
|
bd2b5ae5fc | ||
|
|
2ddfffa6a9 | ||
|
|
a4b54e861d | ||
|
|
c6785f1a4f | ||
|
|
83fe621c56 | ||
|
|
b3a93e17eb | ||
|
|
546a391af3 | ||
|
|
786cf9bd8b | ||
|
|
dfd6fe062d | ||
|
|
b6ef818613 | ||
|
|
b0f9e9c30a | ||
|
|
7e5c88ddc3 | ||
|
|
9824c3e00f | ||
|
|
9171d5ed11 | ||
|
|
a1830ebb82 | ||
|
|
22d2a3d9bf | ||
|
|
c9b97fc965 | ||
|
|
51f85becd5 | ||
|
|
a8a281cfb4 | ||
|
|
ddd1304949 | ||
|
|
86d70b6c02 | ||
|
|
a22bdb39e7 | ||
|
|
7ae4d5ef95 | ||
|
|
a2038cbefa | ||
|
|
992091eade | ||
|
|
e3bfead0c5 | ||
|
|
0de96141fd | ||
|
|
fa18d15c3c | ||
|
|
ea9dbf2371 | ||
|
|
d34e3ec7a9 | ||
|
|
c8833a36af | ||
|
|
a067cb0c0f | ||
|
|
999c8327bd | ||
|
|
c2ec73c70a | ||
|
|
702f782fc1 | ||
|
|
25dbcaaf83 | ||
|
|
b00d454a24 | ||
|
|
dbc921d56d |
No files matched your search
@@ -8,7 +8,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
- name: Build
|
||||
run: ./gradlew build && ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
|
||||
+219
@@ -1,5 +1,224 @@
|
||||
# Changelog
|
||||
|
||||
## 0.20.16
|
||||
|
||||
* `Coroutines`:
|
||||
* Add `SpecialStateFlow`
|
||||
|
||||
## 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`:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
||||
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.android.smalltextfield")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
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.ContentAlpha
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
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 = ContentAlpha.disabled)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
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.ContentAlpha
|
||||
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 boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset
|
||||
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
||||
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.compose.material3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,66 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,10 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@@ -38,3 +42,4 @@ allprojects {
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
apply from: "./github_release.gradle"
|
||||
apply from: "./versions_plugin_setup.gradle"
|
||||
+6
-1
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
@@ -31,5 +31,10 @@ kotlin {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
linuxArm64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,36 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
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)
|
||||
@@ -16,6 +14,7 @@ actual fun Float.fixed(signs: Int): Float {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common"/>
|
||||
@@ -1,12 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
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)
|
||||
@@ -16,6 +14,7 @@ actual fun Float.fixed(signs: Int): Float {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||
import dev.inmo.micro_utils.coroutines.doInUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
class FlowState<T>(
|
||||
private val state: MutableState<T>,
|
||||
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
) : MutableState<T>,
|
||||
SpecialMutableStateFlow.Default<T>(state.value, internalScope) {
|
||||
override var value: T
|
||||
get() = super.value
|
||||
set(value) {
|
||||
tryEmit(value)
|
||||
}
|
||||
|
||||
override fun component1(): T = state.component1()
|
||||
|
||||
override fun component2(): (T) -> Unit = state.component2()
|
||||
|
||||
private val stateState = derivedStateOf {
|
||||
if (value != state.value) {
|
||||
value = state.value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onChange(value: T) {
|
||||
super.onChange(value)
|
||||
|
||||
if (state.value != value) {
|
||||
doInUI {
|
||||
state.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> MutableState<T>.asFlowState(scope: CoroutineScope = CoroutineScope(Dispatchers.Main)) = FlowState(this, scope)
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,105 @@
|
||||
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()
|
||||
@@ -0,0 +1,168 @@
|
||||
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 = MutableStateFlow<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 }
|
||||
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
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
|
||||
|
||||
interface SpecialMutableStateFlow<T> : StateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
|
||||
open class Default<T>(
|
||||
initialValue: T,
|
||||
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : SpecialMutableStateFlow<T> {
|
||||
protected val internalSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
protected val publicSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
protected var _value: T = initialValue
|
||||
override val value: T
|
||||
get() = _value
|
||||
protected open suspend fun onChange(value: T) {
|
||||
_value = value
|
||||
publicSharedFlow.emit(value)
|
||||
}
|
||||
protected val job = internalSharedFlow.subscribeSafelyWithoutExceptions(internalScope) {
|
||||
if (_value != it) {
|
||||
onChange(it)
|
||||
}
|
||||
}
|
||||
|
||||
override val replayCache: List<T>
|
||||
get() = publicSharedFlow.replayCache
|
||||
override val subscriptionCount: StateFlow<Int>
|
||||
get() = publicSharedFlow.subscriptionCount
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun resetReplayCache() = publicSharedFlow.resetReplayCache()
|
||||
|
||||
override fun tryEmit(value: T): Boolean {
|
||||
return internalSharedFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override suspend fun emit(value: T) {
|
||||
internalSharedFlow.emit(value)
|
||||
}
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<T>) = publicSharedFlow.collect(collector)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines"/>
|
||||
+1
-1
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import com.soywiz.krypto.md5
|
||||
import korlibs.crypto.md5
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
||||
@@ -9,6 +9,7 @@ android {
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
namespace "${project.group}.${project.name}"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -26,11 +27,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
+37
-37
@@ -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", "commonMain"))
|
||||
// }
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain"))
|
||||
}
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain"))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.dokka"/>
|
||||
+9
-1
@@ -19,11 +19,19 @@ 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}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.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"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
||||
+4
-3
@@ -3,9 +3,10 @@ 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
|
||||
org.gradle.jvmargs=-Xmx2500m
|
||||
|
||||
# JS NPM
|
||||
|
||||
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.18.2
|
||||
android_code_version=193
|
||||
version=0.20.16
|
||||
android_code_version=222
|
||||
+26
-20
@@ -1,42 +1,45 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.8.20"
|
||||
kt-serialization = "1.5.0"
|
||||
kt-coroutines = "1.6.4"
|
||||
kt = "1.9.21"
|
||||
kt-serialization = "1.6.1"
|
||||
kt-coroutines = "1.7.3"
|
||||
|
||||
kslog = "1.1.1"
|
||||
kslog = "1.3.1"
|
||||
|
||||
jb-compose = "1.4.0"
|
||||
jb-exposed = "0.41.1"
|
||||
jb-dokka = "1.8.10"
|
||||
jb-compose = "1.5.11"
|
||||
jb-exposed = "0.45.0"
|
||||
jb-dokka = "1.9.10"
|
||||
|
||||
korlibs = "3.4.0"
|
||||
uuid = "0.7.0"
|
||||
korlibs = "4.0.10"
|
||||
uuid = "0.8.2"
|
||||
|
||||
ktor = "2.3.0"
|
||||
ktor = "2.3.6"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
koin = "3.4.0"
|
||||
koin = "3.5.0"
|
||||
|
||||
okio = "3.3.0"
|
||||
okio = "3.6.0"
|
||||
|
||||
ksp = "1.8.20-1.0.11"
|
||||
kotlin-poet = "1.13.0"
|
||||
ksp = "1.9.21-1.0.15"
|
||||
kotlin-poet = "1.15.1"
|
||||
|
||||
android-gradle = "7.4.2"
|
||||
versions = "0.50.0"
|
||||
|
||||
android-gradle = "8.1.4"
|
||||
dexcount = "4.0.0"
|
||||
|
||||
android-coreKtx = "1.10.0"
|
||||
android-recyclerView = "1.3.0"
|
||||
android-coreKtx = "1.12.0"
|
||||
android-recyclerView = "1.3.2"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.5.7"
|
||||
android-fragment = "1.6.2"
|
||||
android-espresso = "3.5.1"
|
||||
android-test = "1.1.5"
|
||||
android-compose-material3 = "1.1.2"
|
||||
|
||||
android-props-minSdk = "21"
|
||||
android-props-compileSdk = "33"
|
||||
android-props-buildTools = "33.0.2"
|
||||
android-props-compileSdk = "34"
|
||||
android-props-buildTools = "34.0.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -81,6 +84,7 @@ 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" }
|
||||
@@ -109,3 +113,5 @@ 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" }
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+8
-2
@@ -2,11 +2,11 @@ apply plugin: 'maven-publish'
|
||||
|
||||
task javadocJar(type: Jar) {
|
||||
from javadoc
|
||||
classifier = 'javadoc'
|
||||
archiveClassifier = 'javadoc'
|
||||
}
|
||||
task sourcesJar(type: Jar) {
|
||||
from sourceSets.main.allSource
|
||||
classifier = 'sources'
|
||||
archiveClassifier = 'sources'
|
||||
}
|
||||
|
||||
publishing {
|
||||
@@ -115,4 +115,10 @@ if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
dependsOn(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround to make android sign operations depend on signing tasks
|
||||
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
|
||||
def signingTasks = project.getTasks().withType(Sign.class)
|
||||
mustRunAfter(signingTasks)
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,8 @@ dependencies {
|
||||
api libs.kotlin.poet
|
||||
api libs.ksp
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
@@ -9,19 +9,28 @@ import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSFile
|
||||
import com.google.devtools.ksp.symbol.Modifier
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import com.squareup.kotlinpoet.ksp.toClassName
|
||||
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||
import com.squareup.kotlinpoet.ksp.writeTo
|
||||
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
||||
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.scope.Scope
|
||||
import java.io.File
|
||||
import kotlin.reflect.KClass
|
||||
@@ -32,11 +41,190 @@ class Processor(
|
||||
private val definitionClassName = ClassName("org.koin.core.definition", "Definition")
|
||||
private val koinDefinitionClassName = ClassName("org.koin.core.definition", "KoinDefinition")
|
||||
|
||||
private fun FileSpec.Builder.addCodeForType(
|
||||
targetType: TypeName,
|
||||
name: String,
|
||||
nullable: Boolean,
|
||||
generateSingle: Boolean,
|
||||
generateFactory: Boolean,
|
||||
) {
|
||||
val targetTypeAsGenericType = (targetType as? TypeVariableName) ?.copy(reified = true)
|
||||
|
||||
fun addGetterProperty(
|
||||
receiver: KClass<*>
|
||||
) {
|
||||
addProperty(
|
||||
PropertySpec.builder(
|
||||
name,
|
||||
targetType,
|
||||
).apply {
|
||||
addKdoc(
|
||||
"""
|
||||
@return Definition by key "${name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
getter(
|
||||
FunSpec.getterBuilder().apply {
|
||||
targetTypeAsGenericType ?.let {
|
||||
addModifiers(KModifier.INLINE)
|
||||
}
|
||||
addCode(
|
||||
"return " + (if (nullable) {
|
||||
"getOrNull"
|
||||
} else {
|
||||
"get"
|
||||
}) + "(named(\"${name}\"))"
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
targetTypeAsGenericType ?.let {
|
||||
addTypeVariable(it)
|
||||
}
|
||||
receiver(receiver)
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
|
||||
if (targetTypeAsGenericType == null) {
|
||||
addGetterProperty(Scope::class)
|
||||
addGetterProperty(Koin::class)
|
||||
}
|
||||
|
||||
val parametersDefinitionClassName = ClassName(
|
||||
"org.koin.core.parameter",
|
||||
"ParametersDefinition"
|
||||
)
|
||||
fun addGetterMethod(
|
||||
receiver: KClass<*>
|
||||
) {
|
||||
addFunction(
|
||||
FunSpec.builder(
|
||||
name
|
||||
).apply {
|
||||
addKdoc(
|
||||
"""
|
||||
@return Definition by key "${name}" with [parameters]
|
||||
""".trimIndent()
|
||||
)
|
||||
receiver(receiver)
|
||||
addParameter(
|
||||
ParameterSpec(
|
||||
"parameters",
|
||||
parametersDefinitionClassName.let {
|
||||
if (targetTypeAsGenericType != null) {
|
||||
it.copy(nullable = true)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
KModifier.NOINLINE
|
||||
).toBuilder().apply {
|
||||
if (targetTypeAsGenericType != null) {
|
||||
defaultValue("null")
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
addModifiers(KModifier.INLINE)
|
||||
targetTypeAsGenericType ?.let {
|
||||
addTypeVariable(it)
|
||||
returns(it.copy(nullable = nullable))
|
||||
} ?: returns(targetType)
|
||||
addCode(
|
||||
"return " + (if (nullable) {
|
||||
"getOrNull"
|
||||
} else {
|
||||
"get"
|
||||
}) + "(named(\"${name}\"), parameters)"
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
|
||||
addGetterMethod(Scope::class)
|
||||
addGetterMethod(Koin::class)
|
||||
|
||||
fun FunSpec.Builder.addDefinitionParameter() {
|
||||
val definitionModifiers = if (targetTypeAsGenericType == null) {
|
||||
arrayOf()
|
||||
} else {
|
||||
arrayOf(KModifier.NOINLINE)
|
||||
}
|
||||
addParameter(
|
||||
ParameterSpec.builder(
|
||||
"definition",
|
||||
definitionClassName.parameterizedBy(targetType.copy(nullable = false)),
|
||||
*definitionModifiers
|
||||
).build()
|
||||
)
|
||||
}
|
||||
|
||||
if (generateSingle) {
|
||||
fun FunSpec.Builder.configure() {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.single] and key "${name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
receiver(Module::class)
|
||||
addParameter(
|
||||
ParameterSpec.builder(
|
||||
"createdAtStart",
|
||||
Boolean::class
|
||||
).apply {
|
||||
defaultValue("false")
|
||||
}.build()
|
||||
)
|
||||
addDefinitionParameter()
|
||||
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||
addCode(
|
||||
"return single(named(\"${name}\"), createdAtStart = createdAtStart, definition = definition)"
|
||||
)
|
||||
targetTypeAsGenericType ?.let {
|
||||
addTypeVariable(it)
|
||||
addModifiers(KModifier.INLINE)
|
||||
}
|
||||
}
|
||||
|
||||
val actualSingleName = "single${name.replaceFirstChar { it.uppercase() }}"
|
||||
|
||||
addFunction(
|
||||
FunSpec.builder(actualSingleName).apply { configure() }.build()
|
||||
)
|
||||
}
|
||||
|
||||
if (generateFactory) {
|
||||
fun FunSpec.Builder.configure() {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
receiver(Module::class)
|
||||
addDefinitionParameter()
|
||||
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||
addCode(
|
||||
"return factory(named(\"${name}\"), definition = definition)"
|
||||
)
|
||||
targetTypeAsGenericType ?.let {
|
||||
addTypeVariable(it)
|
||||
addModifiers(KModifier.INLINE)
|
||||
}
|
||||
}
|
||||
val actualFactoryName = "factory${name.replaceFirstChar { it.uppercase() }}"
|
||||
addFunction(
|
||||
FunSpec.builder(actualFactoryName).apply { configure() }.build()
|
||||
)
|
||||
}
|
||||
addImport("org.koin.core.qualifier", "named")
|
||||
}
|
||||
|
||||
@OptIn(KspExperimental::class)
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
resolver.getSymbolsWithAnnotation(
|
||||
(resolver.getSymbolsWithAnnotation(
|
||||
GenerateKoinDefinition::class.qualifiedName!!
|
||||
).filterIsInstance<KSFile>().forEach { ksFile ->
|
||||
) + resolver.getSymbolsWithAnnotation(
|
||||
GenerateGenericKoinDefinition::class.qualifiedName!!
|
||||
)).filterIsInstance<KSFile>().forEach { ksFile ->
|
||||
FileSpec.builder(
|
||||
ksFile.packageName.asString(),
|
||||
"GeneratedDefinitions${ksFile.fileName.removeSuffix(".kt")}"
|
||||
@@ -72,92 +260,12 @@ class Processor(
|
||||
}.copy(
|
||||
nullable = it.nullable
|
||||
)
|
||||
fun addGetterProperty(
|
||||
receiver: KClass<*>
|
||||
) {
|
||||
addProperty(
|
||||
PropertySpec.builder(
|
||||
it.name,
|
||||
targetType,
|
||||
).apply {
|
||||
addKdoc(
|
||||
"""
|
||||
@return Definition by key "${it.name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
getter(
|
||||
FunSpec.getterBuilder().apply {
|
||||
addCode(
|
||||
"return " + (if (it.nullable) {
|
||||
"getOrNull"
|
||||
} else {
|
||||
"get"
|
||||
}) + "(named(\"${it.name}\"))"
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
receiver(receiver)
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
|
||||
addGetterProperty(Scope::class)
|
||||
addGetterProperty(Koin::class)
|
||||
|
||||
if (it.generateSingle) {
|
||||
addFunction(
|
||||
FunSpec.builder("${it.name}Single").apply {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.single] and key "${it.name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
receiver(Module::class)
|
||||
addParameter(
|
||||
ParameterSpec.builder(
|
||||
"createdAtStart",
|
||||
Boolean::class
|
||||
).apply {
|
||||
defaultValue("false")
|
||||
}.build()
|
||||
)
|
||||
addParameter(
|
||||
ParameterSpec.builder(
|
||||
"definition",
|
||||
definitionClassName.parameterizedBy(targetType.copy(nullable = false))
|
||||
).build()
|
||||
)
|
||||
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||
addCode(
|
||||
"return single(named(\"${it.name}\"), createdAtStart = createdAtStart, definition = definition)"
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
|
||||
if (it.generateFactory) {
|
||||
addFunction(
|
||||
FunSpec.builder("${it.name}Factory").apply {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.factory] and key "${it.name}"
|
||||
""".trimIndent()
|
||||
)
|
||||
receiver(Module::class)
|
||||
addParameter(
|
||||
ParameterSpec.builder(
|
||||
"definition",
|
||||
definitionClassName.parameterizedBy(targetType.copy(nullable = false))
|
||||
).build()
|
||||
)
|
||||
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||
addCode(
|
||||
"return factory(named(\"${it.name}\"), definition = definition)"
|
||||
)
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
addImport("org.koin.core.qualifier", "named")
|
||||
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
|
||||
}
|
||||
ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
|
||||
val targetType = TypeVariableName("T", Any::class)
|
||||
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
|
||||
}
|
||||
}.build().let {
|
||||
File(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -3,12 +3,15 @@
|
||||
// ORIGINAL FILE: Test.kt
|
||||
package dev.inmo.micro_utils.koin.generator.test
|
||||
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Deprecated
|
||||
import kotlin.String
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.definition.Definition
|
||||
import org.koin.core.definition.KoinDefinition
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
@@ -24,15 +27,98 @@ public val Scope.sampleInfo: Test<String>
|
||||
public val Koin.sampleInfo: Test<String>
|
||||
get() = get(named("sampleInfo"))
|
||||
|
||||
/**
|
||||
* @return Definition by key "sampleInfo" with [parameters]
|
||||
*/
|
||||
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||
get(named("sampleInfo"), parameters)
|
||||
|
||||
/**
|
||||
* @return Definition by key "sampleInfo" with [parameters]
|
||||
*/
|
||||
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||
get(named("sampleInfo"), parameters)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||
*/
|
||||
@Deprecated(
|
||||
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
|
||||
ReplaceWith("singleSampleInfo"),
|
||||
)
|
||||
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
|
||||
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||
*/
|
||||
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
|
||||
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||
*/
|
||||
@Deprecated(
|
||||
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
|
||||
ReplaceWith("factorySampleInfo"),
|
||||
)
|
||||
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
|
||||
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||
*/
|
||||
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
|
||||
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||
|
||||
/**
|
||||
* @return Definition by key "test" with [parameters]
|
||||
*/
|
||||
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T
|
||||
= get(named("test"), parameters)
|
||||
|
||||
/**
|
||||
* @return Definition by key "test" with [parameters]
|
||||
*/
|
||||
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T
|
||||
= get(named("test"), parameters)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "test"
|
||||
*/
|
||||
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline
|
||||
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
|
||||
createdAtStart, definition = definition)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
|
||||
*/
|
||||
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>):
|
||||
KoinDefinition<T> = factory(named("test"), definition = definition)
|
||||
|
||||
/**
|
||||
* @return Definition by key "testNullable" with [parameters]
|
||||
*/
|
||||
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? =
|
||||
null): T? = getOrNull(named("testNullable"), parameters)
|
||||
|
||||
/**
|
||||
* @return Definition by key "testNullable" with [parameters]
|
||||
*/
|
||||
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? =
|
||||
null): T? = getOrNull(named("testNullable"), parameters)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
|
||||
*/
|
||||
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false,
|
||||
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
|
||||
createdAtStart = createdAtStart, definition = definition)
|
||||
|
||||
/**
|
||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
|
||||
*/
|
||||
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>):
|
||||
KoinDefinition<T> = factory(named("testNullable"), definition = definition)
|
||||
@@ -1,6 +1,9 @@
|
||||
@file:GenerateKoinDefinition("sampleInfo", Test::class, String::class, nullable = false)
|
||||
@file:GenerateGenericKoinDefinition("test", nullable = false)
|
||||
@file:GenerateGenericKoinDefinition("testNullable", nullable = true)
|
||||
package dev.inmo.micro_utils.koin.generator.test
|
||||
|
||||
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
||||
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
||||
import org.koin.core.Koin
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.koin.generator.test"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.koin
|
||||
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.definition.BeanDefinition
|
||||
import org.koin.core.definition.KoinDefinition
|
||||
import org.koin.core.instance.InstanceFactory
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
fun <T> Koin.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.primaryType,
|
||||
definition.qualifier,
|
||||
parameters
|
||||
)
|
||||
|
||||
fun <T> Koin.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.beanDefinition,
|
||||
parameters
|
||||
)
|
||||
|
||||
fun <T> Koin.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.factory,
|
||||
parameters
|
||||
)
|
||||
|
||||
fun <T> Scope.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.primaryType,
|
||||
definition.qualifier,
|
||||
parameters
|
||||
)
|
||||
|
||||
fun <T> Scope.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.beanDefinition,
|
||||
parameters
|
||||
)
|
||||
|
||||
fun <T> Scope.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||
definition.factory,
|
||||
parameters
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.koin.annotations
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Use this annotation to mark files near to which generator should place generated extensions for koin [org.koin.core.scope.Scope]
|
||||
* and [org.koin.core.Koin]
|
||||
*
|
||||
* @param name Name for definitions. This name will be available as extension for [org.koin.core.scope.Scope] and [org.koin.core.Koin]
|
||||
* @param type Type of extensions. It is base star-typed class
|
||||
* @param typeArgs Generic types for [type]. For example, if [type] == `Something::class` and [typeArgs] == `G1::class,
|
||||
* G2::class`, the result type will be `Something<G1, G2>`
|
||||
* @param nullable In case when true, extension will not throw error when definition has not been registered in koin
|
||||
* @param generateSingle Generate definition factory with [org.koin.core.module.Module.single]. You will be able to use
|
||||
* the extension [org.koin.core.module.Module].[name]Single(createdAtStart/* default false */) { /* your definition */ }
|
||||
* @param generateFactory Generate definition factory with [org.koin.core.module.Module.factory]. You will be able to use
|
||||
* the extension [org.koin.core.module.Module].[name]Factory { /* your definition */ }
|
||||
*/
|
||||
@Target(AnnotationTarget.FILE)
|
||||
@Repeatable
|
||||
annotation class GenerateGenericKoinDefinition(
|
||||
val name: String,
|
||||
val nullable: Boolean = true,
|
||||
val generateSingle: Boolean = true,
|
||||
val generateFactory: Boolean = true
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.micro_utils.koin
|
||||
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.java.KoinJavaComponent
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun <T> lazyInject(
|
||||
kClassFactory: () -> KClass<*>,
|
||||
qualifier: Qualifier? = null,
|
||||
parameters: ParametersDefinition? = null
|
||||
): Lazy<T> {
|
||||
return lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
KoinJavaComponent.get(kClassFactory().java, qualifier, parameters)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> lazyInject(
|
||||
kClass: KClass<*>,
|
||||
qualifier: Qualifier? = null,
|
||||
parameters: ParametersDefinition? = null
|
||||
): Lazy<T> = lazyInject({ kClass }, qualifier, parameters)
|
||||
|
||||
inline fun <reified T> lazyInject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): Lazy<T> = lazyInject(T::class, qualifier, parameters)
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.koin"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
@@ -30,5 +30,11 @@ kotlin {
|
||||
api internalProject("micro_utils.mime_types")
|
||||
}
|
||||
}
|
||||
|
||||
linuxArm64Main {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.mime_types")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,40 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import dev.inmo.micro_utils.mime_types.getMimeTypeOrAny
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.onUpload
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
|
||||
internal val MPPFile.mimeType: String
|
||||
get() = getMimeTypeOrAny(filename.extension).raw
|
||||
|
||||
actual suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: OnUploadCallback
|
||||
): TemporalFileId {
|
||||
val inputProvider = file.inputProvider()
|
||||
val fileId = submitFormWithBinaryData(
|
||||
fullTempUploadDraftPath,
|
||||
formData = formData {
|
||||
append(
|
||||
"data",
|
||||
inputProvider,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, file.mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
onUpload(onUpload)
|
||||
}.bodyAsText()
|
||||
return TemporalFileId(fileId)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.Progress
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.mergeHeaders
|
||||
import io.ktor.client.plugins.onUpload
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.forms.submitForm
|
||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
||||
import io.ktor.client.request.headers
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.http.content.PartData
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
import kotlinx.serialization.StringFormat
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
/**
|
||||
* Will execute submitting of multipart data request
|
||||
*
|
||||
* @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass
|
||||
* [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value
|
||||
* in case you wish to pass other source of multipart binary data than regular file
|
||||
* @suppress
|
||||
*/
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
actual suspend fun <T> HttpClient.uniUpload(
|
||||
url: String,
|
||||
data: Map<String, Any>,
|
||||
resultDeserializer: DeserializationStrategy<T>,
|
||||
headers: Headers,
|
||||
stringFormat: StringFormat,
|
||||
onUpload: OnUploadCallback
|
||||
): T? {
|
||||
val withBinary = data.values.any { it is MPPFile || it is UniUploadFileInfo }
|
||||
|
||||
val formData = formData {
|
||||
for (k in data.keys) {
|
||||
val v = data[k] ?: continue
|
||||
when (v) {
|
||||
is MPPFile -> append(
|
||||
k,
|
||||
v.inputProvider(),
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, v.mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${v.name}\"")
|
||||
}
|
||||
)
|
||||
is UniUploadFileInfo -> append(
|
||||
k,
|
||||
InputProvider(block = v.inputAllocator),
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, v.mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${v.fileName.name}\"")
|
||||
}
|
||||
)
|
||||
else -> append(
|
||||
k,
|
||||
stringFormat.encodeToString(v::class.serializer() as SerializationStrategy<in Any>, v)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val requestBuilder: HttpRequestBuilder.() -> Unit = {
|
||||
headers {
|
||||
appendAll(headers)
|
||||
}
|
||||
onUpload { bytesSentTotal, contentLength ->
|
||||
onUpload(bytesSentTotal, contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
val response = if (withBinary) {
|
||||
submitFormWithBinaryData(
|
||||
url,
|
||||
formData,
|
||||
block = requestBuilder
|
||||
)
|
||||
} else {
|
||||
submitForm(
|
||||
url,
|
||||
Parameters.build {
|
||||
for (it in formData) {
|
||||
val formItem = (it as PartData.FormItem)
|
||||
append(it.name!!, it.value)
|
||||
}
|
||||
},
|
||||
block = requestBuilder
|
||||
)
|
||||
}
|
||||
|
||||
return if (response.status == HttpStatusCode.OK) {
|
||||
stringFormat.decodeFromString(resultDeserializer, response.bodyAsText())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.ktor.client"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
|
||||
typealias FromToDateTime = Pair<DateTime?, DateTime?>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.bytesAllocatorSync
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import io.ktor.utils.io.core.Input
|
||||
|
||||
actual fun MPPFile.input(): Input {
|
||||
return ByteReadPacket(bytesAllocatorSync())
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.ktor.common"/>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.ktor.common.FromToDateTime
|
||||
import io.ktor.http.Parameters
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user