mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
151 Commits
Author | SHA1 | Date | |
---|---|---|---|
1bd671685b | |||
48d3fe41f2 | |||
7ab21871cd | |||
65d01b1fb3 | |||
6230accb68 | |||
10e03bb951 | |||
aa4f392948 | |||
f51b59ec02 | |||
8c76834ae4 | |||
4a454f3d67 | |||
151aa1863d | |||
3bf6896296 | |||
0d01561476 | |||
f6ded92251 | |||
d01b735cc6 | |||
6c12001080 | |||
1afbf03606 | |||
f6ef5c61c5 | |||
c18fee8107 | |||
d9df7a4384 | |||
87c2230e8e | |||
da7eb6de0a | |||
ed3815118f | |||
be726f42bd | |||
a91006132f | |||
9a9f741a0b | |||
5028f130e9 | |||
77fa019651 | |||
9715da9384 | |||
f6d5035c1a | |||
43e782ab6f | |||
f3f9920bfb | |||
2bfd615812 | |||
ebfacb3659 | |||
c71d557eec | |||
e0398cef21 | |||
f91599e9c6 | |||
f8f9f93c97 | |||
a8a5340d8b | |||
871b27f37d | |||
6f174cae1d | |||
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 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 "[^/]*$"`"
|
||||
|
2
.github/workflows/dokka_push.yml
vendored
2
.github/workflows/dokka_push.yml
vendored
@@ -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
|
||||
|
220
CHANGELOG.md
220
CHANGELOG.md
@@ -1,5 +1,225 @@
|
||||
# Changelog
|
||||
|
||||
## 0.20.23
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.5.0` -> `3.5.3`
|
||||
* `Okio`: `3.6.0` -> `3.7.0`
|
||||
* `LanguageCodes`:
|
||||
* Fixes in intermediate language codes (like `Chinese.Hans`)
|
||||
* Rename `IetfLanguageCode` to `IetfLang`
|
||||
* Rename all subsequent functions (including serializer)
|
||||
* New lazy properties `knownLanguageCodesMap`, `knownLanguageCodesMapByLowerCasedKeys` and several others
|
||||
|
||||
## 0.20.22
|
||||
|
||||
* `Common`:
|
||||
* Add opportunity to create own `Diff` with base constructor
|
||||
|
||||
## 0.20.21
|
||||
|
||||
* `Resources`:
|
||||
* Inited
|
||||
|
||||
## 0.20.20
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Add opportunity for setup flows in `AbstractExposedCRUDRepo`
|
||||
|
||||
## 0.20.19
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.6` -> `2.3.7`
|
||||
|
||||
## 0.20.18
|
||||
|
||||
* `Coroutines`:
|
||||
* `SpecialMutableStateFlow` now extends `MutableStateFlow`
|
||||
* `Compose`:
|
||||
* Deprecate `FlowState` due to its complexity in fixes
|
||||
|
||||
## 0.20.17
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.6.1` -> `1.6.2`
|
||||
|
||||
## 0.20.16
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.44.1` -> `0.45.0`
|
||||
* `Coroutines`:
|
||||
* Add `SpecialMutableStateFlow`
|
||||
* `Compose`:
|
||||
* Add `FlowState`
|
||||
|
||||
## 0.20.15
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20` -> `1.9.21`
|
||||
* `KSLog`: `1.3.0` -> `1.3.1`
|
||||
* `Compose`: `1.5.10` -> `1.5.11`
|
||||
|
||||
## 0.20.14
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.6.0` -> `1.6.1`
|
||||
* `KSLog`: `1.2.4` -> `1.3.0`
|
||||
|
||||
## 0.20.13
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.5` -> `2.3.6`
|
||||
* `UUID`: `0.8.1` -> `0.8.2`
|
||||
|
||||
## 0.20.12
|
||||
|
||||
**It is experimental migration onto new gradle version. Be careful in use of this version**
|
||||
|
||||
**This update have JDK 17 in `compatibility` and `target` versions**
|
||||
|
||||
## 0.20.11
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-RC2` -> `1.9.20`
|
||||
* `Exposed`: `0.44.0` -> `0.44.1`
|
||||
* `Compose`: `1.5.10-rc02` -> `1.5.10`
|
||||
* `Coroutines`:
|
||||
* `SmartRWLocker` now will wait first unlock of write mutex for acquiring read
|
||||
|
||||
## 0.20.10
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-RC` -> `1.9.20-RC1`
|
||||
* `KSLog`: `1.2.1` -> `1.2.2`
|
||||
* `Compose`: `1.5.10-rc01` -> `1.5.10-rc02`
|
||||
* `RecyclerView`: `1.3.1` -> `1.3.2`
|
||||
|
||||
## 0.20.9
|
||||
|
||||
* Most of common modules now supports `linuxArm64` target
|
||||
|
||||
## 0.20.8
|
||||
|
||||
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION**
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC`
|
||||
* `Compose`: `1.5.10-beta02` -> `1.5.10-rc01`
|
||||
|
||||
## 0.20.7
|
||||
|
||||
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION**
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.10` -> `1.9.20-Beta2`
|
||||
* `Compose`: `1.5.1` -> `1.5.10-beta02`
|
||||
* `Exposed`: `0.43.0` -> `0.44.0`
|
||||
* `Ktor`: `2.3.4` -> `2.3.5`
|
||||
* `Koin`: `3.4.3` -> `3.5.0`
|
||||
* `Okio`: `3.5.0` -> `3.6.0`
|
||||
* `Android Core`: `1.10.1` -> `1.12.0`
|
||||
* `Android Compose Material`: `1.1.1` -> `1.1.2`
|
||||
|
||||
## 0.20.6
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`
|
||||
* Fixes in exposed key-values repos
|
||||
|
||||
## 0.20.5
|
||||
|
||||
* `Coroutines`:
|
||||
* Fixes in `SmartRWLocker`
|
||||
|
||||
## 0.20.4
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.0` -> `1.9.10`
|
||||
* `KSLog`: `1.2.0` -> `1.2.1`
|
||||
* `Compose`: `1.5.0` -> `1.5.1`
|
||||
* `UUID`: `0.8.0` -> `0.8.1`
|
||||
|
||||
## 0.20.3
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.4.3` -> `1.5.0`
|
||||
* `Exposed`: `0.42.1` -> `0.43.0`
|
||||
* `Ktor`: `2.3.3` -> `2.3.4`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Fixes in locks of caches
|
||||
|
||||
## 0.20.2
|
||||
|
||||
* All main repos uses `SmartRWLocker`
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.5.1` -> `1.6.0`
|
||||
* `Exposed`: `0.42.0` -> `0.42.1`
|
||||
* `Korlibs`: `4.0.9` -> `4.0.10`
|
||||
* `Androis SDK`: `33` -> `34`
|
||||
|
||||
## 0.20.1
|
||||
|
||||
* `SmallTextField`:
|
||||
* Module is initialized
|
||||
* `Pickers`:
|
||||
* Module is initialized
|
||||
* `Coroutines`:
|
||||
* Add `SmartSemaphore`
|
||||
* Add `SmartRWLocker`
|
||||
|
||||
## 0.20.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.8.22` -> `1.9.0`
|
||||
* `KSLog`: `1.1.1` -> `1.2.0`
|
||||
* `Exposed`: `0.41.1` -> `0.42.0`
|
||||
* `UUID`: `0.7.1` -> `0.8.0`
|
||||
* `Korlibs`: `4.0.3` -> `4.0.9`
|
||||
* `Ktor`: `2.3.2` -> `2.3.3`
|
||||
* `Okio`: `3.4.0` -> `3.5.0`
|
||||
|
||||
## 0.19.9
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.4.2` -> `3.4.3`
|
||||
* `Startup`:
|
||||
* Now it is possible to start application in synchronous way
|
||||
|
||||
## 0.19.8
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.2` -> `1.7.3`
|
||||
* `Kotlin`: `1.8.20` -> `1.8.22`
|
||||
* `Compose`: `1.4.1` -> `1.4.3`
|
||||
* `Okio`: `3.3.0` -> `3.4.0`
|
||||
* `RecyclerView`: `1.3.0` -> `1.3.1`
|
||||
* `Fragment`: `1.6.0` -> `1.6.1`
|
||||
* `Repos`:
|
||||
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
|
||||
* `Cache`:
|
||||
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
|
||||
will be automatically invalidated on start of their work
|
||||
|
||||
## 0.19.7
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.1` -> `1.7.2`
|
||||
|
||||
## 0.19.6
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.4` -> `1.7.1`
|
||||
* `Ktor`: `2.3.1` -> `2.3.2`
|
||||
* `Compose`: `1.4.0` -> `1.4.1`
|
||||
|
||||
## 0.19.5
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Fixes in new type generation
|
||||
|
||||
## 0.19.4
|
||||
|
||||
* `Versions`:
|
||||
|
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
18
android/pickers/build.gradle
Normal file
18
android/pickers/build.gradle
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal file
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal file
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal file
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
18
android/smalltextfield/build.gradle
Normal file
18
android/smalltextfield/build.gradle
Normal file
@@ -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,13 +17,17 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
@@ -38,3 +42,4 @@ allprojects {
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
apply from: "./github_release.gradle"
|
||||
apply from: "./versions_plugin_setup.gradle"
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
1
common/src/androidMain/AndroidManifest.xml
Normal file
1
common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -27,7 +27,7 @@ private inline fun <T> getObject(
|
||||
* @see calculateDiff
|
||||
*/
|
||||
@Serializable
|
||||
data class Diff<T> internal constructor(
|
||||
data class Diff<T> @Warning(warning) constructor(
|
||||
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
||||
/**
|
||||
* Old-New values pairs
|
||||
@@ -36,6 +36,10 @@ data class Diff<T> internal constructor(
|
||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||
) {
|
||||
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||
|
||||
companion object {
|
||||
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||
|
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -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()
|
||||
}
|
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal file
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal file
@@ -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 {
|
||||
|
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -0,0 +1,46 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* This type works like [MutableState], [kotlinx.coroutines.flow.StateFlow] and [kotlinx.coroutines.flow.MutableSharedFlow].
|
||||
* Based on [SpecialMutableStateFlow]
|
||||
*/
|
||||
@Deprecated("Will be removed soon")
|
||||
class FlowState<T>(
|
||||
initial: T,
|
||||
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : MutableState<T>,
|
||||
SpecialMutableStateFlow<T>(initial, internalScope) {
|
||||
private var internalValue: T = initial
|
||||
override var value: T
|
||||
get() = internalValue
|
||||
set(value) {
|
||||
internalValue = value
|
||||
tryEmit(value)
|
||||
}
|
||||
|
||||
override fun onChangeWithoutSync(value: T) {
|
||||
internalValue = value
|
||||
super.onChangeWithoutSync(value)
|
||||
}
|
||||
|
||||
override fun component1(): T = value
|
||||
|
||||
override fun component2(): (T) -> Unit = { tryEmit(it) }
|
||||
|
||||
override fun tryEmit(value: T): Boolean {
|
||||
internalValue = value
|
||||
return super.tryEmit(value)
|
||||
}
|
||||
|
||||
override suspend fun emit(value: T) {
|
||||
internalValue = value
|
||||
super.emit(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"/>
|
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -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,86 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.internal.SynchronizedObject
|
||||
import kotlinx.coroutines.internal.synchronized
|
||||
|
||||
/**
|
||||
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
|
||||
* each active subscriber
|
||||
*/
|
||||
open class SpecialMutableStateFlow<T>(
|
||||
initialValue: T,
|
||||
internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private val syncObject = SynchronizedObject()
|
||||
protected val internalSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 2,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
protected val publicSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
protected var _value: T = initialValue
|
||||
override var value: T
|
||||
get() = _value
|
||||
set(value) {
|
||||
doOnChangeAction(value)
|
||||
}
|
||||
protected val job = internalSharedFlow.subscribe(internalScope) {
|
||||
doOnChangeAction(it)
|
||||
}
|
||||
|
||||
override val replayCache: List<T>
|
||||
get() = publicSharedFlow.replayCache
|
||||
override val subscriptionCount: StateFlow<Int>
|
||||
get() = publicSharedFlow.subscriptionCount
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
override fun compareAndSet(expect: T, update: T): Boolean {
|
||||
return synchronized(syncObject) {
|
||||
if (expect == _value && update != _value) {
|
||||
doOnChangeAction(update)
|
||||
}
|
||||
expect == _value
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onChangeWithoutSync(value: T) {
|
||||
_value = value
|
||||
publicSharedFlow.tryEmit(value)
|
||||
}
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
protected open fun doOnChangeAction(value: T) {
|
||||
synchronized(syncObject) {
|
||||
if (_value != value) {
|
||||
onChangeWithoutSync(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
@@ -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"/>
|
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.dokka"/>
|
@@ -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 {
|
||||
|
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -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 {
|
||||
|
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
@@ -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=-Xmx2g -XX:MaxMetaspaceSize=2g
|
||||
|
||||
# JS NPM
|
||||
|
||||
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.19.4
|
||||
android_code_version=200
|
||||
version=0.20.23
|
||||
android_code_version=229
|
||||
|
@@ -1,42 +1,45 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.8.20"
|
||||
kt-serialization = "1.5.1"
|
||||
kt-coroutines = "1.6.4"
|
||||
kt = "1.9.21"
|
||||
kt-serialization = "1.6.2"
|
||||
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.20"
|
||||
jb-compose = "1.5.11"
|
||||
jb-exposed = "0.45.0"
|
||||
jb-dokka = "1.9.10"
|
||||
|
||||
korlibs = "4.0.3"
|
||||
uuid = "0.7.1"
|
||||
korlibs = "4.0.10"
|
||||
uuid = "0.8.2"
|
||||
|
||||
ktor = "2.3.1"
|
||||
ktor = "2.3.7"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
|
||||
koin = "3.4.2"
|
||||
koin = "3.5.3"
|
||||
|
||||
okio = "3.3.0"
|
||||
okio = "3.7.0"
|
||||
|
||||
ksp = "1.8.20-1.0.11"
|
||||
kotlin-poet = "1.14.2"
|
||||
ksp = "1.9.21-1.0.16"
|
||||
kotlin-poet = "1.15.3"
|
||||
|
||||
android-gradle = "7.4.2"
|
||||
versions = "0.50.0"
|
||||
|
||||
android-gradle = "8.2.0"
|
||||
dexcount = "4.0.0"
|
||||
|
||||
android-coreKtx = "1.10.1"
|
||||
android-recyclerView = "1.3.0"
|
||||
android-coreKtx = "1.12.0"
|
||||
android-recyclerView = "1.3.2"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.6.0"
|
||||
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" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -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 {
|
||||
@@ -68,18 +68,14 @@ publishing {
|
||||
|
||||
}
|
||||
}
|
||||
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
|
||||
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "Gitea"
|
||||
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
|
||||
name = "InmoNexus"
|
||||
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
|
||||
|
||||
credentials(HttpHeaderCredentials) {
|
||||
name = "Authorization"
|
||||
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
|
||||
}
|
||||
|
||||
authentication {
|
||||
header(HttpHeaderAuthentication)
|
||||
credentials {
|
||||
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
|
||||
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
|
||||
}
|
||||
|
||||
}
|
||||
@@ -115,4 +111,27 @@ 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)
|
||||
}
|
||||
// Workaround to make test tasks use sign
|
||||
project.getTasks().withType(Sign.class).configureEach { signTask ->
|
||||
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
|
||||
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
|
||||
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
|
||||
|
||||
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
|
||||
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
|
||||
if (debugTestTask != null) {
|
||||
signTask.mustRunAfter(debugTestTask)
|
||||
}
|
||||
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
|
||||
def testTask = tasks.findByName("compileTestKotlin$pubName")
|
||||
if (testTask != null) {
|
||||
signTask.mustRunAfter(testTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}
|
@@ -15,6 +15,6 @@ dependencies {
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
@@ -159,9 +159,7 @@ class Processor(
|
||||
}
|
||||
|
||||
if (generateSingle) {
|
||||
fun FunSpec.Builder.configure(
|
||||
useInstead: String? = null
|
||||
) {
|
||||
fun FunSpec.Builder.configure() {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.single] and key "${name}"
|
||||
@@ -185,30 +183,9 @@ class Processor(
|
||||
addTypeVariable(it)
|
||||
addModifiers(KModifier.INLINE)
|
||||
}
|
||||
if (useInstead != null) {
|
||||
addAnnotation(
|
||||
AnnotationSpec.builder(
|
||||
Deprecated::class
|
||||
).apply {
|
||||
addMember(
|
||||
CodeBlock.of(
|
||||
"""
|
||||
"This definition is old style and should not be used anymore. Use $useInstead instead"
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")"))
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val actualSingleName = "single${name.replaceFirstChar { it.uppercase() }}"
|
||||
if (targetTypeAsGenericType == null) { // classic type
|
||||
addFunction(
|
||||
FunSpec.builder("${name}Single").apply { configure(actualSingleName) }.build()
|
||||
)
|
||||
}
|
||||
|
||||
addFunction(
|
||||
FunSpec.builder(actualSingleName).apply { configure() }.build()
|
||||
@@ -216,9 +193,7 @@ class Processor(
|
||||
}
|
||||
|
||||
if (generateFactory) {
|
||||
fun FunSpec.Builder.configure(
|
||||
useInstead: String? = null
|
||||
) {
|
||||
fun FunSpec.Builder.configure() {
|
||||
addKdoc(
|
||||
"""
|
||||
Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}"
|
||||
@@ -234,29 +209,8 @@ class Processor(
|
||||
addTypeVariable(it)
|
||||
addModifiers(KModifier.INLINE)
|
||||
}
|
||||
if (useInstead != null) {
|
||||
addAnnotation(
|
||||
AnnotationSpec.builder(
|
||||
Deprecated::class
|
||||
).apply {
|
||||
addMember(
|
||||
CodeBlock.of(
|
||||
"""
|
||||
"This definition is old style and should not be used anymore. Use $useInstead instead"
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")"))
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
val actualFactoryName = "factory${name.replaceFirstChar { it.uppercase() }}"
|
||||
if (targetTypeAsGenericType == null) { // classic type
|
||||
addFunction(
|
||||
FunSpec.builder("${name}Factory").apply { configure(useInstead = actualFactoryName) }.build()
|
||||
)
|
||||
}
|
||||
addFunction(
|
||||
FunSpec.builder(actualFactoryName).apply { configure() }.build()
|
||||
)
|
||||
|
1
koin/generator/test/src/androidMain/AndroidManifest.xml
Normal file
1
koin/generator/test/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.koin.generator.test"/>
|
1
koin/src/androidMain/AndroidManifest.xml
Normal file
1
koin/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
ktor/client/src/androidMain/AndroidManifest.xml
Normal file
1
ktor/client/src/androidMain/AndroidManifest.xml
Normal file
@@ -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)
|
||||
}
|
107
ktor/client/src/linuxArm64Main/kotlin/ActualUniUpload.kt
Normal file
107
ktor/client/src/linuxArm64Main/kotlin/ActualUniUpload.kt
Normal file
@@ -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 {
|
||||
|
1
ktor/common/src/androidMain/AndroidManifest.xml
Normal file
1
ktor/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
11
ktor/common/src/linuxArm64Main/kotlin/ActualMPPFileInput.kt
Normal file
11
ktor/common/src/linuxArm64Main/kotlin/ActualMPPFileInput.kt
Normal file
@@ -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"/>
|
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
@@ -26,6 +26,6 @@ dependencies {
|
||||
mainClassName="MainKt"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
@@ -12,9 +12,12 @@ private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private const val baseClassName = "IetfLanguageCode"
|
||||
private const val baseClassName = "IetfLang"
|
||||
private const val oldBaseClassName = "IetfLanguageCode"
|
||||
private const val unknownBaseClassName = "Unknown$baseClassName"
|
||||
private const val baseClassSerializerName = "IetfLanguageCodeSerializer"
|
||||
private const val oldUnknownBaseClassName = "Unknown$oldBaseClassName"
|
||||
private const val baseClassSerializerName = "${baseClassName}Serializer"
|
||||
private const val oldBaseClassSerializerName = "IetfLanguageCodeSerializer"
|
||||
private const val baseClassSerializerAnnotationName = "@Serializable(${baseClassSerializerName}::class)"
|
||||
|
||||
@Serializable
|
||||
@@ -78,14 +81,12 @@ private fun printLanguageCodeAndTags(
|
||||
indents: String = " "
|
||||
): String = if (tag.subtags.isEmpty()) {
|
||||
"""${indents}${baseClassSerializerAnnotationName}
|
||||
${indents}object ${tag.title} : ${parent ?.title ?: baseClassName}() { override val code: String = "${tag.tag}"; override val withoutDialect: String get() = ${parent ?.title ?.let { "$it.code" } ?: "code"} }"""
|
||||
${indents}object ${tag.title} : ${parent ?.title ?: baseClassName}() { override val code: String = "${tag.tag}"${parent ?.let { parent -> "; override val parentLang: ${parent.title} get() = ${parent.title};" } ?: ""} }"""
|
||||
} else {
|
||||
"""
|
||||
${indents}${baseClassSerializerAnnotationName}
|
||||
${indents}sealed class ${tag.title} : ${parent ?.title ?: baseClassName}() {
|
||||
${indents} override val code: String = "${tag.tag}"
|
||||
${indents} override val withoutDialect: String
|
||||
${indents} get() = code
|
||||
${indents} override val code: String = "${tag.tag}"${parent ?.let { parent -> "\n${indents} override val parentLang: ${parent.title} get() = ${parent.title};" } ?: ""}
|
||||
|
||||
${tag.subtags.joinToString("\n") { printLanguageCodeAndTags(it, tag, "${indents} ") }}
|
||||
|
||||
@@ -95,7 +96,7 @@ ${indents}}
|
||||
"""
|
||||
}
|
||||
|
||||
fun buildKtFileContent(tags: List<Tag>): String = """
|
||||
fun buildKtFileContent(tags: List<Tag>, prePackage: String): String = """
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@@ -106,20 +107,27 @@ import kotlinx.serialization.Serializable
|
||||
${baseClassSerializerAnnotationName}
|
||||
sealed class $baseClassName {
|
||||
abstract val code: String
|
||||
abstract val withoutDialect: String
|
||||
open val parentLang: $baseClassName?
|
||||
get() = null
|
||||
open val withoutDialect: String
|
||||
get() = parentLang ?.code ?: code
|
||||
|
||||
${tags.joinToString("\n") { printLanguageCodeAndTags(it, indents = " ") } }
|
||||
|
||||
$baseClassSerializerAnnotationName
|
||||
data class $unknownBaseClassName (override val code: String) : $baseClassName() {
|
||||
override val withoutDialect: String = code.takeWhile { it != '-' }
|
||||
override val parentLang = code.dropLastWhile { it != '-' }.removeSuffix("-").takeIf { it.length > 0 } ?.let(::$unknownBaseClassName)
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("$baseClassName.$unknownBaseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName.$unknownBaseClassName"))
|
||||
val $oldUnknownBaseClassName = $unknownBaseClassName
|
||||
|
||||
override fun toString() = code
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("$baseClassName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName"))
|
||||
typealias $oldBaseClassName = $baseClassName
|
||||
""".trimIndent()
|
||||
|
||||
fun createStringConverterCode(tags: List<Tag>): String {
|
||||
fun createStringConverterCode(tags: List<Tag>, prePackage: String): String {
|
||||
fun createDeserializeVariantForTag(
|
||||
tag: Tag,
|
||||
pretitle: String = baseClassName,
|
||||
@@ -128,19 +136,70 @@ fun createStringConverterCode(tags: List<Tag>): String {
|
||||
val currentTitle = "$pretitle.${tag.title}"
|
||||
return """${indents}$currentTitle.code -> $currentTitle${if (tag.subtags.isNotEmpty()) tag.subtags.joinToString("\n", "\n") { createDeserializeVariantForTag(it, currentTitle, indents) } else ""}"""
|
||||
}
|
||||
|
||||
return """fun String.as$baseClassName(): $baseClassName {
|
||||
return when (this) {
|
||||
${tags.joinToString("\n") { createDeserializeVariantForTag(it) }}
|
||||
else -> $baseClassName.${unknownBaseClassName}(this)
|
||||
fun createInheritorVariantForTag(
|
||||
tag: Tag,
|
||||
pretitle: String = baseClassName,
|
||||
indents: String = " "
|
||||
): String {
|
||||
val currentTitle = "$pretitle.${tag.title}"
|
||||
val subtags = if (tag.subtags.isNotEmpty()) {
|
||||
tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForTag(it, currentTitle, "$indents ") }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
return "${indents}$currentTitle$subtags"
|
||||
}
|
||||
fun createInheritorVariantForMapForTag(
|
||||
tag: Tag,
|
||||
pretitle: String = baseClassName,
|
||||
indents: String = " ",
|
||||
codeSuffix: String = ""
|
||||
): String {
|
||||
val currentTitle = "$pretitle.${tag.title}"
|
||||
val subtags = if (tag.subtags.isNotEmpty()) {
|
||||
tag.subtags.joinToString(",\n", ",\n") { createInheritorVariantForMapForTag(it, currentTitle, "$indents ", codeSuffix) }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
return "${indents}$currentTitle.code${codeSuffix} to $currentTitle$subtags"
|
||||
}
|
||||
|
||||
return """val knownLanguageCodesMap: Map<String, $baseClassName> by lazy {
|
||||
mapOf(
|
||||
${tags.joinToString(",\n") { createInheritorVariantForMapForTag(it, indents = " ") }}
|
||||
)
|
||||
}
|
||||
val knownLanguageCodesMapByLowerCasedKeys: Map<String, $baseClassName> by lazy {
|
||||
mapOf(
|
||||
${tags.joinToString(",\n") { createInheritorVariantForMapForTag(it, indents = " ", codeSuffix = ".lowercase()") }}
|
||||
)
|
||||
}
|
||||
val knownLanguageCodes: List<$baseClassName> by lazy {
|
||||
knownLanguageCodesMap.values.toList()
|
||||
}
|
||||
|
||||
fun String.as$baseClassName(): $baseClassName {
|
||||
return knownLanguageCodesMap[this] ?: $baseClassName.${unknownBaseClassName}(this)
|
||||
}
|
||||
fun String.as${baseClassName}CaseInsensitive(): $baseClassName {
|
||||
return knownLanguageCodesMapByLowerCasedKeys[this.lowercase()] ?: $baseClassName.${unknownBaseClassName}(this)
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("this.as$baseClassName()", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}as$baseClassName"))
|
||||
fun String.as$oldBaseClassName(): $baseClassName = as$baseClassName()
|
||||
|
||||
fun convertTo$baseClassName(code: String) = code.as$baseClassName()
|
||||
fun convertTo${baseClassName}CaseInsensitive(code: String) = code.as${baseClassName}CaseInsensitive()
|
||||
@Deprecated("Renamed", ReplaceWith("convertTo$baseClassName(code)", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}convertTo$baseClassName"))
|
||||
fun convertTo$oldBaseClassName(code: String) = convertTo$baseClassName(code)
|
||||
|
||||
fun $baseClassName(code: String) = code.as$baseClassName()
|
||||
fun ${baseClassName}CaseInsensitive(code: String) = code.as${baseClassName}CaseInsensitive()
|
||||
@Deprecated("Renamed", ReplaceWith("$baseClassName(code)", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassName"))
|
||||
fun $oldBaseClassName(code: String) = $baseClassName(code)
|
||||
"""
|
||||
}
|
||||
|
||||
fun createSerializerCode(tags: List<Tag>): String {
|
||||
fun createSerializerCode(tags: List<Tag>, prePackage: String): String {
|
||||
return """import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
@@ -153,16 +212,20 @@ object $baseClassSerializerName : KSerializer<$baseClassName> {
|
||||
return $baseClassName(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IetfLanguageCode) {
|
||||
override fun serialize(encoder: Encoder, value: $baseClassName) {
|
||||
encoder.encodeString(value.code)
|
||||
}
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("$baseClassSerializerName", "${if (prePackage.isNotEmpty()) "$prePackage." else ""}$baseClassSerializerName"))
|
||||
typealias $oldBaseClassSerializerName = $baseClassSerializerName
|
||||
"""
|
||||
}
|
||||
|
||||
suspend fun main(vararg args: String) {
|
||||
val outputFolder = args.firstOrNull() ?.let { File(it) }
|
||||
outputFolder ?.mkdirs()
|
||||
val targetPackage = args.getOrNull(1)
|
||||
val targetPackagePrefix = targetPackage ?.let { "package $it\n\n" } ?: ""
|
||||
val ietfLanguageCodesLink = "https://datahub.io/core/language-codes/r/language-codes.json"
|
||||
val ietfLanguageCodesAdditionalTagsLink = "https://datahub.io/core/language-codes/r/ietf-language-tags.json"
|
||||
|
||||
@@ -203,18 +266,18 @@ suspend fun main(vararg args: String) {
|
||||
File(outputFolder, "LanguageCodes.kt").apply {
|
||||
delete()
|
||||
createNewFile()
|
||||
writeText(buildKtFileContent(tags))
|
||||
writeText(targetPackagePrefix + buildKtFileContent(tags, targetPackage ?: ""))
|
||||
}
|
||||
|
||||
File(outputFolder, "StringToLanguageCodes.kt").apply {
|
||||
delete()
|
||||
createNewFile()
|
||||
writeText(createStringConverterCode(tags))
|
||||
writeText(targetPackagePrefix + createStringConverterCode(tags, targetPackage ?: ""))
|
||||
}
|
||||
|
||||
File(outputFolder, "$baseClassSerializerName.kt").apply {
|
||||
delete()
|
||||
createNewFile()
|
||||
writeText(createSerializerCode(tags))
|
||||
writeText(targetPackagePrefix + createSerializerCode(tags, targetPackage ?: ""))
|
||||
}
|
||||
}
|
||||
|
1
language_codes/src/androidMain/AndroidManifest.xml
Normal file
1
language_codes/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
@@ -0,0 +1,20 @@
|
||||
package dev.inmo.micro_utils.language_codes
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object IetfLangSerializer : KSerializer<IetfLang> {
|
||||
override val descriptor = String.serializer().descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): IetfLang {
|
||||
return IetfLang(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IetfLang) {
|
||||
encoder.encodeString(value.code)
|
||||
}
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("IetfLangSerializer", "dev.inmo.micro_utils.language_codes.IetfLangSerializer"))
|
||||
typealias IetfLanguageCodeSerializer = IetfLangSerializer
|
@@ -1,18 +0,0 @@
|
||||
package dev.inmo.micro_utils.language_codes
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object IetfLanguageCodeSerializer : KSerializer<IetfLanguageCode> {
|
||||
override val descriptor = String.serializer().descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): IetfLanguageCode {
|
||||
return IetfLanguageCode(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IetfLanguageCode) {
|
||||
encoder.encodeString(value.code)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user