mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
6ce1eb3f2d | |||
ce7d4fe9a2 | |||
a65bb2f419 | |||
cbc868448b | |||
9c336a0b56 | |||
0f0d09399e | |||
e13a1162a9 | |||
57ebed903f | |||
4478193d8a | |||
ee948395e3 | |||
0616b051ae | |||
4d155d0505 | |||
a169e733d9 | |||
f081e237c8 | |||
f412d387fa | |||
67354b43e2 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## 0.19.8
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
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 package="dev.inmo.micro_utils.android.pickers"/>
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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 package="dev.inmo.micro_utils.android.smalltextfield"/>
|
@@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@@ -1,12 +1,10 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.cinterop.ByteVar
|
import kotlinx.cinterop.*
|
||||||
import kotlinx.cinterop.allocArray
|
|
||||||
import kotlinx.cinterop.memScoped
|
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
import platform.posix.snprintf
|
import platform.posix.snprintf
|
||||||
import platform.posix.sprintf
|
import platform.posix.sprintf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
actual fun Float.fixed(signs: Int): Float {
|
actual fun Float.fixed(signs: Int): Float {
|
||||||
return memScoped {
|
return memScoped {
|
||||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
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 {
|
actual fun Double.fixed(signs: Int): Double {
|
||||||
return memScoped {
|
return memScoped {
|
||||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.cinterop.ByteVar
|
import kotlinx.cinterop.*
|
||||||
import kotlinx.cinterop.allocArray
|
|
||||||
import kotlinx.cinterop.memScoped
|
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
import platform.posix.snprintf
|
import platform.posix.snprintf
|
||||||
import platform.posix.sprintf
|
import platform.posix.sprintf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
actual fun Float.fixed(signs: Int): Float {
|
actual fun Float.fixed(signs: Int): Float {
|
||||||
return memScoped {
|
return memScoped {
|
||||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
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 {
|
actual fun Double.fixed(signs: Int): Double {
|
||||||
return memScoped {
|
return memScoped {
|
||||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||||
|
@@ -0,0 +1,102 @@
|
|||||||
|
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.withLock {
|
||||||
|
_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.waitRelease(readPermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock [writeMutex]
|
||||||
|
*/
|
||||||
|
suspend fun unlockWrite(): Boolean {
|
||||||
|
return _writeMutex.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,142 @@
|
|||||||
|
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 [_permitsStateFlow]
|
||||||
|
*/
|
||||||
|
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
|
||||||
|
private val _permitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits)
|
||||||
|
override val permitsStateFlow: StateFlow<Int> = _permitsStateFlow.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 while [freePermits] == true, [holds] will
|
||||||
|
* wait for [freePermits] == false and then try to lock
|
||||||
|
*/
|
||||||
|
suspend fun acquire(permits: Int = 1) {
|
||||||
|
do {
|
||||||
|
val checkedPermits = checkedPermits(permits)
|
||||||
|
waitRelease(checkedPermits)
|
||||||
|
val shouldContinue = internalChangesMutex.withLock {
|
||||||
|
if (_permitsStateFlow.value < checkedPermits) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
_permitsStateFlow.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 (_permitsStateFlow.value < checkedPermits) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (_permitsStateFlow.value < checkedPermits) {
|
||||||
|
_permitsStateFlow.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 (_permitsStateFlow.value < this.permits) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (_permitsStateFlow.value < this.permits) {
|
||||||
|
_permitsStateFlow.value = minOf(_permitsStateFlow.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 }
|
60
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
60
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
import dev.inmo.micro_utils.coroutines.withWriteLock
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -29,8 +29,4 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ org.gradle.parallel=true
|
|||||||
kotlin.js.generate.externals=true
|
kotlin.js.generate.externals=true
|
||||||
kotlin.incremental=true
|
kotlin.incremental=true
|
||||||
kotlin.incremental.js=true
|
kotlin.incremental.js=true
|
||||||
|
#kotlin.experimental.tryK2=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g
|
org.gradle.jvmargs=-Xmx2g
|
||||||
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.19.8
|
version=0.20.1
|
||||||
android_code_version=204
|
android_code_version=207
|
||||||
|
@@ -1,27 +1,27 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "1.8.22"
|
kt = "1.9.0"
|
||||||
kt-serialization = "1.5.1"
|
kt-serialization = "1.5.1"
|
||||||
kt-coroutines = "1.7.3"
|
kt-coroutines = "1.7.3"
|
||||||
|
|
||||||
kslog = "1.1.1"
|
kslog = "1.2.0"
|
||||||
|
|
||||||
jb-compose = "1.4.3"
|
jb-compose = "1.4.3"
|
||||||
jb-exposed = "0.41.1"
|
jb-exposed = "0.42.0"
|
||||||
jb-dokka = "1.8.20"
|
jb-dokka = "1.8.20"
|
||||||
|
|
||||||
korlibs = "4.0.3"
|
korlibs = "4.0.9"
|
||||||
uuid = "0.7.1"
|
uuid = "0.8.0"
|
||||||
|
|
||||||
ktor = "2.3.2"
|
ktor = "2.3.3"
|
||||||
|
|
||||||
gh-release = "2.4.1"
|
gh-release = "2.4.1"
|
||||||
|
|
||||||
koin = "3.4.2"
|
koin = "3.4.3"
|
||||||
|
|
||||||
okio = "3.4.0"
|
okio = "3.5.0"
|
||||||
|
|
||||||
ksp = "1.8.22-1.0.11"
|
ksp = "1.9.0-1.0.13"
|
||||||
kotlin-poet = "1.14.2"
|
kotlin-poet = "1.14.2"
|
||||||
|
|
||||||
versions = "0.47.0"
|
versions = "0.47.0"
|
||||||
@@ -35,6 +35,7 @@ android-appCompat = "1.6.1"
|
|||||||
android-fragment = "1.6.1"
|
android-fragment = "1.6.1"
|
||||||
android-espresso = "3.5.1"
|
android-espresso = "3.5.1"
|
||||||
android-test = "1.1.5"
|
android-test = "1.1.5"
|
||||||
|
android-compose-material3 = "1.1.1"
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "33"
|
android-props-compileSdk = "33"
|
||||||
@@ -83,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-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
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-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||||
|
@@ -159,9 +159,7 @@ class Processor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (generateSingle) {
|
if (generateSingle) {
|
||||||
fun FunSpec.Builder.configure(
|
fun FunSpec.Builder.configure() {
|
||||||
useInstead: String? = null
|
|
||||||
) {
|
|
||||||
addKdoc(
|
addKdoc(
|
||||||
"""
|
"""
|
||||||
Will register [definition] with [org.koin.core.module.Module.single] and key "${name}"
|
Will register [definition] with [org.koin.core.module.Module.single] and key "${name}"
|
||||||
@@ -185,30 +183,9 @@ class Processor(
|
|||||||
addTypeVariable(it)
|
addTypeVariable(it)
|
||||||
addModifiers(KModifier.INLINE)
|
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() }}"
|
val actualSingleName = "single${name.replaceFirstChar { it.uppercase() }}"
|
||||||
if (targetTypeAsGenericType == null) { // classic type
|
|
||||||
addFunction(
|
|
||||||
FunSpec.builder("${name}Single").apply { configure(actualSingleName) }.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addFunction(
|
addFunction(
|
||||||
FunSpec.builder(actualSingleName).apply { configure() }.build()
|
FunSpec.builder(actualSingleName).apply { configure() }.build()
|
||||||
@@ -216,9 +193,7 @@ class Processor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (generateFactory) {
|
if (generateFactory) {
|
||||||
fun FunSpec.Builder.configure(
|
fun FunSpec.Builder.configure() {
|
||||||
useInstead: String? = null
|
|
||||||
) {
|
|
||||||
addKdoc(
|
addKdoc(
|
||||||
"""
|
"""
|
||||||
Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}"
|
Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}"
|
||||||
@@ -234,29 +209,8 @@ class Processor(
|
|||||||
addTypeVariable(it)
|
addTypeVariable(it)
|
||||||
addModifiers(KModifier.INLINE)
|
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() }}"
|
val actualFactoryName = "factory${name.replaceFirstChar { it.uppercase() }}"
|
||||||
if (targetTypeAsGenericType == null) { // classic type
|
|
||||||
addFunction(
|
|
||||||
FunSpec.builder("${name}Factory").apply { configure(useInstead = actualFactoryName) }.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
addFunction(
|
addFunction(
|
||||||
FunSpec.builder(actualFactoryName).apply { configure() }.build()
|
FunSpec.builder(actualFactoryName).apply { configure() }.build()
|
||||||
)
|
)
|
||||||
|
12
local.migrate.folder.sh
Executable file
12
local.migrate.folder.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function renameFolders() {
|
||||||
|
for folder in $(find . -depth -type d -name "$1");
|
||||||
|
do
|
||||||
|
sedString="s/$1/$2/g"
|
||||||
|
newFolder="$(echo $folder | sed $sedString)"
|
||||||
|
echo $folder "$newFolder"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
renameFolders "androidTest" "androidUnitTest"
|
@@ -18,6 +18,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test-common')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test-common')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,6 +28,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test-common')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,6 +32,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test-common')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmTest {
|
jvmTest {
|
||||||
@@ -45,7 +46,7 @@ kotlin {
|
|||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidTest {
|
androidUnitTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
implementation libs.android.test.junit
|
implementation libs.android.test.junit
|
||||||
|
@@ -31,6 +31,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-common')
|
implementation kotlin('test-common')
|
||||||
implementation kotlin('test-annotations-common')
|
implementation kotlin('test-annotations-common')
|
||||||
|
implementation libs.kt.coroutines.test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
@@ -54,7 +55,7 @@ kotlin {
|
|||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidTest {
|
androidUnitTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('test-junit')
|
implementation kotlin('test-junit')
|
||||||
implementation libs.android.test.junit
|
implementation libs.android.test.junit
|
||||||
|
@@ -32,6 +32,8 @@ String[] includes = [
|
|||||||
":coroutines",
|
":coroutines",
|
||||||
":coroutines:compose",
|
":coroutines:compose",
|
||||||
":android:recyclerview",
|
":android:recyclerview",
|
||||||
|
":android:pickers",
|
||||||
|
":android:smalltextfield",
|
||||||
":android:alerts:common",
|
":android:alerts:common",
|
||||||
":android:alerts:recyclerview",
|
":android:alerts:recyclerview",
|
||||||
":serialization:base64",
|
":serialization:base64",
|
||||||
|
@@ -9,10 +9,7 @@ import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
|||||||
import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.setupDI
|
import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.setupDI
|
||||||
import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.startPlugin
|
import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin.startPlugin
|
||||||
import dev.inmo.micro_utils.startup.plugin.StartPlugin
|
import dev.inmo.micro_utils.startup.plugin.StartPlugin
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.SerialFormat
|
import kotlinx.serialization.SerialFormat
|
||||||
import kotlinx.serialization.StringFormat
|
import kotlinx.serialization.StringFormat
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -116,12 +113,16 @@ object StartLauncherPlugin : StartPlugin {
|
|||||||
/**
|
/**
|
||||||
* Will create [KoinApplication], init, load modules using [StartLauncherPlugin] and start plugins using the same base
|
* Will create [KoinApplication], init, load modules using [StartLauncherPlugin] and start plugins using the same base
|
||||||
* plugin. It is basic [start] method which accepts both [config] and [rawConfig] which suppose to be the same or
|
* plugin. It is basic [start] method which accepts both [config] and [rawConfig] which suppose to be the same or
|
||||||
* at least [rawConfig] must contain serialized variant of [config]
|
* at least [rawConfig] must contain serialized variant of [config].
|
||||||
|
*
|
||||||
|
* Koin part will be started in-place. This means, that after ending of this method call you will be able to
|
||||||
|
* take any declared dependency from koin
|
||||||
*
|
*
|
||||||
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config] ([StartLauncherPlugin] will
|
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config] ([StartLauncherPlugin] will
|
||||||
* deserialize it in its [StartLauncherPlugin.setupDI]
|
* deserialize it in its [StartLauncherPlugin.setupDI]
|
||||||
|
* @return [KoinApplication] of current start and [Job] which can be used to call [CoroutineScope.join]
|
||||||
*/
|
*/
|
||||||
suspend fun start(config: Config, rawConfig: JsonObject) {
|
fun startAsync(config: Config, rawConfig: JsonObject): Pair<KoinApplication, Job> {
|
||||||
|
|
||||||
logger.i("Start initialization")
|
logger.i("Start initialization")
|
||||||
val koinApp = KoinApplication.init()
|
val koinApp = KoinApplication.init()
|
||||||
@@ -133,8 +134,44 @@ object StartLauncherPlugin : StartPlugin {
|
|||||||
logger.i("Modules loaded")
|
logger.i("Modules loaded")
|
||||||
startKoin(koinApp)
|
startKoin(koinApp)
|
||||||
logger.i("Koin started")
|
logger.i("Koin started")
|
||||||
startPlugin(koinApp.koin)
|
val launchJob = koinApp.koin.get<CoroutineScope>().launch {
|
||||||
logger.i("App has been setup")
|
startPlugin(koinApp.koin)
|
||||||
|
logger.i("App has been started")
|
||||||
|
}
|
||||||
|
|
||||||
|
return koinApp to launchJob
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will create [KoinApplication], init, load modules using [StartLauncherPlugin] and start plugins using the same base
|
||||||
|
* plugin. It is basic [start] method which accepts both [config] and [rawConfig] which suppose to be the same or
|
||||||
|
* at least [rawConfig] must contain serialized variant of [config]
|
||||||
|
*
|
||||||
|
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config] ([StartLauncherPlugin] will
|
||||||
|
* deserialize it in its [StartLauncherPlugin.setupDI]
|
||||||
|
* @return [KoinApplication] of current launch
|
||||||
|
*/
|
||||||
|
suspend fun start(config: Config, rawConfig: JsonObject): KoinApplication {
|
||||||
|
|
||||||
|
val (koinApp, job) = startAsync(config, rawConfig)
|
||||||
|
|
||||||
|
job.join()
|
||||||
|
|
||||||
|
return koinApp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call [start] with deserialized [Config] as config and [rawConfig] as is
|
||||||
|
*
|
||||||
|
* Koin part will be started in-place. This means, that after ending of this method call you will be able to
|
||||||
|
* take any declared dependency from koin
|
||||||
|
*
|
||||||
|
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config]
|
||||||
|
* @return [KoinApplication] of current launch and [Job] of starting launch
|
||||||
|
*/
|
||||||
|
fun startAsync(rawConfig: JsonObject): Pair<KoinApplication, Job> {
|
||||||
|
|
||||||
|
return startAsync(defaultJson.decodeFromJsonElement(Config.serializer(), rawConfig), rawConfig)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +180,30 @@ object StartLauncherPlugin : StartPlugin {
|
|||||||
*
|
*
|
||||||
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config]
|
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config]
|
||||||
*/
|
*/
|
||||||
suspend fun start(rawConfig: JsonObject) {
|
suspend fun start(rawConfig: JsonObject): KoinApplication {
|
||||||
|
|
||||||
start(defaultJson.decodeFromJsonElement(Config.serializer(), rawConfig), rawConfig)
|
val (koinApp, job) = startAsync(rawConfig)
|
||||||
|
|
||||||
|
job.join()
|
||||||
|
|
||||||
|
return koinApp
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call [start] with deserialized [Config] as is and serialize it to [JsonObject] to pass as the first parameter
|
||||||
|
* to the basic [start] method
|
||||||
|
*
|
||||||
|
* Koin part will be started in-place. This means, that after ending of this method call you will be able to
|
||||||
|
* take any declared dependency from koin
|
||||||
|
*
|
||||||
|
* @param config Will be converted to [JsonObject] as raw config. That means that all plugins from [config] will
|
||||||
|
* receive serialized version of [config] in [StartPlugin.setupDI] method
|
||||||
|
* @return [KoinApplication] of current launch and [Job] of starting launch
|
||||||
|
*/
|
||||||
|
fun startAsync(config: Config): Pair<KoinApplication, Job> {
|
||||||
|
|
||||||
|
return startAsync(config, defaultJson.encodeToJsonElement(Config.serializer(), config).jsonObject)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +214,13 @@ object StartLauncherPlugin : StartPlugin {
|
|||||||
* @param config Will be converted to [JsonObject] as raw config. That means that all plugins from [config] will
|
* @param config Will be converted to [JsonObject] as raw config. That means that all plugins from [config] will
|
||||||
* receive serialized version of [config] in [StartPlugin.setupDI] method
|
* receive serialized version of [config] in [StartPlugin.setupDI] method
|
||||||
*/
|
*/
|
||||||
suspend fun start(config: Config) {
|
suspend fun start(config: Config): KoinApplication {
|
||||||
|
|
||||||
start(config, defaultJson.encodeToJsonElement(Config.serializer(), config).jsonObject)
|
val (koinApp, job) = startAsync(config)
|
||||||
|
|
||||||
|
job.join()
|
||||||
|
|
||||||
|
return koinApp
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user