mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-18 23:09:23 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
ccc1dd7857 | |||
5d450e0632 | |||
cd3838f321 |
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
name: Build
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
- name: Rewrite version
|
|
||||||
run: |
|
|
||||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
|
||||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
|
||||||
rm gradle.properties
|
|
||||||
mv gradle.properties.tmp gradle.properties
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew build
|
|
||||||
- name: Publish
|
|
||||||
continue-on-error: true
|
|
||||||
run: ./gradlew publishAllPublicationsToGiteaRepository
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
7
.github/workflows/dokka_push.yml
vendored
7
.github/workflows/dokka_push.yml
vendored
@@ -10,9 +10,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 11
|
||||||
|
- name: Fix android 32.0.0 dx
|
||||||
|
continue-on-error: true
|
||||||
|
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew build && ./gradlew dokkaHtml
|
run: ./gradlew dokkaHtml
|
||||||
- name: Publish KDocs
|
- name: Publish KDocs
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
|
28
.github/workflows/packages_push.yml
vendored
Normal file
28
.github/workflows/packages_push.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
name: Publish package to GitHub Packages
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
publishing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
- name: Fix android 32.0.0 dx
|
||||||
|
continue-on-error: true
|
||||||
|
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||||
|
- name: Rewrite version
|
||||||
|
run: |
|
||||||
|
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||||
|
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||||
|
rm gradle.properties
|
||||||
|
mv gradle.properties.tmp gradle.properties
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build
|
||||||
|
- name: Publish
|
||||||
|
continue-on-error: true
|
||||||
|
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||||
|
env:
|
||||||
|
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||||
|
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,8 +14,3 @@ local.properties
|
|||||||
kotlin-js-store
|
kotlin-js-store
|
||||||
|
|
||||||
publishing.sh
|
publishing.sh
|
||||||
|
|
||||||
local.*
|
|
||||||
local/
|
|
||||||
|
|
||||||
.kotlin/
|
|
||||||
|
8
.space.kts
Normal file
8
.space.kts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
job("Build and run tests") {
|
||||||
|
container(displayName = "Run gradle build", image = "openjdk:11") {
|
||||||
|
kotlinScript { api ->
|
||||||
|
// here can be your complex logic
|
||||||
|
api.gradlew("build")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1280
CHANGELOG.md
1280
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.appCompat.resources
|
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
@@ -1,18 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
|
||||||
id "com.android.library"
|
|
||||||
alias(libs.plugins.jb.compose)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
androidMain {
|
|
||||||
dependencies {
|
|
||||||
api project(":micro_utils.android.smalltextfield")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
@@ -1,27 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.android.pickers
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
|
|
||||||
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
|
|
||||||
initialVelocity: Float,
|
|
||||||
animationSpec: DecayAnimationSpec<Float>,
|
|
||||||
adjustTarget: ((Float) -> Float)?,
|
|
||||||
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
|
|
||||||
): AnimationResult<Float, AnimationVector1D> {
|
|
||||||
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
|
|
||||||
val adjustedTarget = adjustTarget?.invoke(targetValue)
|
|
||||||
|
|
||||||
return if (adjustedTarget != null) {
|
|
||||||
animateTo(
|
|
||||||
targetValue = adjustedTarget,
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
animateDecay(
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
animationSpec = animationSpec,
|
|
||||||
block = block,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,219 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.android.pickers
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.exponentialDecay
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.*
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.ExperimentalTextApi
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.center
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
|
|
||||||
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
fun NumberPicker(
|
|
||||||
number: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
range: IntRange? = null,
|
|
||||||
textStyle: TextStyle = LocalTextStyle.current,
|
|
||||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
|
||||||
allowUseManualInput: Boolean = true,
|
|
||||||
onStateChanged: (Int) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val numbersColumnHeight = 36.dp
|
|
||||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
|
||||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
|
||||||
|
|
||||||
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
|
|
||||||
|
|
||||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
|
||||||
if (range != null) {
|
|
||||||
val offsetRange = remember(number, range) {
|
|
||||||
val value = number
|
|
||||||
val first = -(range.last - value) * halvedNumbersColumnHeightPx
|
|
||||||
val last = -(range.first - value) * halvedNumbersColumnHeightPx
|
|
||||||
first..last
|
|
||||||
}
|
|
||||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
|
||||||
val animatedStateValue = animatedStateValue(animatedOffset.value)
|
|
||||||
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
|
|
||||||
|
|
||||||
val inputFieldShown = if (allowUseManualInput) {
|
|
||||||
remember { mutableStateOf(false) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.wrapContentSize()
|
|
||||||
.draggable(
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
state = rememberDraggableState { deltaY ->
|
|
||||||
if (inputFieldShown ?.value != true) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragStopped = { velocity ->
|
|
||||||
if (inputFieldShown ?.value != true) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
val endValue = animatedOffset.fling(
|
|
||||||
initialVelocity = velocity,
|
|
||||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
|
||||||
adjustTarget = { target ->
|
|
||||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
|
||||||
val coercedAnchors =
|
|
||||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
|
||||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
|
||||||
val base =
|
|
||||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
|
||||||
coercedPoint + base
|
|
||||||
}
|
|
||||||
).endState.value
|
|
||||||
|
|
||||||
onStateChanged(animatedStateValue(endValue))
|
|
||||||
animatedOffset.snapTo(0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
val spacing = 4.dp
|
|
||||||
|
|
||||||
val upEnabled = range == null || range.first < number
|
|
||||||
IconButton(
|
|
||||||
{
|
|
||||||
onStateChanged(number - 1)
|
|
||||||
inputFieldShown ?.value = false
|
|
||||||
},
|
|
||||||
enabled = upEnabled
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(spacing))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
val baseLabelModifier = Modifier.align(Alignment.Center)
|
|
||||||
ProvideTextStyle(textStyle) {
|
|
||||||
Text(
|
|
||||||
text = (animatedStateValue - 1).toString(),
|
|
||||||
modifier = baseLabelModifier
|
|
||||||
.offset(y = -halvedNumbersColumnHeight)
|
|
||||||
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (inputFieldShown ?.value == true) {
|
|
||||||
val currentValue = remember { mutableStateOf(number.toString()) }
|
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
SmallTextField(
|
|
||||||
currentValue.value,
|
|
||||||
{
|
|
||||||
val asDigit = it.toIntOrNull()
|
|
||||||
when {
|
|
||||||
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
|
|
||||||
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
|
|
||||||
else -> { /* do nothing */ }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
|
|
||||||
detectTapGestures {
|
|
||||||
if (!checkContains(it)) {
|
|
||||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
|
||||||
inputFieldShown.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Number
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions {
|
|
||||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
|
||||||
inputFieldShown.value = false
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
textStyle = textStyle
|
|
||||||
)
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = animatedStateValue.toString(),
|
|
||||||
modifier = baseLabelModifier
|
|
||||||
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
|
||||||
.clickable {
|
|
||||||
if (inputFieldShown ?.value == false) {
|
|
||||||
inputFieldShown.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = (animatedStateValue + 1).toString(),
|
|
||||||
modifier = baseLabelModifier
|
|
||||||
.offset(y = halvedNumbersColumnHeight)
|
|
||||||
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(spacing))
|
|
||||||
|
|
||||||
val downEnabled = range == null || range.last > number
|
|
||||||
IconButton(
|
|
||||||
{
|
|
||||||
onStateChanged(number + 1)
|
|
||||||
inputFieldShown ?.value = false
|
|
||||||
},
|
|
||||||
enabled = downEnabled
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,153 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.android.pickers
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.exponentialDecay
|
|
||||||
import androidx.compose.foundation.gestures.*
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.ExperimentalTextApi
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.*
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
fun <T> SetPicker(
|
|
||||||
current: T,
|
|
||||||
dataList: List<T>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
textStyle: TextStyle = LocalTextStyle.current,
|
|
||||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
|
||||||
dataToString: @Composable (T) -> String = { it.toString() },
|
|
||||||
onStateChanged: (T) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
|
|
||||||
textStyle.lineHeight.toDp()
|
|
||||||
}
|
|
||||||
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
|
|
||||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
|
||||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
|
||||||
|
|
||||||
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
|
|
||||||
val lastIndex = dataList.size - 1
|
|
||||||
|
|
||||||
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
|
|
||||||
|
|
||||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
|
||||||
val offsetRange = remember(index, lastIndex) {
|
|
||||||
val value = index
|
|
||||||
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
|
|
||||||
val last = value * halvedNumbersColumnHeightPx
|
|
||||||
first..last
|
|
||||||
}
|
|
||||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
|
||||||
}
|
|
||||||
val indexAnimatedOffset = if (animatedOffset.value > 0) {
|
|
||||||
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
|
||||||
} else {
|
|
||||||
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
|
||||||
}
|
|
||||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
|
||||||
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.wrapContentSize()
|
|
||||||
.draggable(
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
state = rememberDraggableState { deltaY ->
|
|
||||||
coroutineScope.launch {
|
|
||||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragStopped = { velocity ->
|
|
||||||
coroutineScope.launch {
|
|
||||||
val endValue = animatedOffset.fling(
|
|
||||||
initialVelocity = velocity,
|
|
||||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
|
||||||
adjustTarget = { target ->
|
|
||||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
|
||||||
val coercedAnchors =
|
|
||||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
|
||||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
|
||||||
val base =
|
|
||||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
|
||||||
coercedPoint + base
|
|
||||||
}
|
|
||||||
).endState.value
|
|
||||||
|
|
||||||
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
|
|
||||||
animatedOffset.snapTo(0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
val spacing = 4.dp
|
|
||||||
|
|
||||||
val upEnabled = index > 0
|
|
||||||
IconButton(
|
|
||||||
{
|
|
||||||
onStateChanged(dataList.elementAt(index - 1))
|
|
||||||
},
|
|
||||||
enabled = upEnabled
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(spacing))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier,
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
ProvideTextStyle(textStyle) {
|
|
||||||
dataList.forEachIndexed { i, t ->
|
|
||||||
val alpha = when {
|
|
||||||
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
|
||||||
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
|
||||||
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
|
||||||
else -> return@forEachIndexed
|
|
||||||
}
|
|
||||||
val offset = when {
|
|
||||||
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
|
|
||||||
i == indexAnimatedOffset -> coercedAnimatedOffset
|
|
||||||
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
|
|
||||||
else -> return@forEachIndexed
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = dataToString(t),
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(alpha)
|
|
||||||
.offset(y = with(LocalDensity.current) { offset.toDp() })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(spacing))
|
|
||||||
|
|
||||||
val downEnabled = index < lastIndex
|
|
||||||
IconButton(
|
|
||||||
{
|
|
||||||
onStateChanged(dataList.elementAt(index + 1))
|
|
||||||
},
|
|
||||||
enabled = downEnabled
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -10,13 +10,13 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.recyclerView
|
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
1
android/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
@@ -1,18 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
|
||||||
id "com.android.library"
|
|
||||||
alias(libs.plugins.jb.compose)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
androidMain {
|
|
||||||
dependencies {
|
|
||||||
api libs.android.compose.material3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
@@ -1,66 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.android.smalltextfield
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.takeOrElse
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SmallTextField(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
readOnly: Boolean = false,
|
|
||||||
textStyle: TextStyle = LocalTextStyle.current,
|
|
||||||
textColor: Color = textStyle.color.takeOrElse {
|
|
||||||
LocalContentColor.current
|
|
||||||
},
|
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
|
||||||
singleLine: Boolean = false,
|
|
||||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
|
||||||
minLines: Int = 1,
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = value,
|
|
||||||
modifier = modifier,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
enabled = enabled,
|
|
||||||
readOnly = readOnly,
|
|
||||||
textStyle = textStyle.copy(
|
|
||||||
color = textColor
|
|
||||||
),
|
|
||||||
visualTransformation = visualTransformation,
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
singleLine = singleLine,
|
|
||||||
maxLines = maxLines,
|
|
||||||
minLines = minLines,
|
|
||||||
cursorBrush = SolidColor(
|
|
||||||
textStyle.color.takeOrElse {
|
|
||||||
LocalContentColor.current
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
20
build.gradle
20
build.gradle
@@ -7,27 +7,20 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.buildscript.kt.gradle
|
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||||
classpath libs.buildscript.kt.serialization
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath libs.buildscript.kt.ksp
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath libs.buildscript.jb.dokka
|
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||||
classpath libs.buildscript.gh.release
|
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||||
classpath libs.buildscript.android.gradle
|
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||||
classpath libs.buildscript.android.dexcount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.versions)
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
|
||||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||||
@@ -42,4 +35,3 @@ allprojects {
|
|||||||
|
|
||||||
apply from: "./extensions.gradle"
|
apply from: "./extensions.gradle"
|
||||||
apply from: "./github_release.gradle"
|
apply from: "./github_release.gradle"
|
||||||
apply from: "./versions_plugin_setup.gradle"
|
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
|
||||||
id "com.android.library"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
|
@@ -1,174 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.colors.common
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.jvm.JvmInline
|
|
||||||
import kotlin.math.floor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for RGBA colors. Receiving [UInt] in main constructor. Each part in main constructor
|
|
||||||
* configured with `0x00 - 0xff` range. Examples:
|
|
||||||
*
|
|
||||||
* * Red: `0xff0000ffu`
|
|
||||||
* * Red (0.5 capacity): `0xff000088u`
|
|
||||||
*
|
|
||||||
* Anyway it is recommended to use
|
|
||||||
*
|
|
||||||
* @param hexaUInt rgba [UInt] in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
@JvmInline
|
|
||||||
value class HEXAColor (
|
|
||||||
val hexaUInt: UInt
|
|
||||||
) : Comparable<HEXAColor> {
|
|
||||||
/**
|
|
||||||
* @returns [hexaUInt] as a string with format `#FFEEBBAA` where FF - red, EE - green, BB - blue and AA - alpha
|
|
||||||
*/
|
|
||||||
val hexa: String
|
|
||||||
get() = "#${hexaUInt.toString(16).padStart(8, '0')}"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns [hexaUInt] as a string with format `#FFEEBB` where FF - red, EE - green and BB - blue
|
|
||||||
*/
|
|
||||||
val hex: String
|
|
||||||
get() = hexa.take(7)
|
|
||||||
/**
|
|
||||||
* @returns [hexaUInt] as a string with format `#AAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue
|
|
||||||
*/
|
|
||||||
val ahex: String
|
|
||||||
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
|
|
||||||
val rgba: String
|
|
||||||
get() = "rgba($r,$g,$b,${aOfOne.toString().take(5)})"
|
|
||||||
val rgb: String
|
|
||||||
get() = "rgb($r,$g,$b)"
|
|
||||||
val shortHex: String
|
|
||||||
get() = "#${r.shortPart()}${g.shortPart()}${b.shortPart()}"
|
|
||||||
val shortHexa: String
|
|
||||||
get() = "$shortHex${a.shortPart()}"
|
|
||||||
val rgbUInt: UInt
|
|
||||||
get() = (hexaUInt / 256u)
|
|
||||||
val rgbInt: Int
|
|
||||||
get() = rgbUInt.toInt()
|
|
||||||
val ahexUInt
|
|
||||||
get() = (a * 0x1000000).toUInt() + rgbUInt
|
|
||||||
|
|
||||||
val r: Int
|
|
||||||
get() = ((hexaUInt and 0xff000000u) / 0x1000000u).toInt()
|
|
||||||
val g: Int
|
|
||||||
get() = ((hexaUInt and 0x00ff0000u) / 0x10000u).toInt()
|
|
||||||
val b: Int
|
|
||||||
get() = ((hexaUInt and 0x0000ff00u) / 0x100u).toInt()
|
|
||||||
val a: Int
|
|
||||||
get() = ((hexaUInt and 0x000000ffu)).toInt()
|
|
||||||
val aOfOne: Float
|
|
||||||
get() = a.toFloat() / (0xff)
|
|
||||||
init {
|
|
||||||
require(hexaUInt in 0u ..0xffffffffu)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(r: Int, g: Int, b: Int, a: Int) : this(
|
|
||||||
((r * 0x1000000).toLong() + g * 0x10000 + b * 0x100 + a).toUInt()
|
|
||||||
) {
|
|
||||||
require(r in 0 ..0xff)
|
|
||||||
require(g in 0 ..0xff)
|
|
||||||
require(b in 0 ..0xff)
|
|
||||||
require(a in 0 ..0xff)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(r: Int, g: Int, b: Int, aOfOne: Float = 1f) : this(
|
|
||||||
r = r, g = g, b = b, a = (aOfOne * 0xff).toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return hexa
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun compareTo(other: HEXAColor): Int = (hexaUInt - other.hexaUInt).coerceIn(Int.MIN_VALUE.toUInt(), Int.MAX_VALUE.toLong().toUInt()).toInt()
|
|
||||||
|
|
||||||
fun copy(
|
|
||||||
r: Int = this.r,
|
|
||||||
g: Int = this.g,
|
|
||||||
b: Int = this.b,
|
|
||||||
aOfOne: Float = this.aOfOne
|
|
||||||
) = HEXAColor(r = r, g = g, b = b, aOfOne = aOfOne)
|
|
||||||
fun copy(
|
|
||||||
r: Int = this.r,
|
|
||||||
g: Int = this.g,
|
|
||||||
b: Int = this.b,
|
|
||||||
a: Int
|
|
||||||
) = HEXAColor(r = r, g = g, b = b, a = a)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Parsing color from [color]
|
|
||||||
*
|
|
||||||
* Supported formats samples (on Red color based):
|
|
||||||
*
|
|
||||||
* * `#f00`
|
|
||||||
* * `#f00f`
|
|
||||||
* * `#ff0000`
|
|
||||||
* * `#ff0000ff`
|
|
||||||
* * `rgb(255, 0, 0)`
|
|
||||||
* * `rgba(255, 0, 0, 1)`
|
|
||||||
*/
|
|
||||||
fun parseStringColor(color: String): HEXAColor = when {
|
|
||||||
color.startsWith("#") -> color.removePrefix("#").let { color ->
|
|
||||||
when (color.length) {
|
|
||||||
3 -> color.map { "$it$it" }.joinToString(separator = "", postfix = "ff")
|
|
||||||
4 -> color.take(3).map { "$it$it" }.joinToString(separator = "", postfix = color.takeLast(1).let { "${it}0" })
|
|
||||||
6 -> "${color}ff"
|
|
||||||
8 -> color
|
|
||||||
else -> error("Malfurmed color string: $color. It is expected that color started with # will contains 3, 6 or 8 valuable parts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
color.startsWith("rgb(") -> color
|
|
||||||
.removePrefix("rgb(")
|
|
||||||
.removeSuffix(")")
|
|
||||||
.replace(Regex("\\s"), "")
|
|
||||||
.split(",")
|
|
||||||
.joinToString("", postfix = "ff") {
|
|
||||||
it.toInt().toString(16).padStart(2, '0')
|
|
||||||
}
|
|
||||||
color.startsWith("rgba(") -> color
|
|
||||||
.removePrefix("rgba(")
|
|
||||||
.removeSuffix(")")
|
|
||||||
.replace(Regex("\\s"), "")
|
|
||||||
.split(",").let {
|
|
||||||
it.take(3).map { it.toInt().toString(16).padStart(2, '0') } + (it.last().toFloat() * 0xff).toInt().toString(16).padStart(2, '0')
|
|
||||||
}
|
|
||||||
.joinToString("")
|
|
||||||
else -> color
|
|
||||||
}.lowercase().toUInt(16).let(::HEXAColor)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates [HEXAColor] from [uint] presume it is in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
|
|
||||||
*/
|
|
||||||
fun fromHexa(uint: UInt) = HEXAColor(uint)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates [HEXAColor] from [uint] presume it is in format `0xAAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue`
|
|
||||||
*/
|
|
||||||
fun fromAhex(uint: UInt) = HEXAColor(
|
|
||||||
a = ((uint and 0xff000000u) / 0x1000000u).toInt(),
|
|
||||||
r = ((uint and 0x00ff0000u) / 0x10000u).toInt(),
|
|
||||||
g = ((uint and 0x0000ff00u) / 0x100u).toInt(),
|
|
||||||
b = ((uint and 0x000000ffu)).toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsing color from [color]
|
|
||||||
*
|
|
||||||
* Supported formats samples (on Red color based):
|
|
||||||
*
|
|
||||||
* * `#f00`
|
|
||||||
* * `#ff0000`
|
|
||||||
* * `#ff0000ff`
|
|
||||||
* * `rgb(255, 0, 0)`
|
|
||||||
* * `rgba(255, 0, 0, 1)`
|
|
||||||
*/
|
|
||||||
operator fun invoke(color: String) = parseStringColor(color)
|
|
||||||
|
|
||||||
private fun Int.shortPart(): String {
|
|
||||||
return (floor(toFloat() / 16)).toInt().toString(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,209 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.colors.common
|
|
||||||
|
|
||||||
import kotlin.math.floor
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class HexColorTests {
|
|
||||||
val alphaRgbaPrecision = 5
|
|
||||||
class TestColor(
|
|
||||||
val color: HEXAColor,
|
|
||||||
val shortHex: String,
|
|
||||||
val shortHexa: String,
|
|
||||||
val hex: String,
|
|
||||||
val hexa: String,
|
|
||||||
val ahex: String,
|
|
||||||
val ahexUInt: UInt,
|
|
||||||
val rgbUInt: UInt,
|
|
||||||
val rgb: String,
|
|
||||||
val rgba: String,
|
|
||||||
val r: Int,
|
|
||||||
val g: Int,
|
|
||||||
val b: Int,
|
|
||||||
val a: Int,
|
|
||||||
vararg val additionalRGBAVariants: String
|
|
||||||
)
|
|
||||||
val testColors: List<TestColor>
|
|
||||||
get() = listOf(
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(hexaUInt = 0xff0000ffu),
|
|
||||||
shortHex = "#f00",
|
|
||||||
shortHexa = "#f00f",
|
|
||||||
hex = "#ff0000",
|
|
||||||
hexa = "#ff0000ff",
|
|
||||||
ahex = "#ffff0000",
|
|
||||||
ahexUInt = 0xffff0000u,
|
|
||||||
rgbUInt = 0xff0000u,
|
|
||||||
rgb = "rgb(255,0,0)",
|
|
||||||
rgba = "rgba(255,0,0,1.0)",
|
|
||||||
r = 0xff,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0xff,
|
|
||||||
"rgba(255,0,0,1)",
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(hexaUInt = 0x00ff00ffu),
|
|
||||||
shortHex = "#0f0",
|
|
||||||
shortHexa = "#0f0f",
|
|
||||||
hex = "#00ff00",
|
|
||||||
hexa = "#00ff00ff",
|
|
||||||
ahex = "#ff00ff00",
|
|
||||||
ahexUInt = 0xff00ff00u,
|
|
||||||
rgbUInt = 0x00ff00u,
|
|
||||||
rgb = "rgb(0,255,0)",
|
|
||||||
rgba = "rgba(0,255,0,1.0)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0xff,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0xff,
|
|
||||||
"rgba(0,255,0,1)"
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0x0000ffffu),
|
|
||||||
shortHex = "#00f",
|
|
||||||
shortHexa = "#00ff",
|
|
||||||
hex = "#0000ff",
|
|
||||||
hexa = "#0000ffff",
|
|
||||||
ahex = "#ff0000ff",
|
|
||||||
ahexUInt = 0xff0000ffu,
|
|
||||||
rgbUInt = 0x0000ffu,
|
|
||||||
rgb = "rgb(0,0,255)",
|
|
||||||
rgba = "rgba(0,0,255,1.0)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0xff,
|
|
||||||
a = 0xff,
|
|
||||||
"rgba(0,0,255,1)"
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0xff000088u),
|
|
||||||
shortHex = "#f00",
|
|
||||||
shortHexa = "#f008",
|
|
||||||
hex = "#ff0000",
|
|
||||||
hexa = "#ff000088",
|
|
||||||
ahex = "#88ff0000",
|
|
||||||
ahexUInt = 0x88ff0000u,
|
|
||||||
rgbUInt = 0xff0000u,
|
|
||||||
rgb = "rgb(255,0,0)",
|
|
||||||
rgba = "rgba(255,0,0,0.533)",
|
|
||||||
r = 0xff,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0x88,
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0x00ff0088u),
|
|
||||||
shortHex = "#0f0",
|
|
||||||
shortHexa = "#0f08",
|
|
||||||
hex = "#00ff00",
|
|
||||||
hexa = "#00ff0088",
|
|
||||||
ahex = "#8800ff00",
|
|
||||||
ahexUInt = 0x8800ff00u,
|
|
||||||
rgbUInt = 0x00ff00u,
|
|
||||||
rgb = "rgb(0,255,0)",
|
|
||||||
rgba = "rgba(0,255,0,0.533)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0xff,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0x88,
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0x0000ff88u),
|
|
||||||
shortHex = "#00f",
|
|
||||||
shortHexa = "#00f8",
|
|
||||||
hex = "#0000ff",
|
|
||||||
hexa = "#0000ff88",
|
|
||||||
ahex = "#880000ff",
|
|
||||||
ahexUInt = 0x880000ffu,
|
|
||||||
rgbUInt = 0x0000ffu,
|
|
||||||
rgb = "rgb(0,0,255)",
|
|
||||||
rgba = "rgba(0,0,255,0.533)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0xff,
|
|
||||||
a = 0x88,
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0xff000022u),
|
|
||||||
shortHex = "#f00",
|
|
||||||
shortHexa = "#f002",
|
|
||||||
hex = "#ff0000",
|
|
||||||
hexa = "#ff000022",
|
|
||||||
ahex = "#22ff0000",
|
|
||||||
ahexUInt = 0x22ff0000u,
|
|
||||||
rgbUInt = 0xff0000u,
|
|
||||||
rgb = "rgb(255,0,0)",
|
|
||||||
rgba = "rgba(255,0,0,0.133)",
|
|
||||||
r = 0xff,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0x22,
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0x00ff0022u),
|
|
||||||
shortHex = "#0f0",
|
|
||||||
shortHexa = "#0f02",
|
|
||||||
hex = "#00ff00",
|
|
||||||
hexa = "#00ff0022",
|
|
||||||
ahex = "#2200ff00",
|
|
||||||
ahexUInt = 0x2200ff00u,
|
|
||||||
rgbUInt = 0x00ff00u,
|
|
||||||
rgb = "rgb(0,255,0)",
|
|
||||||
rgba = "rgba(0,255,0,0.133)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0xff,
|
|
||||||
b = 0x00,
|
|
||||||
a = 0x22,
|
|
||||||
),
|
|
||||||
TestColor(
|
|
||||||
color = HEXAColor(0x0000ff22u),
|
|
||||||
shortHex = "#00f",
|
|
||||||
shortHexa = "#00f2",
|
|
||||||
hex = "#0000ff",
|
|
||||||
hexa = "#0000ff22",
|
|
||||||
ahex = "#220000ff",
|
|
||||||
ahexUInt = 0x220000ffu,
|
|
||||||
rgbUInt = 0x0000ffu,
|
|
||||||
rgb = "rgb(0,0,255)",
|
|
||||||
rgba = "rgba(0,0,255,0.133)",
|
|
||||||
r = 0x00,
|
|
||||||
g = 0x00,
|
|
||||||
b = 0xff,
|
|
||||||
a = 0x22,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun baseTest() {
|
|
||||||
testColors.forEach {
|
|
||||||
assertEquals(it.hex, it.color.hex)
|
|
||||||
assertEquals(it.hexa, it.color.hexa)
|
|
||||||
assertEquals(it.ahex, it.color.ahex)
|
|
||||||
assertEquals(it.rgbUInt, it.color.rgbUInt)
|
|
||||||
assertEquals(it.ahexUInt, it.color.ahexUInt)
|
|
||||||
assertEquals(it.shortHex, it.color.shortHex)
|
|
||||||
assertEquals(it.shortHexa, it.color.shortHexa)
|
|
||||||
assertEquals(it.rgb, it.color.rgb)
|
|
||||||
assertTrue(it.rgba == it.color.rgba || it.color.rgba in it.additionalRGBAVariants)
|
|
||||||
assertEquals(it.r, it.color.r)
|
|
||||||
assertEquals(it.g, it.color.g)
|
|
||||||
assertEquals(it.b, it.color.b)
|
|
||||||
assertEquals(it.a, it.color.a)
|
|
||||||
assertEquals(it.color, HEXAColor.fromAhex(it.ahexUInt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHexParseColor() {
|
|
||||||
testColors.forEach {
|
|
||||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex))
|
|
||||||
assertEquals(it.color, HEXAColor.parseStringColor(it.hexa))
|
|
||||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.rgb))
|
|
||||||
assertTrue(it.color.hexaUInt.toInt() - HEXAColor.parseStringColor(it.rgba).hexaUInt.toInt() in -0x1 .. 0x1, )
|
|
||||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex))
|
|
||||||
assertEquals(it.color.copy(a = floor(it.color.a.toFloat() / 16).toInt() * 0x10), HEXAColor.parseStringColor(it.shortHexa))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,15 +4,10 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
|
||||||
dependencies {
|
|
||||||
api libs.klock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.coroutines")
|
api project(":micro_utils.coroutines")
|
||||||
@@ -20,13 +15,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.android.fragment
|
api project(":micro_utils.coroutines")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nativeMain {
|
|
||||||
dependencies {
|
|
||||||
api libs.okio
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
|
||||||
id "com.android.library"
|
|
||||||
alias(libs.plugins.jb.compose)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain {
|
|
||||||
dependencies {
|
|
||||||
api project(":micro_utils.common")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
@@ -1,16 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.DisposableEffectResult
|
|
||||||
|
|
||||||
class DefaultDisposableEffectResult(
|
|
||||||
private val onDispose: () -> Unit
|
|
||||||
) : DisposableEffectResult {
|
|
||||||
override fun dispose() {
|
|
||||||
onDispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DoNothing = DefaultDisposableEffectResult {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,10 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts current [MutableState] to immutable [State] using [derivedStateOf]
|
|
||||||
*/
|
|
||||||
fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value }
|
|
@@ -1,9 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composition
|
|
||||||
import dev.inmo.micro_utils.common.onRemoved
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
fun Composition.linkWithElement(element: Element) {
|
|
||||||
element.onRemoved { dispose() }
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import org.jetbrains.compose.web.attributes.ATarget
|
|
||||||
|
|
||||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
|
||||||
link,
|
|
||||||
mode.targetStr,
|
|
||||||
features
|
|
||||||
)
|
|
||||||
|
|
@@ -1,13 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import org.jetbrains.compose.web.dom.DOMScope
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
fun <TElement : Element> renderComposableAndLinkToRoot(
|
|
||||||
root: TElement,
|
|
||||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
|
||||||
content: @Composable DOMScope<TElement>.() -> Unit
|
|
||||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
|
||||||
linkWithElement(root)
|
|
||||||
}
|
|
@@ -1,43 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common.compose
|
|
||||||
|
|
||||||
import org.jetbrains.compose.web.css.*
|
|
||||||
|
|
||||||
object SkeletonAnimation : StyleSheet() {
|
|
||||||
val skeletonKeyFrames: CSSNamedKeyframes by keyframes {
|
|
||||||
to { backgroundPosition("-20% 0") }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CSSBuilder.includeSkeletonStyle(
|
|
||||||
duration: CSSSizeValue<out CSSUnitTime> = 2.s,
|
|
||||||
timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut,
|
|
||||||
iterationCount: Int? = null,
|
|
||||||
direction: AnimationDirection = AnimationDirection.Normal,
|
|
||||||
keyFrames: CSSNamedKeyframes = skeletonKeyFrames,
|
|
||||||
hideChildren: Boolean = true,
|
|
||||||
hideText: Boolean = hideChildren
|
|
||||||
) {
|
|
||||||
backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)")
|
|
||||||
backgroundSize("200% 100%")
|
|
||||||
backgroundPosition("180% 0")
|
|
||||||
animation(keyFrames) {
|
|
||||||
duration(duration)
|
|
||||||
timingFunction(timingFunction)
|
|
||||||
iterationCount(iterationCount)
|
|
||||||
direction(direction)
|
|
||||||
}
|
|
||||||
if (hideText) {
|
|
||||||
property("color", "${Color.transparent} !important")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hideChildren) {
|
|
||||||
child(self, universal) style {
|
|
||||||
property("visibility", "hidden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val skeleton by style {
|
|
||||||
includeSkeletonStyle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,76 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import java.io.Serializable
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
object ArgumentPropertyNullableDelegate {
|
|
||||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? {
|
|
||||||
val arguments = thisRef.arguments ?: return null
|
|
||||||
val key = property.name
|
|
||||||
return when (property.getter.returnType.classifier) {
|
|
||||||
// Scalars
|
|
||||||
String::class -> arguments.getString(key)
|
|
||||||
Boolean::class -> arguments.getBoolean(key)
|
|
||||||
Byte::class -> arguments.getByte(key)
|
|
||||||
Char::class -> arguments.getChar(key)
|
|
||||||
Double::class -> arguments.getDouble(key)
|
|
||||||
Float::class -> arguments.getFloat(key)
|
|
||||||
Int::class -> arguments.getInt(key)
|
|
||||||
Long::class -> arguments.getLong(key)
|
|
||||||
Short::class -> arguments.getShort(key)
|
|
||||||
|
|
||||||
// References
|
|
||||||
Bundle::class -> arguments.getBundle(key)
|
|
||||||
CharSequence::class -> arguments.getCharSequence(key)
|
|
||||||
Parcelable::class -> arguments.getParcelable(key)
|
|
||||||
|
|
||||||
// Scalar arrays
|
|
||||||
BooleanArray::class -> arguments.getBooleanArray(key)
|
|
||||||
ByteArray::class -> arguments.getByteArray(key)
|
|
||||||
CharArray::class -> arguments.getCharArray(key)
|
|
||||||
DoubleArray::class -> arguments.getDoubleArray(key)
|
|
||||||
FloatArray::class -> arguments.getFloatArray(key)
|
|
||||||
IntArray::class -> arguments.getIntArray(key)
|
|
||||||
LongArray::class -> arguments.getLongArray(key)
|
|
||||||
ShortArray::class -> arguments.getShortArray(key)
|
|
||||||
Array::class -> {
|
|
||||||
val componentType = property.returnType.classifier ?.javaClass ?.componentType!!
|
|
||||||
@Suppress("UNCHECKED_CAST") // Checked by reflection.
|
|
||||||
when {
|
|
||||||
Parcelable::class.java.isAssignableFrom(componentType) -> {
|
|
||||||
arguments.getParcelableArray(key)
|
|
||||||
}
|
|
||||||
String::class.java.isAssignableFrom(componentType) -> {
|
|
||||||
arguments.getStringArray(key)
|
|
||||||
}
|
|
||||||
CharSequence::class.java.isAssignableFrom(componentType) -> {
|
|
||||||
arguments.getCharSequenceArray(key)
|
|
||||||
}
|
|
||||||
Serializable::class.java.isAssignableFrom(componentType) -> {
|
|
||||||
arguments.getSerializable(key)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val valueType = componentType.canonicalName
|
|
||||||
throw IllegalArgumentException(
|
|
||||||
"Illegal value array type $valueType for key \"$key\""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Serializable::class -> arguments.getSerializable(key)
|
|
||||||
else -> null
|
|
||||||
} as? T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ArgumentPropertyNonNullableDelegate {
|
|
||||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T {
|
|
||||||
return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun argumentOrNull() = ArgumentPropertyNullableDelegate
|
|
||||||
fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate
|
|
@@ -1,61 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
|
|
||||||
fun findViewsByTag(viewGroup: ViewGroup, tag: Any?): List<View> {
|
|
||||||
return viewGroup.children.flatMap {
|
|
||||||
findViewsByTag(it, tag)
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findViewsByTag(viewGroup: ViewGroup, key: Int, tag: Any?): List<View> {
|
|
||||||
return viewGroup.children.flatMap {
|
|
||||||
findViewsByTag(it, key, tag)
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findViewsByTag(view: View, tag: Any?): List<View> {
|
|
||||||
val result = mutableListOf<View>()
|
|
||||||
if (view.tag == tag) {
|
|
||||||
result.add(view)
|
|
||||||
}
|
|
||||||
if (view is ViewGroup) {
|
|
||||||
result.addAll(findViewsByTag(view, tag))
|
|
||||||
}
|
|
||||||
return result.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findViewsByTag(view: View, key: Int, tag: Any?): List<View> {
|
|
||||||
val result = mutableListOf<View>()
|
|
||||||
if (view.getTag(key) == tag) {
|
|
||||||
result.add(view)
|
|
||||||
}
|
|
||||||
if (view is ViewGroup) {
|
|
||||||
result.addAll(findViewsByTag(view, key, tag))
|
|
||||||
}
|
|
||||||
return result.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.findViewsByTag(tag: Any?) = rootView ?.let {
|
|
||||||
findViewsByTag(it, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.findViewsByTag(key: Int, tag: Any?) = rootView ?.let {
|
|
||||||
findViewsByTag(it, key, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.findViewsByTag(tag: Any?) = view ?.let {
|
|
||||||
findViewsByTag(it, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.findViewsByTag(key: Int, tag: Any?) = view ?.let {
|
|
||||||
findViewsByTag(it, key, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Fragment.findViewsByTagInActivity(tag: Any?) = activity ?.findViewsByTag(tag)
|
|
||||||
|
|
||||||
fun Fragment.findViewsByTagInActivity(key: Int, tag: Any?) = activity ?.findViewsByTag(key, tag)
|
|
@@ -1,7 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
val Activity.rootView: View?
|
|
||||||
get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView
|
|
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import korlibs.time.DateTime
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes [DateTime] as its raw [DateTime.unixMillis] and deserializes in the same way
|
|
||||||
*/
|
|
||||||
object DateTimeSerializer : KSerializer<DateTime> {
|
|
||||||
override val descriptor: SerialDescriptor
|
|
||||||
get() = Double.serializer().descriptor
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): DateTime {
|
|
||||||
return DateTime(decoder.decodeDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: DateTime) {
|
|
||||||
encoder.encodeDouble(value.unixMillis)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
private inline fun <T> getObject(
|
private inline fun <T> getObject(
|
||||||
additional: MutableList<T>,
|
additional: MutableList<T>,
|
||||||
iterator: Iterator<T>
|
iterator: Iterator<T>
|
||||||
@@ -16,33 +14,16 @@ private inline fun <T> getObject(
|
|||||||
/**
|
/**
|
||||||
* Diff object which contains information about differences between two [Iterable]s
|
* Diff object which contains information about differences between two [Iterable]s
|
||||||
*
|
*
|
||||||
* See tests for more info
|
|
||||||
*
|
|
||||||
* @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection
|
|
||||||
* @param added The object which appear in new collection only. Indexes here show the index in the new collection
|
|
||||||
* @param replaced Pair of old-new changes. First object has been presented in the old collection on its
|
|
||||||
* [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in
|
|
||||||
* case when some value has been replaced after adds or removes in original collection the object index will be changed
|
|
||||||
*
|
|
||||||
* @see calculateDiff
|
* @see calculateDiff
|
||||||
*/
|
*/
|
||||||
@Serializable
|
data class Diff<T> internal constructor(
|
||||||
data class Diff<T> @Warning(warning) constructor(
|
val removed: List<IndexedValue<T>>,
|
||||||
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
|
||||||
/**
|
/**
|
||||||
* Old-New values pairs
|
* Old-New values pairs
|
||||||
*/
|
*/
|
||||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
val added: List<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())
|
|
||||||
|
|
||||||
private inline fun <T> performChanges(
|
private inline fun <T> performChanges(
|
||||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||||
@@ -51,18 +32,17 @@ private inline fun <T> performChanges(
|
|||||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||||
removedList: MutableList<IndexedValue<T>>,
|
removedList: MutableList<IndexedValue<T>>,
|
||||||
addedList: MutableList<IndexedValue<T>>,
|
addedList: MutableList<IndexedValue<T>>,
|
||||||
comparisonFun: (T?, T?) -> Boolean
|
strictComparison: Boolean
|
||||||
) {
|
) {
|
||||||
var i = -1
|
var i = -1
|
||||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||||
i++
|
i++
|
||||||
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
||||||
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
||||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||||
changedList.addAll(
|
changedList.addAll(
|
||||||
potentialChanges.take(i).mapNotNull {
|
potentialChanges.take(i).mapNotNull {
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -112,7 +92,7 @@ private inline fun <T> performChanges(
|
|||||||
*/
|
*/
|
||||||
fun <T> Iterable<T>.calculateDiff(
|
fun <T> Iterable<T>.calculateDiff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
comparisonFun: (T?, T?) -> Boolean
|
strictComparison: Boolean = false
|
||||||
): Diff<T> {
|
): Diff<T> {
|
||||||
var i = -1
|
var i = -1
|
||||||
var j = -1
|
var j = -1
|
||||||
@@ -140,60 +120,31 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
comparisonFun(oldObject, newObject) -> {
|
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
|
||||||
})
|
|
||||||
potentiallyChangedObjects.clear()
|
potentiallyChangedObjects.clear()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||||
val previousOldsAdditionsSize = additionalInOld.size
|
val previousOldsAdditionsSize = additionalInOld.size
|
||||||
val previousNewsAdditionsSize = additionalInNew.size
|
val previousNewsAdditionsSize = additionalInNew.size
|
||||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
potentiallyChangedObjects.add(null to null)
|
potentiallyChangedObjects.add(null to null)
|
||||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||||
|
|
||||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculating [Diff] object
|
|
||||||
*
|
|
||||||
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
|
|
||||||
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
|
|
||||||
*/
|
|
||||||
fun <T> Iterable<T>.calculateDiff(
|
|
||||||
other: Iterable<T>,
|
|
||||||
strictComparison: Boolean = false
|
|
||||||
): Diff<T> = calculateDiff(
|
|
||||||
other,
|
|
||||||
comparisonFun = if (strictComparison) {
|
|
||||||
{ t1, t2 ->
|
|
||||||
t1 === t2
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{ t1, t2 ->
|
|
||||||
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
inline fun <T> Iterable<T>.diff(
|
inline fun <T> Iterable<T>.diff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
strictComparison: Boolean = false
|
||||||
): Diff<T> = calculateDiff(other, strictComparison)
|
): Diff<T> = calculateDiff(other, strictComparison)
|
||||||
inline fun <T> Iterable<T>.diff(
|
|
||||||
other: Iterable<T>,
|
|
||||||
noinline comparisonFun: (T?, T?) -> Boolean
|
|
||||||
): Diff<T> = calculateDiff(other, comparisonFun)
|
|
||||||
|
|
||||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,52 +153,3 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
|||||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||||
other: Iterable<T>
|
other: Iterable<T>
|
||||||
) = calculateDiff(other, strictComparison = true)
|
) = calculateDiff(other, strictComparison = true)
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies [diff] to [this] [MutableList]
|
|
||||||
*/
|
|
||||||
fun <T> MutableList<T>.applyDiff(
|
|
||||||
diff: Diff<T>
|
|
||||||
) {
|
|
||||||
for (i in diff.removed.indices.sortedDescending()) {
|
|
||||||
removeAt(diff.removed[i].index)
|
|
||||||
}
|
|
||||||
diff.added.forEach { (i, t) ->
|
|
||||||
add(i, t)
|
|
||||||
}
|
|
||||||
diff.replaced.forEach { (_, new) ->
|
|
||||||
set(new.index, new.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
|
||||||
* mutable list
|
|
||||||
*/
|
|
||||||
fun <T> MutableList<T>.applyDiff(
|
|
||||||
source: Iterable<T>,
|
|
||||||
strictComparison: Boolean = false
|
|
||||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
|
||||||
applyDiff(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method call [calculateDiff] and then apply differences to [this]
|
|
||||||
* mutable list
|
|
||||||
*/
|
|
||||||
fun <T> MutableList<T>.applyDiff(
|
|
||||||
source: Iterable<T>,
|
|
||||||
comparisonFun: (T?, T?) -> Boolean
|
|
||||||
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
|
||||||
applyDiff(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
|
|
||||||
* [Diff.replaced] values will be reversed too
|
|
||||||
*/
|
|
||||||
fun <T> Diff<T>.reversed() = Diff(
|
|
||||||
removed = added,
|
|
||||||
replaced = replaced.map { it.second to it.first },
|
|
||||||
added = removed
|
|
||||||
)
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("unused", "NOTHING_TO_INLINE")
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@@ -23,18 +21,26 @@ import kotlinx.serialization.encoding.*
|
|||||||
sealed interface Either<T1, T2> {
|
sealed interface Either<T1, T2> {
|
||||||
val optionalT1: Optional<T1>
|
val optionalT1: Optional<T1>
|
||||||
val optionalT2: Optional<T2>
|
val optionalT2: Optional<T2>
|
||||||
|
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
|
||||||
val t1OrNull: T1?
|
val t1: T1?
|
||||||
get() = optionalT1.dataOrNull()
|
get() = optionalT1.dataOrNull()
|
||||||
val t2OrNull: T2?
|
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
||||||
|
val t2: T2?
|
||||||
get() = optionalT2.dataOrNull()
|
get() = optionalT2.dataOrNull()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T1, T2> serializer(
|
||||||
|
t1Serializer: KSerializer<T1>,
|
||||||
|
t2Serializer: KSerializer<T2>,
|
||||||
|
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EitherSerializer<T1, T2>(
|
class EitherSerializer<T1, T2>(
|
||||||
t1Serializer: KSerializer<T1>,
|
t1Serializer: KSerializer<T1>,
|
||||||
t2Serializer: KSerializer<T2>,
|
t2Serializer: KSerializer<T2>,
|
||||||
) : KSerializer<Either<T1, T2>> {
|
) : KSerializer<Either<T1, T2>> {
|
||||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||||
"TypedSerializer",
|
"TypedSerializer",
|
||||||
SerialKind.CONTEXTUAL
|
SerialKind.CONTEXTUAL
|
||||||
@@ -45,6 +51,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||||
return decoder.decodeStructure(descriptor) {
|
return decoder.decodeStructure(descriptor) {
|
||||||
var type: String? = null
|
var type: String? = null
|
||||||
@@ -76,6 +83,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||||
encoder.encodeStructure(descriptor) {
|
encoder.encodeStructure(descriptor) {
|
||||||
when (value) {
|
when (value) {
|
||||||
@@ -97,7 +105,7 @@ class EitherSerializer<T1, T2>(
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EitherFirst<T1, T2>(
|
data class EitherFirst<T1, T2>(
|
||||||
val t1: T1
|
override val t1: T1
|
||||||
) : Either<T1, T2> {
|
) : Either<T1, T2> {
|
||||||
override val optionalT1: Optional<T1> = t1.optional
|
override val optionalT1: Optional<T1> = t1.optional
|
||||||
override val optionalT2: Optional<T2> = Optional.absent()
|
override val optionalT2: Optional<T2> = Optional.absent()
|
||||||
@@ -108,7 +116,7 @@ data class EitherFirst<T1, T2>(
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EitherSecond<T1, T2>(
|
data class EitherSecond<T1, T2>(
|
||||||
val t2: T2
|
override val t2: T2
|
||||||
) : Either<T1, T2> {
|
) : Either<T1, T2> {
|
||||||
override val optionalT1: Optional<T1> = Optional.absent()
|
override val optionalT1: Optional<T1> = Optional.absent()
|
||||||
override val optionalT2: Optional<T2> = t2.optional
|
override val optionalT2: Optional<T2> = t2.optional
|
||||||
|
@@ -1,43 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
|
|
||||||
return if (this) {
|
|
||||||
block()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
|
|
||||||
return if (this) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
|
|
||||||
letIfTrue(block)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
|
|
||||||
letIfFalse(block)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
|
|
||||||
return if (this) {
|
|
||||||
block()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
|
|
||||||
return if (this) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
import kotlinx.serialization.builtins.PairSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
|
|
||||||
class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> {
|
|
||||||
private val originalSerializer = PairSerializer(Int.serializer(), subSerializer)
|
|
||||||
override val descriptor: SerialDescriptor
|
|
||||||
get() = originalSerializer.descriptor
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): IndexedValue<T> {
|
|
||||||
val pair = originalSerializer.deserialize(decoder)
|
|
||||||
return IndexedValue(
|
|
||||||
pair.first,
|
|
||||||
pair.second
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: IndexedValue<T>) {
|
|
||||||
originalSerializer.serialize(
|
|
||||||
encoder,
|
|
||||||
Pair(value.index, value.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,135 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains diff based on the comparison of objects with the same [K].
|
|
||||||
*
|
|
||||||
* @param removed Contains map with keys removed from parent map
|
|
||||||
* @param changed Contains map with keys values changed new map in comparison with old one
|
|
||||||
* @param added Contains map with new keys and values
|
|
||||||
*/
|
|
||||||
data class MapDiff<K, V> @Warning(warning) constructor(
|
|
||||||
val removed: Map<K, V>,
|
|
||||||
val changed: Map<K, Pair<V, V>>,
|
|
||||||
val added: Map<K, V>
|
|
||||||
) {
|
|
||||||
fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty()
|
|
||||||
inline fun isNotEmpty() = !isEmpty()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
|
|
||||||
fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <K, V> createCompareFun(
|
|
||||||
strictComparison: Boolean
|
|
||||||
): (K, V, V) -> Boolean = if (strictComparison) {
|
|
||||||
{ _, first, second -> first === second }
|
|
||||||
} else {
|
|
||||||
{ _, first, second -> first == second }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
|
||||||
*
|
|
||||||
* @param compareFun Will be used to determine changed values
|
|
||||||
*/
|
|
||||||
fun <K, V> Map<K, V>.diff(
|
|
||||||
other: Map<K, V>,
|
|
||||||
compareFun: (K, V, V) -> Boolean
|
|
||||||
): MapDiff<K, V> {
|
|
||||||
val removed: Map<K, V> = (keys - other.keys).associateWith {
|
|
||||||
getValue(it)
|
|
||||||
}
|
|
||||||
val added: Map<K, V> = (other.keys - keys).associateWith {
|
|
||||||
other.getValue(it)
|
|
||||||
}
|
|
||||||
val changed = keys.intersect(other.keys).mapNotNull {
|
|
||||||
val old = getValue(it)
|
|
||||||
val new = other.getValue(it)
|
|
||||||
if (compareFun(it, old, new)) {
|
|
||||||
return@mapNotNull null
|
|
||||||
} else {
|
|
||||||
it to (old to new)
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
return MapDiff(
|
|
||||||
removed,
|
|
||||||
changed,
|
|
||||||
added
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
|
||||||
*
|
|
||||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
|
||||||
* `equals` will be used
|
|
||||||
*/
|
|
||||||
fun <K, V> Map<K, V>.diff(
|
|
||||||
other: Map<K, V>,
|
|
||||||
strictComparison: Boolean = false
|
|
||||||
): MapDiff<K, V> = diff(
|
|
||||||
other,
|
|
||||||
compareFun = createCompareFun(strictComparison)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will apply [mapDiff] to [this] [MutableMap]
|
|
||||||
*/
|
|
||||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
|
||||||
mapDiff: MapDiff<K, V>
|
|
||||||
) {
|
|
||||||
mapDiff.apply {
|
|
||||||
removed.keys.forEach { remove(it) }
|
|
||||||
changed.forEach { (k, oldNew) ->
|
|
||||||
put(k, oldNew.second)
|
|
||||||
}
|
|
||||||
added.forEach { (k, new) ->
|
|
||||||
put(k, new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will apply changes with [from] map into [this] one
|
|
||||||
*
|
|
||||||
* @param compareFun Will be used to determine changed values
|
|
||||||
*
|
|
||||||
* @return [MapDiff] applied to [this] [MutableMap]
|
|
||||||
*/
|
|
||||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
|
||||||
from: Map<K, V>,
|
|
||||||
compareFun: (K, V, V) -> Boolean
|
|
||||||
): MapDiff<K, V> {
|
|
||||||
return diff(from, compareFun).also {
|
|
||||||
applyDiff(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will apply changes with [from] map into [this] one
|
|
||||||
*
|
|
||||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
|
||||||
* `equals` will be used
|
|
||||||
*
|
|
||||||
* @return [MapDiff] applied to [this] [MutableMap]
|
|
||||||
*/
|
|
||||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
|
||||||
from: Map<K, V>,
|
|
||||||
strictComparison: Boolean = false
|
|
||||||
): MapDiff<K, V> = applyDiff(
|
|
||||||
from,
|
|
||||||
compareFun = createCompareFun(strictComparison)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the
|
|
||||||
* [MapDiff.changed] values will be reversed too
|
|
||||||
*/
|
|
||||||
fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff(
|
|
||||||
removed = added,
|
|
||||||
changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first },
|
|
||||||
added = removed
|
|
||||||
)
|
|
@@ -1,53 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlin.jvm.JvmName
|
|
||||||
|
|
||||||
interface SimpleMapper<T1, T2> {
|
|
||||||
fun convertToT1(from: T2): T1
|
|
||||||
fun convertToT2(from: T1): T2
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmName("convertFromT2")
|
|
||||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
|
||||||
@JvmName("convertFromT1")
|
|
||||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
|
||||||
|
|
||||||
class SimpleMapperImpl<T1, T2>(
|
|
||||||
private val t1: (T2) -> T1,
|
|
||||||
private val t2: (T1) -> T2,
|
|
||||||
) : SimpleMapper<T1, T2> {
|
|
||||||
override fun convertToT1(from: T2): T1 = t1.invoke(from)
|
|
||||||
|
|
||||||
override fun convertToT2(from: T1): T2 = t2.invoke(from)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T1, T2> simpleMapper(
|
|
||||||
noinline t1: (T2) -> T1,
|
|
||||||
noinline t2: (T1) -> T2,
|
|
||||||
) = SimpleMapperImpl(t1, t2)
|
|
||||||
|
|
||||||
interface SimpleSuspendableMapper<T1, T2> {
|
|
||||||
suspend fun convertToT1(from: T2): T1
|
|
||||||
suspend fun convertToT2(from: T1): T2
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmName("convertFromT2")
|
|
||||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
|
||||||
@JvmName("convertFromT1")
|
|
||||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
|
||||||
|
|
||||||
class SimpleSuspendableMapperImpl<T1, T2>(
|
|
||||||
private val t1: suspend (T2) -> T1,
|
|
||||||
private val t2: suspend (T1) -> T2,
|
|
||||||
) : SimpleSuspendableMapper<T1, T2> {
|
|
||||||
override suspend fun convertToT1(from: T2): T1 = t1.invoke(from)
|
|
||||||
|
|
||||||
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T1, T2> simpleSuspendableMapper(
|
|
||||||
noinline t1: suspend (T2) -> T1,
|
|
||||||
noinline t2: suspend (T1) -> T2,
|
|
||||||
) = SimpleSuspendableMapperImpl(t1, t2)
|
|
@@ -41,18 +41,10 @@ data class Optional<T> internal constructor(
|
|||||||
inline val <T> T.optional
|
inline val <T> T.optional
|
||||||
get() = Optional.presented(this)
|
get() = Optional.presented(this)
|
||||||
|
|
||||||
inline val <T : Any> T?.optionalOrAbsentIfNull
|
|
||||||
get() = if (this == null) {
|
|
||||||
Optional.absent<T>()
|
|
||||||
} else {
|
|
||||||
Optional.presented(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
*/
|
*/
|
||||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||||
@OptIn(Warning::class)
|
|
||||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +52,6 @@ inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply
|
|||||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
*/
|
*/
|
||||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||||
@OptIn(Warning::class)
|
|
||||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +59,6 @@ inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
|||||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||||
*/
|
*/
|
||||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||||
@OptIn(Warning::class)
|
|
||||||
if (!dataPresented) { block() }
|
if (!dataPresented) { block() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,22 +66,27 @@ inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
|||||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||||
*/
|
*/
|
||||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||||
@OptIn(Warning::class)
|
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
|
||||||
if (!dataPresented) { block() } else null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||||
*/
|
*/
|
||||||
fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||||
*/
|
*/
|
||||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||||
*/
|
*/
|
||||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||||
|
*/
|
||||||
|
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||||
|
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
|
|
||||||
/**
|
|
||||||
* Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort()
|
|
||||||
/**
|
|
||||||
* Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
|
|
||||||
/**
|
|
||||||
* Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte()
|
|
@@ -1,37 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.jvm.JvmInline
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@JvmInline
|
|
||||||
value class Progress private constructor(
|
|
||||||
val of1: Double
|
|
||||||
) {
|
|
||||||
val of1Float
|
|
||||||
get() = of1.toFloat()
|
|
||||||
val of100
|
|
||||||
get() = of1 * 100
|
|
||||||
val of100Float
|
|
||||||
get() = of100.toFloat()
|
|
||||||
val of100Int
|
|
||||||
get() = of100.toInt()
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(of1 in rangeOfValues) {
|
|
||||||
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val rangeOfValues = 0.0 .. 1.0
|
|
||||||
|
|
||||||
val START = Progress(rangeOfValues.start)
|
|
||||||
val COMPLETED = Progress(rangeOfValues.endInclusive)
|
|
||||||
|
|
||||||
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
|
|
||||||
operator fun invoke(part: Number, total: Number) = Progress(
|
|
||||||
part.toDouble() / total.toDouble()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,80 +0,0 @@
|
|||||||
@file:Suppress(
|
|
||||||
"RemoveRedundantCallsOfConversionMethods",
|
|
||||||
"RedundantVisibilityModifier",
|
|
||||||
)
|
|
||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlin.Byte
|
|
||||||
import kotlin.Double
|
|
||||||
import kotlin.Float
|
|
||||||
import kotlin.Int
|
|
||||||
import kotlin.Long
|
|
||||||
import kotlin.Short
|
|
||||||
import kotlin.Suppress
|
|
||||||
|
|
||||||
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
|
|
||||||
|
|
||||||
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
|
|
||||||
|
|
||||||
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()
|
|
@@ -1,27 +1,5 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the given [action] until getting of successful result specified number of [times].
|
|
||||||
*
|
|
||||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
|
||||||
*/
|
|
||||||
inline fun <R> repeatOnFailure(
|
|
||||||
onFailure: (Throwable) -> Boolean,
|
|
||||||
action: () -> R
|
|
||||||
): Result<R> {
|
|
||||||
do {
|
|
||||||
runCatching {
|
|
||||||
action()
|
|
||||||
}.onFailure {
|
|
||||||
if (!onFailure(it)) {
|
|
||||||
return Result.failure(it)
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
return Result.success(it)
|
|
||||||
}
|
|
||||||
} while (true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given [action] until getting of successful result specified number of [times].
|
* Executes the given [action] until getting of successful result specified number of [times].
|
||||||
*
|
*
|
||||||
@@ -32,23 +10,12 @@ inline fun <R> repeatOnFailure(
|
|||||||
onEachFailure: (Throwable) -> Unit = {},
|
onEachFailure: (Throwable) -> Unit = {},
|
||||||
action: (Int) -> R
|
action: (Int) -> R
|
||||||
): Optional<R> {
|
): Optional<R> {
|
||||||
var i = 0
|
repeat(times) {
|
||||||
val result = repeatOnFailure(
|
runCatching {
|
||||||
{
|
action(it)
|
||||||
onEachFailure(it)
|
}.onFailure(onEachFailure).onSuccess {
|
||||||
if (i < times) {
|
return Optional.presented(it)
|
||||||
i++
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
action(i)
|
|
||||||
}
|
|
||||||
return if (result.isSuccess) {
|
|
||||||
Optional.presented(result.getOrThrow())
|
|
||||||
} else {
|
|
||||||
Optional.absent()
|
|
||||||
}
|
}
|
||||||
|
return Optional.absent()
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
val FixedSignsRange = 0 .. 100
|
|
||||||
|
|
||||||
expect fun Float.fixed(signs: Int): Float
|
|
||||||
expect fun Double.fixed(signs: Int): Double
|
|
@@ -32,7 +32,7 @@ class DiffUtilsTests {
|
|||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
for ((i, _) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
if (i + count > oldList.lastIndex) {
|
if (i + count > oldList.lastIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ class DiffUtilsTests {
|
|||||||
val oldList = (0 until 10).map { it.toString() }
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (step in oldList.indices) {
|
for (step in 0 until oldList.size) {
|
||||||
for ((i, _) in withIndex) {
|
for ((i, v) in withIndex) {
|
||||||
val mutable = oldList.toMutableList()
|
val mutable = oldList.toMutableList()
|
||||||
val changes = (
|
val changes = (
|
||||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||||
@@ -73,83 +73,4 @@ class DiffUtilsTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testThatSimpleRemoveApplyWorks() {
|
|
||||||
val oldList = (0 until 10).toList()
|
|
||||||
val withIndex = oldList.withIndex()
|
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
|
||||||
for ((i, _) in withIndex) {
|
|
||||||
if (i + count > oldList.lastIndex) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val removedSublist = oldList.subList(i, i + count)
|
|
||||||
val mutableOldList = oldList.toMutableList()
|
|
||||||
val targetList = oldList - removedSublist
|
|
||||||
|
|
||||||
mutableOldList.applyDiff(targetList)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
targetList,
|
|
||||||
mutableOldList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testThatSimpleAddApplyWorks() {
|
|
||||||
val oldList = (0 until 10).map { it.toString() }
|
|
||||||
val withIndex = oldList.withIndex()
|
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
|
||||||
for ((i, _) in withIndex) {
|
|
||||||
if (i + count > oldList.lastIndex) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
|
||||||
val mutable = oldList.toMutableList()
|
|
||||||
mutable.addAll(i, addedSublist)
|
|
||||||
val mutableOldList = oldList.toMutableList()
|
|
||||||
|
|
||||||
mutableOldList.applyDiff(mutable)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
mutable,
|
|
||||||
mutableOldList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testThatSimpleChangesApplyWorks() {
|
|
||||||
val oldList = (0 until 10).map { it.toString() }
|
|
||||||
val withIndex = oldList.withIndex()
|
|
||||||
|
|
||||||
for (step in oldList.indices) {
|
|
||||||
for ((i, _) in withIndex) {
|
|
||||||
val mutable = oldList.toMutableList()
|
|
||||||
|
|
||||||
val newList = if (step == 0) {
|
|
||||||
i until oldList.size
|
|
||||||
} else {
|
|
||||||
i until oldList.size step step
|
|
||||||
}
|
|
||||||
newList.forEach { index ->
|
|
||||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
|
||||||
mutable[index] = it.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mutableOldList = oldList.toMutableList()
|
|
||||||
mutableOldList.applyDiff(mutable)
|
|
||||||
assertEquals(
|
|
||||||
mutable,
|
|
||||||
mutableOldList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import org.w3c.dom.*
|
|
||||||
|
|
||||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
|
||||||
lateinit var observer: MutationObserver
|
|
||||||
|
|
||||||
observer = MutationObserver { _, _ ->
|
|
||||||
fun checkIfRemoved(node: Node): Boolean {
|
|
||||||
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkIfRemoved(this)) {
|
|
||||||
observer.disconnect()
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
|
|
||||||
return observer
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
|
||||||
var previousIntersectionRatio = -1f
|
|
||||||
val observer = IntersectionObserver { entries, observer ->
|
|
||||||
entries.forEach {
|
|
||||||
if (previousIntersectionRatio != it.intersectionRatio) {
|
|
||||||
previousIntersectionRatio = it.intersectionRatio.toFloat()
|
|
||||||
it.block(previousIntersectionRatio, observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.observe(this)
|
|
||||||
return observer
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
|
|
||||||
var previous = -1f
|
|
||||||
onVisibilityChanged { intersectionRatio, observer ->
|
|
||||||
if (previous != intersectionRatio) {
|
|
||||||
if (intersectionRatio > 0 && previous == 0f) {
|
|
||||||
block(observer)
|
|
||||||
}
|
|
||||||
previous = intersectionRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
|
|
||||||
var previous = -1f
|
|
||||||
return onVisibilityChanged { intersectionRatio, observer ->
|
|
||||||
if (previous != intersectionRatio) {
|
|
||||||
if (intersectionRatio == 0f && previous != 0f) {
|
|
||||||
block(observer)
|
|
||||||
}
|
|
||||||
previous = intersectionRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import org.w3c.dom.DOMRectReadOnly
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
external interface IntersectionObserverOptions {
|
|
||||||
/**
|
|
||||||
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
|
|
||||||
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
|
|
||||||
* visible.
|
|
||||||
*/
|
|
||||||
var root: Element?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
|
|
||||||
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
|
|
||||||
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
|
|
||||||
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
|
|
||||||
*/
|
|
||||||
var rootMargin: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
|
|
||||||
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
|
|
||||||
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
|
|
||||||
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
|
|
||||||
*/
|
|
||||||
var threshold: Array<Number>?
|
|
||||||
}
|
|
||||||
fun IntersectionObserverOptions(
|
|
||||||
block: IntersectionObserverOptions.() -> Unit = {}
|
|
||||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
|
||||||
|
|
||||||
external interface IntersectionObserverEntry {
|
|
||||||
/**
|
|
||||||
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
|
|
||||||
* the documentation for Element.getBoundingClientRect().
|
|
||||||
*/
|
|
||||||
val boundingClientRect: DOMRectReadOnly
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ratio of the intersectionRect to the boundingClientRect.
|
|
||||||
*/
|
|
||||||
val intersectionRatio: Number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a DOMRectReadOnly representing the target's visible area.
|
|
||||||
*/
|
|
||||||
val intersectionRect: DOMRectReadOnly
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
|
|
||||||
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
|
|
||||||
* then you know the transition is from intersecting to not-intersecting.
|
|
||||||
*/
|
|
||||||
val isIntersecting: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a DOMRectReadOnly for the intersection observer's root.
|
|
||||||
*/
|
|
||||||
val rootBounds: DOMRectReadOnly
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Element whose intersection with the root changed.
|
|
||||||
*/
|
|
||||||
val target: Element
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
|
|
||||||
* IntersectionObserver's time origin.
|
|
||||||
*/
|
|
||||||
val time: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
|
|
||||||
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
|
|
||||||
*/
|
|
||||||
external class IntersectionObserver(callback: IntersectionObserverCallback) {
|
|
||||||
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
|
|
||||||
* was passed to the constructor or its value is null, the top-level document's viewport is used.
|
|
||||||
*/
|
|
||||||
val root: Element
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
|
|
||||||
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
|
|
||||||
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
|
|
||||||
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
|
|
||||||
*/
|
|
||||||
val rootMargin: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
|
|
||||||
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
|
|
||||||
* crossed for that target. If no value was passed to the constructor, 0 is used.
|
|
||||||
*/
|
|
||||||
val thresholds: Array<Number>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the IntersectionObserver object from observing any target.
|
|
||||||
*/
|
|
||||||
fun disconnect()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the IntersectionObserver a target element to observe.
|
|
||||||
*/
|
|
||||||
fun observe(targetElement: Element)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of IntersectionObserverEntry objects for all observed targets.
|
|
||||||
*/
|
|
||||||
fun takeRecords(): Array<IntersectionObserverEntry>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the IntersectionObserver to stop observing a particular target element.
|
|
||||||
*/
|
|
||||||
fun unobserve(targetElement: Element)
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
inline val Element.isOverflowWidth
|
|
||||||
get() = scrollWidth > clientWidth
|
|
||||||
|
|
||||||
inline val Element.isOverflowHeight
|
|
||||||
get() = scrollHeight > clientHeight
|
|
||||||
|
|
||||||
inline val Element.isOverflow
|
|
||||||
get() = isOverflowHeight || isOverflowWidth
|
|
@@ -1,38 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import org.w3c.dom.*
|
|
||||||
import org.w3c.dom.events.Event
|
|
||||||
import org.w3c.dom.events.EventListener
|
|
||||||
|
|
||||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
|
||||||
lateinit var observer: MutationObserver
|
|
||||||
val listener = EventListener {
|
|
||||||
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
|
|
||||||
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
|
|
||||||
val childrenGettingElement = elementsToCheck.removeFirst()
|
|
||||||
for (i in 0 until childrenGettingElement.childElementCount) {
|
|
||||||
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (elementsToCheck.isEmpty()) {
|
|
||||||
callback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options == null) {
|
|
||||||
document.addEventListener(type, listener)
|
|
||||||
} else {
|
|
||||||
document.addEventListener(type, listener, options)
|
|
||||||
}
|
|
||||||
observer = onRemoved {
|
|
||||||
if (options == null) {
|
|
||||||
document.removeEventListener(type, listener)
|
|
||||||
} else {
|
|
||||||
document.removeEventListener(type, listener, options)
|
|
||||||
}
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
return listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
|
@@ -1,8 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.browser.window
|
|
||||||
|
|
||||||
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
|
||||||
window.open(link, target, features) ?.focus()
|
|
||||||
}
|
|
||||||
|
|
@@ -1,58 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import org.w3c.dom.*
|
|
||||||
import kotlin.js.Json
|
|
||||||
import kotlin.js.json
|
|
||||||
|
|
||||||
external class ResizeObserver(
|
|
||||||
callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit
|
|
||||||
) {
|
|
||||||
fun observe(target: Element, options: Json = definedExternally)
|
|
||||||
|
|
||||||
fun unobserve(target: Element)
|
|
||||||
|
|
||||||
fun disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
external interface ResizeObserverSize {
|
|
||||||
val blockSize: Float
|
|
||||||
val inlineSize: Float
|
|
||||||
}
|
|
||||||
|
|
||||||
external interface ResizeObserverEntry {
|
|
||||||
val borderBoxSize: Array<ResizeObserverSize>
|
|
||||||
val contentBoxSize: Array<ResizeObserverSize>
|
|
||||||
val devicePixelContentBoxSize: Array<ResizeObserverSize>
|
|
||||||
val contentRect: DOMRectReadOnly
|
|
||||||
val target: Element
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
|
|
||||||
target,
|
|
||||||
json(
|
|
||||||
"box" to options.box ?.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
class ResizeObserverObserveOptions(
|
|
||||||
val box: Box? = null
|
|
||||||
) {
|
|
||||||
sealed interface Box {
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
object Content : Box {
|
|
||||||
override val name: String
|
|
||||||
get() = "content-box"
|
|
||||||
}
|
|
||||||
|
|
||||||
object Border : Box {
|
|
||||||
override val name: String
|
|
||||||
get() = "border-box"
|
|
||||||
}
|
|
||||||
|
|
||||||
object DevicePixelContent : Box {
|
|
||||||
override val name: String
|
|
||||||
get() = "device-pixel-content-box"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import kotlinx.dom.createElement
|
|
||||||
import org.w3c.dom.HTMLElement
|
|
||||||
import org.w3c.dom.HTMLInputElement
|
|
||||||
import org.w3c.files.get
|
|
||||||
|
|
||||||
fun selectFile(
|
|
||||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
|
||||||
onFailure: (Throwable) -> Unit = {},
|
|
||||||
onFile: (MPPFile) -> Unit
|
|
||||||
) {
|
|
||||||
(document.createElement("input") {
|
|
||||||
(this as HTMLInputElement).apply {
|
|
||||||
type = "file"
|
|
||||||
onchange = {
|
|
||||||
runCatching {
|
|
||||||
files ?.get(0) ?: error("File must not be null")
|
|
||||||
}.onSuccess {
|
|
||||||
onFile(it)
|
|
||||||
}.onFailure {
|
|
||||||
onFailure(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputSetup(this)
|
|
||||||
}
|
|
||||||
} as HTMLElement).click()
|
|
||||||
}
|
|
||||||
|
|
@@ -1,14 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import org.w3c.dom.HTMLAnchorElement
|
|
||||||
|
|
||||||
fun triggerDownloadFile(filename: String, fileLink: String) {
|
|
||||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
|
||||||
|
|
||||||
hiddenElement.href = fileLink
|
|
||||||
hiddenElement.target = "_blank"
|
|
||||||
hiddenElement.download = filename
|
|
||||||
hiddenElement.click()
|
|
||||||
}
|
|
||||||
|
|
@@ -1,4 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
|
|
||||||
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()
|
|
@@ -1,20 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
fun InputStream.downloadToTempFile(
|
|
||||||
fileName: String = UUID.randomUUID().toString(),
|
|
||||||
fileExtension: String? = ".temp",
|
|
||||||
folder: File? = null
|
|
||||||
) = File.createTempFile(
|
|
||||||
fileName,
|
|
||||||
fileExtension,
|
|
||||||
folder
|
|
||||||
).apply {
|
|
||||||
outputStream().use {
|
|
||||||
copyTo(it)
|
|
||||||
}
|
|
||||||
deleteOnExit()
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.math.RoundingMode
|
|
||||||
|
|
||||||
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
|
|
||||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
|
||||||
.toFloat();
|
|
||||||
|
|
||||||
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
|
|
||||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
|
||||||
.toDouble();
|
|
1
common/src/main/AndroidManifest.xml
Normal file
1
common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.common"/>
|
@@ -1,36 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import okio.FileSystem
|
|
||||||
import okio.Path
|
|
||||||
import okio.use
|
|
||||||
|
|
||||||
actual typealias MPPFile = Path
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual val MPPFile.filename: FileName
|
|
||||||
get() = FileName(toString())
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual val MPPFile.filesize: Long
|
|
||||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
|
||||||
it.size()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
|
||||||
get() = {
|
|
||||||
FileSystem.SYSTEM.read(this) {
|
|
||||||
readByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
|
||||||
get() = {
|
|
||||||
bytesAllocatorSync()
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlinx.cinterop.*
|
|
||||||
import platform.posix.snprintf
|
|
||||||
import platform.posix.sprintf
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
actual fun Float.fixed(signs: Int): Float {
|
|
||||||
return memScoped {
|
|
||||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
|
||||||
|
|
||||||
sprintf(buff, "%.${signs}f", this@fixed)
|
|
||||||
buff.toKString().toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
actual fun Double.fixed(signs: Int): Double {
|
|
||||||
return memScoped {
|
|
||||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
|
||||||
|
|
||||||
sprintf(buff, "%.${signs}f", this@fixed)
|
|
||||||
buff.toKString().toDouble()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,23 +4,18 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
}
|
|
||||||
}
|
|
||||||
jsMain {
|
|
||||||
dependencies {
|
|
||||||
api project(":micro_utils.common")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kt.coroutines.android
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
|
||||||
id "com.android.library"
|
|
||||||
alias(libs.plugins.jb.compose)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain {
|
|
||||||
dependencies {
|
|
||||||
api libs.kt.coroutines
|
|
||||||
api project(":micro_utils.coroutines")
|
|
||||||
api project(":micro_utils.common.compose")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import dev.inmo.micro_utils.common.applyDiff
|
|
||||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
|
||||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList]
|
|
||||||
*
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*/
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): SnapshotStateList<T> {
|
|
||||||
val state = mutableStateListOf<T>()
|
|
||||||
val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
|
|
||||||
{
|
|
||||||
withContext(useContextOnChange) {
|
|
||||||
state.applyDiff(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: {
|
|
||||||
state.applyDiff(it)
|
|
||||||
}
|
|
||||||
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In fact, it is just classcast of [asMutableComposeListState] to [List]
|
|
||||||
*
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*
|
|
||||||
* @return Changing in time [List] which follow [Flow] values
|
|
||||||
*/
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <reified T> Flow<List<T>>.asComposeList(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): List<T> = asMutableComposeListState(scope, useContextOnChange, onException)
|
|
||||||
|
|
@@ -1,94 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import dev.inmo.micro_utils.common.compose.asState
|
|
||||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
|
||||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
|
||||||
import dev.inmo.micro_utils.coroutines.doInUI
|
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow]
|
|
||||||
*
|
|
||||||
* @param initial First value which will be passed to the result [MutableState]
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*/
|
|
||||||
fun <T> Flow<T>.asMutableComposeState(
|
|
||||||
initial: T,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): MutableState<T> {
|
|
||||||
val state = mutableStateOf(initial)
|
|
||||||
val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let {
|
|
||||||
{
|
|
||||||
withContext(useContextOnChange) {
|
|
||||||
state.value = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: {
|
|
||||||
state.value = it
|
|
||||||
}
|
|
||||||
subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock)
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow].
|
|
||||||
* This conversation will pass its [StateFlow.value] as the first value
|
|
||||||
*
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*/
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T> StateFlow<T>.asMutableComposeState(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state
|
|
||||||
*
|
|
||||||
* @param initial First value which will be passed to the result [State]
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*/
|
|
||||||
fun <T> Flow<T>.asComposeState(
|
|
||||||
initial: T,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): State<T> {
|
|
||||||
val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
|
|
||||||
return state.asState()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value
|
|
||||||
*
|
|
||||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
|
||||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
|
||||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
|
||||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
|
||||||
*/
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T> StateFlow<T>.asComposeState(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
|
||||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
|
||||||
): State<T> = asComposeState(value, scope, useContextOnChange, onException)
|
|
||||||
|
|
@@ -1,14 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.job
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
fun Composition.linkWithJob(job: Job) {
|
|
||||||
job.invokeOnCompletion {
|
|
||||||
this@linkWithJob.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
|
@@ -1,26 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import dev.inmo.micro_utils.common.compose.linkWithElement
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.jetbrains.compose.web.dom.DOMScope
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
|
|
||||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
|
||||||
root: TElement,
|
|
||||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
|
||||||
content: @Composable DOMScope<TElement>.() -> Unit
|
|
||||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
|
||||||
linkWithContext(
|
|
||||||
currentCoroutineContext()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
|
|
||||||
root: TElement,
|
|
||||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
|
||||||
content: @Composable DOMScope<TElement>.() -> Unit
|
|
||||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
|
||||||
linkWithContext(currentCoroutineContext())
|
|
||||||
linkWithElement(root)
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.ui.test.*
|
|
||||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
|
||||||
import org.jetbrains.annotations.TestOnly
|
|
||||||
import kotlin.test.Test
|
|
||||||
|
|
||||||
class FlowStateTests {
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
@Test
|
|
||||||
@TestOnly
|
|
||||||
fun simpleTest() = runComposeUiTest {
|
|
||||||
val flowState = SpecialMutableStateFlow(0)
|
|
||||||
setContent {
|
|
||||||
Button({ flowState.value++ }) { Text("Click") }
|
|
||||||
Text(flowState.collectAsState().value.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
onNodeWithText(0.toString()).assertExists()
|
|
||||||
onNodeWithText("Click").performClick()
|
|
||||||
onNodeWithText(1.toString()).assertExists()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
@@ -1,50 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener]
|
|
||||||
*
|
|
||||||
* @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this
|
|
||||||
* [ViewGroup] too
|
|
||||||
* @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow
|
|
||||||
* @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow
|
|
||||||
*/
|
|
||||||
class FlowOnHierarchyChangeListener(
|
|
||||||
private val recursive: Boolean = false,
|
|
||||||
private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE),
|
|
||||||
private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
|
|
||||||
) : ViewGroup.OnHierarchyChangeListener {
|
|
||||||
val onChildViewAdded = _onChildViewAdded.asSharedFlow()
|
|
||||||
val onChildViewRemoved = _onChildViewRemoved.asSharedFlow()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also
|
|
||||||
* subscribe to [child] hierarchy changes.
|
|
||||||
*
|
|
||||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
|
||||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is
|
|
||||||
* [Int.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
override fun onChildViewAdded(parent: View, child: View) {
|
|
||||||
_onChildViewAdded.tryEmit(parent to child)
|
|
||||||
|
|
||||||
if (recursive && child is ViewGroup) {
|
|
||||||
child.setOnHierarchyChangeListener(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Just emit data into [onChildViewRemoved]
|
|
||||||
*
|
|
||||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
|
||||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is
|
|
||||||
* [Int.MAX_VALUE]
|
|
||||||
*/
|
|
||||||
override fun onChildViewRemoved(parent: View, child: View) {
|
|
||||||
_onChildViewRemoved.tryEmit(parent to child)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.OnHierarchyChangeListener
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this].
|
|
||||||
* This extension DO NOT guarantee that recursive subscription will happen after this method call
|
|
||||||
*/
|
|
||||||
fun ViewGroup.setOnHierarchyChangeListenerRecursively(
|
|
||||||
listener: OnHierarchyChangeListener
|
|
||||||
) {
|
|
||||||
setOnHierarchyChangeListener(listener)
|
|
||||||
(0 until childCount).forEach {
|
|
||||||
(getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -6,12 +6,11 @@ import kotlinx.coroutines.channels.Channel
|
|||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
|
|
||||||
private sealed interface AccumulatorFlowStep<T>
|
private sealed interface AccumulatorFlowStep
|
||||||
private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T>
|
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
|
||||||
private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||||
private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||||
@@ -27,12 +26,12 @@ class AccumulatorFlow<T>(
|
|||||||
private val subscope = scope.LinkedSupervisorScope()
|
private val subscope = scope.LinkedSupervisorScope()
|
||||||
private val activeData = ArrayDeque<T>()
|
private val activeData = ArrayDeque<T>()
|
||||||
private val dataMutex = Mutex()
|
private val dataMutex = Mutex()
|
||||||
private val channelsForBroadcast = mutableListOf<Channel<T>>()
|
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||||
private val channelsMutex = Mutex()
|
private val channelsMutex = Mutex()
|
||||||
private val steps = subscope.actor<AccumulatorFlowStep<T>> { step ->
|
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||||
when (step) {
|
when (step) {
|
||||||
is DataRetrievedAccumulatorFlowStep -> {
|
is DataRetrievedAccumulatorFlowStep -> {
|
||||||
if (activeData.firstOrNull() === step.data) {
|
if (activeData.first() === step.data) {
|
||||||
dataMutex.withLock {
|
dataMutex.withLock {
|
||||||
activeData.removeFirst()
|
activeData.removeFirst()
|
||||||
}
|
}
|
||||||
@@ -43,7 +42,7 @@ class AccumulatorFlow<T>(
|
|||||||
dataMutex.withLock {
|
dataMutex.withLock {
|
||||||
val dataToSend = activeData.toList()
|
val dataToSend = activeData.toList()
|
||||||
safelyWithoutExceptions {
|
safelyWithoutExceptions {
|
||||||
dataToSend.forEach { step.channel.send(it) }
|
dataToSend.forEach { step.channel.send(it as Any) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,29 +58,24 @@ class AccumulatorFlow<T>(
|
|||||||
channelsMutex.withLock {
|
channelsMutex.withLock {
|
||||||
channelsForBroadcast.forEach { channel ->
|
channelsForBroadcast.forEach { channel ->
|
||||||
safelyWithResult {
|
safelyWithResult {
|
||||||
channel.send(it)
|
channel.send(it as Any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||||
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||||
val result = runCatchingSafely {
|
for (data in channel) {
|
||||||
for (data in channel) {
|
try {
|
||||||
val emitResult = runCatchingSafely {
|
collector.emit(data as T)
|
||||||
collector.emit(data)
|
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||||
}
|
} finally {
|
||||||
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
channel.cancel()
|
||||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||||
}
|
|
||||||
emitResult.getOrThrow()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
channel.cancel()
|
|
||||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
|
||||||
result.getOrThrow()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
fun <T> CoroutineScope.actor(
|
fun <T> CoroutineScope.actor(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
): Channel<T> {
|
): Channel<T> {
|
||||||
val channel = Channel<T>(channelCapacity)
|
val channel = Channel<T>(channelCapacity)
|
||||||
channel.consumeAsFlow().subscribe(this, block)
|
launch {
|
||||||
|
for (data in channel) {
|
||||||
|
block(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
|
|
||||||
fun <T> CoroutineScope.actorAsync(
|
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
|
||||||
markerFactory: suspend (T) -> Any? = { null },
|
|
||||||
block: suspend (T) -> Unit
|
|
||||||
): Channel<T> {
|
|
||||||
val channel = Channel<T>(channelCapacity)
|
|
||||||
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
|
|
||||||
return channel
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> CoroutineScope.safeActorAsync(
|
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
|
||||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
|
||||||
noinline markerFactory: suspend (T) -> Any? = { null },
|
|
||||||
crossinline block: suspend (T) -> Unit
|
|
||||||
): Channel<T> = actorAsync(
|
|
||||||
channelCapacity,
|
|
||||||
markerFactory
|
|
||||||
) {
|
|
||||||
safely(onException) {
|
|
||||||
block(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -6,19 +6,23 @@ import kotlin.coroutines.*
|
|||||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
cancelOnResult: Boolean = true
|
cancelOnResult: Boolean = true
|
||||||
): Pair<Deferred<T>, T> {
|
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||||
val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>()
|
scope.launch(SupervisorJob()) {
|
||||||
val scope = scope.LinkedSupervisorScope()
|
val scope = this
|
||||||
forEach {
|
forEach {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
resultDeferred.complete(it to it.await())
|
continuation.resume(it to it.await())
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resultDeferred.await().also {
|
}.also {
|
||||||
if (cancelOnResult) {
|
if (cancelOnResult) {
|
||||||
forEach {
|
forEach {
|
||||||
runCatchingSafely { it.cancel() }
|
try {
|
||||||
|
it.cancel()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
|
||||||
*/
|
|
||||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
|
||||||
*/
|
|
||||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
|
||||||
try {
|
|
||||||
defaultSafelyExceptionHandler(it)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
|
||||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
|
||||||
*
|
|
||||||
* @see safelyWithContextExceptionHandler
|
|
||||||
* @see ContextSafelyExceptionHandler
|
|
||||||
*/
|
|
||||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
|
||||||
*
|
|
||||||
* @see safelyWithContextExceptionHandler
|
|
||||||
* @see ContextSafelyExceptionHandlerKey
|
|
||||||
*/
|
|
||||||
class ContextSafelyExceptionHandler(
|
|
||||||
val handler: ExceptionHandler<Unit>
|
|
||||||
) : CoroutineContext.Element {
|
|
||||||
override val key: CoroutineContext.Key<*>
|
|
||||||
get() = ContextSafelyExceptionHandlerKey
|
|
||||||
}
|
|
@@ -16,8 +16,8 @@ class DoWithFirstBuilder<T>(
|
|||||||
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||||
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||||
}
|
}
|
||||||
fun add(block: suspend CoroutineScope.() -> T) = plus(block)
|
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
fun include(block: suspend CoroutineScope.() -> T) = plus(block)
|
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
|
|
||||||
fun build() = deferreds.toList()
|
fun build() = deferreds.toList()
|
||||||
}
|
}
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlin.js.JsName
|
|
||||||
import kotlin.jvm.JvmName
|
|
||||||
|
|
||||||
inline fun <T, R> Flow<Flow<T>>.flatMap(
|
|
||||||
crossinline mapper: suspend (T) -> R
|
|
||||||
) = flow {
|
|
||||||
collect {
|
|
||||||
it.collect {
|
|
||||||
emit(mapper(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsName("flatMapIterable")
|
|
||||||
@JvmName("flatMapIterable")
|
|
||||||
inline fun <T, R> Flow<Iterable<T>>.flatMap(
|
|
||||||
crossinline mapper: suspend (T) -> R
|
|
||||||
) = map {
|
|
||||||
it.asFlow()
|
|
||||||
}.flatMap(mapper)
|
|
||||||
|
|
||||||
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
|
|
||||||
crossinline mapper: suspend (T) -> R
|
|
||||||
) = flatMap(mapper).takeNotNull()
|
|
||||||
|
|
||||||
@JsName("flatMapNotNullIterable")
|
|
||||||
@JvmName("flatMapNotNullIterable")
|
|
||||||
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
|
|
||||||
crossinline mapper: suspend (T) -> R
|
|
||||||
) = flatMap(mapper).takeNotNull()
|
|
||||||
|
|
||||||
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
|
|
||||||
|
|
||||||
@JsName("flattenIterable")
|
|
||||||
@JvmName("flattenIterable")
|
|
||||||
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user