mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-11-01 17:43:03 +00:00
Compare commits
358
Commits
v0.18.0
...
17d6377902
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d6377902 | ||
|
|
ad401105a1 | ||
|
|
4913e99c2e | ||
|
|
16b97029cf | ||
|
|
481130c9bb | ||
|
|
698ed6718d | ||
|
|
d164813bb4 | ||
|
|
52157ee0e7 | ||
|
|
877b62fe5d | ||
|
|
d823a02971 | ||
|
|
e950056e3b | ||
|
|
70014ba233 | ||
|
|
4425f24a20 | ||
|
|
410964a44b | ||
|
|
30389e8536 | ||
|
|
5314833041 | ||
|
|
40f7cf7678 | ||
|
|
83a0b07062 | ||
|
|
1b4900d691 | ||
|
|
f9795d53a0 | ||
|
|
2b9bb4f141 | ||
|
|
9196e4c367 | ||
|
|
374a5a1a37 | ||
|
|
827cf32c1b | ||
|
|
98ad6dbeb2 | ||
|
|
63c8f642ec | ||
|
|
3bfe64f797 | ||
|
|
ec98029467 | ||
|
|
ab58478686 | ||
|
|
90247667d1 | ||
|
|
e661185534 | ||
|
|
d73e4e8e1f | ||
|
|
a6905e73cb | ||
|
|
93b054d55e | ||
|
|
5db4c5c717 | ||
|
|
5e04521929 | ||
|
|
30440b4ed1 | ||
|
|
09bb90604d | ||
|
|
4d55ec6f36 | ||
|
|
f373524f34 | ||
|
|
0398a7bebd | ||
|
|
fa8a5bcd97 | ||
|
|
758a92410b | ||
|
|
77f56c5dda | ||
|
|
72f2fe3cc3 | ||
|
|
7e0e520f03 | ||
|
|
92c4784e42 | ||
|
|
a634229dc0 | ||
|
|
de3f36ef2a | ||
|
|
c623a265ee | ||
|
|
e75125f6df | ||
|
|
4901a8844c | ||
|
|
a1854b68d8 | ||
|
|
aee4a6243b | ||
|
|
4c0cb73d69 | ||
|
|
c6eab182f6 | ||
|
|
3e3fbd97eb | ||
|
|
feb695caa7 | ||
|
|
1bd46d9651 | ||
|
|
32eabb6b36 | ||
|
|
9bfe6dc6d8 | ||
|
|
3f366aeea4 | ||
|
|
4338fd46f2 | ||
|
|
36974f5b49 | ||
|
|
d48f767408 | ||
|
|
bd2558e852 | ||
|
|
9f7c963cd5 | ||
|
|
2db0eadbfe | ||
|
|
580c7b8842 | ||
|
|
88fba347ea | ||
|
|
c8c5255e62 | ||
|
|
01a1a2760a | ||
|
|
6d34df8d48 | ||
|
|
b124ea65e1 | ||
|
|
f3214347a9 | ||
|
|
6b8724b59f | ||
|
|
ac3e378edf | ||
|
|
5d31fd1c91 | ||
|
|
046fe1ec08 | ||
|
|
721873c843 | ||
|
|
f6ffbfc10a | ||
|
|
4e91649e0a | ||
|
|
4f7f8abec4 | ||
|
|
6370562dbc | ||
|
|
94e2e67522 | ||
|
|
1d8330015d | ||
|
|
e5017b0258 | ||
|
|
cd412ca31b | ||
|
|
d2b6473095 | ||
|
|
ab42507275 | ||
|
|
dcef844e20 | ||
|
|
3244708c1b | ||
|
|
73ac1f1741 | ||
|
|
f104e9f352 | ||
|
|
393c9a7d06 | ||
|
|
ea497ea488 | ||
|
|
5add89cad0 | ||
|
|
2db447d2ef | ||
|
|
31c83813e6 | ||
|
|
27483a282d | ||
|
|
48b816aa22 | ||
|
|
0065f94f52 | ||
|
|
ccc0002eb2 | ||
|
|
15a2eee141 | ||
|
|
b9faac71e5 | ||
|
|
0a4465de33 | ||
|
|
f9dfd09628 | ||
|
|
8638d7afce | ||
|
|
8311793a43 | ||
|
|
0d552cfcd2 | ||
|
|
4b0f20dbd1 | ||
|
|
cf531c949d | ||
|
|
ba5c5f17d5 | ||
|
|
35825ad9b7 | ||
|
|
b1eb26a89e | ||
|
|
c9d04b6698 | ||
|
|
496133e014 | ||
|
|
f2857ee2be | ||
|
|
22541f2d5e | ||
|
|
e235c52b6c | ||
|
|
e89b8c72dd | ||
|
|
a374e53a3a | ||
|
|
afb066c4ee | ||
|
|
f05761d4a5 | ||
|
|
76adc9ea33 | ||
|
|
99dd291413 | ||
|
|
f904eb27e1 | ||
|
|
eeb8214812 | ||
|
|
f7215b039e | ||
|
|
c07fe5a0f9 | ||
|
|
0d28cb6e20 | ||
|
|
a1a17bfd1f | ||
|
|
f386f09592 | ||
|
|
a47e17fe6e | ||
|
|
01dc3b63ff | ||
|
|
2d97e0699e | ||
|
|
75f514d99b | ||
|
|
9a687cfc1c | ||
|
|
14edf8b6b7 | ||
|
|
23aa2d8917 | ||
|
|
7651388b5c | ||
|
|
805ab32b24 | ||
|
|
cc623b1097 | ||
|
|
1420416b3e | ||
|
|
9a0b67f938 | ||
|
|
303e1e6281 | ||
|
|
ff59b0cc9c | ||
|
|
be5d2ee715 | ||
|
|
8dd2e3f6f9 | ||
|
|
2eedd196d2 | ||
|
|
759a3f2784 | ||
|
|
386fa830c3 | ||
|
|
c382423d77 | ||
|
|
bb466ce66c | ||
|
|
72cd3dd8a1 | ||
|
|
595cedaaf1 | ||
|
|
eeaceb6cf5 | ||
|
|
1bd671685b | ||
|
|
48d3fe41f2 | ||
|
|
7ab21871cd | ||
|
|
65d01b1fb3 | ||
|
|
6230accb68 | ||
|
|
c6ed821934 | ||
|
|
10e03bb951 | ||
|
|
aa4f392948 | ||
|
|
f51b59ec02 | ||
|
|
8c76834ae4 | ||
|
|
4a454f3d67 | ||
|
|
151aa1863d | ||
|
|
3bf6896296 | ||
|
|
0d01561476 | ||
|
|
f6ded92251 | ||
|
|
d01b735cc6 | ||
|
|
6c12001080 | ||
|
|
1afbf03606 | ||
|
|
f6ef5c61c5 | ||
|
|
c18fee8107 | ||
|
|
d9df7a4384 | ||
|
|
87c2230e8e | ||
|
|
da7eb6de0a | ||
|
|
ed3815118f | ||
|
|
be726f42bd | ||
|
|
a91006132f | ||
|
|
9a9f741a0b | ||
|
|
5028f130e9 | ||
|
|
77fa019651 | ||
|
|
9715da9384 | ||
|
|
f6d5035c1a | ||
|
|
43e782ab6f | ||
|
|
f3f9920bfb | ||
|
|
2bfd615812 | ||
|
|
ebfacb3659 | ||
|
|
c71d557eec | ||
|
|
e0398cef21 | ||
|
|
f91599e9c6 | ||
|
|
f8f9f93c97 | ||
|
|
a8a5340d8b | ||
|
|
871b27f37d | ||
|
|
6f174cae1d | ||
|
|
22d7ac3e22 | ||
|
|
9b30c3a155 | ||
|
|
915bac64b1 | ||
|
|
9d2b50e55d | ||
|
|
bde100f63d | ||
|
|
05b035a13d | ||
|
|
eefb56bed7 | ||
|
|
fcc0dc4189 | ||
|
|
47ff20317f | ||
|
|
1558b9103d | ||
|
|
7a78742162 | ||
|
|
c01e240f66 | ||
|
|
fef4fcbac6 | ||
|
|
5ab18bce4b | ||
|
|
24aec7271a | ||
|
|
9b19a2cb95 | ||
|
|
efdd7b8a57 | ||
|
|
6df4cc9c3b | ||
|
|
b9d93db0f5 | ||
|
|
d7ee45ca64 | ||
|
|
d7c31b1b22 | ||
|
|
7d6794a358 | ||
|
|
473eb87346 | ||
|
|
8b18b07790 | ||
|
|
ab3c80a5ec | ||
|
|
075b93ecd6 | ||
|
|
f6d0f72e49 | ||
|
|
fcda3af862 | ||
|
|
d5c7a589b1 | ||
|
|
86e099ed25 | ||
|
|
ebd7befe73 | ||
|
|
b8c7e581a1 | ||
|
|
8281259179 | ||
|
|
d3e06b07df | ||
|
|
4967018418 | ||
|
|
537a3c38fa | ||
|
|
0124957833 | ||
|
|
f0420e2d61 | ||
|
|
7090566041 | ||
|
|
f0d5035cd0 | ||
|
|
0fb9b8dc30 | ||
|
|
eef6e81134 | ||
|
|
1593159a3f | ||
|
|
3da9eb9dbe | ||
|
|
f17613f3fb | ||
|
|
14337ccb46 | ||
|
|
1a3913b09c | ||
|
|
039aed2747 | ||
|
|
173991e3cb | ||
|
|
8b3f8cab01 | ||
|
|
2a20d24589 | ||
|
|
53c2d552ec | ||
|
|
af11c1a83d | ||
|
|
a65cf1481c | ||
|
|
0318716236 | ||
|
|
88eb4b3342 | ||
|
|
4810d1ef6a | ||
|
|
f2a9514d89 | ||
|
|
925ba6ac24 | ||
|
|
ef407268a2 | ||
|
|
cd6c4bbe38 | ||
|
|
c058e18408 | ||
|
|
6d3ca565ca | ||
|
|
236a7b4fd2 | ||
|
|
e1f387dbf7 | ||
|
|
3d113dd31e | ||
|
|
e0e57f0336 | ||
|
|
e775b58d41 | ||
|
|
5b070a8478 | ||
|
|
8a61193500 | ||
|
|
fad522b8fe | ||
|
|
0acac205af | ||
|
|
069d4c61b7 | ||
|
|
7c113f5700 | ||
|
|
dfdaf4225b | ||
|
|
bd39ab2467 | ||
|
|
6ce1eb3f2d | ||
|
|
ce7d4fe9a2 | ||
|
|
a65bb2f419 | ||
|
|
cbc868448b | ||
|
|
9c336a0b56 | ||
|
|
0f0d09399e | ||
|
|
e13a1162a9 | ||
|
|
57ebed903f | ||
|
|
4478193d8a | ||
|
|
ee948395e3 | ||
|
|
0616b051ae | ||
|
|
4d155d0505 | ||
|
|
a169e733d9 | ||
|
|
f081e237c8 | ||
|
|
f412d387fa | ||
|
|
67354b43e2 | ||
|
|
39135a4000 | ||
|
|
eaa014cebd | ||
|
|
856e657f81 | ||
|
|
3a609e5b66 | ||
|
|
d0022dd599 | ||
|
|
7ba6eed453 | ||
|
|
beeb6ecc0a | ||
|
|
7cdc17a714 | ||
|
|
4765a950a9 | ||
|
|
65e8137e08 | ||
|
|
ae546dd9ad | ||
|
|
8110c42be0 | ||
|
|
bd2b5ae5fc | ||
|
|
2ddfffa6a9 | ||
|
|
a4b54e861d | ||
|
|
c6785f1a4f | ||
|
|
83fe621c56 | ||
|
|
b3a93e17eb | ||
|
|
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 |
No files matched your search
@@ -8,7 +8,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
- name: Build
|
||||
run: ./gradlew build && ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
|
||||
@@ -14,3 +14,8 @@ local.properties
|
||||
kotlin-js-store
|
||||
|
||||
publishing.sh
|
||||
|
||||
local.*
|
||||
local/
|
||||
|
||||
.kotlin/
|
||||
+541
@@ -1,5 +1,546 @@
|
||||
# Changelog
|
||||
|
||||
## 0.21.6
|
||||
|
||||
* `KSP`:
|
||||
* `Sealed`:
|
||||
* Fixes in generation
|
||||
|
||||
## 0.21.5
|
||||
|
||||
* `KSP`:
|
||||
* Add utility functions `KSClassDeclaration.findSubClasses`
|
||||
* `Sealed`:
|
||||
* Improve generation
|
||||
|
||||
## 0.21.4
|
||||
|
||||
* `Common`:
|
||||
* `Compose`:
|
||||
* Add support of mingw, linux, arm64 targets
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* Add support of mingw, linux, arm64 targets
|
||||
* `Koin`:
|
||||
* Add support of mingw, linux, arm64 targets
|
||||
* `KSP`:
|
||||
* `ClassCasts`:
|
||||
* Add support of mingw, linux, arm64 targets
|
||||
* `Sealed`:
|
||||
* Add support of mingw, linux, arm64 targets
|
||||
|
||||
## 0.21.3
|
||||
|
||||
* `Colors`:
|
||||
* Added as a module. It should be used by default in case you wish all the API currently realized for `HEXAColor`
|
||||
* `Coroutines`:
|
||||
* Fix of [#374](https://github.com/InsanusMokrassar/MicroUtils/issues/374):
|
||||
* Add vararg variants of `awaitFirst`
|
||||
* Add `joinFirst`
|
||||
|
||||
## 0.21.2
|
||||
|
||||
* `KSP`:
|
||||
* `ClassCasts`:
|
||||
* Module has been initialized
|
||||
|
||||
## 0.21.1
|
||||
|
||||
* `KSP`:
|
||||
* Module has been initialized
|
||||
* `Generator`:
|
||||
* Module has been initialized
|
||||
* `Sealed`:
|
||||
* Module has been initialized
|
||||
|
||||
## 0.21.0
|
||||
|
||||
**THIS UPDATE CONTAINS BREAKING CHANGES IN `safely*`-ORIENTED FUNCTIONS**
|
||||
|
||||
* `Coroutines`:
|
||||
* **All `safely` functions lost their `supervisorScope` in favor to wrapping `runCatching`**
|
||||
* `runCatchingSafely` is the main handling function of all `safely` functions
|
||||
* `launchSafely*` and `asyncSafely*` blocks lost `CoroutineScope` as their receiver
|
||||
|
||||
## 0.20.52
|
||||
|
||||
* `Coroutines`:
|
||||
* Small rework of weak jobs: add `WeakScope` factory, rename old weal extensions and add kdocs
|
||||
|
||||
## 0.20.51
|
||||
|
||||
* `Versions`:
|
||||
* `Android Fragment`: `1.7.0` -> `1.7.1`
|
||||
* `Pagination`:
|
||||
* Add `Pagination.nextPageIfTrue` and `Pagination.thisPageIftrue` extensions to get the page according to condition
|
||||
pass status
|
||||
* Add `PaginationResult.nextPageIfNotEmptyOrLastPage` and `PaginationResult.thisPageIfNotEmptyOrLastPage`
|
||||
* Change all `doForAll` and `getAll` extensions fo pagination to work basing on `nextPageIfNotEmptyOrLastPage` and
|
||||
`thisPageIfNotEmptyOrLastPage`
|
||||
|
||||
## 0.20.50
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.8.0` -> `1.8.1`
|
||||
* `KSLog`: `1.3.3` -> `1.3.4`
|
||||
* `Exposed`: `0.50.0` -> `0.50.1`
|
||||
* `Ktor`: `2.3.10` -> `2.3.11`
|
||||
* A lot of inline functions became common functions due to inline with only noinline callbacks in arguments leads to
|
||||
low performance
|
||||
* `Coroutines`:
|
||||
* `SmartMutex`, `SmartSemaphore` and `SmartRWLocker` as their user changed their state flow to `SpecialMutableStateFlow`
|
||||
|
||||
## 0.20.49
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* All `Repo`s get `diff` extensions
|
||||
* `KeyValueRepo` and `KeyValuesRepo` get `applyDiff` extension
|
||||
* Add new extensions `on*` flows for crud repos
|
||||
|
||||
## 0.20.48
|
||||
|
||||
* `Versions`:
|
||||
* `Android Core KTX`: `1.13.0` -> `1.13.1`
|
||||
* `AndroidX Fragment`: `1.6.2` -> `1.7.0`
|
||||
|
||||
## 0.20.47
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.49.0` -> `0.50.0`
|
||||
|
||||
## 0.20.46
|
||||
|
||||
* `Common`:
|
||||
* Now this repo depends on `klock`
|
||||
* Add new object-serializer `DateTimeSerializer` for `klock` serializer
|
||||
|
||||
## 0.20.45
|
||||
|
||||
* `Versions`:
|
||||
* `Android Core KTX`: `1.12.0` -> `1.13.0`
|
||||
|
||||
## 0.20.44
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.6.1` -> `1.6.2`
|
||||
* `Koin`: `3.5.4` -> `3.5.6`
|
||||
|
||||
## 0.20.43
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.9` -> `2.3.10`
|
||||
* `Koin`: `3.5.3` -> `3.5.4`
|
||||
|
||||
## 0.20.42
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Improvements
|
||||
|
||||
## 0.20.41
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* `AbstractExposedKeyValueRepo`, `ExposedKeyValueRepo`, `AbstractExposedKeyValuesRepo`, `ExposedKeyValuesRepo` got opportunity to setup some part of their flows
|
||||
|
||||
## 0.20.40
|
||||
|
||||
* `Versions`:
|
||||
* `KSLog`: `1.3.2` -> `1.3.3`
|
||||
* `Exposed`: `0.48.0` -> `0.49.0`
|
||||
* `UUID`: `0.8.2` -> `0.8.4`
|
||||
|
||||
## 0.20.39
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.22` -> `1.9.23`
|
||||
* `Korlibs`: `5.3.2` -> `5.4.0`
|
||||
* `Okio`: `3.8.0` -> `3.9.0`
|
||||
* `Compose`: `1.6.0` -> `1.6.1`
|
||||
* `ComposeMaterial3`: `1.2.0` -> `1.2.1`
|
||||
|
||||
## 0.20.38
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.8` -> `2.3.9`
|
||||
|
||||
## 0.20.37
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.5.12` -> `1.6.0`
|
||||
* `Exposed`: `0.47.0` -> `0.48.0`
|
||||
|
||||
## 0.20.36
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.6.2` -> `1.6.3`
|
||||
* `Korlibs`: `5.3.1` -> `5.3.2`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Improve work and functionality of `actualizeAll` and subsequent functions
|
||||
* All internal repos `invalidate`/`actualizeAll` now use common `actualizeAll` functions
|
||||
|
||||
## 0.20.35
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.3` -> `1.8.0`
|
||||
* `Material3`: `1.1.2` -> `1.2.0`
|
||||
|
||||
## 0.20.34
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Improve default `set` realization of `KeyValuesRepo`
|
||||
|
||||
## 0.20.33
|
||||
|
||||
* `Colors`
|
||||
* `Common`:
|
||||
* Add opportunity to use `HEXAColor` with `ahex` colors
|
||||
|
||||
## 0.20.32
|
||||
|
||||
* `Versions`:
|
||||
* `Okio`: `3.7.0` -> `3.8.0`
|
||||
* `Resources`:
|
||||
* Make `StringResource` serializable
|
||||
* Add several variants of builder usages
|
||||
|
||||
## 0.20.31
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.7` -> `2.3.8`
|
||||
|
||||
## 0.20.30
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.46.0` -> `0.47.0`
|
||||
|
||||
## 0.20.29
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.21` -> `1.9.22`
|
||||
* `Compose`: `1.5.11` -> `1.5.12`
|
||||
* `Korlibs`: `5.3.0` -> `5.3.1`
|
||||
|
||||
## 0.20.28
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.22` -> `1.9.21` (downgrade)
|
||||
* `Compose`: `1.6.0-dev13691` -> `1.5.11` (downgrade)
|
||||
|
||||
## 0.20.27
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.21` -> `1.9.22`
|
||||
* `Compose`: `1.5.11` -> `1.6.0-dev13691`
|
||||
|
||||
## 0.20.26
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.45.0` -> `0.46.0`. **This update brinds new api deprecations in exposed**
|
||||
* `Resources`:
|
||||
* Add opportunity to get default translation by passing `null` as `IetfLang` argument
|
||||
* Add several useful extensions to get translations in `JS` target
|
||||
|
||||
## 0.20.25
|
||||
|
||||
* `Colors`:
|
||||
* `Common`:
|
||||
* Module inited
|
||||
|
||||
## 0.20.24
|
||||
|
||||
**Since this version depdendencies of klock and krypto replaced with `com.soywiz.korge:korlibs-time` and `com.soywiz.korge:korlibs-crypto`**
|
||||
|
||||
* `Versions`:
|
||||
* `Klock` (since now `KorlibsTime`): `4.0.10` -> `5.3.0`
|
||||
* `Krypto` (since now `KorlibsCrypto`): `4.0.10` -> `5.3.0`
|
||||
* `Serialization`:
|
||||
* `Mapper`:
|
||||
* `Mapper` pass decoder into callback of deserialization strategy
|
||||
* `Mapper` pass encoder into callback of serialization strategy
|
||||
|
||||
## 0.20.23
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.5.0` -> `3.5.3`
|
||||
* `Okio`: `3.6.0` -> `3.7.0`
|
||||
* `LanguageCodes`:
|
||||
* Fixes in intermediate language codes (like `Chinese.Hans`)
|
||||
* Rename `IetfLanguageCode` to `IetfLang`
|
||||
* Rename all subsequent functions (including serializer)
|
||||
* New lazy properties `knownLanguageCodesMap`, `knownLanguageCodesMapByLowerCasedKeys` and several others
|
||||
|
||||
## 0.20.22
|
||||
|
||||
* `Common`:
|
||||
* Add opportunity to create own `Diff` with base constructor
|
||||
|
||||
## 0.20.21
|
||||
|
||||
* `Resources`:
|
||||
* Inited
|
||||
|
||||
## 0.20.20
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Add opportunity for setup flows in `AbstractExposedCRUDRepo`
|
||||
|
||||
## 0.20.19
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.6` -> `2.3.7`
|
||||
|
||||
## 0.20.18
|
||||
|
||||
* `Coroutines`:
|
||||
* `SpecialMutableStateFlow` now extends `MutableStateFlow`
|
||||
* `Compose`:
|
||||
* Deprecate `FlowState` due to its complexity in fixes
|
||||
|
||||
## 0.20.17
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.6.1` -> `1.6.2`
|
||||
|
||||
## 0.20.16
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.44.1` -> `0.45.0`
|
||||
* `Coroutines`:
|
||||
* Add `SpecialMutableStateFlow`
|
||||
* `Compose`:
|
||||
* Add `FlowState`
|
||||
|
||||
## 0.20.15
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20` -> `1.9.21`
|
||||
* `KSLog`: `1.3.0` -> `1.3.1`
|
||||
* `Compose`: `1.5.10` -> `1.5.11`
|
||||
|
||||
## 0.20.14
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.6.0` -> `1.6.1`
|
||||
* `KSLog`: `1.2.4` -> `1.3.0`
|
||||
|
||||
## 0.20.13
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.5` -> `2.3.6`
|
||||
* `UUID`: `0.8.1` -> `0.8.2`
|
||||
|
||||
## 0.20.12
|
||||
|
||||
**It is experimental migration onto new gradle version. Be careful in use of this version**
|
||||
|
||||
**This update have JDK 17 in `compatibility` and `target` versions**
|
||||
|
||||
## 0.20.11
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-RC2` -> `1.9.20`
|
||||
* `Exposed`: `0.44.0` -> `0.44.1`
|
||||
* `Compose`: `1.5.10-rc02` -> `1.5.10`
|
||||
* `Coroutines`:
|
||||
* `SmartRWLocker` now will wait first unlock of write mutex for acquiring read
|
||||
|
||||
## 0.20.10
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-RC` -> `1.9.20-RC1`
|
||||
* `KSLog`: `1.2.1` -> `1.2.2`
|
||||
* `Compose`: `1.5.10-rc01` -> `1.5.10-rc02`
|
||||
* `RecyclerView`: `1.3.1` -> `1.3.2`
|
||||
|
||||
## 0.20.9
|
||||
|
||||
* Most of common modules now supports `linuxArm64` target
|
||||
|
||||
## 0.20.8
|
||||
|
||||
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION**
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC`
|
||||
* `Compose`: `1.5.10-beta02` -> `1.5.10-rc01`
|
||||
|
||||
## 0.20.7
|
||||
|
||||
**THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION**
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.10` -> `1.9.20-Beta2`
|
||||
* `Compose`: `1.5.1` -> `1.5.10-beta02`
|
||||
* `Exposed`: `0.43.0` -> `0.44.0`
|
||||
* `Ktor`: `2.3.4` -> `2.3.5`
|
||||
* `Koin`: `3.4.3` -> `3.5.0`
|
||||
* `Okio`: `3.5.0` -> `3.6.0`
|
||||
* `Android Core`: `1.10.1` -> `1.12.0`
|
||||
* `Android Compose Material`: `1.1.1` -> `1.1.2`
|
||||
|
||||
## 0.20.6
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`
|
||||
* Fixes in exposed key-values repos
|
||||
|
||||
## 0.20.5
|
||||
|
||||
* `Coroutines`:
|
||||
* Fixes in `SmartRWLocker`
|
||||
|
||||
## 0.20.4
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.0` -> `1.9.10`
|
||||
* `KSLog`: `1.2.0` -> `1.2.1`
|
||||
* `Compose`: `1.5.0` -> `1.5.1`
|
||||
* `UUID`: `0.8.0` -> `0.8.1`
|
||||
|
||||
## 0.20.3
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.4.3` -> `1.5.0`
|
||||
* `Exposed`: `0.42.1` -> `0.43.0`
|
||||
* `Ktor`: `2.3.3` -> `2.3.4`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Fixes in locks of caches
|
||||
|
||||
## 0.20.2
|
||||
|
||||
* All main repos uses `SmartRWLocker`
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.5.1` -> `1.6.0`
|
||||
* `Exposed`: `0.42.0` -> `0.42.1`
|
||||
* `Korlibs`: `4.0.9` -> `4.0.10`
|
||||
* `Androis SDK`: `33` -> `34`
|
||||
|
||||
## 0.20.1
|
||||
|
||||
* `SmallTextField`:
|
||||
* Module is initialized
|
||||
* `Pickers`:
|
||||
* Module is initialized
|
||||
* `Coroutines`:
|
||||
* Add `SmartSemaphore`
|
||||
* Add `SmartRWLocker`
|
||||
|
||||
## 0.20.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.8.22` -> `1.9.0`
|
||||
* `KSLog`: `1.1.1` -> `1.2.0`
|
||||
* `Exposed`: `0.41.1` -> `0.42.0`
|
||||
* `UUID`: `0.7.1` -> `0.8.0`
|
||||
* `Korlibs`: `4.0.3` -> `4.0.9`
|
||||
* `Ktor`: `2.3.2` -> `2.3.3`
|
||||
* `Okio`: `3.4.0` -> `3.5.0`
|
||||
|
||||
## 0.19.9
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.4.2` -> `3.4.3`
|
||||
* `Startup`:
|
||||
* Now it is possible to start application in synchronous way
|
||||
|
||||
## 0.19.8
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.2` -> `1.7.3`
|
||||
* `Kotlin`: `1.8.20` -> `1.8.22`
|
||||
* `Compose`: `1.4.1` -> `1.4.3`
|
||||
* `Okio`: `3.3.0` -> `3.4.0`
|
||||
* `RecyclerView`: `1.3.0` -> `1.3.1`
|
||||
* `Fragment`: `1.6.0` -> `1.6.1`
|
||||
* `Repos`:
|
||||
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
|
||||
* `Cache`:
|
||||
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
|
||||
will be automatically invalidated on start of their work
|
||||
|
||||
## 0.19.7
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.1` -> `1.7.2`
|
||||
|
||||
## 0.19.6
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.4` -> `1.7.1`
|
||||
* `Ktor`: `2.3.1` -> `2.3.2`
|
||||
* `Compose`: `1.4.0` -> `1.4.1`
|
||||
|
||||
## 0.19.5
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Fixes in new type generation
|
||||
|
||||
## 0.19.4
|
||||
|
||||
* `Versions`:
|
||||
* `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**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
||||
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.android.smalltextfield")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
|
||||
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
|
||||
initialVelocity: Float,
|
||||
animationSpec: DecayAnimationSpec<Float>,
|
||||
adjustTarget: ((Float) -> Float)?,
|
||||
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
|
||||
val adjustedTarget = adjustTarget?.invoke(targetValue)
|
||||
|
||||
return if (adjustedTarget != null) {
|
||||
animateTo(
|
||||
targetValue = adjustedTarget,
|
||||
initialVelocity = initialVelocity,
|
||||
block = block
|
||||
)
|
||||
} else {
|
||||
animateDecay(
|
||||
initialVelocity = initialVelocity,
|
||||
animationSpec = animationSpec,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.center
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
|
||||
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
|
||||
}
|
||||
|
||||
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
|
||||
|
||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NumberPicker(
|
||||
number: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
range: IntRange? = null,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||
allowUseManualInput: Boolean = true,
|
||||
onStateChanged: (Int) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val numbersColumnHeight = 36.dp
|
||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||
|
||||
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||
|
||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||
if (range != null) {
|
||||
val offsetRange = remember(number, range) {
|
||||
val value = number
|
||||
val first = -(range.last - value) * halvedNumbersColumnHeightPx
|
||||
val last = -(range.first - value) * halvedNumbersColumnHeightPx
|
||||
first..last
|
||||
}
|
||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||
}
|
||||
}
|
||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||
val animatedStateValue = animatedStateValue(animatedOffset.value)
|
||||
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
|
||||
|
||||
val inputFieldShown = if (allowUseManualInput) {
|
||||
remember { mutableStateOf(false) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { deltaY ->
|
||||
if (inputFieldShown ?.value != true) {
|
||||
coroutineScope.launch {
|
||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
if (inputFieldShown ?.value != true) {
|
||||
coroutineScope.launch {
|
||||
val endValue = animatedOffset.fling(
|
||||
initialVelocity = velocity,
|
||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||
adjustTarget = { target ->
|
||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||
val coercedAnchors =
|
||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||
val base =
|
||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||
coercedPoint + base
|
||||
}
|
||||
).endState.value
|
||||
|
||||
onStateChanged(animatedStateValue(endValue))
|
||||
animatedOffset.snapTo(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val spacing = 4.dp
|
||||
|
||||
val upEnabled = range == null || range.first < number
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(number - 1)
|
||||
inputFieldShown ?.value = false
|
||||
},
|
||||
enabled = upEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val baseLabelModifier = Modifier.align(Alignment.Center)
|
||||
ProvideTextStyle(textStyle) {
|
||||
Text(
|
||||
text = (animatedStateValue - 1).toString(),
|
||||
modifier = baseLabelModifier
|
||||
.offset(y = -halvedNumbersColumnHeight)
|
||||
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||
)
|
||||
|
||||
if (inputFieldShown ?.value == true) {
|
||||
val currentValue = remember { mutableStateOf(number.toString()) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
SmallTextField(
|
||||
currentValue.value,
|
||||
{
|
||||
val asDigit = it.toIntOrNull()
|
||||
when {
|
||||
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
|
||||
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
|
||||
else -> { /* do nothing */ }
|
||||
}
|
||||
},
|
||||
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
|
||||
detectTapGestures {
|
||||
if (!checkContains(it)) {
|
||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||
inputFieldShown.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||
inputFieldShown.value = false
|
||||
},
|
||||
singleLine = true,
|
||||
textStyle = textStyle
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = animatedStateValue.toString(),
|
||||
modifier = baseLabelModifier
|
||||
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||
.clickable {
|
||||
if (inputFieldShown ?.value == false) {
|
||||
inputFieldShown.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = (animatedStateValue + 1).toString(),
|
||||
modifier = baseLabelModifier
|
||||
.offset(y = halvedNumbersColumnHeight)
|
||||
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
|
||||
val downEnabled = range == null || range.last > number
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(number + 1)
|
||||
inputFieldShown ?.value = false
|
||||
},
|
||||
enabled = downEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.*
|
||||
|
||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun <T> SetPicker(
|
||||
current: T,
|
||||
dataList: List<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||
dataToString: @Composable (T) -> String = { it.toString() },
|
||||
onStateChanged: (T) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
|
||||
textStyle.lineHeight.toDp()
|
||||
}
|
||||
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
|
||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||
|
||||
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
|
||||
val lastIndex = dataList.size - 1
|
||||
|
||||
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||
|
||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||
val offsetRange = remember(index, lastIndex) {
|
||||
val value = index
|
||||
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
|
||||
val last = value * halvedNumbersColumnHeightPx
|
||||
first..last
|
||||
}
|
||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||
}
|
||||
val indexAnimatedOffset = if (animatedOffset.value > 0) {
|
||||
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||
} else {
|
||||
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||
}
|
||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||
val disabledArrowsColor = arrowsColor.copy(alpha = 0f)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { deltaY ->
|
||||
coroutineScope.launch {
|
||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||
}
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
coroutineScope.launch {
|
||||
val endValue = animatedOffset.fling(
|
||||
initialVelocity = velocity,
|
||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||
adjustTarget = { target ->
|
||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||
val coercedAnchors =
|
||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||
val base =
|
||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||
coercedPoint + base
|
||||
}
|
||||
).endState.value
|
||||
|
||||
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
|
||||
animatedOffset.snapTo(0f)
|
||||
}
|
||||
}
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val spacing = 4.dp
|
||||
|
||||
val upEnabled = index > 0
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(dataList.elementAt(index - 1))
|
||||
},
|
||||
enabled = upEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
Box(
|
||||
modifier = Modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProvideTextStyle(textStyle) {
|
||||
dataList.forEachIndexed { i, t ->
|
||||
val alpha = when {
|
||||
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
val offset = when {
|
||||
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
|
||||
i == indexAnimatedOffset -> coercedAnimatedOffset
|
||||
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
Text(
|
||||
text = dataToString(t),
|
||||
modifier = Modifier
|
||||
.alpha(alpha)
|
||||
.offset(y = with(LocalDensity.current) { offset.toDp() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
|
||||
val downEnabled = index < lastIndex
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(dataList.elementAt(index + 1))
|
||||
},
|
||||
enabled = downEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
||||
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.compose.material3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,66 @@
|
||||
package dev.inmo.micro_utils.android.smalltextfield
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SmallTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
textColor: Color = textStyle.color.takeOrElse {
|
||||
LocalContentColor.current
|
||||
},
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier = modifier,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle.copy(
|
||||
color = textColor
|
||||
),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
cursorBrush = SolidColor(
|
||||
textStyle.color.takeOrElse {
|
||||
LocalContentColor.current
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
+6
-1
@@ -17,13 +17,17 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
@@ -38,3 +42,4 @@ allprojects {
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
apply from: "./github_release.gradle"
|
||||
apply from: "./versions_plugin_setup.gradle"
|
||||
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.colors.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
@@ -0,0 +1,174 @@
|
||||
package dev.inmo.micro_utils.colors.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Wrapper for RGBA colors. Receiving [UInt] in main constructor. Each part in main constructor
|
||||
* configured with `0x00 - 0xff` range. Examples:
|
||||
*
|
||||
* * Red: `0xff0000ffu`
|
||||
* * Red (0.5 capacity): `0xff000088u`
|
||||
*
|
||||
* Anyway it is recommended to use
|
||||
*
|
||||
* @param hexaUInt rgba [UInt] in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class HEXAColor (
|
||||
val hexaUInt: UInt
|
||||
) : Comparable<HEXAColor> {
|
||||
/**
|
||||
* @returns [hexaUInt] as a string with format `#FFEEBBAA` where FF - red, EE - green, BB - blue and AA - alpha
|
||||
*/
|
||||
val hexa: String
|
||||
get() = "#${hexaUInt.toString(16).padStart(8, '0')}"
|
||||
|
||||
/**
|
||||
* @returns [hexaUInt] as a string with format `#FFEEBB` where FF - red, EE - green and BB - blue
|
||||
*/
|
||||
val hex: String
|
||||
get() = hexa.take(7)
|
||||
/**
|
||||
* @returns [hexaUInt] as a string with format `#AAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue
|
||||
*/
|
||||
val ahex: String
|
||||
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
|
||||
val rgba: String
|
||||
get() = "rgba($r,$g,$b,${aOfOne.toString().take(5)})"
|
||||
val rgb: String
|
||||
get() = "rgb($r,$g,$b)"
|
||||
val shortHex: String
|
||||
get() = "#${r.shortPart()}${g.shortPart()}${b.shortPart()}"
|
||||
val shortHexa: String
|
||||
get() = "$shortHex${a.shortPart()}"
|
||||
val rgbUInt: UInt
|
||||
get() = (hexaUInt / 256u)
|
||||
val rgbInt: Int
|
||||
get() = rgbUInt.toInt()
|
||||
val ahexUInt
|
||||
get() = (a * 0x1000000).toUInt() + rgbUInt
|
||||
|
||||
val r: Int
|
||||
get() = ((hexaUInt and 0xff000000u) / 0x1000000u).toInt()
|
||||
val g: Int
|
||||
get() = ((hexaUInt and 0x00ff0000u) / 0x10000u).toInt()
|
||||
val b: Int
|
||||
get() = ((hexaUInt and 0x0000ff00u) / 0x100u).toInt()
|
||||
val a: Int
|
||||
get() = ((hexaUInt and 0x000000ffu)).toInt()
|
||||
val aOfOne: Float
|
||||
get() = a.toFloat() / (0xff)
|
||||
init {
|
||||
require(hexaUInt in 0u ..0xffffffffu)
|
||||
}
|
||||
|
||||
constructor(r: Int, g: Int, b: Int, a: Int) : this(
|
||||
((r * 0x1000000).toLong() + g * 0x10000 + b * 0x100 + a).toUInt()
|
||||
) {
|
||||
require(r in 0 ..0xff)
|
||||
require(g in 0 ..0xff)
|
||||
require(b in 0 ..0xff)
|
||||
require(a in 0 ..0xff)
|
||||
}
|
||||
|
||||
constructor(r: Int, g: Int, b: Int, aOfOne: Float = 1f) : this(
|
||||
r = r, g = g, b = b, a = (aOfOne * 0xff).toInt()
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
return hexa
|
||||
}
|
||||
|
||||
override fun compareTo(other: HEXAColor): Int = (hexaUInt - other.hexaUInt).coerceIn(Int.MIN_VALUE.toUInt(), Int.MAX_VALUE.toLong().toUInt()).toInt()
|
||||
|
||||
fun copy(
|
||||
r: Int = this.r,
|
||||
g: Int = this.g,
|
||||
b: Int = this.b,
|
||||
aOfOne: Float = this.aOfOne
|
||||
) = HEXAColor(r = r, g = g, b = b, aOfOne = aOfOne)
|
||||
fun copy(
|
||||
r: Int = this.r,
|
||||
g: Int = this.g,
|
||||
b: Int = this.b,
|
||||
a: Int
|
||||
) = HEXAColor(r = r, g = g, b = b, a = a)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Parsing color from [color]
|
||||
*
|
||||
* Supported formats samples (on Red color based):
|
||||
*
|
||||
* * `#f00`
|
||||
* * `#f00f`
|
||||
* * `#ff0000`
|
||||
* * `#ff0000ff`
|
||||
* * `rgb(255, 0, 0)`
|
||||
* * `rgba(255, 0, 0, 1)`
|
||||
*/
|
||||
fun parseStringColor(color: String): HEXAColor = when {
|
||||
color.startsWith("#") -> color.removePrefix("#").let { color ->
|
||||
when (color.length) {
|
||||
3 -> color.map { "$it$it" }.joinToString(separator = "", postfix = "ff")
|
||||
4 -> color.take(3).map { "$it$it" }.joinToString(separator = "", postfix = color.takeLast(1).let { "${it}0" })
|
||||
6 -> "${color}ff"
|
||||
8 -> color
|
||||
else -> error("Malfurmed color string: $color. It is expected that color started with # will contains 3, 6 or 8 valuable parts")
|
||||
}
|
||||
}
|
||||
color.startsWith("rgb(") -> color
|
||||
.removePrefix("rgb(")
|
||||
.removeSuffix(")")
|
||||
.replace(Regex("\\s"), "")
|
||||
.split(",")
|
||||
.joinToString("", postfix = "ff") {
|
||||
it.toInt().toString(16).padStart(2, '0')
|
||||
}
|
||||
color.startsWith("rgba(") -> color
|
||||
.removePrefix("rgba(")
|
||||
.removeSuffix(")")
|
||||
.replace(Regex("\\s"), "")
|
||||
.split(",").let {
|
||||
it.take(3).map { it.toInt().toString(16).padStart(2, '0') } + (it.last().toFloat() * 0xff).toInt().toString(16).padStart(2, '0')
|
||||
}
|
||||
.joinToString("")
|
||||
else -> color
|
||||
}.lowercase().toUInt(16).let(::HEXAColor)
|
||||
|
||||
/**
|
||||
* Creates [HEXAColor] from [uint] presume it is in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha
|
||||
*/
|
||||
fun fromHexa(uint: UInt) = HEXAColor(uint)
|
||||
|
||||
/**
|
||||
* Creates [HEXAColor] from [uint] presume it is in format `0xAAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue`
|
||||
*/
|
||||
fun fromAhex(uint: UInt) = HEXAColor(
|
||||
a = ((uint and 0xff000000u) / 0x1000000u).toInt(),
|
||||
r = ((uint and 0x00ff0000u) / 0x10000u).toInt(),
|
||||
g = ((uint and 0x0000ff00u) / 0x100u).toInt(),
|
||||
b = ((uint and 0x000000ffu)).toInt()
|
||||
)
|
||||
|
||||
/**
|
||||
* Parsing color from [color]
|
||||
*
|
||||
* Supported formats samples (on Red color based):
|
||||
*
|
||||
* * `#f00`
|
||||
* * `#ff0000`
|
||||
* * `#ff0000ff`
|
||||
* * `rgb(255, 0, 0)`
|
||||
* * `rgba(255, 0, 0, 1)`
|
||||
*/
|
||||
operator fun invoke(color: String) = parseStringColor(color)
|
||||
|
||||
private fun Int.shortPart(): String {
|
||||
return (floor(toFloat() / 16)).toInt().toString(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package dev.inmo.micro_utils.colors.common
|
||||
|
||||
import kotlin.math.floor
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HexColorTests {
|
||||
val alphaRgbaPrecision = 5
|
||||
class TestColor(
|
||||
val color: HEXAColor,
|
||||
val shortHex: String,
|
||||
val shortHexa: String,
|
||||
val hex: String,
|
||||
val hexa: String,
|
||||
val ahex: String,
|
||||
val ahexUInt: UInt,
|
||||
val rgbUInt: UInt,
|
||||
val rgb: String,
|
||||
val rgba: String,
|
||||
val r: Int,
|
||||
val g: Int,
|
||||
val b: Int,
|
||||
val a: Int,
|
||||
vararg val additionalRGBAVariants: String
|
||||
)
|
||||
val testColors: List<TestColor>
|
||||
get() = listOf(
|
||||
TestColor(
|
||||
color = HEXAColor(hexaUInt = 0xff0000ffu),
|
||||
shortHex = "#f00",
|
||||
shortHexa = "#f00f",
|
||||
hex = "#ff0000",
|
||||
hexa = "#ff0000ff",
|
||||
ahex = "#ffff0000",
|
||||
ahexUInt = 0xffff0000u,
|
||||
rgbUInt = 0xff0000u,
|
||||
rgb = "rgb(255,0,0)",
|
||||
rgba = "rgba(255,0,0,1.0)",
|
||||
r = 0xff,
|
||||
g = 0x00,
|
||||
b = 0x00,
|
||||
a = 0xff,
|
||||
"rgba(255,0,0,1)",
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(hexaUInt = 0x00ff00ffu),
|
||||
shortHex = "#0f0",
|
||||
shortHexa = "#0f0f",
|
||||
hex = "#00ff00",
|
||||
hexa = "#00ff00ff",
|
||||
ahex = "#ff00ff00",
|
||||
ahexUInt = 0xff00ff00u,
|
||||
rgbUInt = 0x00ff00u,
|
||||
rgb = "rgb(0,255,0)",
|
||||
rgba = "rgba(0,255,0,1.0)",
|
||||
r = 0x00,
|
||||
g = 0xff,
|
||||
b = 0x00,
|
||||
a = 0xff,
|
||||
"rgba(0,255,0,1)"
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0x0000ffffu),
|
||||
shortHex = "#00f",
|
||||
shortHexa = "#00ff",
|
||||
hex = "#0000ff",
|
||||
hexa = "#0000ffff",
|
||||
ahex = "#ff0000ff",
|
||||
ahexUInt = 0xff0000ffu,
|
||||
rgbUInt = 0x0000ffu,
|
||||
rgb = "rgb(0,0,255)",
|
||||
rgba = "rgba(0,0,255,1.0)",
|
||||
r = 0x00,
|
||||
g = 0x00,
|
||||
b = 0xff,
|
||||
a = 0xff,
|
||||
"rgba(0,0,255,1)"
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0xff000088u),
|
||||
shortHex = "#f00",
|
||||
shortHexa = "#f008",
|
||||
hex = "#ff0000",
|
||||
hexa = "#ff000088",
|
||||
ahex = "#88ff0000",
|
||||
ahexUInt = 0x88ff0000u,
|
||||
rgbUInt = 0xff0000u,
|
||||
rgb = "rgb(255,0,0)",
|
||||
rgba = "rgba(255,0,0,0.533)",
|
||||
r = 0xff,
|
||||
g = 0x00,
|
||||
b = 0x00,
|
||||
a = 0x88,
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0x00ff0088u),
|
||||
shortHex = "#0f0",
|
||||
shortHexa = "#0f08",
|
||||
hex = "#00ff00",
|
||||
hexa = "#00ff0088",
|
||||
ahex = "#8800ff00",
|
||||
ahexUInt = 0x8800ff00u,
|
||||
rgbUInt = 0x00ff00u,
|
||||
rgb = "rgb(0,255,0)",
|
||||
rgba = "rgba(0,255,0,0.533)",
|
||||
r = 0x00,
|
||||
g = 0xff,
|
||||
b = 0x00,
|
||||
a = 0x88,
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0x0000ff88u),
|
||||
shortHex = "#00f",
|
||||
shortHexa = "#00f8",
|
||||
hex = "#0000ff",
|
||||
hexa = "#0000ff88",
|
||||
ahex = "#880000ff",
|
||||
ahexUInt = 0x880000ffu,
|
||||
rgbUInt = 0x0000ffu,
|
||||
rgb = "rgb(0,0,255)",
|
||||
rgba = "rgba(0,0,255,0.533)",
|
||||
r = 0x00,
|
||||
g = 0x00,
|
||||
b = 0xff,
|
||||
a = 0x88,
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0xff000022u),
|
||||
shortHex = "#f00",
|
||||
shortHexa = "#f002",
|
||||
hex = "#ff0000",
|
||||
hexa = "#ff000022",
|
||||
ahex = "#22ff0000",
|
||||
ahexUInt = 0x22ff0000u,
|
||||
rgbUInt = 0xff0000u,
|
||||
rgb = "rgb(255,0,0)",
|
||||
rgba = "rgba(255,0,0,0.133)",
|
||||
r = 0xff,
|
||||
g = 0x00,
|
||||
b = 0x00,
|
||||
a = 0x22,
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0x00ff0022u),
|
||||
shortHex = "#0f0",
|
||||
shortHexa = "#0f02",
|
||||
hex = "#00ff00",
|
||||
hexa = "#00ff0022",
|
||||
ahex = "#2200ff00",
|
||||
ahexUInt = 0x2200ff00u,
|
||||
rgbUInt = 0x00ff00u,
|
||||
rgb = "rgb(0,255,0)",
|
||||
rgba = "rgba(0,255,0,0.133)",
|
||||
r = 0x00,
|
||||
g = 0xff,
|
||||
b = 0x00,
|
||||
a = 0x22,
|
||||
),
|
||||
TestColor(
|
||||
color = HEXAColor(0x0000ff22u),
|
||||
shortHex = "#00f",
|
||||
shortHexa = "#00f2",
|
||||
hex = "#0000ff",
|
||||
hexa = "#0000ff22",
|
||||
ahex = "#220000ff",
|
||||
ahexUInt = 0x220000ffu,
|
||||
rgbUInt = 0x0000ffu,
|
||||
rgb = "rgb(0,0,255)",
|
||||
rgba = "rgba(0,0,255,0.133)",
|
||||
r = 0x00,
|
||||
g = 0x00,
|
||||
b = 0xff,
|
||||
a = 0x22,
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun baseTest() {
|
||||
testColors.forEach {
|
||||
assertEquals(it.hex, it.color.hex)
|
||||
assertEquals(it.hexa, it.color.hexa)
|
||||
assertEquals(it.ahex, it.color.ahex)
|
||||
assertEquals(it.rgbUInt, it.color.rgbUInt)
|
||||
assertEquals(it.ahexUInt, it.color.ahexUInt)
|
||||
assertEquals(it.shortHex, it.color.shortHex)
|
||||
assertEquals(it.shortHexa, it.color.shortHexa)
|
||||
assertEquals(it.rgb, it.color.rgb)
|
||||
assertTrue(it.rgba == it.color.rgba || it.color.rgba in it.additionalRGBAVariants)
|
||||
assertEquals(it.r, it.color.r)
|
||||
assertEquals(it.g, it.color.g)
|
||||
assertEquals(it.b, it.color.b)
|
||||
assertEquals(it.a, it.color.a)
|
||||
assertEquals(it.color, HEXAColor.fromAhex(it.ahexUInt))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHexParseColor() {
|
||||
testColors.forEach {
|
||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex))
|
||||
assertEquals(it.color, HEXAColor.parseStringColor(it.hexa))
|
||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.rgb))
|
||||
assertTrue(it.color.hexaUInt.toInt() - HEXAColor.parseStringColor(it.rgba).hexaUInt.toInt() in -0x1 .. 0x1, )
|
||||
assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex))
|
||||
assertEquals(it.color.copy(a = floor(it.color.a.toFloat() / 16).toInt() * 0x10), HEXAColor.parseStringColor(it.shortHexa))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package dev.inmo.micro_utils.colors
|
||||
|
||||
import dev.inmo.micro_utils.colors.common.HEXAColor
|
||||
|
||||
val HEXAColor.Companion.aliceblue
|
||||
get() = HEXAColor(0xF0F8FFFFu)
|
||||
val HEXAColor.Companion.antiquewhite
|
||||
get() = HEXAColor(0xFAEBD7FFu)
|
||||
val HEXAColor.Companion.aqua
|
||||
get() = HEXAColor(0x00FFFFFFu)
|
||||
val HEXAColor.Companion.aquamarine
|
||||
get() = HEXAColor(0x7FFFD4FFu)
|
||||
val HEXAColor.Companion.azure
|
||||
get() = HEXAColor(0xF0FFFFFFu)
|
||||
val HEXAColor.Companion.beige
|
||||
get() = HEXAColor(0xF5F5DCFFu)
|
||||
val HEXAColor.Companion.bisque
|
||||
get() = HEXAColor(0xFFE4C4FFu)
|
||||
val HEXAColor.Companion.black
|
||||
get() = HEXAColor(0x000000FFu)
|
||||
val HEXAColor.Companion.blanchedalmond
|
||||
get() = HEXAColor(0xFFEBCDFFu)
|
||||
val HEXAColor.Companion.blue
|
||||
get() = HEXAColor(0x0000FFFFu)
|
||||
val HEXAColor.Companion.blueviolet
|
||||
get() = HEXAColor(0x8A2BE2FFu)
|
||||
val HEXAColor.Companion.brown
|
||||
get() = HEXAColor(0xA52A2AFFu)
|
||||
val HEXAColor.Companion.burlywood
|
||||
get() = HEXAColor(0xDEB887FFu)
|
||||
val HEXAColor.Companion.cadetblue
|
||||
get() = HEXAColor(0x5F9EA0FFu)
|
||||
val HEXAColor.Companion.chartreuse
|
||||
get() = HEXAColor(0x7FFF00FFu)
|
||||
val HEXAColor.Companion.chocolate
|
||||
get() = HEXAColor(0xD2691EFFu)
|
||||
val HEXAColor.Companion.coral
|
||||
get() = HEXAColor(0xFF7F50FFu)
|
||||
val HEXAColor.Companion.cornflowerblue
|
||||
get() = HEXAColor(0x6495EDFFu)
|
||||
val HEXAColor.Companion.cornsilk
|
||||
get() = HEXAColor(0xFFF8DCFFu)
|
||||
val HEXAColor.Companion.crimson
|
||||
get() = HEXAColor(0xDC143CFFu)
|
||||
val HEXAColor.Companion.cyan
|
||||
get() = HEXAColor(0x00FFFFFFu)
|
||||
val HEXAColor.Companion.darkblue
|
||||
get() = HEXAColor(0x00008BFFu)
|
||||
val HEXAColor.Companion.darkcyan
|
||||
get() = HEXAColor(0x008B8BFFu)
|
||||
val HEXAColor.Companion.darkgoldenrod
|
||||
get() = HEXAColor(0xB8860BFFu)
|
||||
val HEXAColor.Companion.darkgray
|
||||
get() = HEXAColor(0xA9A9A9FFu)
|
||||
val HEXAColor.Companion.darkgreen
|
||||
get() = HEXAColor(0x006400FFu)
|
||||
val HEXAColor.Companion.darkgrey
|
||||
get() = HEXAColor(0xA9A9A9FFu)
|
||||
val HEXAColor.Companion.darkkhaki
|
||||
get() = HEXAColor(0xBDB76BFFu)
|
||||
val HEXAColor.Companion.darkmagenta
|
||||
get() = HEXAColor(0x8B008BFFu)
|
||||
val HEXAColor.Companion.darkolivegreen
|
||||
get() = HEXAColor(0x556B2FFFu)
|
||||
val HEXAColor.Companion.darkorange
|
||||
get() = HEXAColor(0xFF8C00FFu)
|
||||
val HEXAColor.Companion.darkorchid
|
||||
get() = HEXAColor(0x9932CCFFu)
|
||||
val HEXAColor.Companion.darkred
|
||||
get() = HEXAColor(0x8B0000FFu)
|
||||
val HEXAColor.Companion.darksalmon
|
||||
get() = HEXAColor(0xE9967AFFu)
|
||||
val HEXAColor.Companion.darkseagreen
|
||||
get() = HEXAColor(0x8FBC8FFFu)
|
||||
val HEXAColor.Companion.darkslateblue
|
||||
get() = HEXAColor(0x483D8BFFu)
|
||||
val HEXAColor.Companion.darkslategray
|
||||
get() = HEXAColor(0x2F4F4FFFu)
|
||||
val HEXAColor.Companion.darkslategrey
|
||||
get() = HEXAColor(0x2F4F4FFFu)
|
||||
val HEXAColor.Companion.darkturquoise
|
||||
get() = HEXAColor(0x00CED1FFu)
|
||||
val HEXAColor.Companion.darkviolet
|
||||
get() = HEXAColor(0x9400D3FFu)
|
||||
val HEXAColor.Companion.deeppink
|
||||
get() = HEXAColor(0xFF1493FFu)
|
||||
val HEXAColor.Companion.deepskyblue
|
||||
get() = HEXAColor(0x00BFFFFFu)
|
||||
val HEXAColor.Companion.dimgray
|
||||
get() = HEXAColor(0x696969FFu)
|
||||
val HEXAColor.Companion.dimgrey
|
||||
get() = HEXAColor(0x696969FFu)
|
||||
val HEXAColor.Companion.dodgerblue
|
||||
get() = HEXAColor(0x1E90FFFFu)
|
||||
val HEXAColor.Companion.firebrick
|
||||
get() = HEXAColor(0xB22222FFu)
|
||||
val HEXAColor.Companion.floralwhite
|
||||
get() = HEXAColor(0xFFFAF0FFu)
|
||||
val HEXAColor.Companion.forestgreen
|
||||
get() = HEXAColor(0x228B22FFu)
|
||||
val HEXAColor.Companion.fuchsia
|
||||
get() = HEXAColor(0xFF00FFFFu)
|
||||
val HEXAColor.Companion.gainsboro
|
||||
get() = HEXAColor(0xDCDCDCFFu)
|
||||
val HEXAColor.Companion.ghostwhite
|
||||
get() = HEXAColor(0xF8F8FFFFu)
|
||||
val HEXAColor.Companion.gold
|
||||
get() = HEXAColor(0xFFD700FFu)
|
||||
val HEXAColor.Companion.goldenrod
|
||||
get() = HEXAColor(0xDAA520FFu)
|
||||
val HEXAColor.Companion.gray
|
||||
get() = HEXAColor(0x808080FFu)
|
||||
val HEXAColor.Companion.green
|
||||
get() = HEXAColor(0x008000FFu)
|
||||
val HEXAColor.Companion.greenyellow
|
||||
get() = HEXAColor(0xADFF2FFFu)
|
||||
val HEXAColor.Companion.grey
|
||||
get() = HEXAColor(0x808080FFu)
|
||||
val HEXAColor.Companion.honeydew
|
||||
get() = HEXAColor(0xF0FFF0FFu)
|
||||
val HEXAColor.Companion.hotpink
|
||||
get() = HEXAColor(0xFF69B4FFu)
|
||||
val HEXAColor.Companion.indianred
|
||||
get() = HEXAColor(0xCD5C5CFFu)
|
||||
val HEXAColor.Companion.indigo
|
||||
get() = HEXAColor(0x4B0082FFu)
|
||||
val HEXAColor.Companion.ivory
|
||||
get() = HEXAColor(0xFFFFF0FFu)
|
||||
val HEXAColor.Companion.khaki
|
||||
get() = HEXAColor(0xF0E68CFFu)
|
||||
val HEXAColor.Companion.lavender
|
||||
get() = HEXAColor(0xE6E6FAFFu)
|
||||
val HEXAColor.Companion.lavenderblush
|
||||
get() = HEXAColor(0xFFF0F5FFu)
|
||||
val HEXAColor.Companion.lawngreen
|
||||
get() = HEXAColor(0x7CFC00FFu)
|
||||
val HEXAColor.Companion.lemonchiffon
|
||||
get() = HEXAColor(0xFFFACDFFu)
|
||||
val HEXAColor.Companion.lightblue
|
||||
get() = HEXAColor(0xADD8E6FFu)
|
||||
val HEXAColor.Companion.lightcoral
|
||||
get() = HEXAColor(0xF08080FFu)
|
||||
val HEXAColor.Companion.lightcyan
|
||||
get() = HEXAColor(0xE0FFFFFFu)
|
||||
val HEXAColor.Companion.lightgoldenrodyellow
|
||||
get() = HEXAColor(0xFAFAD2FFu)
|
||||
val HEXAColor.Companion.lightgray
|
||||
get() = HEXAColor(0xD3D3D3FFu)
|
||||
val HEXAColor.Companion.lightgreen
|
||||
get() = HEXAColor(0x90EE90FFu)
|
||||
val HEXAColor.Companion.lightgrey
|
||||
get() = HEXAColor(0xD3D3D3FFu)
|
||||
val HEXAColor.Companion.lightpink
|
||||
get() = HEXAColor(0xFFB6C1FFu)
|
||||
val HEXAColor.Companion.lightsalmon
|
||||
get() = HEXAColor(0xFFA07AFFu)
|
||||
val HEXAColor.Companion.lightseagreen
|
||||
get() = HEXAColor(0x20B2AAFFu)
|
||||
val HEXAColor.Companion.lightskyblue
|
||||
get() = HEXAColor(0x87CEFAFFu)
|
||||
val HEXAColor.Companion.lightslategray
|
||||
get() = HEXAColor(0x778899FFu)
|
||||
val HEXAColor.Companion.lightslategrey
|
||||
get() = HEXAColor(0x778899FFu)
|
||||
val HEXAColor.Companion.lightsteelblue
|
||||
get() = HEXAColor(0xB0C4DEFFu)
|
||||
val HEXAColor.Companion.lightyellow
|
||||
get() = HEXAColor(0xFFFFE0FFu)
|
||||
val HEXAColor.Companion.lime
|
||||
get() = HEXAColor(0x00FF00FFu)
|
||||
val HEXAColor.Companion.limegreen
|
||||
get() = HEXAColor(0x32CD32FFu)
|
||||
val HEXAColor.Companion.linen
|
||||
get() = HEXAColor(0xFAF0E6FFu)
|
||||
val HEXAColor.Companion.magenta
|
||||
get() = HEXAColor(0xFF00FFFFu)
|
||||
val HEXAColor.Companion.maroon
|
||||
get() = HEXAColor(0x800000FFu)
|
||||
val HEXAColor.Companion.mediumaquamarine
|
||||
get() = HEXAColor(0x66CDAAFFu)
|
||||
val HEXAColor.Companion.mediumblue
|
||||
get() = HEXAColor(0x0000CDFFu)
|
||||
val HEXAColor.Companion.mediumorchid
|
||||
get() = HEXAColor(0xBA55D3FFu)
|
||||
val HEXAColor.Companion.mediumpurple
|
||||
get() = HEXAColor(0x9370DBFFu)
|
||||
val HEXAColor.Companion.mediumseagreen
|
||||
get() = HEXAColor(0x3CB371FFu)
|
||||
val HEXAColor.Companion.mediumslateblue
|
||||
get() = HEXAColor(0x7B68EEFFu)
|
||||
val HEXAColor.Companion.mediumspringgreen
|
||||
get() = HEXAColor(0x00FA9AFFu)
|
||||
val HEXAColor.Companion.mediumturquoise
|
||||
get() = HEXAColor(0x48D1CCFFu)
|
||||
val HEXAColor.Companion.mediumvioletred
|
||||
get() = HEXAColor(0xC71585FFu)
|
||||
val HEXAColor.Companion.midnightblue
|
||||
get() = HEXAColor(0x191970FFu)
|
||||
val HEXAColor.Companion.mintcream
|
||||
get() = HEXAColor(0xF5FFFAFFu)
|
||||
val HEXAColor.Companion.mistyrose
|
||||
get() = HEXAColor(0xFFE4E1FFu)
|
||||
val HEXAColor.Companion.moccasin
|
||||
get() = HEXAColor(0xFFE4B5FFu)
|
||||
val HEXAColor.Companion.navajowhite
|
||||
get() = HEXAColor(0xFFDEADFFu)
|
||||
val HEXAColor.Companion.navy
|
||||
get() = HEXAColor(0x000080FFu)
|
||||
val HEXAColor.Companion.oldlace
|
||||
get() = HEXAColor(0xFDF5E6FFu)
|
||||
val HEXAColor.Companion.olive
|
||||
get() = HEXAColor(0x808000FFu)
|
||||
val HEXAColor.Companion.olivedrab
|
||||
get() = HEXAColor(0x6B8E23FFu)
|
||||
val HEXAColor.Companion.orange
|
||||
get() = HEXAColor(0xFFA500FFu)
|
||||
val HEXAColor.Companion.orangered
|
||||
get() = HEXAColor(0xFF4500FFu)
|
||||
val HEXAColor.Companion.orchid
|
||||
get() = HEXAColor(0xDA70D6FFu)
|
||||
val HEXAColor.Companion.palegoldenrod
|
||||
get() = HEXAColor(0xEEE8AAFFu)
|
||||
val HEXAColor.Companion.palegreen
|
||||
get() = HEXAColor(0x98FB98FFu)
|
||||
val HEXAColor.Companion.paleturquoise
|
||||
get() = HEXAColor(0xAFEEEEFFu)
|
||||
val HEXAColor.Companion.palevioletred
|
||||
get() = HEXAColor(0xDB7093FFu)
|
||||
val HEXAColor.Companion.papayawhip
|
||||
get() = HEXAColor(0xFFEFD5FFu)
|
||||
val HEXAColor.Companion.peachpuff
|
||||
get() = HEXAColor(0xFFDAB9FFu)
|
||||
val HEXAColor.Companion.peru
|
||||
get() = HEXAColor(0xCD853FFFu)
|
||||
val HEXAColor.Companion.pink
|
||||
get() = HEXAColor(0xFFC0CBFFu)
|
||||
val HEXAColor.Companion.plum
|
||||
get() = HEXAColor(0xDDA0DDFFu)
|
||||
val HEXAColor.Companion.powderblue
|
||||
get() = HEXAColor(0xB0E0E6FFu)
|
||||
val HEXAColor.Companion.purple
|
||||
get() = HEXAColor(0x800080FFu)
|
||||
val HEXAColor.Companion.red
|
||||
get() = HEXAColor(0xFF0000FFu)
|
||||
val HEXAColor.Companion.rosybrown
|
||||
get() = HEXAColor(0xBC8F8FFFu)
|
||||
val HEXAColor.Companion.royalblue
|
||||
get() = HEXAColor(0x4169E1FFu)
|
||||
val HEXAColor.Companion.saddlebrown
|
||||
get() = HEXAColor(0x8B4513FFu)
|
||||
val HEXAColor.Companion.salmon
|
||||
get() = HEXAColor(0xFA8072FFu)
|
||||
val HEXAColor.Companion.sandybrown
|
||||
get() = HEXAColor(0xF4A460FFu)
|
||||
val HEXAColor.Companion.seagreen
|
||||
get() = HEXAColor(0x2E8B57FFu)
|
||||
val HEXAColor.Companion.seashell
|
||||
get() = HEXAColor(0xFFF5EEFFu)
|
||||
val HEXAColor.Companion.sienna
|
||||
get() = HEXAColor(0xA0522DFFu)
|
||||
val HEXAColor.Companion.silver
|
||||
get() = HEXAColor(0xC0C0C0FFu)
|
||||
val HEXAColor.Companion.skyblue
|
||||
get() = HEXAColor(0x87CEEBFFu)
|
||||
val HEXAColor.Companion.slateblue
|
||||
get() = HEXAColor(0x6A5ACDFFu)
|
||||
val HEXAColor.Companion.slategray
|
||||
get() = HEXAColor(0x708090FFu)
|
||||
val HEXAColor.Companion.slategrey
|
||||
get() = HEXAColor(0x708090FFu)
|
||||
val HEXAColor.Companion.snow
|
||||
get() = HEXAColor(0xFFFAFAFFu)
|
||||
val HEXAColor.Companion.springgreen
|
||||
get() = HEXAColor(0x00FF7FFFu)
|
||||
val HEXAColor.Companion.steelblue
|
||||
get() = HEXAColor(0x4682B4FFu)
|
||||
val HEXAColor.Companion.tan
|
||||
get() = HEXAColor(0xD2B48CFFu)
|
||||
val HEXAColor.Companion.teal
|
||||
get() = HEXAColor(0x008080FFu)
|
||||
val HEXAColor.Companion.thistle
|
||||
get() = HEXAColor(0xD8BFD8FFu)
|
||||
val HEXAColor.Companion.tomato
|
||||
get() = HEXAColor(0xFF6347FFu)
|
||||
val HEXAColor.Companion.turquoise
|
||||
get() = HEXAColor(0x40E0D0FFu)
|
||||
val HEXAColor.Companion.violet
|
||||
get() = HEXAColor(0xEE82EEFFu)
|
||||
val HEXAColor.Companion.wheat
|
||||
get() = HEXAColor(0xF5DEB3FFu)
|
||||
val HEXAColor.Companion.white
|
||||
get() = HEXAColor(0xFFFFFFFFu)
|
||||
val HEXAColor.Companion.whitesmoke
|
||||
get() = HEXAColor(0xF5F5F5FFu)
|
||||
val HEXAColor.Companion.yellow
|
||||
get() = HEXAColor(0xFFFF00FFu)
|
||||
val HEXAColor.Companion.yellowgreen
|
||||
get() = HEXAColor(0x9ACD32FFu)
|
||||
+7
-9
@@ -4,10 +4,15 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.klock
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
@@ -15,18 +20,11 @@ kotlin {
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
api libs.android.fragment
|
||||
}
|
||||
dependsOn jvmMain
|
||||
}
|
||||
|
||||
linuxX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
mingwX64Main {
|
||||
nativeMain {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ plugins {
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import korlibs.time.DateTime
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* Serializes [DateTime] as its raw [DateTime.unixMillis] and deserializes in the same way
|
||||
*/
|
||||
object DateTimeSerializer : KSerializer<DateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = Double.serializer().descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): DateTime {
|
||||
return DateTime(decoder.decodeDouble())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: DateTime) {
|
||||
encoder.encodeDouble(value.unixMillis)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ private inline fun <T> getObject(
|
||||
* @see calculateDiff
|
||||
*/
|
||||
@Serializable
|
||||
data class Diff<T> internal constructor(
|
||||
data class Diff<T> @Warning(warning) constructor(
|
||||
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
||||
/**
|
||||
* Old-New values pairs
|
||||
@@ -36,6 +36,10 @@ data class Diff<T> internal constructor(
|
||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||
) {
|
||||
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||
|
||||
companion object {
|
||||
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||
@@ -200,20 +204,18 @@ inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||
) = calculateDiff(other, strictComparison = true)
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
* Applies [diff] to [this] [MutableList]
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||
for (i in it.removed.indices.sortedDescending()) {
|
||||
removeAt(it.removed[i].index)
|
||||
diff: Diff<T>
|
||||
) {
|
||||
for (i in diff.removed.indices.sortedDescending()) {
|
||||
removeAt(diff.removed[i].index)
|
||||
}
|
||||
it.added.forEach { (i, t) ->
|
||||
diff.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
it.replaced.forEach { (_, new) ->
|
||||
diff.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
@@ -222,17 +224,30 @@ fun <T> MutableList<T>.applyDiff(
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||
for (i in it.removed.indices.sortedDescending()) {
|
||||
removeAt(it.removed[i].index)
|
||||
}
|
||||
it.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
it.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
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
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.common"/>
|
||||
@@ -1,36 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
actual typealias MPPFile = Path
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(toString())
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||
it.size()
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = {
|
||||
FileSystem.SYSTEM.read(this) {
|
||||
readByteArray()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
bytesAllocatorSync()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,12 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Float.fixed(signs: Int): Float {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||
@@ -16,6 +14,7 @@ actual fun Float.fixed(signs: Int): Float {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
@@ -22,7 +22,6 @@ kotlin {
|
||||
dependencies {
|
||||
api libs.kt.coroutines.android
|
||||
}
|
||||
dependsOn(jvmMain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ plugins {
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.test.*
|
||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import kotlin.test.Test
|
||||
|
||||
class FlowStateTests {
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
@TestOnly
|
||||
fun simpleTest() = runComposeUiTest {
|
||||
val flowState = SpecialMutableStateFlow(0)
|
||||
setContent {
|
||||
Button({ flowState.value++ }) { Text("Click") }
|
||||
Text(flowState.collectAsState().value.toString())
|
||||
}
|
||||
|
||||
onNodeWithText(0.toString()).assertExists()
|
||||
onNodeWithText("Click").performClick()
|
||||
onNodeWithText(1.toString()).assertExists()
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
File renamed without changes.
File renamed without changes.
@@ -3,6 +3,12 @@ package dev.inmo.micro_utils.coroutines
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.*
|
||||
|
||||
/**
|
||||
* Trying to [Deferred.await] on all [this] [Deferred]s. The first [Deferred] completed its work will interrupt all
|
||||
* others awaits and, if [cancelOnResult] passed as true (**by default**), will also cancel all the others [Deferred]s
|
||||
*
|
||||
* @param scope Will be used to create [CoroutineScope.LinkedSupervisorScope] and launch joining of all [Job]s in it
|
||||
*/
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
@@ -24,10 +30,45 @@ suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trying to [Deferred.await] on all [this] [Deferred]s. The first [Deferred] completed its work will interrupt all
|
||||
* others awaits and, if [cancelOnResult] passed as true (**by default**), will also cancel all the others [Deferred]s
|
||||
*
|
||||
* @param scope Will be used to create [CoroutineScope.LinkedSupervisorScope] and launch joining of all [Job]s in it
|
||||
*/
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): T = awaitFirstWithDeferred(scope, cancelOnResult).second
|
||||
|
||||
/**
|
||||
* Trying to [Deferred.await] on all [this] [Deferred]s. The first [Deferred] completed its work will interrupt all
|
||||
* others awaits and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [Deferred]s
|
||||
*
|
||||
* Creates [CoroutineScope] using [coroutineContext] for internal purposes
|
||||
*/
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||
cancelOthers: Boolean = true
|
||||
): T = awaitFirst(CoroutineScope(coroutineContext), cancelOthers)
|
||||
|
||||
/**
|
||||
* Trying to [Deferred.await] on all [deferreds]. The first [Deferred] completed its work will interrupt all
|
||||
* others awaits and, if [cancelOnResult] passed as true (**by default**), will also cancel all the others [deferreds]
|
||||
*
|
||||
* @param scope Will be used to create [CoroutineScope.LinkedSupervisorScope] and launch joining of all [Job]s in it
|
||||
*/
|
||||
suspend fun <T> awaitFirst(
|
||||
vararg deferreds: Deferred<T>,
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): T = deferreds.toList().awaitFirstWithDeferred(scope, cancelOnResult).second
|
||||
/**
|
||||
* Trying to [Deferred.await] on all [deferreds]. The first [Deferred] completed its work will interrupt all
|
||||
* others awaits and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [deferreds]
|
||||
*
|
||||
* Creates [CoroutineScope] using [coroutineContext] for internal purposes
|
||||
*/
|
||||
suspend fun <T> awaitFirst(
|
||||
vararg deferreds: Deferred<T>,
|
||||
cancelOthers: Boolean = true
|
||||
): T = awaitFirst(*deferreds, scope = CoroutineScope(coroutineContext), cancelOnResult = cancelOthers)
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||
*/
|
||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||
*/
|
||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||
try {
|
||||
defaultSafelyExceptionHandler(it)
|
||||
} catch (e: Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandler
|
||||
*/
|
||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||
|
||||
/**
|
||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
class ContextSafelyExceptionHandler(
|
||||
val handler: ExceptionHandler<Unit>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*>
|
||||
get() = ContextSafelyExceptionHandlerKey
|
||||
}
|
||||
@@ -16,8 +16,8 @@ class DoWithFirstBuilder<T>(
|
||||
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||
}
|
||||
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
fun add(block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
fun include(block: suspend CoroutineScope.() -> T) = plus(block)
|
||||
|
||||
fun build() = deferreds.toList()
|
||||
}
|
||||
|
||||
+11
-11
@@ -85,32 +85,32 @@ fun <T, M> Flow<T>.subscribeAsync(
|
||||
return job
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||
fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
markerFactory: suspend (T) -> M,
|
||||
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend (T) -> Unit
|
||||
markerFactory: suspend (T) -> M,
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline block: suspend (T) -> Unit
|
||||
markerFactory: suspend (T) -> M,
|
||||
block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions({ /* do nothing */}) {
|
||||
block(it)
|
||||
|
||||
@@ -4,46 +4,58 @@ import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||
* Launching [block] in [runCatching]. In case of failure, it will:
|
||||
*
|
||||
* * Try to get [ContextSafelyExceptionHandler] from current [coroutineContext] and call its
|
||||
* [ContextSafelyExceptionHandler.handler] invoke. **Thrown exception from its handler
|
||||
* will pass out of [runCatchingSafely]**
|
||||
* * Execute [onException] inside of new [runCatching] and return its result. Throws exception
|
||||
* will be caught by [runCatching] and wrapped in [Result]
|
||||
*
|
||||
* @return [Result] with result of [block] if no exceptions or [Result] from [onException] execution
|
||||
*/
|
||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||
*/
|
||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||
try {
|
||||
defaultSafelyExceptionHandler(it)
|
||||
} catch (e: Throwable) {
|
||||
// do nothing
|
||||
suspend inline fun <T> runCatchingSafely(
|
||||
onException: ExceptionHandler<T>,
|
||||
block: suspend () -> T
|
||||
): Result<T> {
|
||||
return runCatching {
|
||||
block()
|
||||
}.onFailure {
|
||||
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(it)
|
||||
return runCatching {
|
||||
onException(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandler
|
||||
*/
|
||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||
suspend inline fun <T, R> R.runCatchingSafely(
|
||||
onException: ExceptionHandler<T>,
|
||||
block: suspend R.() -> T
|
||||
): Result<T> = runCatchingSafely<T>(onException) {
|
||||
block()
|
||||
}
|
||||
|
||||
/**
|
||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
* Launching [runCatchingSafely] with [defaultSafelyExceptionHandler] as `onException` parameter
|
||||
*/
|
||||
class ContextSafelyExceptionHandler(
|
||||
val handler: ExceptionHandler<Unit>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*>
|
||||
get() = ContextSafelyExceptionHandlerKey
|
||||
suspend inline fun <T> runCatchingSafely(
|
||||
block: suspend () -> T
|
||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
suspend inline fun <T, R> R.runCatchingSafely(
|
||||
block: suspend R.() -> T
|
||||
): Result<T> = runCatchingSafely<T> {
|
||||
block()
|
||||
}
|
||||
|
||||
//suspend inline fun <T, R> T.runCatchingSafely(
|
||||
// onException: ExceptionHandler<R>,
|
||||
// block: suspend T.() -> R
|
||||
//): Result<R> = runCatchingSafely(onException) {
|
||||
// block()
|
||||
//}
|
||||
|
||||
/**
|
||||
* @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if
|
||||
* exists
|
||||
@@ -51,7 +63,7 @@ class ContextSafelyExceptionHandler(
|
||||
* @see ContextSafelyExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||
suspend fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||
|
||||
/**
|
||||
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
|
||||
@@ -64,7 +76,7 @@ suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSaf
|
||||
suspend fun <T> safelyWithContextExceptionHandler(
|
||||
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
block: suspend () -> T
|
||||
): T {
|
||||
val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler ->
|
||||
ContextSafelyExceptionHandler {
|
||||
@@ -78,57 +90,35 @@ suspend fun <T> safelyWithContextExceptionHandler(
|
||||
}
|
||||
|
||||
/**
|
||||
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
||||
* Calls [runCatchingSafely] and getting the result via [Result.getOrThrow]
|
||||
*
|
||||
* Priorities of [ExceptionHandler]s:
|
||||
*
|
||||
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
|
||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||
* * [defaultSafelyExceptionHandler]
|
||||
*
|
||||
* Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will
|
||||
* be called [onException]
|
||||
*
|
||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||
* exception will be available for catching
|
||||
*
|
||||
* @see defaultSafelyExceptionHandler
|
||||
* @see safelyWithoutExceptions
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see runCatchingSafely
|
||||
*/
|
||||
suspend inline fun <T> safely(
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
return try {
|
||||
supervisorScope(block)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
onException: ExceptionHandler<T>,
|
||||
block: suspend () -> T
|
||||
): T = runCatchingSafely(onException, block).getOrThrow()
|
||||
|
||||
suspend inline fun <T> runCatchingSafely(
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T> = runCatching {
|
||||
safely(onException, block)
|
||||
}
|
||||
/**
|
||||
* Calls [safely] with passing of [defaultSafelyExceptionHandler] as `onException`
|
||||
*
|
||||
* @see runCatchingSafely
|
||||
*/
|
||||
suspend inline fun <T> safely(
|
||||
block: suspend () -> T
|
||||
): T = safely(defaultSafelyExceptionHandler, block)
|
||||
suspend inline fun <T, R> R.safely(
|
||||
block: suspend R.() -> T
|
||||
): T = safely<T> { block() }
|
||||
|
||||
suspend inline fun <T, R> T.runCatchingSafely(
|
||||
noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatching {
|
||||
safely(onException) { block() }
|
||||
}
|
||||
|
||||
suspend inline fun <T> safelyWithResult(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
@Deprecated("Renamed", ReplaceWith("runCatchingSafely(block)", "dev.inmo.micro_utils.coroutines.runCatchingSafely"))
|
||||
suspend fun <T> safelyWithResult(
|
||||
block: suspend () -> T
|
||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
suspend inline fun <T, R> T.safelyWithResult(
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
@Deprecated("Renamed", ReplaceWith("this.runCatchingSafely(block)", "dev.inmo.micro_utils.coroutines.runCatchingSafely"))
|
||||
suspend fun <T, R> R.safelyWithResult(
|
||||
block: suspend R.() -> T
|
||||
): Result<T> = safelyWithResult<T> { block() }
|
||||
|
||||
/**
|
||||
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||
@@ -147,21 +137,23 @@ val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||
* result from exception (instead of throwing it, by default always returns null)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safely(onException, block)
|
||||
suspend fun <T> safelyWithoutExceptions(
|
||||
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend () -> T
|
||||
): T? = runCatchingSafely(onException, block).getOrNull()
|
||||
|
||||
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T?> = runCatching {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
suspend fun <T> runCatchingSafelyWithoutExceptions(
|
||||
onException: ExceptionHandler<T?> = defaultSafelyExceptionHandler,
|
||||
block: suspend () -> T
|
||||
): Result<T?> = runCatchingSafely(onException, block).let {
|
||||
if (it.isFailure) return Result.success<T?>(null)
|
||||
|
||||
it
|
||||
}
|
||||
|
||||
inline fun CoroutineScope(
|
||||
fun CoroutineScopeWithDefaultFallback(
|
||||
context: CoroutineContext,
|
||||
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||
defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||
) = CoroutineScope(
|
||||
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.*
|
||||
|
||||
/**
|
||||
* Trying to [Job.join] on all [this] [Job]s. The first [Job] completed its work will interrupt all others joins
|
||||
* and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [Job]s
|
||||
*
|
||||
* @param scope Will be used to create [CoroutineScope.LinkedSupervisorScope] and launch joining of all [Job]s in it
|
||||
*/
|
||||
suspend fun Iterable<Job>.joinFirst(
|
||||
scope: CoroutineScope,
|
||||
cancelOthers: Boolean = true
|
||||
): Job {
|
||||
val resultDeferred = CompletableDeferred<Job>()
|
||||
val scope = scope.LinkedSupervisorScope()
|
||||
forEach {
|
||||
scope.launch {
|
||||
it.join()
|
||||
resultDeferred.complete(it)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
return resultDeferred.await().also {
|
||||
if (cancelOthers) {
|
||||
forEach {
|
||||
runCatchingSafely { it.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Trying to [Job.join] on all [this] [Job]s. The first [Job] completed its work will interrupt all others joins
|
||||
* and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [Job]s
|
||||
*
|
||||
* Creates [CoroutineScope] using [coroutineContext] for internal purposes
|
||||
*/
|
||||
suspend fun Iterable<Job>.joinFirst(
|
||||
cancelOthers: Boolean = true
|
||||
): Job = joinFirst(CoroutineScope(coroutineContext), cancelOthers)
|
||||
|
||||
/**
|
||||
* Trying to [Job.join] on all [jobs]. The first [Job] completed its work will interrupt all others joins
|
||||
* and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [Job]s
|
||||
*
|
||||
* @param scope Will be used to create [CoroutineScope.LinkedSupervisorScope] and launch joining of all [Job]s in it
|
||||
*/
|
||||
suspend fun joinFirst(
|
||||
vararg jobs: Job,
|
||||
scope: CoroutineScope,
|
||||
cancelOthers: Boolean = true
|
||||
): Job = jobs.toList().joinFirst(scope, cancelOthers)
|
||||
|
||||
/**
|
||||
* Trying to [Job.join] on all [jobs]. The first [Job] completed its work will interrupt all others joins
|
||||
* and, if [cancelOthers] passed as true (**by default**), will also cancel all the others [Job]s
|
||||
*
|
||||
* Creates [CoroutineScope] using [coroutineContext] for internal purposes
|
||||
*/
|
||||
suspend fun joinFirst(
|
||||
vararg jobs: Job,
|
||||
cancelOthers: Boolean = true
|
||||
): Job = joinFirst(*jobs, scope = CoroutineScope(coroutineContext), cancelOthers = cancelOthers)
|
||||
@@ -4,38 +4,46 @@ import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
inline fun CoroutineScope.launchSafely(
|
||||
fun CoroutineScope.launchSafely(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> Unit
|
||||
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) = launch(context, start) {
|
||||
safely(onException, block)
|
||||
runCatchingSafely(onException) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||
fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> Unit
|
||||
onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) = launch(context, start) {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
runCatchingSafelyWithoutExceptions(onException) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.asyncSafely(
|
||||
fun <T> CoroutineScope.asyncSafely(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = async(context, start) {
|
||||
safely(onException, block)
|
||||
runCatchingSafely(onException) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||
fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = async(context, start) {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
runCatchingSafelyWithoutExceptions(onException) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
@@ -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 = SpecialMutableStateFlow<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,105 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Composite mutex which works with next rules:
|
||||
*
|
||||
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
|
||||
* * [releaseRead] will just free up one permit in [readSemaphore]
|
||||
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
|
||||
* * [unlockWrite] will just unlock [writeMutex]
|
||||
*/
|
||||
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
||||
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
|
||||
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
|
||||
|
||||
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
||||
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
|
||||
|
||||
/**
|
||||
* Do lock in [readSemaphore] inside of [writeMutex] locking
|
||||
*/
|
||||
suspend fun acquireRead() {
|
||||
_writeMutex.waitUnlock()
|
||||
_readSemaphore.acquire()
|
||||
}
|
||||
|
||||
/**
|
||||
* Release one read permit in [readSemaphore]
|
||||
*/
|
||||
suspend fun releaseRead(): Boolean {
|
||||
return _readSemaphore.release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
|
||||
*/
|
||||
suspend fun lockWrite() {
|
||||
_writeMutex.lock()
|
||||
_readSemaphore.acquire(readPermits)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock [writeMutex]
|
||||
*/
|
||||
suspend fun unlockWrite(): Boolean {
|
||||
return _writeMutex.unlock().also {
|
||||
if (it) {
|
||||
_readSemaphore.release(readPermits)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
acquireRead()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
releaseRead()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
|
||||
* the fact that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
|
||||
|
||||
/**
|
||||
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
lockWrite()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
unlockWrite()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||
* that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()
|
||||
@@ -0,0 +1,168 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
|
||||
* [SmartSemaphore] state.
|
||||
*
|
||||
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
|
||||
*
|
||||
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
|
||||
* [Mutable] creator
|
||||
*/
|
||||
sealed interface SmartSemaphore {
|
||||
val permitsStateFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* * True - locked
|
||||
* * False - unlocked
|
||||
*/
|
||||
val freePermits: Int
|
||||
get() = permitsStateFlow.value
|
||||
|
||||
/**
|
||||
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
|
||||
*/
|
||||
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
|
||||
|
||||
/**
|
||||
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||
* [Immutable] variant of [this] instance with [immutable] factory
|
||||
*
|
||||
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
|
||||
*/
|
||||
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
|
||||
private val _freePermitsStateFlow = SpecialMutableStateFlow<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 }
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.internal.SynchronizedObject
|
||||
import kotlinx.coroutines.internal.synchronized
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
|
||||
* each active subscriber
|
||||
*/
|
||||
open class SpecialMutableStateFlow<T>(
|
||||
initialValue: T
|
||||
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private val syncObject = SynchronizedObject()
|
||||
protected val sharingFlow: MutableSharedFlow<T> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
override var value: T = initialValue
|
||||
set(value) {
|
||||
synchronized(syncObject) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
sharingFlow.tryEmit(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val replayCache: List<T>
|
||||
get() = sharingFlow.replayCache
|
||||
override val subscriptionCount: StateFlow<Int>
|
||||
get() = sharingFlow.subscriptionCount
|
||||
|
||||
init {
|
||||
sharingFlow.tryEmit(initialValue)
|
||||
}
|
||||
|
||||
override fun compareAndSet(expect: T, update: T): Boolean {
|
||||
if (expect == value) {
|
||||
value = update
|
||||
}
|
||||
return expect == value
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun resetReplayCache() = sharingFlow.resetReplayCache()
|
||||
|
||||
override fun tryEmit(value: T): Boolean {
|
||||
return compareAndSet(this.value, value)
|
||||
}
|
||||
|
||||
override suspend fun emit(value: T) {
|
||||
compareAndSet(this.value, value)
|
||||
}
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
|
||||
}
|
||||
@@ -10,8 +10,12 @@ fun CoroutineScope.LinkedSupervisorJob(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||
|
||||
fun CoroutineScope.LinkedSupervisorScope(
|
||||
|
||||
fun CoroutineContext.LinkedSupervisorScope(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = CoroutineScope(
|
||||
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||
this + LinkedSupervisorJob(additionalContext)
|
||||
)
|
||||
fun CoroutineScope.LinkedSupervisorScope(
|
||||
additionalContext: CoroutineContext? = null
|
||||
) = coroutineContext.LinkedSupervisorScope(additionalContext)
|
||||
@@ -4,28 +4,71 @@ import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
|
||||
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
|
||||
/**
|
||||
* Created [CoroutineScope] which will [launch] listening of [context] job completing and drop itself. Current weak
|
||||
* scope **will not** be attached to [context] directly. So, this [CoroutineScope] will not prevent parent one from
|
||||
* cancelling if it is launched with [supervisorScope] or [coroutineScope], but still will follow closing status
|
||||
* of parent [Job]
|
||||
*/
|
||||
fun WeakScope(
|
||||
context: CoroutineContext
|
||||
) = CoroutineScope(context.minusKey(Job) + Job()).also { newScope ->
|
||||
newScope.launch {
|
||||
context.job.join()
|
||||
newScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.weakLaunch(
|
||||
/**
|
||||
* Created [CoroutineScope] which will [launch] listening of [scope] [CoroutineContext] job completing and drop itself. Current weak
|
||||
* scope **will not** be attached to [scope] [CoroutineContext] directly. So, this [CoroutineScope] will not prevent parent one from
|
||||
* cancelling if it is launched with [supervisorScope] or [coroutineScope], but still will follow closing status
|
||||
* of parent [Job]
|
||||
*/
|
||||
fun WeakScope(
|
||||
scope: CoroutineScope
|
||||
) = WeakScope(scope.coroutineContext)
|
||||
|
||||
/**
|
||||
* [this] [CoroutineScope] will be used as base for [WeakScope]. Other parameters ([context], [start], [block])
|
||||
* will be used to [launch] [Job]
|
||||
*/
|
||||
fun CoroutineScope.launchWeak(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job {
|
||||
val scope = createWeakSubScope()
|
||||
val scope = WeakScope(this)
|
||||
val job = scope.launch(context, start, block)
|
||||
job.invokeOnCompletion { scope.cancel() }
|
||||
return job
|
||||
}
|
||||
|
||||
fun <T> CoroutineScope.weakAsync(
|
||||
/**
|
||||
* [this] [CoroutineScope] will be used as base for [WeakScope]. Other parameters ([context], [start], [block])
|
||||
* will be used to create [async] [Deferred]
|
||||
*/
|
||||
fun <T> CoroutineScope.asyncWeak(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Deferred<T> {
|
||||
val scope = createWeakSubScope()
|
||||
val scope = WeakScope(this)
|
||||
val deferred = scope.async(context, start, block)
|
||||
deferred.invokeOnCompletion { scope.cancel() }
|
||||
return deferred
|
||||
}
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("launchWeak(context, start, block)", "dev.inmo.micro_utils.coroutines.launchWeak"))
|
||||
fun CoroutineScope.weakLaunch(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job = launchWeak(context, start, block)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("asyncWeak(context, start, block)", "dev.inmo.micro_utils.coroutines.asyncWeak"))
|
||||
fun <T> CoroutineScope.weakAsync(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Deferred<T> = asyncWeak(context, start, block)
|
||||
@@ -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,33 @@
|
||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
|
||||
import dev.inmo.micro_utils.coroutines.asDeferred
|
||||
import dev.inmo.micro_utils.coroutines.subscribe
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SpecialMutableStateFlowTests {
|
||||
@Test
|
||||
fun simpleTest() = runTest {
|
||||
val specialMutableStateFlow = SpecialMutableStateFlow(0)
|
||||
specialMutableStateFlow.value = 1
|
||||
specialMutableStateFlow.first { it == 1 }
|
||||
assertEquals(1, specialMutableStateFlow.value)
|
||||
}
|
||||
@Test
|
||||
fun specialTest() = runTest {
|
||||
val specialMutableStateFlow = SpecialMutableStateFlow(0)
|
||||
lateinit var subscriberJob: Job
|
||||
subscriberJob = specialMutableStateFlow.subscribe(this) {
|
||||
when (it) {
|
||||
1 -> specialMutableStateFlow.value = 2
|
||||
2 -> subscriberJob.cancel()
|
||||
}
|
||||
}
|
||||
specialMutableStateFlow.value = 1
|
||||
subscriberJob.join()
|
||||
assertEquals(2, specialMutableStateFlow.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import dev.inmo.micro_utils.coroutines.asyncWeak
|
||||
import dev.inmo.micro_utils.coroutines.launchWeak
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class WeakJobTests {
|
||||
@Test
|
||||
fun testWeakJob() = runTest {
|
||||
var commonJobDone = false
|
||||
var weakJobStarted = false
|
||||
try {
|
||||
coroutineScope {
|
||||
launch {
|
||||
delay(1000)
|
||||
commonJobDone = true
|
||||
}
|
||||
asyncWeak {
|
||||
weakJobStarted = true
|
||||
delay(100500L)
|
||||
error("This must never happen")
|
||||
}
|
||||
}.await()
|
||||
} catch (error: Throwable) {
|
||||
assertTrue(error is CancellationException)
|
||||
assertTrue(commonJobDone)
|
||||
assertTrue(weakJobStarted)
|
||||
return@runTest
|
||||
}
|
||||
error("Cancellation exception has not been thrown")
|
||||
}
|
||||
@Test
|
||||
fun testThatWeakJobsWorksCorrectly() = runTest {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
lateinit var weakLaunchJob: Job
|
||||
lateinit var weakAsyncJob: Job
|
||||
val completeDeferred = Job()
|
||||
coroutineScope {
|
||||
weakLaunchJob = launchWeak {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
weakAsyncJob = asyncWeak {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineContext.job.invokeOnCompletion {
|
||||
scope.launch {
|
||||
delay(1000L)
|
||||
completeDeferred.complete()
|
||||
}
|
||||
}
|
||||
launch { delay(1000L); cancel() }
|
||||
}
|
||||
completeDeferred.join()
|
||||
|
||||
assertTrue(!weakLaunchJob.isActive)
|
||||
assertTrue(!weakAsyncJob.isActive)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Test
|
||||
|
||||
class WeakJob {
|
||||
@Test
|
||||
fun `test that weak jobs works correctly`() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
lateinit var weakLaunchJob: Job
|
||||
lateinit var weakAsyncJob: Job
|
||||
scope.launchSynchronously {
|
||||
val completeDeferred = Job()
|
||||
coroutineScope {
|
||||
weakLaunchJob = weakLaunch {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
weakAsyncJob = weakAsync {
|
||||
while (isActive) {
|
||||
delay(100L)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineContext.job.invokeOnCompletion {
|
||||
scope.launch {
|
||||
delay(1000L)
|
||||
completeDeferred.complete()
|
||||
}
|
||||
}
|
||||
launch { delay(1000L); cancel() }
|
||||
}
|
||||
completeDeferred.join()
|
||||
}
|
||||
|
||||
assert(!weakLaunchJob.isActive)
|
||||
assert(!weakAsyncJob.isActive)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines"/>
|
||||
+1
-1
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import com.soywiz.krypto.md5
|
||||
import korlibs.crypto.md5
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.crypto"/>
|
||||
@@ -9,6 +9,7 @@ android {
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
namespace "${project.group}.${project.name}"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -26,11 +27,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
+37
-37
@@ -13,10 +13,10 @@ repositories {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
// js(IR) {
|
||||
// browser()
|
||||
// nodejs()
|
||||
// }
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
android {}
|
||||
|
||||
sourceSets {
|
||||
@@ -26,44 +26,44 @@ kotlin {
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// jsMain {
|
||||
// dependencies {
|
||||
// implementation kotlin('stdlib')
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
// project.parent.subprojects.forEach {
|
||||
// if (
|
||||
// it != project
|
||||
// && it.hasProperty("kotlin")
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
// && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
// ) {
|
||||
// api it
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
@@ -76,10 +76,10 @@ kotlin {
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
@@ -106,7 +106,7 @@ tasks.dokkaHtml {
|
||||
skipDeprecated.set(true)
|
||||
|
||||
sourceLink {
|
||||
localDirectory.set(file("./"))
|
||||
localDirectory.set(file("../"))
|
||||
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
||||
remoteLineSuffix.set("#L")
|
||||
}
|
||||
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
|
||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||
}
|
||||
|
||||
// named("jsMain") {
|
||||
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
// }
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain"))
|
||||
}
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain"))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.dokka"/>
|
||||
+10
-1
@@ -19,11 +19,20 @@ allprojects {
|
||||
}
|
||||
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
// String compilerPluginVersionFromProperties = (String) project.properties["compose.kotlinCompilerPluginVersion"]
|
||||
// String compilerPluginVersionFromLibrariesVersions = libs.versions.compose.kotlin.get()
|
||||
// composePluginKotlinVersion = compilerPluginVersionFromProperties
|
||||
// if (compilerPluginVersionFromProperties == null) {
|
||||
// composePluginKotlinVersion = compilerPluginVersionFromLibrariesVersions
|
||||
// }
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidProject.gradle"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
|
||||
mppJvmJsLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwLinuxArm64Project.gradle"
|
||||
mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
|
||||
mppComposeJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1,7 +1,6 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
import dev.inmo.micro_utils.common.onPresented
|
||||
import dev.inmo.micro_utils.coroutines.*
|
||||
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
||||
@@ -118,23 +117,28 @@ open class DefaultStatesMachine <T: State>(
|
||||
* [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
|
||||
launch {
|
||||
statesJobsMutex.withLock {
|
||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||
if (stateInMap === removedState) {
|
||||
statesJobs[stateInMap] ?.cancel()
|
||||
override fun start(scope: CoroutineScope): Job {
|
||||
val supervisorScope = scope.LinkedSupervisorScope()
|
||||
supervisorScope.launchSafelyWithoutExceptions {
|
||||
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(supervisorScope) {
|
||||
supervisorScope.launch { performStateUpdate(Optional.absent(), it, supervisorScope) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(supervisorScope) {
|
||||
supervisorScope.launch { performStateUpdate(Optional.presented(it.first), it.second, supervisorScope) }
|
||||
}
|
||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(supervisorScope) { removedState ->
|
||||
supervisorScope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||
if (stateInMap === removedState) {
|
||||
statesJobs[stateInMap] ?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return supervisorScope.coroutineContext.job
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
||||
@@ -18,13 +18,13 @@ if (new File(projectDir, "secret.gradle").exists()) {
|
||||
githubRelease {
|
||||
token "${project.property('GITHUB_RELEASE_TOKEN')}"
|
||||
|
||||
owner "InsanusMokrassar"
|
||||
repo "MicroUtils"
|
||||
owner = "InsanusMokrassar"
|
||||
repo = "MicroUtils"
|
||||
|
||||
tagName "v${project.version}"
|
||||
releaseName "${project.version}"
|
||||
targetCommitish "${project.version}"
|
||||
tagName = "v${project.version}"
|
||||
releaseName = "${project.version}"
|
||||
targetCommitish = "${project.version}"
|
||||
|
||||
body getCurrentVersionChangelog()
|
||||
body = getCurrentVersionChangelog()
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -3,9 +3,10 @@ org.gradle.parallel=true
|
||||
kotlin.js.generate.externals=true
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.js=true
|
||||
#kotlin.experimental.tryK2=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
|
||||
|
||||
# JS NPM
|
||||
|
||||
@@ -14,5 +15,5 @@ crypto_js_version=4.1.1
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.18.0
|
||||
android_code_version=191
|
||||
version=0.21.6
|
||||
android_code_version=265
|
||||
+29
-23
@@ -1,42 +1,45 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.8.20"
|
||||
kt-serialization = "1.5.0"
|
||||
kt-coroutines = "1.6.4"
|
||||
kt = "1.9.23"
|
||||
kt-serialization = "1.6.3"
|
||||
kt-coroutines = "1.8.1"
|
||||
|
||||
kslog = "1.1.1"
|
||||
kslog = "1.3.4"
|
||||
|
||||
jb-compose = "1.4.0"
|
||||
jb-exposed = "0.41.1"
|
||||
jb-dokka = "1.8.10"
|
||||
jb-compose = "1.6.2"
|
||||
jb-exposed = "0.50.1"
|
||||
jb-dokka = "1.9.20"
|
||||
|
||||
korlibs = "3.4.0"
|
||||
uuid = "0.7.0"
|
||||
korlibs = "5.4.0"
|
||||
uuid = "0.8.4"
|
||||
|
||||
ktor = "2.3.0"
|
||||
ktor = "2.3.11"
|
||||
|
||||
gh-release = "2.4.1"
|
||||
gh-release = "2.5.2"
|
||||
|
||||
koin = "3.4.0"
|
||||
koin = "3.5.6"
|
||||
|
||||
okio = "3.3.0"
|
||||
okio = "3.9.0"
|
||||
|
||||
ksp = "1.8.20-1.0.11"
|
||||
kotlin-poet = "1.13.0"
|
||||
ksp = "1.9.23-1.0.20"
|
||||
kotlin-poet = "1.16.0"
|
||||
|
||||
android-gradle = "7.4.2"
|
||||
versions = "0.51.0"
|
||||
|
||||
android-gradle = "8.2.0"
|
||||
dexcount = "4.0.0"
|
||||
|
||||
android-coreKtx = "1.10.0"
|
||||
android-recyclerView = "1.3.0"
|
||||
android-coreKtx = "1.13.1"
|
||||
android-recyclerView = "1.3.2"
|
||||
android-appCompat = "1.6.1"
|
||||
android-fragment = "1.5.7"
|
||||
android-fragment = "1.7.1"
|
||||
android-espresso = "3.5.1"
|
||||
android-test = "1.1.5"
|
||||
android-compose-material3 = "1.2.1"
|
||||
|
||||
android-props-minSdk = "21"
|
||||
android-props-compileSdk = "33"
|
||||
android-props-buildTools = "33.0.2"
|
||||
android-props-compileSdk = "34"
|
||||
android-props-buildTools = "34.0.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -69,8 +72,8 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
|
||||
|
||||
kslog = { module = "dev.inmo:kslog", version.ref = "kslog" }
|
||||
|
||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "korlibs" }
|
||||
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "korlibs" }
|
||||
klock = { module = "com.soywiz.korge:korlibs-time", version.ref = "korlibs" }
|
||||
krypto = { module = "com.soywiz.korge:korlibs-crypto", version.ref = "korlibs" }
|
||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
|
||||
@@ -81,6 +84,7 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
|
||||
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
|
||||
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
|
||||
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
|
||||
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
|
||||
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
|
||||
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||
@@ -109,3 +113,5 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
|
||||
[plugins]
|
||||
|
||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
||||
|
||||
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Loaded 100 of 311 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user