mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
226 Commits
Author | SHA1 | Date | |
---|---|---|---|
b8c7e581a1 | |||
8281259179 | |||
d3e06b07df | |||
4967018418 | |||
537a3c38fa | |||
0124957833 | |||
f0420e2d61 | |||
7090566041 | |||
f0d5035cd0 | |||
0fb9b8dc30 | |||
eef6e81134 | |||
1593159a3f | |||
3da9eb9dbe | |||
f17613f3fb | |||
14337ccb46 | |||
1a3913b09c | |||
039aed2747 | |||
173991e3cb | |||
8b3f8cab01 | |||
2a20d24589 | |||
53c2d552ec | |||
af11c1a83d | |||
a65cf1481c | |||
0318716236 | |||
88eb4b3342 | |||
4810d1ef6a | |||
f2a9514d89 | |||
925ba6ac24 | |||
|
ef407268a2 | ||
cd6c4bbe38 | |||
c058e18408 | |||
6d3ca565ca | |||
236a7b4fd2 | |||
e1f387dbf7 | |||
3d113dd31e | |||
e0e57f0336 | |||
e775b58d41 | |||
5b070a8478 | |||
8a61193500 | |||
fad522b8fe | |||
0acac205af | |||
069d4c61b7 | |||
7c113f5700 | |||
dfdaf4225b | |||
bd39ab2467 | |||
6ce1eb3f2d | |||
ce7d4fe9a2 | |||
a65bb2f419 | |||
cbc868448b | |||
9c336a0b56 | |||
0f0d09399e | |||
e13a1162a9 | |||
57ebed903f | |||
4478193d8a | |||
ee948395e3 | |||
0616b051ae | |||
4d155d0505 | |||
a169e733d9 | |||
f081e237c8 | |||
f412d387fa | |||
67354b43e2 | |||
39135a4000 | |||
eaa014cebd | |||
856e657f81 | |||
3a609e5b66 | |||
d0022dd599 | |||
7ba6eed453 | |||
beeb6ecc0a | |||
7cdc17a714 | |||
4765a950a9 | |||
65e8137e08 | |||
ae546dd9ad | |||
8110c42be0 | |||
bd2b5ae5fc | |||
2ddfffa6a9 | |||
a4b54e861d | |||
c6785f1a4f | |||
83fe621c56 | |||
b3a93e17eb | |||
546a391af3 | |||
786cf9bd8b | |||
dfd6fe062d | |||
b6ef818613 | |||
b0f9e9c30a | |||
7e5c88ddc3 | |||
9824c3e00f | |||
9171d5ed11 | |||
a1830ebb82 | |||
22d2a3d9bf | |||
c9b97fc965 | |||
51f85becd5 | |||
a8a281cfb4 | |||
ddd1304949 | |||
86d70b6c02 | |||
a22bdb39e7 | |||
7ae4d5ef95 | |||
a2038cbefa | |||
992091eade | |||
e3bfead0c5 | |||
0de96141fd | |||
fa18d15c3c | |||
ea9dbf2371 | |||
d34e3ec7a9 | |||
c8833a36af | |||
a067cb0c0f | |||
999c8327bd | |||
c2ec73c70a | |||
702f782fc1 | |||
25dbcaaf83 | |||
b00d454a24 | |||
dbc921d56d | |||
438fefa7a3 | |||
5d74eac814 | |||
9fb62e1e25 | |||
3e366ea73b | |||
0ff895bffa | |||
c5bb120280 | |||
4b26a92b37 | |||
0a8453b4d2 | |||
c9872a61b6 | |||
149a1aa278 | |||
13d0e1b682 | |||
6f9be2a9f8 | |||
93ba98d993 | |||
940ee3df06 | |||
2e7bab10fd | |||
|
3ed70a37ea | ||
fe8f80b9d9 | |||
d81fb32fb9 | |||
2877b5532c | |||
b938b21395 | |||
58836359cc | |||
5edb0e1331 | |||
0f0d0b5d58 | |||
46c1887cbe | |||
5f231c2212 | |||
4e97ce86aa | |||
315a7cb29e | |||
aa7cc503f2 | |||
4bbe7e5a80 | |||
d9c05f38d2 | |||
cd0c4c9650 | |||
fc3407f104 | |||
3a5544206b | |||
e17e2f7fb8 | |||
|
d32c95f143 | ||
6d8a8ab018 | |||
a7dce8fa78 | |||
ca73ff8e19 | |||
d01ad10d7d | |||
81041ee43c | |||
|
6e004c2ae4 | ||
0e2fac5b22 | |||
269da7f155 | |||
3cb6b73ee0 | |||
a938ee1efb | |||
6ea5e2e5a6 | |||
617dfb54e0 | |||
d23e005985 | |||
e5207f5bc5 | |||
c96cea8db0 | |||
0a8e71d76a | |||
cf1fd32b08 | |||
cc4224ea1f | |||
f4c148bc58 | |||
022297ad3f | |||
5180d6fc3e | |||
eeebbff70d | |||
afc6aeea15 | |||
486515eddd | |||
0e21699cd1 | |||
f1678ef7cf | |||
cea65fc76e | |||
c9e320b72a | |||
555956087d | |||
b3f468f901 | |||
f5f7511781 | |||
4be1d93f60 | |||
7d684608ef | |||
2c7fd320eb | |||
88ee82e1c6 | |||
d6402c624e | |||
8b9c93bc10 | |||
4f5e261d01 | |||
cf455aebe6 | |||
1380d5f8e1 | |||
5f65260bfe | |||
11f0dcfc01 | |||
555b7886a4 | |||
3707a6c0ce | |||
4c8d92b4c3 | |||
8bbd33c896 | |||
ac33a3580f | |||
a64a32fbe6 | |||
9493e97a38 | |||
88bd770260 | |||
a7bd33b7bf | |||
73c724a2e5 | |||
d8cf3c6726 | |||
15dee238b5 | |||
c70626734e | |||
5a765ea1bc | |||
8215f9d2c6 | |||
d2e6d2ec80 | |||
3718181e8f | |||
0d825cf424 | |||
28a804d988 | |||
9e4bb9d678 | |||
9c40d7da3d | |||
2b76ad0aa9 | |||
e4b619e050 | |||
36c09feaf2 | |||
2d68321503 | |||
85455ab21c | |||
18d63eb980 | |||
2e429e9704 | |||
f4af28059b | |||
c1476bd075 | |||
16c720fddd | |||
8b4b4a5eca | |||
32e6e5b7e2 | |||
a9f7fd8e32 | |||
95be1a26f2 | |||
ef9b31aee0 | |||
df3c01ff0a | |||
551d8ec480 |
306
CHANGELOG.md
306
CHANGELOG.md
@@ -1,5 +1,311 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.20.9
|
||||||
|
|
||||||
|
* Most of common modules now supports `linuxArm64` target
|
||||||
|
|
||||||
|
## 0.20.8
|
||||||
|
|
||||||
|
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC`
|
||||||
|
* `Compose`: `1.5.10-beta02` -> `1.5.10-rc01`
|
||||||
|
|
||||||
|
## 0.20.7
|
||||||
|
|
||||||
|
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.9.10` -> `1.9.20-Beta2`
|
||||||
|
* `Compose`: `1.5.1` -> `1.5.10-beta02`
|
||||||
|
* `Exposed`: `0.43.0` -> `0.44.0`
|
||||||
|
* `Ktor`: `2.3.4` -> `2.3.5`
|
||||||
|
* `Koin`: `3.4.3` -> `3.5.0`
|
||||||
|
* `Okio`: `3.5.0` -> `3.6.0`
|
||||||
|
* `Android Core`: `1.10.1` -> `1.12.0`
|
||||||
|
* `Android Compose Material`: `1.1.1` -> `1.1.2`
|
||||||
|
|
||||||
|
## 0.20.6
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`
|
||||||
|
* Fixes in exposed key-values repos
|
||||||
|
|
||||||
|
## 0.20.5
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* Fixes in `SmartRWLocker`
|
||||||
|
|
||||||
|
## 0.20.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.9.0` -> `1.9.10`
|
||||||
|
* `KSLog`: `1.2.0` -> `1.2.1`
|
||||||
|
* `Compose`: `1.5.0` -> `1.5.1`
|
||||||
|
* `UUID`: `0.8.0` -> `0.8.1`
|
||||||
|
|
||||||
|
## 0.20.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Compose`: `1.4.3` -> `1.5.0`
|
||||||
|
* `Exposed`: `0.42.1` -> `0.43.0`
|
||||||
|
* `Ktor`: `2.3.3` -> `2.3.4`
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* Fixes in locks of caches
|
||||||
|
|
||||||
|
## 0.20.2
|
||||||
|
|
||||||
|
* All main repos uses `SmartRWLocker`
|
||||||
|
* `Versions`:
|
||||||
|
* `Serialization`: `1.5.1` -> `1.6.0`
|
||||||
|
* `Exposed`: `0.42.0` -> `0.42.1`
|
||||||
|
* `Korlibs`: `4.0.9` -> `4.0.10`
|
||||||
|
* `Androis SDK`: `33` -> `34`
|
||||||
|
|
||||||
|
## 0.20.1
|
||||||
|
|
||||||
|
* `SmallTextField`:
|
||||||
|
* Module is initialized
|
||||||
|
* `Pickers`:
|
||||||
|
* Module is initialized
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add `SmartSemaphore`
|
||||||
|
* Add `SmartRWLocker`
|
||||||
|
|
||||||
|
## 0.20.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.8.22` -> `1.9.0`
|
||||||
|
* `KSLog`: `1.1.1` -> `1.2.0`
|
||||||
|
* `Exposed`: `0.41.1` -> `0.42.0`
|
||||||
|
* `UUID`: `0.7.1` -> `0.8.0`
|
||||||
|
* `Korlibs`: `4.0.3` -> `4.0.9`
|
||||||
|
* `Ktor`: `2.3.2` -> `2.3.3`
|
||||||
|
* `Okio`: `3.4.0` -> `3.5.0`
|
||||||
|
|
||||||
|
## 0.19.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Koin`: `3.4.2` -> `3.4.3`
|
||||||
|
* `Startup`:
|
||||||
|
* Now it is possible to start application in synchronous way
|
||||||
|
|
||||||
|
## 0.19.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.7.2` -> `1.7.3`
|
||||||
|
* `Kotlin`: `1.8.20` -> `1.8.22`
|
||||||
|
* `Compose`: `1.4.1` -> `1.4.3`
|
||||||
|
* `Okio`: `3.3.0` -> `3.4.0`
|
||||||
|
* `RecyclerView`: `1.3.0` -> `1.3.1`
|
||||||
|
* `Fragment`: `1.6.0` -> `1.6.1`
|
||||||
|
* `Repos`:
|
||||||
|
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
|
||||||
|
* `Cache`:
|
||||||
|
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
|
||||||
|
will be automatically invalidated on start of their work
|
||||||
|
|
||||||
|
## 0.19.7
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.7.1` -> `1.7.2`
|
||||||
|
|
||||||
|
## 0.19.6
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.6.4` -> `1.7.1`
|
||||||
|
* `Ktor`: `2.3.1` -> `2.3.2`
|
||||||
|
* `Compose`: `1.4.0` -> `1.4.1`
|
||||||
|
|
||||||
|
## 0.19.5
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Generator`:
|
||||||
|
* Fixes in new type generation
|
||||||
|
|
||||||
|
## 0.19.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Koin`: `3.4.1` -> `3.4.2`
|
||||||
|
* `Android Fragments`: `1.5.7` -> `1.6.0`
|
||||||
|
* `Koin`
|
||||||
|
* `Generator`
|
||||||
|
* Fixes in new generic generator part
|
||||||
|
|
||||||
|
## 0.19.3
|
||||||
|
|
||||||
|
* `Koin`
|
||||||
|
* `Generator`
|
||||||
|
* New getter methods now available with opportunity to use parameters
|
||||||
|
* Old notation `*Single` and `*Factory` is deprecated since this release. With old
|
||||||
|
will be generated new `single*` and `factory*` notations for new generations
|
||||||
|
* Add opportunity to use generic-oriented koin definitions
|
||||||
|
|
||||||
|
## 0.19.2
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.3.0` -> `2.3.1`
|
||||||
|
* `Koin`: `3.4.0` -> `3.4.1`
|
||||||
|
* `Uuid`: `0.7.0` -> `0.7.1`
|
||||||
|
|
||||||
|
## 0.19.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Korlibs`: `4.0.1` -> `4.0.3`
|
||||||
|
* `Kotlin Poet`: `1.13.2` -> `1.14.0`
|
||||||
|
|
||||||
|
## 0.19.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Korlibs`: `3.4.0` -> `4.0.1`
|
||||||
|
|
||||||
|
## 0.18.4
|
||||||
|
|
||||||
|
* `Koin`:
|
||||||
|
* New extension `lazyInject`
|
||||||
|
|
||||||
|
## 0.18.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Serialization`: `1.5.0` -> `1.5.1`
|
||||||
|
* `Android Cor Ktx`: `1.10.0` -> `1.10.1`
|
||||||
|
|
||||||
|
## 0.18.2
|
||||||
|
|
||||||
|
* `Startup`:
|
||||||
|
* Now internal `Json` is fully customizable
|
||||||
|
|
||||||
|
## 0.18.1
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add `MapDiff`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add `SmartMutex`
|
||||||
|
|
||||||
|
## 0.18.0
|
||||||
|
|
||||||
|
**ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED**
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Android Fragments`: `1.5.6` -> `1.5.7`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Server`:
|
||||||
|
* Now it is possible to take query parameters as list
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* New `WriteKeyValuesRepo.removeWithValue`
|
||||||
|
* `Cache`:
|
||||||
|
* Rename full caching factories functions to `fullyCached`
|
||||||
|
|
||||||
|
## 0.17.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.2.4` -> `2.3.0`
|
||||||
|
|
||||||
|
## 0.17.7
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Android CoreKtx`: `1.9.0` -> `1.10.0`
|
||||||
|
* `Startup`:
|
||||||
|
* Add support of `linuxX64` and `mingwX64` platforms
|
||||||
|
|
||||||
|
## 0.17.6
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.8.10` -> `1.8.20`
|
||||||
|
* `KSLog`: `1.0.0` -> `1.1.1`
|
||||||
|
* `Compose`: `1.3.1` -> `1.4.0`
|
||||||
|
* `Koin`: `3.3.2` -> `3.4.0`
|
||||||
|
* `RecyclerView`: `1.2.1` -> `1.3.0`
|
||||||
|
* `Fragment`: `1.5.5` -> `1.5.6`
|
||||||
|
* Experimentally (`!!!`) add `linuxX64` and `mingwX64` targets
|
||||||
|
|
||||||
|
## 0.17.5
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Conversations of number primitives with bounds care
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* By default, `getAll` for repos will take all the size of repo as page size
|
||||||
|
* New extension for all built-in repos `maxPagePagination`
|
||||||
|
* All the repos got `getAll` functions
|
||||||
|
|
||||||
|
## 0.17.4
|
||||||
|
|
||||||
|
* `Serialization`:
|
||||||
|
* `Mapper`:
|
||||||
|
* Module inited
|
||||||
|
* `Versions`:
|
||||||
|
* `Compose`: `1.3.1-rc02` -> `1.3.1`
|
||||||
|
|
||||||
|
## 0.17.3
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add `fixed` extensions for `Float` and `Double`
|
||||||
|
* New function `emptyDiff`
|
||||||
|
* Now you may pass custom `comparisonFun` to all `diff` functions
|
||||||
|
|
||||||
|
## 0.17.2
|
||||||
|
|
||||||
|
* `FSM`:
|
||||||
|
* `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default
|
||||||
|
|
||||||
|
## 0.17.1
|
||||||
|
|
||||||
|
* **Hotfix** for absence of jvm dependencies in android modules
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.2.3` -> `2.2.4`
|
||||||
|
|
||||||
|
## 0.17.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.7.20` -> `1.8.10`
|
||||||
|
* `Serialization`: `1.4.1` -> `1.5.0`
|
||||||
|
* `KSLog`: `0.5.4` -> `1.0.0`
|
||||||
|
* `AppCompat`: `1.6.0` -> `1.6.1`
|
||||||
|
|
||||||
|
## 0.16.13
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Generator`:
|
||||||
|
* Module has been created
|
||||||
|
|
||||||
|
## 0.16.12
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Exposed`:
|
||||||
|
* `CommonExposedRepo.selectByIds` uses `foldRight` by default instead of raw foreach
|
||||||
|
* `Koin`:
|
||||||
|
* `Generator`:
|
||||||
|
* Module has been created
|
||||||
|
|
||||||
|
## 0.16.11
|
||||||
|
|
||||||
|
* `LanguageCodes`:
|
||||||
|
* In android and JVM targets now available `toJavaLocale` and from Java `Locale` conversations from/to
|
||||||
|
`IetfLanguageCode`
|
||||||
|
|
||||||
|
## 0.16.10
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Cache`:
|
||||||
|
* New transformer type: `ReadCRUDFromKeyValueRepo`
|
||||||
|
* New transformer type: `ReadKeyValueFromCRUDRepo`
|
||||||
|
* `Pagination`:
|
||||||
|
* New `paginate` extensions with `reversed` support for `List`/`Set`
|
||||||
|
|
||||||
|
## 0.16.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Koin`: `3.2.2` -> `3.3.2`
|
||||||
|
* `AppCompat`: `1.5.1` -> `1.6.0`
|
||||||
|
* `Ktor`:
|
||||||
|
* `Client`
|
||||||
|
* `HttpResponse.bodyOrNull` now retrieve callback to check if body should be received or null
|
||||||
|
* New extension `HttpResponse.bodyOrNullOnNoContent`
|
||||||
|
|
||||||
## 0.16.8
|
## 0.16.8
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
|
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
|
18
android/pickers/build.gradle
Normal file
18
android/pickers/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.jb.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.android.smalltextfield")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
1
android/pickers/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal file
27
android/pickers/src/androidMain/kotlin/Fling.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.inmo.micro_utils.android.pickers
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
|
||||||
|
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
|
||||||
|
initialVelocity: Float,
|
||||||
|
animationSpec: DecayAnimationSpec<Float>,
|
||||||
|
adjustTarget: ((Float) -> Float)?,
|
||||||
|
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
|
||||||
|
): AnimationResult<Float, AnimationVector1D> {
|
||||||
|
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
|
||||||
|
val adjustedTarget = adjustTarget?.invoke(targetValue)
|
||||||
|
|
||||||
|
return if (adjustedTarget != null) {
|
||||||
|
animateTo(
|
||||||
|
targetValue = adjustedTarget,
|
||||||
|
initialVelocity = initialVelocity,
|
||||||
|
block = block
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
animateDecay(
|
||||||
|
initialVelocity = initialVelocity,
|
||||||
|
animationSpec = animationSpec,
|
||||||
|
block = block,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal file
222
android/pickers/src/androidMain/kotlin/NumberPicker.kt
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package dev.inmo.micro_utils.android.pickers
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.exponentialDecay
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.ProvideTextStyle
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.center
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
|
||||||
|
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NumberPicker(
|
||||||
|
number: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
range: IntRange? = null,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
allowUseManualInput: Boolean = true,
|
||||||
|
onStateChanged: (Int) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val numbersColumnHeight = 36.dp
|
||||||
|
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||||
|
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||||
|
|
||||||
|
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||||
|
|
||||||
|
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||||
|
if (range != null) {
|
||||||
|
val offsetRange = remember(number, range) {
|
||||||
|
val value = number
|
||||||
|
val first = -(range.last - value) * halvedNumbersColumnHeightPx
|
||||||
|
val last = -(range.first - value) * halvedNumbersColumnHeightPx
|
||||||
|
first..last
|
||||||
|
}
|
||||||
|
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||||
|
val animatedStateValue = animatedStateValue(animatedOffset.value)
|
||||||
|
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
|
||||||
|
|
||||||
|
val inputFieldShown = if (allowUseManualInput) {
|
||||||
|
remember { mutableStateOf(false) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.draggable(
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { deltaY ->
|
||||||
|
if (inputFieldShown ?.value != true) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
if (inputFieldShown ?.value != true) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val endValue = animatedOffset.fling(
|
||||||
|
initialVelocity = velocity,
|
||||||
|
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||||
|
adjustTarget = { target ->
|
||||||
|
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||||
|
val coercedAnchors =
|
||||||
|
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||||
|
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||||
|
val base =
|
||||||
|
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||||
|
coercedPoint + base
|
||||||
|
}
|
||||||
|
).endState.value
|
||||||
|
|
||||||
|
onStateChanged(animatedStateValue(endValue))
|
||||||
|
animatedOffset.snapTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
val spacing = 4.dp
|
||||||
|
|
||||||
|
val upEnabled = range == null || range.first < number
|
||||||
|
IconButton(
|
||||||
|
{
|
||||||
|
onStateChanged(number - 1)
|
||||||
|
inputFieldShown ?.value = false
|
||||||
|
},
|
||||||
|
enabled = upEnabled
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val baseLabelModifier = Modifier.align(Alignment.Center)
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Text(
|
||||||
|
text = (animatedStateValue - 1).toString(),
|
||||||
|
modifier = baseLabelModifier
|
||||||
|
.offset(y = -halvedNumbersColumnHeight)
|
||||||
|
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inputFieldShown ?.value == true) {
|
||||||
|
val currentValue = remember { mutableStateOf(number.toString()) }
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
SmallTextField(
|
||||||
|
currentValue.value,
|
||||||
|
{
|
||||||
|
val asDigit = it.toIntOrNull()
|
||||||
|
when {
|
||||||
|
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
|
||||||
|
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
|
||||||
|
else -> { /* do nothing */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
|
||||||
|
detectTapGestures {
|
||||||
|
if (!checkContains(it)) {
|
||||||
|
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||||
|
inputFieldShown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions {
|
||||||
|
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||||
|
inputFieldShown.value = false
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = textStyle
|
||||||
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = animatedStateValue.toString(),
|
||||||
|
modifier = baseLabelModifier
|
||||||
|
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||||
|
.clickable {
|
||||||
|
if (inputFieldShown ?.value == false) {
|
||||||
|
inputFieldShown.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = (animatedStateValue + 1).toString(),
|
||||||
|
modifier = baseLabelModifier
|
||||||
|
.offset(y = halvedNumbersColumnHeight)
|
||||||
|
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing))
|
||||||
|
|
||||||
|
val downEnabled = range == null || range.last > number
|
||||||
|
IconButton(
|
||||||
|
{
|
||||||
|
onStateChanged(number + 1)
|
||||||
|
inputFieldShown ?.value = false
|
||||||
|
},
|
||||||
|
enabled = downEnabled
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal file
156
android/pickers/src/androidMain/kotlin/SetPicker.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package dev.inmo.micro_utils.android.pickers
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.exponentialDecay
|
||||||
|
import androidx.compose.foundation.gestures.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> SetPicker(
|
||||||
|
current: T,
|
||||||
|
dataList: List<T>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
dataToString: @Composable (T) -> String = { it.toString() },
|
||||||
|
onStateChanged: (T) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
|
||||||
|
textStyle.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
|
||||||
|
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||||
|
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||||
|
|
||||||
|
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
|
||||||
|
val lastIndex = dataList.size - 1
|
||||||
|
|
||||||
|
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||||
|
|
||||||
|
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||||
|
val offsetRange = remember(index, lastIndex) {
|
||||||
|
val value = index
|
||||||
|
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
|
||||||
|
val last = value * halvedNumbersColumnHeightPx
|
||||||
|
first..last
|
||||||
|
}
|
||||||
|
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||||
|
}
|
||||||
|
val indexAnimatedOffset = if (animatedOffset.value > 0) {
|
||||||
|
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||||
|
} else {
|
||||||
|
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||||
|
}
|
||||||
|
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||||
|
val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset
|
||||||
|
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.draggable(
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { deltaY ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
val endValue = animatedOffset.fling(
|
||||||
|
initialVelocity = velocity,
|
||||||
|
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||||
|
adjustTarget = { target ->
|
||||||
|
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||||
|
val coercedAnchors =
|
||||||
|
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||||
|
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||||
|
val base =
|
||||||
|
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||||
|
coercedPoint + base
|
||||||
|
}
|
||||||
|
).endState.value
|
||||||
|
|
||||||
|
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
|
||||||
|
animatedOffset.snapTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
val spacing = 4.dp
|
||||||
|
|
||||||
|
val upEnabled = index > 0
|
||||||
|
IconButton(
|
||||||
|
{
|
||||||
|
onStateChanged(dataList.elementAt(index - 1))
|
||||||
|
},
|
||||||
|
enabled = upEnabled
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
dataList.forEachIndexed { i, t ->
|
||||||
|
val alpha = when {
|
||||||
|
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||||
|
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||||
|
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||||
|
else -> return@forEachIndexed
|
||||||
|
}
|
||||||
|
val offset = when {
|
||||||
|
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
|
||||||
|
i == indexAnimatedOffset -> coercedAnimatedOffset
|
||||||
|
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
|
||||||
|
else -> return@forEachIndexed
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = dataToString(t),
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(alpha)
|
||||||
|
.offset(y = with(LocalDensity.current) { offset.toDp() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(spacing))
|
||||||
|
|
||||||
|
val downEnabled = index < lastIndex
|
||||||
|
IconButton(
|
||||||
|
{
|
||||||
|
onStateChanged(dataList.elementAt(index + 1))
|
||||||
|
},
|
||||||
|
enabled = downEnabled
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
1
android/recyclerview/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
|
18
android/smalltextfield/build.gradle
Normal file
18
android/smalltextfield/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
alias(libs.plugins.jb.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api libs.android.compose.material3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -0,0 +1,66 @@
|
|||||||
|
package dev.inmo.micro_utils.android.smalltextfield
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SmallTextField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
textColor: Color = textStyle.color.takeOrElse {
|
||||||
|
LocalContentColor.current
|
||||||
|
},
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
singleLine: Boolean = false,
|
||||||
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
minLines: Int = 1,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
modifier = modifier,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
textStyle = textStyle.copy(
|
||||||
|
color = textColor
|
||||||
|
),
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
singleLine = singleLine,
|
||||||
|
maxLines = maxLines,
|
||||||
|
minLines = minLines,
|
||||||
|
cursorBrush = SolidColor(
|
||||||
|
textStyle.color.takeOrElse {
|
||||||
|
LocalContentColor.current
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@@ -9,6 +9,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.buildscript.kt.gradle
|
classpath libs.buildscript.kt.gradle
|
||||||
classpath libs.buildscript.kt.serialization
|
classpath libs.buildscript.kt.serialization
|
||||||
|
classpath libs.buildscript.kt.ksp
|
||||||
classpath libs.buildscript.jb.dokka
|
classpath libs.buildscript.jb.dokka
|
||||||
classpath libs.buildscript.gh.release
|
classpath libs.buildscript.gh.release
|
||||||
classpath libs.buildscript.android.gradle
|
classpath libs.buildscript.android.gradle
|
||||||
@@ -16,12 +17,17 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||||
|
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -36,3 +42,4 @@ 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"
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -18,6 +18,23 @@ kotlin {
|
|||||||
api project(":micro_utils.coroutines")
|
api project(":micro_utils.coroutines")
|
||||||
api libs.android.fragment
|
api libs.android.fragment
|
||||||
}
|
}
|
||||||
|
dependsOn jvmMain
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxX64Main {
|
||||||
|
dependencies {
|
||||||
|
api libs.okio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mingwX64Main {
|
||||||
|
dependencies {
|
||||||
|
api libs.okio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
linuxArm64Main {
|
||||||
|
dependencies {
|
||||||
|
api libs.okio
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
1
common/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
|
1
common/src/androidMain/AndroidManifest.xml
Normal file
1
common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -34,7 +34,11 @@ data class Diff<T> internal constructor(
|
|||||||
*/
|
*/
|
||||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||||
)
|
) {
|
||||||
|
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
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>?>>,
|
||||||
@@ -43,14 +47,14 @@ 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>>,
|
||||||
strictComparison: Boolean
|
comparisonFun: (T?, T?) -> 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 = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||||
changedList.addAll(
|
changedList.addAll(
|
||||||
potentialChanges.take(i).mapNotNull {
|
potentialChanges.take(i).mapNotNull {
|
||||||
@@ -104,7 +108,7 @@ private inline fun <T> performChanges(
|
|||||||
*/
|
*/
|
||||||
fun <T> Iterable<T>.calculateDiff(
|
fun <T> Iterable<T>.calculateDiff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
): Diff<T> {
|
): Diff<T> {
|
||||||
var i = -1
|
var i = -1
|
||||||
var j = -1
|
var j = -1
|
||||||
@@ -132,7 +136,7 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
comparisonFun(oldObject, newObject) -> {
|
||||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||||
@@ -143,23 +147,49 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
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, strictComparison)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||||
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, strictComparison)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||||
|
|
||||||
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)
|
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
||||||
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)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,6 +199,23 @@ 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]
|
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||||
* mutable list
|
* mutable list
|
||||||
@@ -177,13 +224,26 @@ fun <T> MutableList<T>.applyDiff(
|
|||||||
source: Iterable<T>,
|
source: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
strictComparison: Boolean = false
|
||||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||||
for (i in it.removed.indices.sortedDescending()) {
|
applyDiff(it)
|
||||||
removeAt(it.removed[i].index)
|
|
||||||
}
|
|
||||||
it.added.forEach { (i, t) ->
|
|
||||||
add(i, t)
|
|
||||||
}
|
|
||||||
it.replaced.forEach { (_, new) ->
|
|
||||||
set(new.index, new.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method call [calculateDiff] and then apply differences to [this]
|
||||||
|
* mutable list
|
||||||
|
*/
|
||||||
|
fun <T> MutableList<T>.applyDiff(
|
||||||
|
source: Iterable<T>,
|
||||||
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
|
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||||
|
applyDiff(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
|
||||||
|
* [Diff.replaced] values will be reversed too
|
||||||
|
*/
|
||||||
|
fun <T> Diff<T>.reversed() = Diff(
|
||||||
|
removed = added,
|
||||||
|
replaced = replaced.map { it.second to it.first },
|
||||||
|
added = removed
|
||||||
|
)
|
||||||
|
@@ -0,0 +1,135 @@
|
|||||||
|
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
|
||||||
|
)
|
@@ -0,0 +1,28 @@
|
|||||||
|
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()
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
val FixedSignsRange = 0 .. 100
|
||||||
|
|
||||||
|
expect fun Float.fixed(signs: Int): Float
|
||||||
|
expect fun Double.fixed(signs: Int): Double
|
@@ -0,0 +1,4 @@
|
|||||||
|
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()
|
@@ -0,0 +1,12 @@
|
|||||||
|
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();
|
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/linuxArm64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
import okio.use
|
||||||
|
|
||||||
|
actual typealias MPPFile = Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(toString())
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||||
|
it.size()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
FileSystem.SYSTEM.read(this) {
|
||||||
|
readByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
bytesAllocatorSync()
|
||||||
|
}
|
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal file
25
common/src/linuxArm64Main/kotlin/fixed.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.snprintf
|
||||||
|
import platform.posix.sprintf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Float.fixed(signs: Int): Float {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Double.fixed(signs: Int): Double {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toDouble()
|
||||||
|
}
|
||||||
|
}
|
36
common/src/linuxX64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/linuxX64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
import okio.use
|
||||||
|
|
||||||
|
actual typealias MPPFile = Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(toString())
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||||
|
it.size()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
FileSystem.SYSTEM.read(this) {
|
||||||
|
readByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
bytesAllocatorSync()
|
||||||
|
}
|
25
common/src/linuxX64Main/kotlin/fixed.kt
Normal file
25
common/src/linuxX64Main/kotlin/fixed.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.snprintf
|
||||||
|
import platform.posix.sprintf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Float.fixed(signs: Int): Float {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Double.fixed(signs: Int): Double {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toDouble()
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.common"/>
|
|
36
common/src/mingwX64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/mingwX64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
import okio.use
|
||||||
|
|
||||||
|
actual typealias MPPFile = Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(toString())
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||||
|
it.size()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
FileSystem.SYSTEM.read(this) {
|
||||||
|
readByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
*/
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
bytesAllocatorSync()
|
||||||
|
}
|
25
common/src/mingwX64Main/kotlin/fixed.kt
Normal file
25
common/src/mingwX64Main/kotlin/fixed.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.snprintf
|
||||||
|
import platform.posix.sprintf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Float.fixed(signs: Int): Float {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual fun Double.fixed(signs: Int): Double {
|
||||||
|
return memScoped {
|
||||||
|
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||||
|
|
||||||
|
sprintf(buff, "%.${signs}f", this@fixed)
|
||||||
|
buff.toKString().toDouble()
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/compose/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1,21 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines.compose
|
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(initial, scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
|
|
||||||
fun <T> Flow<T>.toMutableState(
|
|
||||||
initial: T,
|
|
||||||
scope: CoroutineScope
|
|
||||||
): MutableState<T> = asMutableComposeState(initial, scope)
|
|
||||||
|
|
||||||
@Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState"))
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
inline fun <T> StateFlow<T>.toMutableState(
|
|
||||||
scope: CoroutineScope
|
|
||||||
): MutableState<T> = asMutableComposeState(scope)
|
|
||||||
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
|
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
1
coroutines/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -0,0 +1,136 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the
|
||||||
|
* [SmartMutex] state.
|
||||||
|
*
|
||||||
|
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||||
|
* [Mutable] [SmartMutex]. Otherwise, [Immutable].
|
||||||
|
*
|
||||||
|
* Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its
|
||||||
|
* [Mutable] creator
|
||||||
|
*/
|
||||||
|
sealed interface SmartMutex {
|
||||||
|
val lockStateFlow: StateFlow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * True - locked
|
||||||
|
* * False - unlocked
|
||||||
|
*/
|
||||||
|
val isLocked: Boolean
|
||||||
|
get() = lockStateFlow.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow]
|
||||||
|
*/
|
||||||
|
class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||||
|
* [Immutable] variant of [this] instance with [immutable] factory
|
||||||
|
*
|
||||||
|
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
|
||||||
|
*/
|
||||||
|
class Mutable(locked: Boolean = false) : SmartMutex {
|
||||||
|
private val _lockStateFlow = MutableStateFlow<Boolean>(locked)
|
||||||
|
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val internalChangesMutex = Mutex()
|
||||||
|
|
||||||
|
fun immutable() = Immutable(lockStateFlow)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
|
||||||
|
* wait for [isLocked] == false and then try to lock
|
||||||
|
*/
|
||||||
|
suspend fun lock() {
|
||||||
|
do {
|
||||||
|
waitUnlock()
|
||||||
|
val shouldContinue = internalChangesMutex.withLock {
|
||||||
|
if (_lockStateFlow.value) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
_lockStateFlow.value = true
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will try to lock this [SmartMutex] immediataly
|
||||||
|
*
|
||||||
|
* @return True if lock was successful. False otherwise
|
||||||
|
*/
|
||||||
|
suspend fun tryLock(): Boolean {
|
||||||
|
return if (!_lockStateFlow.value) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (!_lockStateFlow.value) {
|
||||||
|
_lockStateFlow.value = true
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
|
||||||
|
* [SmartMutex] - false
|
||||||
|
*/
|
||||||
|
suspend fun unlock(): Boolean {
|
||||||
|
return if (_lockStateFlow.value) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (_lockStateFlow.value) {
|
||||||
|
_lockStateFlow.value = false
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
lock()
|
||||||
|
try {
|
||||||
|
return action()
|
||||||
|
} finally {
|
||||||
|
unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||||
|
*
|
||||||
|
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||||
|
* that some other parties may lock it again
|
||||||
|
*/
|
||||||
|
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it }
|
@@ -0,0 +1,104 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite mutex which works with next rules:
|
||||||
|
*
|
||||||
|
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
|
||||||
|
* * [releaseRead] will just free up one permit in [readSemaphore]
|
||||||
|
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
|
||||||
|
* * [unlockWrite] will just unlock [writeMutex]
|
||||||
|
*/
|
||||||
|
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
||||||
|
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
|
||||||
|
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
|
||||||
|
|
||||||
|
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
||||||
|
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do lock in [readSemaphore] inside of [writeMutex] locking
|
||||||
|
*/
|
||||||
|
suspend fun acquireRead() {
|
||||||
|
_readSemaphore.acquire()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release one read permit in [readSemaphore]
|
||||||
|
*/
|
||||||
|
suspend fun releaseRead(): Boolean {
|
||||||
|
return _readSemaphore.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
|
||||||
|
*/
|
||||||
|
suspend fun lockWrite() {
|
||||||
|
_writeMutex.lock()
|
||||||
|
_readSemaphore.acquire(readPermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock [writeMutex]
|
||||||
|
*/
|
||||||
|
suspend fun unlockWrite(): Boolean {
|
||||||
|
return _writeMutex.unlock().also {
|
||||||
|
if (it) {
|
||||||
|
_readSemaphore.release(readPermits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
acquireRead()
|
||||||
|
try {
|
||||||
|
return action()
|
||||||
|
} finally {
|
||||||
|
releaseRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
|
||||||
|
*
|
||||||
|
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
|
||||||
|
* the fact that some other parties may lock it again
|
||||||
|
*/
|
||||||
|
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockWrite()
|
||||||
|
try {
|
||||||
|
return action()
|
||||||
|
} finally {
|
||||||
|
unlockWrite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||||
|
*
|
||||||
|
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||||
|
* that some other parties may lock it again
|
||||||
|
*/
|
||||||
|
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()
|
@@ -0,0 +1,168 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
|
||||||
|
* [SmartSemaphore] state.
|
||||||
|
*
|
||||||
|
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||||
|
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
|
||||||
|
*
|
||||||
|
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
|
||||||
|
* [Mutable] creator
|
||||||
|
*/
|
||||||
|
sealed interface SmartSemaphore {
|
||||||
|
val permitsStateFlow: StateFlow<Int>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * True - locked
|
||||||
|
* * False - unlocked
|
||||||
|
*/
|
||||||
|
val freePermits: Int
|
||||||
|
get() = permitsStateFlow.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
|
||||||
|
*/
|
||||||
|
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||||
|
* [Immutable] variant of [this] instance with [immutable] factory
|
||||||
|
*
|
||||||
|
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
|
||||||
|
*/
|
||||||
|
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
|
||||||
|
private val _freePermitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits)
|
||||||
|
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val internalChangesMutex = Mutex(false)
|
||||||
|
|
||||||
|
fun immutable() = Immutable(permitsStateFlow)
|
||||||
|
|
||||||
|
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
|
||||||
|
*/
|
||||||
|
suspend fun acquire(permits: Int = 1) {
|
||||||
|
var acquiredPermits = 0
|
||||||
|
val checkedPermits = checkedPermits(permits)
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
val shouldContinue = internalChangesMutex.withLock {
|
||||||
|
val requiredPermits = checkedPermits - acquiredPermits
|
||||||
|
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true
|
||||||
|
acquiredPermits += acquiring
|
||||||
|
_freePermitsStateFlow.value -= acquiring
|
||||||
|
|
||||||
|
acquiredPermits != checkedPermits
|
||||||
|
}
|
||||||
|
if (shouldContinue) {
|
||||||
|
waitRelease()
|
||||||
|
}
|
||||||
|
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
release(acquiredPermits)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
|
||||||
|
* wait for [freePermits] == false and then try to lock
|
||||||
|
*/
|
||||||
|
suspend fun acquireByOne(permits: Int = 1) {
|
||||||
|
val checkedPermits = checkedPermits(permits)
|
||||||
|
do {
|
||||||
|
waitRelease(checkedPermits)
|
||||||
|
val shouldContinue = internalChangesMutex.withLock {
|
||||||
|
if (_freePermitsStateFlow.value < checkedPermits) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
_freePermitsStateFlow.value -= checkedPermits
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will try to lock this [SmartSemaphore] immediataly
|
||||||
|
*
|
||||||
|
* @return True if lock was successful. False otherwise
|
||||||
|
*/
|
||||||
|
suspend fun tryAcquire(permits: Int = 1): Boolean {
|
||||||
|
val checkedPermits = checkedPermits(permits)
|
||||||
|
return if (_freePermitsStateFlow.value < checkedPermits) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (_freePermitsStateFlow.value < checkedPermits) {
|
||||||
|
_freePermitsStateFlow.value -= checkedPermits
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
|
||||||
|
* [SmartSemaphore] - false
|
||||||
|
*/
|
||||||
|
suspend fun release(permits: Int = 1): Boolean {
|
||||||
|
val checkedPermits = checkedPermits(permits)
|
||||||
|
return if (_freePermitsStateFlow.value < this.permits) {
|
||||||
|
internalChangesMutex.withLock {
|
||||||
|
if (_freePermitsStateFlow.value < this.permits) {
|
||||||
|
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire(permits)
|
||||||
|
try {
|
||||||
|
return action()
|
||||||
|
} finally {
|
||||||
|
release(permits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
|
||||||
|
*
|
||||||
|
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
|
||||||
|
* the fact that some other parties may lock it again
|
||||||
|
*/
|
||||||
|
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
|
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
151
coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class SmartRWLockerTests {
|
||||||
|
@Test
|
||||||
|
fun compositeTest() {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
|
||||||
|
val readAndWriteWorkers = 10
|
||||||
|
runTest {
|
||||||
|
var started = 0
|
||||||
|
var done = 0
|
||||||
|
val doneMutex = Mutex()
|
||||||
|
val readWorkers = (0 until readAndWriteWorkers).map {
|
||||||
|
launch(start = CoroutineStart.LAZY) {
|
||||||
|
locker.withReadAcquire {
|
||||||
|
doneMutex.withLock {
|
||||||
|
started++
|
||||||
|
}
|
||||||
|
delay(100L)
|
||||||
|
doneMutex.withLock {
|
||||||
|
done++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var doneWrites = 0
|
||||||
|
|
||||||
|
val writeWorkers = (0 until readAndWriteWorkers).map {
|
||||||
|
launch(start = CoroutineStart.LAZY) {
|
||||||
|
locker.withWriteLock {
|
||||||
|
assertTrue(done == readAndWriteWorkers || started == 0)
|
||||||
|
delay(10L)
|
||||||
|
doneWrites++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readWorkers.forEach { it.start() }
|
||||||
|
writeWorkers.forEach { it.start() }
|
||||||
|
|
||||||
|
readWorkers.joinAll()
|
||||||
|
writeWorkers.joinAll()
|
||||||
|
|
||||||
|
assertEquals(expected = readAndWriteWorkers, actual = done)
|
||||||
|
assertEquals(expected = readAndWriteWorkers, actual = doneWrites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simpleWithWriteLockTest() {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
|
||||||
|
runTest {
|
||||||
|
locker.withWriteLock {
|
||||||
|
assertEquals(0, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(true, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun failureWithWriteLockTest() {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
|
||||||
|
val exception = IllegalArgumentException()
|
||||||
|
try {
|
||||||
|
runTest {
|
||||||
|
val subscope = kotlinx.coroutines.CoroutineScope(this.coroutineContext)
|
||||||
|
var happenException: Throwable? = null
|
||||||
|
try {
|
||||||
|
locker.withWriteLock {
|
||||||
|
val checkFunction = fun (): Deferred<Unit> {
|
||||||
|
return subscope.async {
|
||||||
|
assertEquals(0, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(true, locker.writeMutex.isLocked)
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doInDefault {
|
||||||
|
assertEquals(0, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(true, locker.writeMutex.isLocked)
|
||||||
|
checkFunction().await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
happenException = e
|
||||||
|
}
|
||||||
|
if (exception != happenException) {
|
||||||
|
assertEquals(exception, happenException ?.cause)
|
||||||
|
}
|
||||||
|
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
assertEquals(exception, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simpleWithReadAcquireTest() {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
|
||||||
|
runTest {
|
||||||
|
locker.withReadAcquire {
|
||||||
|
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
locker.withReadAcquire {
|
||||||
|
assertEquals(Int.MAX_VALUE - 2, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simple2WithWriteLockTest() {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
|
||||||
|
val unlockDelay = 1000L // 1 sec
|
||||||
|
var unlocked: Boolean = false
|
||||||
|
runTest {
|
||||||
|
launch {
|
||||||
|
locker.withReadAcquire {
|
||||||
|
delay(unlockDelay)
|
||||||
|
}
|
||||||
|
unlocked = true
|
||||||
|
}
|
||||||
|
locker.readSemaphore.permitsStateFlow.first { it == Int.MAX_VALUE - 1 }
|
||||||
|
assertEquals(false, unlocked)
|
||||||
|
locker.withWriteLock {
|
||||||
|
assertEquals(true, unlocked)
|
||||||
|
assertEquals(0, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(true, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits)
|
||||||
|
assertEquals(false, locker.writeMutex.isLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import org.w3c.dom.Image
|
||||||
|
|
||||||
|
suspend fun preloadImage(src: String): Image {
|
||||||
|
val image = Image()
|
||||||
|
image.src = src
|
||||||
|
|
||||||
|
val job = Job()
|
||||||
|
|
||||||
|
image.addEventListener("load", {
|
||||||
|
runCatching { job.complete() }
|
||||||
|
})
|
||||||
|
|
||||||
|
runCatchingSafely {
|
||||||
|
job.join()
|
||||||
|
}.onFailure {
|
||||||
|
if (it is CancellationException) {
|
||||||
|
image.src = ""
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.coroutines"/>
|
|
@@ -4,13 +4,14 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
|
api libs.krypto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
|
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
1
crypto/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1,6 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.crypto
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
import korlibs.crypto.md5
|
||||||
|
|
||||||
typealias MD5 = String
|
typealias MD5 = String
|
||||||
|
|
||||||
expect fun SourceBytes.md5(): MD5
|
fun SourceBytes.md5(): MD5 = md5().hexLower
|
||||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.crypto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
|
@@ -1,12 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.crypto
|
|
||||||
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @suppress
|
|
||||||
*/
|
|
||||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
|
||||||
1,
|
|
||||||
MessageDigest.getInstance("MD5").digest(this)
|
|
||||||
).toString(16)
|
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
|
@@ -1,30 +1,5 @@
|
|||||||
apply plugin: 'com.getkeepsafe.dexcount'
|
apply plugin: 'com.getkeepsafe.dexcount'
|
||||||
|
|
||||||
ext {
|
|
||||||
jvmKotlinFolderFile = {
|
|
||||||
String sep = File.separator
|
|
||||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
enableIncludingJvmCodeInAndroidPart = {
|
|
||||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
|
||||||
if (jvmKotlinFolder.exists()) {
|
|
||||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disableIncludingJvmCodeInAndroidPart = {
|
|
||||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
|
||||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
|
||||||
android.sourceSets.main.java.srcDirs = []
|
|
||||||
for (oldDir in oldDirs) {
|
|
||||||
if (oldDir != jvmKotlinFolder.path) {
|
|
||||||
android.sourceSets.main.java.srcDirs += oldDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||||
@@ -34,6 +9,7 @@ android {
|
|||||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
versionCode "${android_code_version}".toInteger()
|
versionCode "${android_code_version}".toInteger()
|
||||||
versionName "$version"
|
versionName "$version"
|
||||||
|
namespace "${project.group}.${project.name}"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -54,14 +30,4 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
String sep = File.separator
|
|
||||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
|
||||||
enableIncludingJvmCodeInAndroidPart()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -13,10 +13,10 @@ repositories {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
// js(IR) {
|
js(IR) {
|
||||||
// browser()
|
browser()
|
||||||
// nodejs()
|
nodejs()
|
||||||
// }
|
}
|
||||||
android {}
|
android {}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -26,44 +26,44 @@ kotlin {
|
|||||||
|
|
||||||
project.parent.subprojects.forEach {
|
project.parent.subprojects.forEach {
|
||||||
if (
|
if (
|
||||||
it != project
|
it != project
|
||||||
&& it.hasProperty("kotlin")
|
&& it.hasProperty("kotlin")
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||||
) {
|
) {
|
||||||
api it
|
api it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// jsMain {
|
jsMain {
|
||||||
// dependencies {
|
dependencies {
|
||||||
// implementation kotlin('stdlib')
|
implementation kotlin('stdlib')
|
||||||
|
|
||||||
// project.parent.subprojects.forEach {
|
project.parent.subprojects.forEach {
|
||||||
// if (
|
if (
|
||||||
// it != project
|
it != project
|
||||||
// && it.hasProperty("kotlin")
|
&& it.hasProperty("kotlin")
|
||||||
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||||
// ) {
|
) {
|
||||||
// api it
|
api it
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('stdlib')
|
implementation kotlin('stdlib')
|
||||||
|
|
||||||
project.parent.subprojects.forEach {
|
project.parent.subprojects.forEach {
|
||||||
if (
|
if (
|
||||||
it != project
|
it != project
|
||||||
&& it.hasProperty("kotlin")
|
&& it.hasProperty("kotlin")
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||||
) {
|
) {
|
||||||
api it
|
api it
|
||||||
}
|
}
|
||||||
@@ -76,10 +76,10 @@ kotlin {
|
|||||||
|
|
||||||
project.parent.subprojects.forEach {
|
project.parent.subprojects.forEach {
|
||||||
if (
|
if (
|
||||||
it != project
|
it != project
|
||||||
&& it.hasProperty("kotlin")
|
&& it.hasProperty("kotlin")
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||||
) {
|
) {
|
||||||
api it
|
api it
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ tasks.dokkaHtml {
|
|||||||
skipDeprecated.set(true)
|
skipDeprecated.set(true)
|
||||||
|
|
||||||
sourceLink {
|
sourceLink {
|
||||||
localDirectory.set(file("./"))
|
localDirectory.set(file("../"))
|
||||||
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
|
|||||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// named("jsMain") {
|
named("jsMain") {
|
||||||
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
sourceRoots.setFrom(findSourcesWithName("jsMain"))
|
||||||
// }
|
}
|
||||||
|
|
||||||
named("jvmMain") {
|
named("jvmMain") {
|
||||||
sourceRoots.setFrom(findSourcesWithName("jvmMain"))
|
sourceRoots.setFrom(findSourcesWithName("jvmMain"))
|
||||||
|
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
1
dokka/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.dokka"/>
|
|
@@ -19,16 +19,24 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||||
|
// String compilerPluginVersionFromProperties = (String) project.properties["compose.kotlinCompilerPluginVersion"]
|
||||||
|
// String compilerPluginVersionFromLibrariesVersions = libs.versions.compose.kotlin.get()
|
||||||
|
// composePluginKotlinVersion = compilerPluginVersionFromProperties
|
||||||
|
// if (compilerPluginVersionFromProperties == null) {
|
||||||
|
// composePluginKotlinVersion = compilerPluginVersionFromLibrariesVersions
|
||||||
|
// }
|
||||||
|
|
||||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.gradle"
|
||||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||||
mppJsAndJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsAndJavaProject.gradle"
|
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
|
||||||
|
mppJvmJsLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwLinuxArm64Project.gradle"
|
||||||
|
mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
|
||||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||||
|
|
||||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||||
|
|
||||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -48,8 +48,8 @@ interface DefaultStatesManagerRepo<T : State> {
|
|||||||
*/
|
*/
|
||||||
open class DefaultStatesManager<T : State>(
|
open class DefaultStatesManager<T : State>(
|
||||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> false },
|
||||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> false }
|
||||||
) : StatesManager<T> {
|
) : StatesManager<T> {
|
||||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
|
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
1
fsm/repos/common/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -1 +0,0 @@
|
|||||||
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
|
@@ -3,9 +3,10 @@ org.gradle.parallel=true
|
|||||||
kotlin.js.generate.externals=true
|
kotlin.js.generate.externals=true
|
||||||
kotlin.incremental=true
|
kotlin.incremental=true
|
||||||
kotlin.incremental.js=true
|
kotlin.incremental.js=true
|
||||||
|
#kotlin.experimental.tryK2=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g
|
org.gradle.jvmargs=-Xmx2500m
|
||||||
|
|
||||||
# JS NPM
|
# JS NPM
|
||||||
|
|
||||||
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.16.8
|
version=0.20.9
|
||||||
android_code_version=176
|
android_code_version=215
|
||||||
|
@@ -1,37 +1,46 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "1.7.20"
|
kt = "1.9.20-RC"
|
||||||
kt-serialization = "1.4.1"
|
#compose-kotlin = "1.5.10-beta02"
|
||||||
kt-coroutines = "1.6.4"
|
kt-serialization = "1.6.0"
|
||||||
|
kt-coroutines = "1.7.3"
|
||||||
|
|
||||||
kslog = "0.5.4"
|
kslog = "1.2.1"
|
||||||
|
|
||||||
jb-compose = "1.2.2"
|
jb-compose = "1.5.10-rc01"
|
||||||
jb-exposed = "0.41.1"
|
jb-exposed = "0.44.0"
|
||||||
jb-dokka = "1.7.20"
|
jb-dokka = "1.9.0"
|
||||||
|
|
||||||
klock = "3.4.0"
|
korlibs = "4.0.10"
|
||||||
uuid = "0.6.0"
|
uuid = "0.8.1"
|
||||||
|
|
||||||
ktor = "2.2.3"
|
ktor = "2.3.5"
|
||||||
|
|
||||||
gh-release = "2.4.1"
|
gh-release = "2.4.1"
|
||||||
|
|
||||||
koin = "3.2.2"
|
koin = "3.5.0"
|
||||||
|
|
||||||
android-gradle = "7.3.0"
|
okio = "3.6.0"
|
||||||
dexcount = "3.1.0"
|
|
||||||
|
|
||||||
android-coreKtx = "1.9.0"
|
ksp = "1.9.20-RC-1.0.13"
|
||||||
android-recyclerView = "1.2.1"
|
kotlin-poet = "1.14.2"
|
||||||
android-appCompat = "1.5.1"
|
|
||||||
android-fragment = "1.5.5"
|
versions = "0.49.0"
|
||||||
android-espresso = "3.4.0"
|
|
||||||
android-test = "1.1.3"
|
android-gradle = "7.4.2"
|
||||||
|
dexcount = "4.0.0"
|
||||||
|
|
||||||
|
android-coreKtx = "1.12.0"
|
||||||
|
android-recyclerView = "1.3.1"
|
||||||
|
android-appCompat = "1.6.1"
|
||||||
|
android-fragment = "1.6.1"
|
||||||
|
android-espresso = "3.5.1"
|
||||||
|
android-test = "1.1.5"
|
||||||
|
android-compose-material3 = "1.1.2"
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "33"
|
android-props-compileSdk = "34"
|
||||||
android-props-buildTools = "33.0.0"
|
android-props-buildTools = "34.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
||||||
@@ -64,7 +73,8 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
|
|||||||
|
|
||||||
kslog = { module = "dev.inmo:kslog", version.ref = "kslog" }
|
kslog = { module = "dev.inmo:kslog", version.ref = "kslog" }
|
||||||
|
|
||||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "korlibs" }
|
||||||
|
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "korlibs" }
|
||||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||||
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
|
|
||||||
@@ -75,6 +85,7 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
|
|||||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||||
|
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
|
||||||
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||||
@@ -83,9 +94,18 @@ android-test-junit = { module = "androidx.test.ext:junit", version.ref = "androi
|
|||||||
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
|
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
|
||||||
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
|
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
|
||||||
|
|
||||||
|
# ksp dependencies
|
||||||
|
|
||||||
|
kotlin-poet = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin-poet" }
|
||||||
|
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||||
|
|
||||||
|
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||||
|
|
||||||
|
# Buildscript
|
||||||
|
|
||||||
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
||||||
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
|
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
|
||||||
|
buildscript-kt-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
|
||||||
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
|
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
|
||||||
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
|
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
|
||||||
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
|
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
|
||||||
@@ -94,3 +114,5 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
|
|||||||
[plugins]
|
[plugins]
|
||||||
|
|
||||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
||||||
|
|
||||||
|
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
118
jvm.publish.gradle
Normal file
118
jvm.publish.gradle
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
apply plugin: 'maven-publish'
|
||||||
|
|
||||||
|
task javadocJar(type: Jar) {
|
||||||
|
from javadoc
|
||||||
|
classifier = 'javadoc'
|
||||||
|
}
|
||||||
|
task sourcesJar(type: Jar) {
|
||||||
|
from sourceSets.main.allSource
|
||||||
|
classifier = 'sources'
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
maven(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
|
||||||
|
artifact javadocJar
|
||||||
|
artifact sourcesJar
|
||||||
|
|
||||||
|
pom {
|
||||||
|
resolveStrategy = Closure.DELEGATE_FIRST
|
||||||
|
|
||||||
|
description = "It is set of projects with micro tools for avoiding of routines coding"
|
||||||
|
name = "${project.name}"
|
||||||
|
url = "https://github.com/InsanusMokrassar/MicroUtils/"
|
||||||
|
|
||||||
|
scm {
|
||||||
|
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/MicroUtils.git[push=]https://github.com/InsanusMokrassar/MicroUtils.git"
|
||||||
|
url = "https://github.com/InsanusMokrassar/MicroUtils.git"
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
|
||||||
|
developer {
|
||||||
|
id = "InsanusMokrassar"
|
||||||
|
name = "Aleksei Ovsiannikov"
|
||||||
|
email = "ovsyannikov.alexey95@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
developer {
|
||||||
|
id = "000Sanya"
|
||||||
|
name = "Syrov Aleksandr"
|
||||||
|
email = "000sanya.000sanya@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
|
||||||
|
license {
|
||||||
|
name = "Apache Software License 2.0"
|
||||||
|
url = "https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
|
||||||
|
maven {
|
||||||
|
name = "GithubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils")
|
||||||
|
|
||||||
|
credentials {
|
||||||
|
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
|
||||||
|
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
|
||||||
|
maven {
|
||||||
|
name = "Gitea"
|
||||||
|
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
|
||||||
|
|
||||||
|
credentials(HttpHeaderCredentials) {
|
||||||
|
name = "Authorization"
|
||||||
|
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication {
|
||||||
|
header(HttpHeaderAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||||
|
maven {
|
||||||
|
name = "sonatype"
|
||||||
|
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||||
|
|
||||||
|
credentials {
|
||||||
|
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||||
|
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.hasProperty("signing.gnupg.keyName")) {
|
||||||
|
apply plugin: 'signing'
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
|
||||||
|
sign publishing.publications
|
||||||
|
}
|
||||||
|
|
||||||
|
task signAll {
|
||||||
|
tasks.withType(Sign).forEach {
|
||||||
|
dependsOn(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
jvm.publish.kpsb
Normal file
1
jvm.publish.kpsb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}
|
100
koin/generator/README.md
Normal file
100
koin/generator/README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Koin generator
|
||||||
|
|
||||||
|
It is Kotlin Symbol Processing generator for `Koin` module in `MicroUtils`.
|
||||||
|
|
||||||
|
1. [What may do this generator](#what-may-do-this-generator)
|
||||||
|
2. [How to add generator](#how-to-add-generator)
|
||||||
|
|
||||||
|
## What may do this generator
|
||||||
|
|
||||||
|
Let's imagine you want to have shortcuts in koin, to get something easily:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val koin: Koin// some initialization
|
||||||
|
|
||||||
|
val someUrl = koin.serverUrl
|
||||||
|
```
|
||||||
|
|
||||||
|
So, in that case you may mark containing file with next annotation (in the beginning of file):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@file:GenerateKoinDefinition("serverUrl", String::class, nullable = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
If file is called like `Sample.kt`, will be generated file `GeneratedDefinitionsSample.kt` with next content:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
public val Scope.serverUrl: String
|
||||||
|
get() = get(named("serverUrl"))
|
||||||
|
|
||||||
|
public val Koin.serverUrl: String
|
||||||
|
get() = get(named("serverUrl"))
|
||||||
|
|
||||||
|
public fun Module.serverUrlSingle(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<String>): KoinDefinition<String> =
|
||||||
|
single(named("serverUrl"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
public fun Module.serverUrlFactory(definition: Definition<String>):
|
||||||
|
KoinDefinition<String> = factory(named("serverUrl"), definition = definition)
|
||||||
|
```
|
||||||
|
|
||||||
|
Besides, you may use the generics:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@file:GenerateKoinDefinition("sampleInfo", Sample::class, G1::class, G2::class, nullable = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
Will generate:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
public val Scope.sampleInfo: Sample<G1, G2>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
public val Koin.sampleInfo: Sample<G1, G2>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<Sample<G1, G2>>): KoinDefinition<Sample<G1, G2>> =
|
||||||
|
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
public fun Module.sampleInfoFactory(definition: Definition<Sample<G1, G2>>):
|
||||||
|
KoinDefinition<Sample<G1, G2>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you wish not to generate single:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@file:GenerateKoinDefinition("sampleInfo", Sample::class, G1::class, G2::class, nullable = false, generateSingle = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
And you will take next code:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
public val Scope.sampleInfo: Sample<G1, G2>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
public val Koin.sampleInfo: Sample<G1, G2>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
public fun Module.sampleInfoFactory(definition: Definition<Sample<G1, G2>>):
|
||||||
|
KoinDefinition<Sample<G1, G2>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to add generator
|
||||||
|
|
||||||
|
**Note: $ksp_version in the samples above is equal to supported `ksp` version presented in `/gradle/libs.versions.toml` of project**
|
||||||
|
|
||||||
|
**Note: $microutils_version in the version of MicroUtils library in your project**
|
||||||
|
|
||||||
|
1. Add `classpath` in `build.gradle` (`classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:$ksp_version"`)
|
||||||
|
2. Add plugin to the plugins list of your module: `id "com.google.devtools.ksp"`
|
||||||
|
3. In `dependencies` block add to the required target/compile the dependency `dev.inmo:micro_utils.koin.generator:$microutils_version`:
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
add("kspCommonMainMetadata", "dev.inmo:micro_utils.koin.generator:$microutils_version") // will work in commonMain of your multiplatform module
|
||||||
|
add("kspJvm", "dev.inmo:micro_utils.koin.generator:$microutils_version") // will work in main of your JVM module
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp { // this generator do not require any arguments and we should left `ksp` empty
|
||||||
|
}
|
||||||
|
```
|
20
koin/generator/build.gradle
Normal file
20
koin/generator/build.gradle
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.jvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$publishJvmOnlyPath"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.koin")
|
||||||
|
api libs.kotlin.poet
|
||||||
|
api libs.ksp
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
286
koin/generator/src/main/kotlin/Processor.kt
Normal file
286
koin/generator/src/main/kotlin/Processor.kt
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package dev.inmo.micro_utils.koin.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.KSTypeNotPresentException
|
||||||
|
import com.google.devtools.ksp.KSTypesNotPresentException
|
||||||
|
import com.google.devtools.ksp.KspExperimental
|
||||||
|
import com.google.devtools.ksp.getAnnotationsByType
|
||||||
|
import com.google.devtools.ksp.processing.CodeGenerator
|
||||||
|
import com.google.devtools.ksp.processing.Resolver
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||||
|
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||||
|
import com.google.devtools.ksp.symbol.KSFile
|
||||||
|
import com.google.devtools.ksp.symbol.Modifier
|
||||||
|
import com.squareup.kotlinpoet.AnnotationSpec
|
||||||
|
import com.squareup.kotlinpoet.ClassName
|
||||||
|
import com.squareup.kotlinpoet.CodeBlock
|
||||||
|
import com.squareup.kotlinpoet.FileSpec
|
||||||
|
import com.squareup.kotlinpoet.FunSpec
|
||||||
|
import com.squareup.kotlinpoet.KModifier
|
||||||
|
import com.squareup.kotlinpoet.ParameterSpec
|
||||||
|
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||||
|
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||||
|
import com.squareup.kotlinpoet.PropertySpec
|
||||||
|
import com.squareup.kotlinpoet.TypeName
|
||||||
|
import com.squareup.kotlinpoet.TypeVariableName
|
||||||
|
import com.squareup.kotlinpoet.asTypeName
|
||||||
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
|
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||||
|
import com.squareup.kotlinpoet.ksp.writeTo
|
||||||
|
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
||||||
|
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.parameter.ParametersDefinition
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class Processor(
|
||||||
|
private val codeGenerator: CodeGenerator
|
||||||
|
) : SymbolProcessor {
|
||||||
|
private val definitionClassName = ClassName("org.koin.core.definition", "Definition")
|
||||||
|
private val koinDefinitionClassName = ClassName("org.koin.core.definition", "KoinDefinition")
|
||||||
|
|
||||||
|
private fun FileSpec.Builder.addCodeForType(
|
||||||
|
targetType: TypeName,
|
||||||
|
name: String,
|
||||||
|
nullable: Boolean,
|
||||||
|
generateSingle: Boolean,
|
||||||
|
generateFactory: Boolean,
|
||||||
|
) {
|
||||||
|
val targetTypeAsGenericType = (targetType as? TypeVariableName) ?.copy(reified = true)
|
||||||
|
|
||||||
|
fun addGetterProperty(
|
||||||
|
receiver: KClass<*>
|
||||||
|
) {
|
||||||
|
addProperty(
|
||||||
|
PropertySpec.builder(
|
||||||
|
name,
|
||||||
|
targetType,
|
||||||
|
).apply {
|
||||||
|
addKdoc(
|
||||||
|
"""
|
||||||
|
@return Definition by key "${name}"
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
getter(
|
||||||
|
FunSpec.getterBuilder().apply {
|
||||||
|
targetTypeAsGenericType ?.let {
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}
|
||||||
|
addCode(
|
||||||
|
"return " + (if (nullable) {
|
||||||
|
"getOrNull"
|
||||||
|
} else {
|
||||||
|
"get"
|
||||||
|
}) + "(named(\"${name}\"))"
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
targetTypeAsGenericType ?.let {
|
||||||
|
addTypeVariable(it)
|
||||||
|
}
|
||||||
|
receiver(receiver)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTypeAsGenericType == null) {
|
||||||
|
addGetterProperty(Scope::class)
|
||||||
|
addGetterProperty(Koin::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
val parametersDefinitionClassName = ClassName(
|
||||||
|
"org.koin.core.parameter",
|
||||||
|
"ParametersDefinition"
|
||||||
|
)
|
||||||
|
fun addGetterMethod(
|
||||||
|
receiver: KClass<*>
|
||||||
|
) {
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder(
|
||||||
|
name
|
||||||
|
).apply {
|
||||||
|
addKdoc(
|
||||||
|
"""
|
||||||
|
@return Definition by key "${name}" with [parameters]
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
receiver(receiver)
|
||||||
|
addParameter(
|
||||||
|
ParameterSpec(
|
||||||
|
"parameters",
|
||||||
|
parametersDefinitionClassName.let {
|
||||||
|
if (targetTypeAsGenericType != null) {
|
||||||
|
it.copy(nullable = true)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KModifier.NOINLINE
|
||||||
|
).toBuilder().apply {
|
||||||
|
if (targetTypeAsGenericType != null) {
|
||||||
|
defaultValue("null")
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
targetTypeAsGenericType ?.let {
|
||||||
|
addTypeVariable(it)
|
||||||
|
returns(it.copy(nullable = nullable))
|
||||||
|
} ?: returns(targetType)
|
||||||
|
addCode(
|
||||||
|
"return " + (if (nullable) {
|
||||||
|
"getOrNull"
|
||||||
|
} else {
|
||||||
|
"get"
|
||||||
|
}) + "(named(\"${name}\"), parameters)"
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addGetterMethod(Scope::class)
|
||||||
|
addGetterMethod(Koin::class)
|
||||||
|
|
||||||
|
fun FunSpec.Builder.addDefinitionParameter() {
|
||||||
|
val definitionModifiers = if (targetTypeAsGenericType == null) {
|
||||||
|
arrayOf()
|
||||||
|
} else {
|
||||||
|
arrayOf(KModifier.NOINLINE)
|
||||||
|
}
|
||||||
|
addParameter(
|
||||||
|
ParameterSpec.builder(
|
||||||
|
"definition",
|
||||||
|
definitionClassName.parameterizedBy(targetType.copy(nullable = false)),
|
||||||
|
*definitionModifiers
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generateSingle) {
|
||||||
|
fun FunSpec.Builder.configure() {
|
||||||
|
addKdoc(
|
||||||
|
"""
|
||||||
|
Will register [definition] with [org.koin.core.module.Module.single] and key "${name}"
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
receiver(Module::class)
|
||||||
|
addParameter(
|
||||||
|
ParameterSpec.builder(
|
||||||
|
"createdAtStart",
|
||||||
|
Boolean::class
|
||||||
|
).apply {
|
||||||
|
defaultValue("false")
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
addDefinitionParameter()
|
||||||
|
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||||
|
addCode(
|
||||||
|
"return single(named(\"${name}\"), createdAtStart = createdAtStart, definition = definition)"
|
||||||
|
)
|
||||||
|
targetTypeAsGenericType ?.let {
|
||||||
|
addTypeVariable(it)
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val actualSingleName = "single${name.replaceFirstChar { it.uppercase() }}"
|
||||||
|
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder(actualSingleName).apply { configure() }.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generateFactory) {
|
||||||
|
fun FunSpec.Builder.configure() {
|
||||||
|
addKdoc(
|
||||||
|
"""
|
||||||
|
Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}"
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
receiver(Module::class)
|
||||||
|
addDefinitionParameter()
|
||||||
|
returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false)))
|
||||||
|
addCode(
|
||||||
|
"return factory(named(\"${name}\"), definition = definition)"
|
||||||
|
)
|
||||||
|
targetTypeAsGenericType ?.let {
|
||||||
|
addTypeVariable(it)
|
||||||
|
addModifiers(KModifier.INLINE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val actualFactoryName = "factory${name.replaceFirstChar { it.uppercase() }}"
|
||||||
|
addFunction(
|
||||||
|
FunSpec.builder(actualFactoryName).apply { configure() }.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addImport("org.koin.core.qualifier", "named")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(KspExperimental::class)
|
||||||
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
|
(resolver.getSymbolsWithAnnotation(
|
||||||
|
GenerateKoinDefinition::class.qualifiedName!!
|
||||||
|
) + resolver.getSymbolsWithAnnotation(
|
||||||
|
GenerateGenericKoinDefinition::class.qualifiedName!!
|
||||||
|
)).filterIsInstance<KSFile>().forEach { ksFile ->
|
||||||
|
FileSpec.builder(
|
||||||
|
ksFile.packageName.asString(),
|
||||||
|
"GeneratedDefinitions${ksFile.fileName.removeSuffix(".kt")}"
|
||||||
|
).apply {
|
||||||
|
addFileComment(
|
||||||
|
"""
|
||||||
|
THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
TO REGENERATE IT JUST DELETE FILE
|
||||||
|
ORIGINAL FILE: ${ksFile.fileName}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
|
||||||
|
val type = runCatching {
|
||||||
|
it.type.asTypeName()
|
||||||
|
}.getOrElse { e ->
|
||||||
|
if (e is KSTypeNotPresentException) {
|
||||||
|
e.ksType.toClassName()
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val targetType = runCatching {
|
||||||
|
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
|
||||||
|
}.getOrElse { e ->
|
||||||
|
when (e) {
|
||||||
|
is KSTypeNotPresentException -> e.ksType.toClassName()
|
||||||
|
}
|
||||||
|
if (e is KSTypesNotPresentException) {
|
||||||
|
type.parameterizedBy(*e.ksTypes.map { it.toTypeName() }.toTypedArray())
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}.copy(
|
||||||
|
nullable = it.nullable
|
||||||
|
)
|
||||||
|
|
||||||
|
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
|
||||||
|
}
|
||||||
|
ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
|
||||||
|
val targetType = TypeVariableName("T", Any::class)
|
||||||
|
addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory)
|
||||||
|
}
|
||||||
|
}.build().let {
|
||||||
|
File(
|
||||||
|
File(ksFile.filePath).parent,
|
||||||
|
"GeneratedDefinitions${ksFile.fileName}"
|
||||||
|
).takeIf { !it.exists() } ?.apply {
|
||||||
|
parentFile.mkdirs()
|
||||||
|
|
||||||
|
writer().use { writer ->
|
||||||
|
it.writeTo(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
11
koin/generator/src/main/kotlin/Provider.kt
Normal file
11
koin/generator/src/main/kotlin/Provider.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.koin.generator
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||||
|
|
||||||
|
class Provider : SymbolProcessorProvider {
|
||||||
|
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = Processor(
|
||||||
|
environment.codeGenerator
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
dev.inmo.micro_utils.koin.generator.Provider
|
27
koin/generator/test/build.gradle
Normal file
27
koin/generator/test/build.gradle
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
id "com.google.devtools.ksp"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.koin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
add("kspCommonMainMetadata", project(":micro_utils.koin.generator"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
}
|
1
koin/generator/test/src/androidMain/AndroidManifest.xml
Normal file
1
koin/generator/test/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
@@ -0,0 +1,124 @@
|
|||||||
|
// THIS CODE HAVE BEEN GENERATED AUTOMATICALLY
|
||||||
|
// TO REGENERATE IT JUST DELETE FILE
|
||||||
|
// ORIGINAL FILE: Test.kt
|
||||||
|
package dev.inmo.micro_utils.koin.generator.test
|
||||||
|
|
||||||
|
import kotlin.Any
|
||||||
|
import kotlin.Boolean
|
||||||
|
import kotlin.Deprecated
|
||||||
|
import kotlin.String
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import org.koin.core.definition.Definition
|
||||||
|
import org.koin.core.definition.KoinDefinition
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.core.parameter.ParametersDefinition
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public val Scope.sampleInfo: Test<String>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public val Koin.sampleInfo: Test<String>
|
||||||
|
get() = get(named("sampleInfo"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "sampleInfo" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||||
|
get(named("sampleInfo"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "sampleInfo" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||||
|
get(named("sampleInfo"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
|
||||||
|
ReplaceWith("singleSampleInfo"),
|
||||||
|
)
|
||||||
|
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||||
|
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||||
|
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
|
||||||
|
ReplaceWith("factorySampleInfo"),
|
||||||
|
)
|
||||||
|
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
|
||||||
|
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
|
||||||
|
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "test" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T
|
||||||
|
= get(named("test"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "test" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T
|
||||||
|
= get(named("test"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "test"
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline
|
||||||
|
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
|
||||||
|
createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>):
|
||||||
|
KoinDefinition<T> = factory(named("test"), definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "testNullable" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? =
|
||||||
|
null): T? = getOrNull(named("testNullable"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Definition by key "testNullable" with [parameters]
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? =
|
||||||
|
null): T? = getOrNull(named("testNullable"), parameters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false,
|
||||||
|
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
|
||||||
|
createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
|
||||||
|
*/
|
||||||
|
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>):
|
||||||
|
KoinDefinition<T> = factory(named("testNullable"), definition = definition)
|
16
koin/generator/test/src/commonMain/kotlin/Test.kt
Normal file
16
koin/generator/test/src/commonMain/kotlin/Test.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@file:GenerateKoinDefinition("sampleInfo", Test::class, String::class, nullable = false)
|
||||||
|
@file:GenerateGenericKoinDefinition("test", nullable = false)
|
||||||
|
@file:GenerateGenericKoinDefinition("testNullable", nullable = true)
|
||||||
|
package dev.inmo.micro_utils.koin.generator.test
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
||||||
|
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
||||||
|
import org.koin.core.Koin
|
||||||
|
|
||||||
|
class Test<T>(
|
||||||
|
koin: Koin
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
koin.sampleInfo
|
||||||
|
}
|
||||||
|
}
|
1
koin/src/androidMain/AndroidManifest.xml
Normal file
1
koin/src/androidMain/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
40
koin/src/commonMain/kotlin/GetWithDefinition.kt
Normal file
40
koin/src/commonMain/kotlin/GetWithDefinition.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.inmo.micro_utils.koin
|
||||||
|
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import org.koin.core.definition.BeanDefinition
|
||||||
|
import org.koin.core.definition.KoinDefinition
|
||||||
|
import org.koin.core.instance.InstanceFactory
|
||||||
|
import org.koin.core.parameter.ParametersDefinition
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
|
||||||
|
fun <T> Koin.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.primaryType,
|
||||||
|
definition.qualifier,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Koin.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.beanDefinition,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Koin.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.factory,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Scope.get(definition: BeanDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.primaryType,
|
||||||
|
definition.qualifier,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Scope.get(definition: InstanceFactory<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.beanDefinition,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T> Scope.get(definition: KoinDefinition<T>, parameters: ParametersDefinition? = null): T = get(
|
||||||
|
definition.factory,
|
||||||
|
parameters
|
||||||
|
)
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.inmo.micro_utils.koin.annotations
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this annotation to mark files near to which generator should place generated extensions for koin [org.koin.core.scope.Scope]
|
||||||
|
* and [org.koin.core.Koin]
|
||||||
|
*
|
||||||
|
* @param name Name for definitions. This name will be available as extension for [org.koin.core.scope.Scope] and [org.koin.core.Koin]
|
||||||
|
* @param type Type of extensions. It is base star-typed class
|
||||||
|
* @param typeArgs Generic types for [type]. For example, if [type] == `Something::class` and [typeArgs] == `G1::class,
|
||||||
|
* G2::class`, the result type will be `Something<G1, G2>`
|
||||||
|
* @param nullable In case when true, extension will not throw error when definition has not been registered in koin
|
||||||
|
* @param generateSingle Generate definition factory with [org.koin.core.module.Module.single]. You will be able to use
|
||||||
|
* the extension [org.koin.core.module.Module].[name]Single(createdAtStart/* default false */) { /* your definition */ }
|
||||||
|
* @param generateFactory Generate definition factory with [org.koin.core.module.Module.factory]. You will be able to use
|
||||||
|
* the extension [org.koin.core.module.Module].[name]Factory { /* your definition */ }
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.FILE)
|
||||||
|
@Repeatable
|
||||||
|
annotation class GenerateGenericKoinDefinition(
|
||||||
|
val name: String,
|
||||||
|
val nullable: Boolean = true,
|
||||||
|
val generateSingle: Boolean = true,
|
||||||
|
val generateFactory: Boolean = true
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user