mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
214 Commits
Author | SHA1 | Date | |
---|---|---|---|
19857930a4 | |||
d0dbe3ed2f | |||
8b7e78b63a | |||
92a4ecb523 | |||
6a5ad4d728 | |||
be4aa8daac | |||
b5eac37782 | |||
b1ad3c5a39 | |||
ba16bad029 | |||
ca8ae4cd72 | |||
53d35d74b3 | |||
49c139e235 | |||
caf9c821f3 | |||
ca4c6db96f | |||
6b2298c752 | |||
a1bf43def9 | |||
15e9254e00 | |||
afe5a72c6f | |||
750a8b9ecf | |||
27fc3f93e0 | |||
8166d4b99b | |||
b61d2ae2eb | |||
4790fe0aea | |||
bc37b11cee | |||
223fed910f | |||
b85ab7b061 | |||
888dc299c9 | |||
e113dc28ed | |||
31e55d2307 | |||
e90645f248 | |||
4bb7ba2571 | |||
8d31c25bf8 | |||
c7ee1c28b2 | |||
99b09c8b28 | |||
a328c4425a | |||
c0f61ca896 | |||
86e70c0961 | |||
d87a3a039f | |||
6279a2c40a | |||
f377ebea88 | |||
7373fef964 | |||
41ef86dbda | |||
7bc2b2336d | |||
0a615e6d78 | |||
8f928e16e1 | |||
72bc8da1e7 | |||
51e349a5db | |||
3cbb19ba2c | |||
ae3a5bf45d | |||
9c667f4b78 | |||
21195e1bcb | |||
03117ac565 | |||
d13fbdf176 | |||
7cecc0e0b6 | |||
203e781f5d | |||
3eb6cd77cd | |||
51855b2405 | |||
00cc26d874 | |||
02520636ad | |||
76813fae8e | |||
4693483c2b | |||
6dbd12df59 | |||
9e84dc5031 | |||
8f790360bc | |||
808375cea6 | |||
d9c15db9de | |||
cf2be8ed43 | |||
97339c9b1d | |||
eb35903de9 | |||
43e88e00da | |||
1e51f9d77b | |||
dad738357c | |||
cb828ab3f2 | |||
aefaf4a8cc | |||
b29f37a251 | |||
88854020ac | |||
14ebe01fc6 | |||
771aed0f0f | |||
16c57fcd6a | |||
75397a7ccb | |||
b05404f828 | |||
edea942874 | |||
8a8f568b9a | |||
4dc8d30c52 | |||
cb4e08e823 | |||
c443bf4fa0 | |||
a6982de822 | |||
4f1a663e75 | |||
dab262d626 | |||
87a3f61ca6 | |||
506e937a68 | |||
5a037c76dd | |||
313f622f7e | |||
6cba1fe1a2 | |||
fd2d0e80b7 | |||
96ab2e8aca | |||
0202988cae | |||
d619d59947 | |||
85b3e48d18 | |||
7a9b7d98a1 | |||
b212acfcaf | |||
3a45e5dc70 | |||
73190518d5 | |||
03f78180dc | |||
1c0b8cf842 | |||
a1624ea2a9 | |||
23a050cf1e | |||
916f2f96f4 | |||
00cc214754 | |||
b2e38f72b9 | |||
e7107d238d | |||
ed9ebdbd1a | |||
e80676d3d2 | |||
02d02fa8f2 | |||
bd783fb74f | |||
50386adf70 | |||
f4ee6c2890 | |||
d45aef9fe5 | |||
a56cd3dddd | |||
419e7070ee | |||
612cf40b5f | |||
8b39882e83 | |||
e639ae172b | |||
d0446850ae | |||
c48465b90b | |||
f419fd03d2 | |||
494812a660 | |||
eb78f21eec | |||
4bda70268b | |||
f037ce4371 | |||
3d2196e35d | |||
a74f061b02 | |||
11ade14676 | |||
eb562d8784 | |||
1ee5b4bfd4 | |||
d97892080b | |||
6f37125724 | |||
ed1baaade7 | |||
bb9669f8fd | |||
bdac715d48 | |||
acf4971298 | |||
249bc83a8c | |||
0fbb92f03f | |||
ca27cb3f82 | |||
3a5771a0cc | |||
527a2a91ac | |||
6763e5c4c6 | |||
06918d8310 | |||
89ccaa1b57 | |||
5d0bdb9bcf | |||
31fdcf74a5 | |||
afca09cc1d | |||
531d89d9db | |||
6bbbea0bc3 | |||
e337cd98c8 | |||
bcbab3b380 | |||
fb63de7568 | |||
aa45a4ab13 | |||
2af7e2f681 | |||
34fd9edce0 | |||
2a4cb8c5f9 | |||
50ea40bc3a | |||
a77654052d | |||
88aafce552 | |||
4e95d6bfff | |||
38d0e34fb5 | |||
8fbc6b9041 | |||
e8219d6cf4 | |||
6c20fc4ca6 | |||
85cd975492 | |||
1171a717fe | |||
bbe5320312 | |||
00acb9fddd | |||
de3d14dc41 | |||
67ff9cc9b3 | |||
af132103a0 | |||
3b1124a804 | |||
f226c2dfd6 | |||
69d6e63846 | |||
02c3d397ad | |||
67a1050646 | |||
8cd0775a6c | |||
162294d6c6 | |||
c4dd19dd00 | |||
d2314422f1 | |||
6fedd6f859 | |||
e52b59665f | |||
cda9d09689 | |||
c9237b3f00 | |||
18bba66c4a | |||
63418c4a8a | |||
2e66c6f4e3 | |||
e9c5df4c13 | |||
bc7789ad2c | |||
e3da761249 | |||
4082f65afa | |||
5d1cab075d | |||
bcf67f7e59 | |||
7d3b1f8e75 | |||
119a0588cc | |||
fab789d9c0 | |||
ceba81c08f | |||
a061af0558 | |||
c7a53846ad | |||
a683cccf0c | |||
50d41e35c1 | |||
aa0e831cea | |||
44e26ccb4f | |||
2a783f6e2b | |||
6058d6a724 | |||
2e9c7eb5fa | |||
e75465ad10 | |||
de01ad54e9 | |||
eeea7ddbe3 |
6
.github/workflows/dokka_push.yml
vendored
6
.github/workflows/dokka_push.yml
vendored
@@ -10,10 +10,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Fix android 31.0.0 dx
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Build
|
||||
run: ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
|
8
.github/workflows/packages_push.yml
vendored
8
.github/workflows/packages_push.yml
vendored
@@ -8,10 +8,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Fix android 31.0.0 dx
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
||||
|
||||
secret.gradle
|
||||
local.properties
|
||||
kotlin-js-store
|
||||
|
||||
publishing.sh
|
||||
|
360
CHANGELOG.md
360
CHANGELOG.md
@@ -1,5 +1,365 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.24
|
||||
|
||||
* `Ktor`:
|
||||
* `Common`:
|
||||
* New extension fun `MPPFile#input`
|
||||
|
||||
## 0.9.23
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* New property `ExposedRepo#selectAll` to retrieve all the rows in the table
|
||||
|
||||
## 0.9.22
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Now `createKtorServer` fun is fully customizable
|
||||
|
||||
## 0.9.21
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* fixes in `AbstractExposedWriteCRUDRepo`
|
||||
|
||||
## 0.9.20
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Fixes in `OneToManyAndroidRepo`
|
||||
* New `CursorIterator`
|
||||
|
||||
## 0.9.19
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.0` -> `1.6.1`
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fixes in `ExposedStandardVersionsRepoProxy`
|
||||
|
||||
## 0.9.18
|
||||
|
||||
* `Common`
|
||||
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
|
||||
|
||||
## 0.9.17
|
||||
|
||||
* `Common`:
|
||||
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
|
||||
* `Coroutines`:
|
||||
* New extension `Element.visibilityFlow()`
|
||||
* `FSM`:
|
||||
* Now it is possible to resolve conflicts on `startChain`
|
||||
|
||||
## 0.9.16
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.3` -> `2.7.0`
|
||||
* `Common`:
|
||||
* New extension `Node#onRemoved`
|
||||
* `Compose`:
|
||||
* New extension `Composition#linkWithRoot` for removing of composition with root element
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
|
||||
|
||||
## 0.9.15
|
||||
|
||||
* `FSM`:
|
||||
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
|
||||
* `DefaultStatesManager` now is extendable
|
||||
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
|
||||
|
||||
## 0.9.14
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.2` -> `2.6.3`
|
||||
* `Ktor`: `1.6.7` -> `1.6.8`
|
||||
* `Ktor`:
|
||||
* Add temporal files uploading functionality (for clients to upload and for server to receive)
|
||||
|
||||
## 0.9.13
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.1.0` -> `1.1.1`
|
||||
|
||||
## 0.9.12
|
||||
|
||||
* `Common`:
|
||||
* `JS`:
|
||||
* New function `openLink`
|
||||
* New function `selectFile`
|
||||
* New function `triggerDownloadFile`
|
||||
* `Compose`:
|
||||
* Created :)
|
||||
* `Common`:
|
||||
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
|
||||
* `JS`:
|
||||
* `openLink` on top of `openLink` with `String` target from common
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* `Common`:
|
||||
* New extension `Flow.toMutableState`
|
||||
* New extension `StateFlow.toMutableState`
|
||||
* `JS`:
|
||||
* New function `selectFileOrThrow` on top of `selectFile` from `common`
|
||||
* New function `selectFileOrNull` on top of `selectFile` from `common`
|
||||
|
||||
## 0.9.11
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.6.1` -> `2.6.2`
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* Created :)
|
||||
* New extensions and function:
|
||||
* `Composition#linkWithJob`
|
||||
* `Composition#linkWithContext`
|
||||
* `renderComposableAndLinkToContext`
|
||||
|
||||
## 0.9.10
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.5.2` -> `2.6.1`
|
||||
* Ktor:
|
||||
* Client:
|
||||
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
|
||||
* Server:
|
||||
* Now it is possible to filter data in `Route#includeWebsocketHandling`
|
||||
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
|
||||
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
|
||||
|
||||
## 0.9.9
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.5.1` -> `2.5.2`
|
||||
* `Common`:
|
||||
* Add new diff tool - `applyDiff`
|
||||
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
|
||||
|
||||
## 0.9.8
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.37.2` -> `0.37.3`
|
||||
* `Klock`: `2.4.13` -> `2.5.1`
|
||||
* `AppCompat`: `1.4.0` -> `1.4.1`
|
||||
|
||||
## 0.9.7
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
|
||||
|
||||
## 0.9.6
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
|
||||
|
||||
## 0.9.5
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.12` -> `2.4.13`
|
||||
|
||||
## 0.9.4
|
||||
|
||||
* `Pagination`:
|
||||
* `Common`:
|
||||
* Add several `optionallyReverse` functions
|
||||
* `Common`:
|
||||
* Changes in `Either`:
|
||||
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
|
||||
* `Either#t1` and `Either#t2` are deprecated
|
||||
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
|
||||
|
||||
## 0.9.3
|
||||
|
||||
* `Versions`:
|
||||
* `UUID`: `0.3.1` -> `0.4.0`
|
||||
|
||||
## 0.9.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.10` -> `2.4.12`
|
||||
|
||||
## 0.9.1
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Default realizations of standard interfaces for exposed DB are using public fields for now:
|
||||
* `ExposedReadKeyValueRepo`
|
||||
* `ExposedReadOneToManyKeyValueRepo`
|
||||
* `ExposedStandardVersionsRepoProxy`
|
||||
* New typealiases for one to many exposed realizations:
|
||||
* `ExposedReadKeyValuesRepo`
|
||||
* `ExposedKeyValuesRepo`
|
||||
|
||||
## 0.9.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.5.31` -> `1.6.10`
|
||||
* `Coroutines`: `1.5.2` -> `1.6.0`
|
||||
* `Serialization`: `1.3.1` -> `1.3.2`
|
||||
* `Exposed`: `0.36.2` -> `0.37.2`
|
||||
* `Ktor`: `1.6.5` -> `1.6.7`
|
||||
* `Klock`: `2.4.8` -> `2.4.10`
|
||||
|
||||
## 0.8.9
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Fixes in `uniloadMultipart`
|
||||
* `Client`:
|
||||
* Fixes in `unimultipart`
|
||||
* `FSM`:
|
||||
* Fixes in `DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.8
|
||||
|
||||
* `Versions`:
|
||||
* `AppCompat`: `1.3.1` -> `1.4.0`
|
||||
* Android Compile SDK: `31.0.0` -> `32.0.0`
|
||||
* `FSM`:
|
||||
* `DefaultStatesMachine` now is extendable
|
||||
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
|
||||
|
||||
## 0.8.7
|
||||
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `UnifiedRequester` now have no private fields
|
||||
* Add preview work with multipart
|
||||
* `Server`
|
||||
* `UnifiedRouter` now have no private fields
|
||||
* Add preview work with multipart
|
||||
|
||||
## 0.8.6
|
||||
|
||||
* `Common`:
|
||||
* `Either` extensions `onFirst` and `onSecond` now accept not `crossinline` callbacks
|
||||
* All `joinTo` now accept not `crossinline` callbacks
|
||||
|
||||
## 0.8.5
|
||||
|
||||
* `Common`:
|
||||
* `repeatOnFailure`
|
||||
|
||||
## 0.8.4
|
||||
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Several new `createKtorServer`
|
||||
|
||||
## 0.8.3
|
||||
|
||||
* `Common`:
|
||||
* Ranges intersection functionality
|
||||
* New type `Optional`
|
||||
* `Pagination`:
|
||||
* `Pagination` now extends `ClosedRange<Int>`
|
||||
* `Pagination` intersection functionality
|
||||
|
||||
## 0.8.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.7` -> `2.4.8`
|
||||
* `Serialization`: `1.3.0` -> `1.3.1`
|
||||
* `FSM`:
|
||||
* Now it is possible to pass any `CheckableHandlerHolder` in `FSMBuilder`
|
||||
* Now `StatesMachine` works with `CheckableHandlerHolder` instead of `CustomizableHandlerHolder`
|
||||
|
||||
## 0.8.1
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.36.1` -> `0.36.2`
|
||||
* `Core KTX`: `1.6.0` -> `1.7.0`
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.6` -> `2.4.7`
|
||||
* `Ktor`: `1.6.4` -> `1.6.5`
|
||||
* `Exposed`: `0.35.3` -> `0.36.1`
|
||||
* `Common`:
|
||||
* Type `Either` got its own serializer
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* Full rework of FSM:
|
||||
* Now it is more flexible for checking of handler opportunity to handle state
|
||||
* Now machine and states managers are type-oriented
|
||||
* `StateHandlerHolder` has been renamed to `CheckableHandlerHolder`
|
||||
* Add opportunity for comfortable adding default state handler
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* `Common`:
|
||||
* New type `Either`
|
||||
* `Serialization`:
|
||||
* `TypedSerializer`
|
||||
* New factory fun which accept vararg pairs of type and its serializer
|
||||
* `Repos`:
|
||||
* `Common` (`Android`):
|
||||
* `AbstractMutableAndroidCRUDRepo` flows now will have extra buffer capacity instead of reply. It means that
|
||||
android crud repo _WILL NOT_ send previous events to the
|
||||
* `Exposed`:
|
||||
* New parameter `AbstractExposedWriteCRUDRepo#replyCacheInFlows`
|
||||
* KeyValue realization `ExposedKeyValueRepo` properties `_onNewValue` and `_onValueRemoved` now are available in
|
||||
inheritors
|
||||
* `Pagination`:
|
||||
* `Common`:
|
||||
* New types `getAllBy*` for current, next and custom paging
|
||||
|
||||
## 0.7.3
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.35.2` -> `0.35.3`
|
||||
|
||||
## 0.7.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.5` -> `2.4.6`
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.3` -> `2.4.5`
|
||||
* `Exposed`: `0.35.1` -> `0.35.2`
|
||||
* `Coroutines`:
|
||||
* `Common`:
|
||||
* New `Flow` - `AccumulatorFlow`
|
||||
* `FSM`:
|
||||
* `Common`:
|
||||
* `InMemoryStatesManager` has been replaced
|
||||
* `StatesMachine` became an interface
|
||||
* New manager `DefaultStatesManager` with `DefaultStatesManagerRepo` for abstraction of manager and storing of
|
||||
data info
|
||||
|
||||
## 0.7.0
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KOTLINX DATETIME TO KORLIBS KLOCK. CAREFUL**
|
||||
|
||||
* `Versions`
|
||||
* `kotlinx.datetime` -> `Klock`
|
||||
|
||||
## 0.6.0 DO NOT RECOMMENDED
|
||||
|
||||
**THIS VERSION HAS MIGRATED FROM KORLIBS KLOCK TO KOTLINX DATETIME. CAREFUL**
|
||||
**ALL DEPRECATION HAVE BEEN REMOVED**
|
||||
|
||||
* `Versions`
|
||||
* `Klock` -> `kotlinx.datetime`
|
||||
|
||||
## 0.5.31
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.4.2` -> `2.4.3`
|
||||
* `Ktor`: `1.6.3` -> `1.6.4`
|
||||
|
||||
## 0.5.30
|
||||
|
||||
* `Versions`:
|
||||
|
@@ -10,7 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
api libs.android.appCompat.resources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,13 +10,13 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||
api libs.android.recyclerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
build.gradle
12
build.gradle
@@ -7,12 +7,12 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
classpath libs.buildscript.jb.dokka
|
||||
classpath libs.buildscript.gh.release
|
||||
classpath libs.buildscript.android.gradle
|
||||
classpath libs.buildscript.android.dexcount
|
||||
}
|
||||
}
|
||||
|
||||
|
18
common/compose/build.gradle
Normal file
18
common/compose/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
|
||||
class DefaultDisposableEffectResult(
|
||||
private val onDispose: () -> Unit
|
||||
) : DisposableEffectResult {
|
||||
override fun dispose() {
|
||||
onDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DoNothing = DefaultDisposableEffectResult {}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composition
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Composition.linkWithElement(element: Element) {
|
||||
element.onRemoved { dispose() }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.attributes.ATarget
|
||||
|
||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||
link,
|
||||
mode.targetStr,
|
||||
features
|
||||
)
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun <TElement : Element> renderComposableAndLinkToRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithElement(root)
|
||||
}
|
1
common/compose/src/main/AndroidManifest.xml
Normal file
1
common/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
@@ -1,5 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Deprecated("Redundant", ReplaceWith("coerceIn(min, max)"))
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max)
|
@@ -2,8 +2,6 @@
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
private inline fun <T> getObject(
|
||||
additional: MutableList<T>,
|
||||
iterator: Iterator<T>
|
||||
@@ -155,3 +153,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||
other: Iterable<T>
|
||||
) = calculateDiff(other, strictComparison = true)
|
||||
|
||||
/**
|
||||
* 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
|
||||
) = calculateDiff(source, strictComparison).let {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,166 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
|
||||
*
|
||||
* @see EitherFirst
|
||||
* @see EitherSecond
|
||||
* @see Either.Companion.first
|
||||
* @see Either.Companion.second
|
||||
* @see Either.onFirst
|
||||
* @see Either.onSecond
|
||||
* @see Either.mapOnFirst
|
||||
* @see Either.mapOnSecond
|
||||
*/
|
||||
@Serializable(EitherSerializer::class)
|
||||
sealed interface Either<T1, T2> {
|
||||
val optionalT1: Optional<T1>
|
||||
val optionalT2: Optional<T2>
|
||||
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
|
||||
val t1: T1?
|
||||
get() = optionalT1.dataOrNull()
|
||||
@Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2"))
|
||||
val t2: T2?
|
||||
get() = optionalT2.dataOrNull()
|
||||
|
||||
companion object {
|
||||
fun <T1, T2> serializer(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
|
||||
}
|
||||
}
|
||||
|
||||
class EitherSerializer<T1, T2>(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
) : KSerializer<Either<T1, T2>> {
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(Either::class).descriptor)
|
||||
}
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: Either<T1, T2>
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> type = decodeStringElement(descriptor, 0)
|
||||
1 -> {
|
||||
result = when (type) {
|
||||
"t1" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t1EitherSerializer
|
||||
)
|
||||
"t2" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t2EitherSerializer
|
||||
)
|
||||
else -> error("Unknown type of either: $type")
|
||||
}
|
||||
}
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
is EitherFirst -> {
|
||||
encodeStringElement(descriptor, 0, "t1")
|
||||
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
|
||||
}
|
||||
is EitherSecond -> {
|
||||
encodeStringElement(descriptor, 0, "t2")
|
||||
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT1]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
override val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = t1.optional
|
||||
override val optionalT2: Optional<T2> = Optional.absent()
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT2]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
override val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = Optional.absent()
|
||||
override val optionalT2: Optional<T2> = t2.optional
|
||||
}
|
||||
|
||||
/**
|
||||
* @return New instance of [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
|
||||
/**
|
||||
* @return New instance of [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||
optionalT1.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||
optionalT2.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
|
||||
return optionalT1.mapOnPresented(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
|
||||
return optionalT2.mapOnPresented(block)
|
||||
}
|
||||
|
||||
inline fun <reified T1, reified T2> Any.either() = when (this) {
|
||||
is T1 -> Either.first<T1, T2>(this)
|
||||
is T2 -> Either.second<T1, T2>(this)
|
||||
else -> error("Incorrect type of either argument $this")
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
crossinline separatorFun: (I) -> R?,
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
crossinline transform: (I) -> R?
|
||||
transform: (I) -> R?
|
||||
): List<R> {
|
||||
val result = mutableListOf<R>()
|
||||
val iterator = iterator()
|
||||
@@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
crossinline transform: (I) -> R?
|
||||
transform: (I) -> R?
|
||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
crossinline separatorFun: (I) -> I?,
|
||||
separatorFun: (I) -> I?,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
|
||||
@@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo(
|
||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
crossinline separatorFun: (I) -> R?,
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
crossinline transform: (I) -> R?
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
crossinline transform: (I) -> R?
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
|
||||
|
@@ -23,11 +23,12 @@ value class FileName(val string: String) {
|
||||
}
|
||||
|
||||
|
||||
@PreviewFeature
|
||||
expect class MPPFile
|
||||
|
||||
expect val MPPFile.filename: FileName
|
||||
expect val MPPFile.filesize: Long
|
||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
|
||||
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||
|
||||
|
@@ -0,0 +1,92 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
|
||||
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
|
||||
* presented by some third type.
|
||||
*
|
||||
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
|
||||
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
|
||||
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
|
||||
*
|
||||
* @see Optional.presented
|
||||
* @see Optional.absent
|
||||
* @see Optional.optional
|
||||
* @see Optional.onPresented
|
||||
* @see Optional.onAbsent
|
||||
*/
|
||||
@Serializable
|
||||
data class Optional<T> internal constructor(
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val data: T?,
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val dataPresented: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Will create [Optional] with presented data
|
||||
*/
|
||||
fun <T> presented(data: T) = Optional(data, true)
|
||||
/**
|
||||
* Will create [Optional] without data
|
||||
*/
|
||||
fun <T> absent() = Optional<T>(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
if (!dataPresented) { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||
if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
|
||||
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
|
||||
start > other.endInclusive || other.start > endInclusive -> null
|
||||
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
|
||||
}
|
||||
|
||||
fun IntRange.intersect(
|
||||
other: IntRange
|
||||
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
||||
|
||||
fun LongRange.intersect(
|
||||
other: LongRange
|
||||
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
times: Int,
|
||||
onEachFailure: (Throwable) -> Unit = {},
|
||||
action: (Int) -> R
|
||||
): Optional<R> {
|
||||
repeat(times) {
|
||||
runCatching {
|
||||
action(it)
|
||||
}.onFailure(onEachFailure).onSuccess {
|
||||
return Optional.presented(it)
|
||||
}
|
||||
}
|
||||
return Optional.absent()
|
||||
}
|
@@ -54,7 +54,7 @@ class DiffUtilsTests {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in 0 until oldList.size) {
|
||||
for (step in oldList.indices) {
|
||||
for ((i, v) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
@@ -73,4 +73,78 @@ class DiffUtilsTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleRemoveApplyWorks() {
|
||||
val oldList = (0 until 10).toList()
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val removedSublist = oldList.subList(i, i + count)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
val targetList = oldList - removedSublist
|
||||
|
||||
mutableOldList.applyDiff(targetList)
|
||||
|
||||
assertEquals(
|
||||
targetList,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleAddApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||
val mutable = oldList.toMutableList()
|
||||
mutable.addAll(i, addedSublist)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
|
||||
mutableOldList.applyDiff(mutable)
|
||||
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleChangesApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in oldList.indices) {
|
||||
for ((i, v) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||
).map { index ->
|
||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||
mutable[index] = it.value
|
||||
}
|
||||
}
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
mutableOldList.applyDiff(mutable)
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
|
||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||
lateinit var observer: MutationObserver
|
||||
|
||||
observer = MutationObserver { _, _ ->
|
||||
fun checkIfRemoved(node: Node): Boolean {
|
||||
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
|
||||
}
|
||||
|
||||
if (checkIfRemoved(this)) {
|
||||
observer.disconnect()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previousIntersectionRatio = -1f
|
||||
val observer = IntersectionObserver { entries, observer ->
|
||||
entries.forEach {
|
||||
if (previousIntersectionRatio != it.intersectionRatio) {
|
||||
previousIntersectionRatio = it.intersectionRatio.toFloat()
|
||||
it.block(previousIntersectionRatio, observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(this)
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
|
||||
var previous = -1f
|
||||
onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio > 0 && previous == 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previous = -1f
|
||||
return onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio == 0f && previous != 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.DOMRectReadOnly
|
||||
import org.w3c.dom.Element
|
||||
|
||||
external interface IntersectionObserverOptions {
|
||||
/**
|
||||
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
|
||||
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
|
||||
* visible.
|
||||
*/
|
||||
var root: Element?
|
||||
|
||||
/**
|
||||
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
|
||||
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
|
||||
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
|
||||
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
var rootMargin: String?
|
||||
|
||||
/**
|
||||
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
|
||||
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
|
||||
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
|
||||
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
|
||||
*/
|
||||
var threshold: Array<Number>?
|
||||
}
|
||||
fun IntersectionObserverOptions(
|
||||
block: IntersectionObserverOptions.() -> Unit = {}
|
||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
||||
|
||||
external interface IntersectionObserverEntry {
|
||||
/**
|
||||
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
|
||||
* the documentation for Element.getBoundingClientRect().
|
||||
*/
|
||||
val boundingClientRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
*/
|
||||
val intersectionRatio: Number
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly representing the target's visible area.
|
||||
*/
|
||||
val intersectionRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
|
||||
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
|
||||
* then you know the transition is from intersecting to not-intersecting.
|
||||
*/
|
||||
val isIntersecting: Boolean
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
*/
|
||||
val rootBounds: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* The Element whose intersection with the root changed.
|
||||
*/
|
||||
val target: Element
|
||||
|
||||
/**
|
||||
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
|
||||
* IntersectionObserver's time origin.
|
||||
*/
|
||||
val time: Double
|
||||
}
|
||||
|
||||
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
|
||||
|
||||
/**
|
||||
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
|
||||
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
|
||||
*/
|
||||
external class IntersectionObserver(callback: IntersectionObserverCallback) {
|
||||
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
|
||||
|
||||
/**
|
||||
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
|
||||
* was passed to the constructor or its value is null, the top-level document's viewport is used.
|
||||
*/
|
||||
val root: Element
|
||||
|
||||
/**
|
||||
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
|
||||
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
|
||||
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
|
||||
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
val rootMargin: String
|
||||
|
||||
/**
|
||||
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
|
||||
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
|
||||
* crossed for that target. If no value was passed to the constructor, 0 is used.
|
||||
*/
|
||||
val thresholds: Array<Number>
|
||||
|
||||
/**
|
||||
* Stops the IntersectionObserver object from observing any target.
|
||||
*/
|
||||
fun disconnect()
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver a target element to observe.
|
||||
*/
|
||||
fun observe(targetElement: Element)
|
||||
|
||||
/**
|
||||
* Returns an array of IntersectionObserverEntry objects for all observed targets.
|
||||
*/
|
||||
fun takeRecords(): Array<IntersectionObserverEntry>
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver to stop observing a particular target element.
|
||||
*/
|
||||
fun unobserve(targetElement: Element)
|
||||
}
|
@@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common
|
||||
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.ErrorEvent
|
||||
import org.w3c.files.File
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
@@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
reader.readAsArrayBuffer(this)
|
||||
}
|
||||
|
||||
fun MPPFile.readBytes(): ByteArray {
|
||||
val reader = FileReaderSync()
|
||||
return reader.readAsArrayBuffer(this).toByteArray()
|
||||
}
|
||||
|
||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = size.toLong()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = ::dirtyReadBytes
|
||||
|
@@ -0,0 +1,38 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventListener
|
||||
|
||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
||||
lateinit var observer: MutationObserver
|
||||
val listener = EventListener {
|
||||
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
|
||||
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
|
||||
val childrenGettingElement = elementsToCheck.removeFirst()
|
||||
for (i in 0 until childrenGettingElement.childElementCount) {
|
||||
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
|
||||
}
|
||||
}
|
||||
if (elementsToCheck.isEmpty()) {
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
if (options == null) {
|
||||
document.addEventListener(type, listener)
|
||||
} else {
|
||||
document.addEventListener(type, listener, options)
|
||||
}
|
||||
observer = onRemoved {
|
||||
if (options == null) {
|
||||
document.removeEventListener(type, listener)
|
||||
} else {
|
||||
document.removeEventListener(type, listener, options)
|
||||
}
|
||||
observer.disconnect()
|
||||
}
|
||||
return listener
|
||||
}
|
||||
|
||||
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
||||
window.open(link, target, features) ?.focus()
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.dom.createElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.files.get
|
||||
|
||||
fun selectFile(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {},
|
||||
onFile: (MPPFile) -> Unit
|
||||
) {
|
||||
(document.createElement("input") {
|
||||
(this as HTMLInputElement).apply {
|
||||
type = "file"
|
||||
onchange = {
|
||||
runCatching {
|
||||
files ?.get(0) ?: error("File must not be null")
|
||||
}.onSuccess {
|
||||
onFile(it)
|
||||
}.onFailure {
|
||||
onFailure(it)
|
||||
}
|
||||
}
|
||||
inputSetup(this)
|
||||
}
|
||||
} as HTMLElement).click()
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
|
||||
fun triggerDownloadFile(filename: String, fileLink: String) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
hiddenElement.href = fileLink
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = filename
|
||||
hiddenElement.click()
|
||||
}
|
||||
|
@@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO
|
||||
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = length()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
doInIO {
|
||||
|
@@ -10,12 +10,17 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines.android
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
coroutines/compose/build.gradle
Normal file
20
coroutines/compose/build.gradle
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.coroutines")
|
||||
api project(":micro_utils.common.compose")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
fun <T> Flow<T>.toMutableState(
|
||||
initial: T,
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> {
|
||||
val state = mutableStateOf(initial)
|
||||
subscribeSafelyWithoutExceptions(scope) { state.value = it }
|
||||
return state
|
||||
}
|
||||
|
||||
inline fun <T> StateFlow<T>.toMutableState(
|
||||
scope: CoroutineScope
|
||||
): MutableState<T> = toMutableState(value, scope)
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun Composition.linkWithJob(job: Job) {
|
||||
job.invokeOnCompletion {
|
||||
this@linkWithJob.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.linkWithElement
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(
|
||||
currentCoroutineContext()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(currentCoroutineContext())
|
||||
linkWithElement(root)
|
||||
}
|
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
@@ -0,0 +1,94 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private sealed interface AccumulatorFlowStep
|
||||
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
|
||||
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
|
||||
|
||||
/**
|
||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||
*
|
||||
* * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new
|
||||
* [FlowCollector]s until anybody will handle it
|
||||
* * Here there are an [activeData] where data [T] will be stored until somebody will handle it
|
||||
*/
|
||||
class AccumulatorFlow<T>(
|
||||
sourceDataFlow: Flow<T>,
|
||||
scope: CoroutineScope
|
||||
) : AbstractFlow<T>() {
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.first() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.add(step.channel)
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it as Any) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.remove(step.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) {
|
||||
dataMutex.withLock {
|
||||
activeData.addLast(it)
|
||||
}
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it as Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||
for (data in channel) {
|
||||
try {
|
||||
collector.emit(data as T)
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
} finally {
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] as base [Flow]
|
||||
*/
|
||||
fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return AccumulatorFlow(this, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get
|
||||
*/
|
||||
fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return receiveAsFlow().accumulatorFlow(scope)
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import dev.inmo.micro_utils.common.onVisibilityChanged
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
|
||||
var previousData: Boolean? = null
|
||||
|
||||
val observer = onVisibilityChanged { intersectionRatio, _ ->
|
||||
val currentData = intersectionRatio > 0
|
||||
if (currentData != previousData) {
|
||||
trySend(currentData)
|
||||
}
|
||||
previousData = currentData
|
||||
}
|
||||
|
||||
val removeObserver = onRemoved {
|
||||
observer.disconnect()
|
||||
close()
|
||||
}
|
||||
|
||||
invokeOnClose {
|
||||
observer.disconnect()
|
||||
removeObserver.disconnect()
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.selectFile
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
||||
suspend fun selectFileOrThrow(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {}
|
||||
): MPPFile {
|
||||
val result = CompletableDeferred<MPPFile>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.completeExceptionally(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
||||
|
||||
suspend fun selectFileOrNull(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {}
|
||||
): MPPFile? {
|
||||
val result = CompletableDeferred<MPPFile?>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.complete(null)
|
||||
onFailure(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
@@ -1,3 +1,6 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
||||
|
@@ -3,6 +3,9 @@ package dev.inmo.micro_utils.crypto
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("MD5").digest(this)
|
||||
|
@@ -26,12 +26,12 @@ ext {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
|
@@ -13,10 +13,10 @@ repositories {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
// js(IR) {
|
||||
// browser()
|
||||
// nodejs()
|
||||
// }
|
||||
android {}
|
||||
|
||||
sourceSets {
|
||||
@@ -29,7 +29,7 @@ kotlin {
|
||||
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("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
@@ -38,22 +38,22 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
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')
|
||||
@@ -116,9 +116,9 @@ tasks.dokkaHtml {
|
||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||
}
|
||||
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
}
|
||||
// named("jsMain") {
|
||||
// sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
// }
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
|
||||
|
@@ -21,6 +21,7 @@ allprojects {
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
|
@@ -10,6 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,81 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
|
||||
*/
|
||||
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
|
||||
suspend fun checkHandleable(state: O): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
|
||||
* casting in [handleState]
|
||||
*/
|
||||
class CustomizableHandlerHolder<I : O, O : State>(
|
||||
private val delegateTo: StatesHandler<I, O>,
|
||||
private val filter: suspend (state: O) -> Boolean
|
||||
) : CheckableHandlerHolder<I, O> {
|
||||
/**
|
||||
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
|
||||
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
|
||||
*/
|
||||
override suspend fun checkHandleable(state: O) = filter(state)
|
||||
|
||||
/**
|
||||
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
|
||||
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
|
||||
*/
|
||||
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
|
||||
return delegateTo.run { handleState(state) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : O, O : State> CheckableHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CustomizableHandlerHolder(
|
||||
StatesHandler<O, O> {
|
||||
delegateTo.run { handleState(it as I) }
|
||||
},
|
||||
if (strict) {
|
||||
{ it::class == inputKlass }
|
||||
} else {
|
||||
{ inputKlass.isInstance(it) }
|
||||
}
|
||||
)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
fun <I : O, O : State> StateHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O : State> CheckableHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(I::class, strict, delegateTo)
|
||||
|
||||
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
|
||||
inline fun <reified I : O, O : State> StateHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
|
||||
strict: Boolean = true
|
||||
) = CheckableHandlerHolder<I, O>(
|
||||
I::class,
|
||||
strict,
|
||||
this
|
||||
)
|
||||
|
||||
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
|
||||
noinline filter: suspend (state: State) -> Boolean
|
||||
) = CustomizableHandlerHolder<O, O>(
|
||||
{ this@holder.run { handleState(it as I) } },
|
||||
filter
|
||||
)
|
@@ -1,15 +0,0 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class StateHandlerHolder<I : State>(
|
||||
private val inputKlass: KClass<I>,
|
||||
private val strict: Boolean = false,
|
||||
private val delegateTo: StatesHandler<I>
|
||||
) : StatesHandler<State> {
|
||||
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
|
||||
|
||||
override suspend fun StatesMachine.handleState(state: State): State? {
|
||||
return delegateTo.run { handleState(state as I) }
|
||||
}
|
||||
}
|
@@ -1,5 +1,12 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
fun interface StatesHandler<I : State> {
|
||||
suspend fun StatesMachine.handleState(state: I): State?
|
||||
/**
|
||||
* Default realization of states handler
|
||||
*/
|
||||
fun interface StatesHandler<I : State, O: State> {
|
||||
/**
|
||||
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
|
||||
* [State] it is assumed that chain is not completed.
|
||||
*/
|
||||
suspend fun StatesMachine<in O>.handleState(state: I): O?
|
||||
}
|
||||
|
@@ -1,46 +1,130 @@
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private suspend fun <I : State> StatesMachine.launchStateHandling(
|
||||
state: State,
|
||||
handlers: List<StateHandlerHolder<out I>>
|
||||
): State? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
/**
|
||||
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
||||
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
|
||||
* handling until [start] method will be called
|
||||
*/
|
||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
): T? {
|
||||
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts handling of [State]s
|
||||
*/
|
||||
fun start(scope: CoroutineScope): Job
|
||||
|
||||
/**
|
||||
* Start chain of [State]s witn [state]
|
||||
*/
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates [DefaultStatesMachine]
|
||||
*/
|
||||
operator fun <T: State> invoke(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
) = DefaultStatesMachine(statesManager, handlers)
|
||||
}
|
||||
}
|
||||
|
||||
class StatesMachine (
|
||||
private val statesManager: StatesManager,
|
||||
private val handlers: List<StateHandlerHolder<*>>
|
||||
) : StatesHandler<State> {
|
||||
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
|
||||
/**
|
||||
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
||||
* resolving, and uses [launchStateHandling] for [State] handling.
|
||||
*
|
||||
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
|
||||
*/
|
||||
open class DefaultStatesMachine <T: State>(
|
||||
protected val statesManager: StatesManager<T>,
|
||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : StatesMachine<T> {
|
||||
/**
|
||||
* Will call [launchStateHandling] for state handling
|
||||
*/
|
||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||
|
||||
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
val statePerformer: suspend (State) -> Unit = { state: State ->
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
statesManager.endChain(state)
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
protected val statesJobs = mutableMapOf<T, Job>()
|
||||
protected val statesJobsMutex = Mutex()
|
||||
|
||||
protected open suspend fun performUpdate(state: T) {
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState != null) {
|
||||
statesManager.update(state, newState)
|
||||
} else {
|
||||
statesManager.endChain(state)
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
statesJobs[actualState] = scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
if (statesJobs[actualState] == job) {
|
||||
statesJobs.remove(actualState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
||||
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
||||
* [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { statePerformer(it) }
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { statePerformer(it.second) }
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statesManager.getActiveStates().forEach {
|
||||
launch { statePerformer(it) }
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startChain(state: State) {
|
||||
/**
|
||||
* Just calls [StatesManager.startChain] of [statesManager]
|
||||
*/
|
||||
override suspend fun startChain(state: T) {
|
||||
statesManager.startChain(state)
|
||||
}
|
||||
}
|
||||
|
@@ -1,92 +1,30 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StatesManager {
|
||||
val onChainStateUpdated: Flow<Pair<State, State>>
|
||||
val onStartChain: Flow<State>
|
||||
val onEndChain: Flow<State>
|
||||
interface StatesManager<T : State> {
|
||||
val onChainStateUpdated: Flow<Pair<T, T>>
|
||||
val onStartChain: Flow<T>
|
||||
val onEndChain: Flow<T>
|
||||
|
||||
|
||||
/**
|
||||
* Must set current set using [State.context]
|
||||
*/
|
||||
suspend fun update(old: State, new: State)
|
||||
suspend fun update(old: T, new: T)
|
||||
|
||||
/**
|
||||
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
|
||||
* busy by the other [State]
|
||||
*/
|
||||
suspend fun startChain(state: State)
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
/**
|
||||
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
|
||||
* ignored
|
||||
*/
|
||||
suspend fun endChain(state: State)
|
||||
suspend fun endChain(state: T)
|
||||
|
||||
suspend fun getActiveStates(): List<State>
|
||||
suspend fun getActiveStates(): List<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
class InMemoryStatesManager(
|
||||
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onStartChain = MutableSharedFlow<State>(0)
|
||||
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||
|
||||
private val contextsToStates = mutableMapOf<Any, State>()
|
||||
private val mapMutex = Mutex()
|
||||
|
||||
override suspend fun update(old: State, new: State) = mapMutex.withLock {
|
||||
when {
|
||||
contextsToStates[old.context] != old -> return@withLock
|
||||
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
|
||||
contextsToStates[old.context] = new
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = contextsToStates.getValue(new.context)
|
||||
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
endChainWithoutLock(stateOnNewOneContext)
|
||||
contextsToStates.remove(old.context)
|
||||
contextsToStates[new.context] = new
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: State) = mapMutex.withLock {
|
||||
if (!contextsToStates.containsKey(state.context)) {
|
||||
contextsToStates[state.context] = state
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: State) {
|
||||
if (contextsToStates[state.context] == state) {
|
||||
contextsToStates.remove(state.context)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: State) {
|
||||
mapMutex.withLock {
|
||||
endChainWithoutLock(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
|
||||
* must be able to perform update of chain in internal [StatesManager]
|
||||
*/
|
||||
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
|
||||
/**
|
||||
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
|
||||
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
|
||||
* [StatesManager.update] function
|
||||
*/
|
||||
suspend fun updateChain(currentState: T, newState: T)
|
||||
}
|
||||
|
||||
open class DefaultUpdatableStatesMachine<T : State>(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
) : DefaultStatesMachine<T>(
|
||||
statesManager,
|
||||
handlers
|
||||
), UpdatableStatesMachine<T> {
|
||||
protected val jobsStates = mutableMapOf<Job, T>()
|
||||
|
||||
/**
|
||||
* Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
|
||||
* [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
|
||||
* other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
|
||||
* states is solved in [shouldReplaceJob] and can be rewritten in subclasses
|
||||
*/
|
||||
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
if (compare(previousState, actualState)) {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
}
|
||||
val job = previousState.mapOnPresented {
|
||||
statesJobs.remove(it)
|
||||
} ?.takeIf { it.isActive } ?: scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs.remove(
|
||||
jobsStates[job] ?: return@withLock
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobsStates.remove(job)
|
||||
statesJobs[actualState] = job
|
||||
jobsStates[job] = actualState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if [previous] potentially lead to the same behaviour with [new]
|
||||
*/
|
||||
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
|
||||
|
||||
@Deprecated("Overwrite shouldReplaceJob instead")
|
||||
protected open suspend fun compare(previous: Optional<T>, new: T): Boolean = shouldReplaceJob(previous, new)
|
||||
|
||||
override suspend fun updateChain(currentState: T, newState: T) {
|
||||
statesManager.update(currentState, newState)
|
||||
}
|
||||
}
|
@@ -1,35 +1,61 @@
|
||||
package dev.inmo.micro_utils.fsm.common.dsl
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class FSMBuilder(
|
||||
var statesManager: StatesManager = InMemoryStatesManager()
|
||||
class FSMBuilder<T : State>(
|
||||
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
|
||||
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
|
||||
StatesMachine(
|
||||
statesManager,
|
||||
states
|
||||
)
|
||||
},
|
||||
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
|
||||
) {
|
||||
private var states = mutableListOf<StateHandlerHolder<*>>()
|
||||
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
|
||||
|
||||
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||
states.add(StateHandlerHolder(kClass, false, handler))
|
||||
fun add(handler: CheckableHandlerHolder<T, T>) {
|
||||
states.add(handler)
|
||||
}
|
||||
|
||||
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||
states.add(StateHandlerHolder(kClass, true, handler))
|
||||
fun <I : T> add(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
add(CheckableHandlerHolder(kClass, false, handler))
|
||||
}
|
||||
|
||||
fun build() = StatesMachine(
|
||||
fun <I : T> add(filter: suspend (state: State) -> Boolean, handler: StatesHandler<I, T>) {
|
||||
add(handler.holder(filter))
|
||||
}
|
||||
|
||||
fun <I : T> addStrict(kClass: KClass<I>, handler: StatesHandler<I, T>) {
|
||||
states.add(CheckableHandlerHolder(kClass, true, handler))
|
||||
}
|
||||
|
||||
inline fun <reified I : T> onStateOrSubstate(handler: StatesHandler<I, T>) {
|
||||
add(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> strictlyOn(handler: StatesHandler<I, T>) {
|
||||
addStrict(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : T> doWhen(
|
||||
noinline filter: suspend (state: State) -> Boolean,
|
||||
handler: StatesHandler<I, T>
|
||||
) {
|
||||
add(filter, handler)
|
||||
}
|
||||
|
||||
fun build() = fsmBuilder(
|
||||
statesManager,
|
||||
states.toList()
|
||||
states.toList().let { list ->
|
||||
defaultStateHandler ?.let { list + it.holder { true } } ?: list
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
|
||||
add(I::class, handler)
|
||||
}
|
||||
|
||||
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
|
||||
addStrict(I::class, handler)
|
||||
}
|
||||
|
||||
fun buildFSM(
|
||||
block: FSMBuilder.() -> Unit
|
||||
): StatesMachine = FSMBuilder().apply(block).build()
|
||||
fun <T : State> buildFSM(
|
||||
block: FSMBuilder<T>.() -> Unit
|
||||
): StatesMachine<T> = FSMBuilder<T>().apply(block).build()
|
||||
|
@@ -0,0 +1,118 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Implement this repo if you want to use some custom repo for [DefaultStatesManager]
|
||||
*/
|
||||
interface DefaultStatesManagerRepo<T : State> {
|
||||
/**
|
||||
* Must save [state] as current state of chain with [State.context] of [state]
|
||||
*/
|
||||
suspend fun set(state: T)
|
||||
/**
|
||||
* Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should
|
||||
* NOT be removed
|
||||
*/
|
||||
suspend fun removeState(state: T)
|
||||
/**
|
||||
* @return Current list of available and saved states
|
||||
*/
|
||||
suspend fun getStates(): List<T>
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun getContextState(context: Any): T?
|
||||
|
||||
/**
|
||||
* @return Current state by [context]
|
||||
*/
|
||||
suspend fun contains(context: Any): Boolean = getContextState(context) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
|
||||
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
|
||||
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
|
||||
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
|
||||
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
|
||||
* with [endChain] with current state
|
||||
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
open class DefaultStatesManager<T : State>(
|
||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager<T> {
|
||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||
protected val _onStartChain = MutableSharedFlow<T>(0)
|
||||
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
|
||||
protected val _onEndChain = MutableSharedFlow<T>(0)
|
||||
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
|
||||
|
||||
protected val mapMutex = Mutex()
|
||||
|
||||
constructor(
|
||||
repo: DefaultStatesManagerRepo<T>,
|
||||
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
|
||||
) : this (
|
||||
repo,
|
||||
onUpdateContextsConflictResolver = onContextsConflictResolver
|
||||
)
|
||||
|
||||
override suspend fun update(old: T, new: T) = mapMutex.withLock {
|
||||
val stateByOldContext: T? = repo.getContextState(old.context)
|
||||
when {
|
||||
stateByOldContext != old -> return@withLock
|
||||
stateByOldContext == null || old.context == new.context -> {
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = repo.getContextState(new.context)
|
||||
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
|
||||
repo.removeState(old)
|
||||
repo.set(new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: T) = mapMutex.withLock {
|
||||
val stateOnContext = repo.getContextState(state.context)
|
||||
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
|
||||
stateOnContext ?.let {
|
||||
endChainWithoutLock(it)
|
||||
}
|
||||
repo.set(state)
|
||||
_onStartChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun endChainWithoutLock(state: T) {
|
||||
if (repo.getContextState(state.context) == state) {
|
||||
repo.removeState(state)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: T) {
|
||||
mapMutex.withLock {
|
||||
endChainWithoutLock(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<T> = repo.getStates()
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
|
||||
/**
|
||||
* Simple [DefaultStatesManagerRepo] for [DefaultStatesManager] which will store data in [map] and use primitive
|
||||
* functionality
|
||||
*/
|
||||
class InMemoryDefaultStatesManagerRepo<T : State>(
|
||||
private val map: MutableMap<Any, T> = mutableMapOf()
|
||||
) : DefaultStatesManagerRepo<T> {
|
||||
override suspend fun set(state: T) {
|
||||
map[state.context] = state
|
||||
}
|
||||
|
||||
override suspend fun removeState(state: T) {
|
||||
map.remove(state.context)
|
||||
}
|
||||
|
||||
override suspend fun getStates(): List<T> = map.values.toList()
|
||||
|
||||
override suspend fun getContextState(context: Any): T? = map[context]
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = map.contains(context)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.fsm.common.managers
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
|
||||
*
|
||||
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||
* new state by using [endChain] with that state
|
||||
*/
|
||||
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
|
||||
fun <T: State> InMemoryStatesManager(
|
||||
onStartContextsConflictResolver: suspend (old: T, new: T) -> Boolean = { _, _ -> true },
|
||||
onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
||||
) = DefaultStatesManager(onStartContextsConflictResolver = onStartContextsConflictResolver, onUpdateContextsConflictResolver = onUpdateContextsConflictResolver)
|
@@ -1,6 +1,7 @@
|
||||
import dev.inmo.micro_utils.fsm.common.*
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
|
||||
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
sealed interface TrafficLightState : State {
|
||||
@@ -25,9 +26,9 @@ class PlayableMain {
|
||||
}
|
||||
}
|
||||
|
||||
val statesManager = InMemoryStatesManager()
|
||||
val statesManager = DefaultStatesManager<TrafficLightState>()
|
||||
|
||||
val machine = buildFSM {
|
||||
val machine = buildFSM<TrafficLightState> {
|
||||
strictlyOn<GreenCommon> {
|
||||
delay(1000L)
|
||||
YellowCommon(it.context).also(::println)
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.micro_utils.fsm.repos.common
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||
|
||||
class KeyValueBasedDefaultStatesManagerRepo<T : State>(
|
||||
private val keyValueRepo: KeyValueRepo<Any, T>
|
||||
) : DefaultStatesManagerRepo<T> {
|
||||
override suspend fun set(state: T) {
|
||||
keyValueRepo.set(state.context, state)
|
||||
}
|
||||
|
||||
override suspend fun removeState(state: T) {
|
||||
if (keyValueRepo.get(state.context) == state) {
|
||||
keyValueRepo.unset(state.context)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStates(): List<T> = keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||
override suspend fun getContextState(context: Any): T? = keyValueRepo.get(context)
|
||||
|
||||
override suspend fun contains(context: Any): Boolean = keyValueRepo.contains(context)
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
package dev.inmo.micro_utils.fsm.repos.common
|
||||
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class KeyValueBasedStatesManager(
|
||||
private val keyValueRepo: KeyValueRepo<Any, State>,
|
||||
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||
) : StatesManager {
|
||||
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||
|
||||
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override suspend fun update(old: State, new: State) {
|
||||
mutex.withLock {
|
||||
when {
|
||||
keyValueRepo.get(old.context) != old -> return@withLock
|
||||
old.context == new.context || !keyValueRepo.contains(new.context) -> {
|
||||
keyValueRepo.set(old.context, new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
else -> {
|
||||
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
|
||||
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||
endChainWithoutLock(stateOnNewOneContext)
|
||||
keyValueRepo.unset(old.context)
|
||||
keyValueRepo.set(new.context, new)
|
||||
_onChainStateUpdated.emit(old to new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startChain(state: State) {
|
||||
if (!keyValueRepo.contains(state.context)) {
|
||||
keyValueRepo.set(state.context, state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun endChainWithoutLock(state: State) {
|
||||
if (keyValueRepo.get(state.context) == state) {
|
||||
keyValueRepo.unset(state.context)
|
||||
_onEndChain.emit(state)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun endChain(state: State) {
|
||||
mutex.withLock { endChainWithoutLock(state) }
|
||||
}
|
||||
|
||||
override suspend fun getActiveStates(): List<State> {
|
||||
return keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
|
||||
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
|
||||
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
|
||||
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
|
||||
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
|
||||
noinline outToStateTransformer: suspend TargetStateType.() -> State,
|
||||
) = KeyValueBasedStatesManager(
|
||||
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
|
||||
contextToOutTransformer,
|
||||
stateToOutTransformer,
|
||||
outToContextTransformer,
|
||||
outToStateTransformer
|
||||
)
|
||||
)
|
@@ -7,43 +7,12 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2g
|
||||
|
||||
kotlin_version=1.5.31
|
||||
kotlin_coroutines_version=1.5.2
|
||||
kotlin_serialisation_core_version=1.3.0
|
||||
kotlin_exposed_version=0.35.1
|
||||
|
||||
ktor_version=1.6.3
|
||||
|
||||
klockVersion=2.4.2
|
||||
|
||||
github_release_plugin_version=2.2.12
|
||||
|
||||
uuidVersion=0.3.1
|
||||
|
||||
# ANDROID
|
||||
|
||||
core_ktx_version=1.6.0
|
||||
androidx_recycler_version=1.2.1
|
||||
appcompat_version=1.3.1
|
||||
|
||||
android_minSdkVersion=19
|
||||
android_compileSdkVersion=31
|
||||
android_buildToolsVersion=31.0.0
|
||||
dexcount_version=3.0.0
|
||||
junit_version=4.12
|
||||
test_ext_junit_version=1.1.2
|
||||
espresso_core=3.3.0
|
||||
|
||||
# JS NPM
|
||||
|
||||
crypto_js_version=4.1.1
|
||||
|
||||
# Dokka
|
||||
|
||||
dokka_version=1.5.30
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.5.30
|
||||
android_code_version=71
|
||||
version=0.9.24
|
||||
android_code_version=114
|
||||
|
78
gradle/libs.versions.toml
Normal file
78
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
[versions]
|
||||
|
||||
kt = "1.6.10"
|
||||
kt-serialization = "1.3.2"
|
||||
kt-coroutines = "1.6.1"
|
||||
|
||||
jb-compose = "1.1.1"
|
||||
jb-exposed = "0.37.3"
|
||||
jb-dokka = "1.6.10"
|
||||
|
||||
klock = "2.7.0"
|
||||
uuid = "0.4.0"
|
||||
|
||||
ktor = "1.6.8"
|
||||
|
||||
gh-release = "2.2.12"
|
||||
|
||||
android-gradle = "7.0.4"
|
||||
dexcount = "3.0.1"
|
||||
|
||||
android-coreKtx = "1.7.0"
|
||||
android-recyclerView = "1.2.1"
|
||||
android-appCompat = "1.4.1"
|
||||
android-espresso = "3.3.0"
|
||||
android-test = "1.1.2"
|
||||
|
||||
android-props-minSdk = "19"
|
||||
android-props-compileSdk = "32"
|
||||
android-props-buildTools = "32.0.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
|
||||
|
||||
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
|
||||
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
|
||||
|
||||
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
|
||||
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
|
||||
|
||||
|
||||
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
|
||||
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
|
||||
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
|
||||
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
|
||||
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
|
||||
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
|
||||
|
||||
|
||||
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
|
||||
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
|
||||
|
||||
|
||||
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
|
||||
|
||||
|
||||
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-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
|
||||
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
|
||||
|
||||
|
||||
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
|
||||
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
|
||||
|
||||
|
||||
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
|
||||
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
|
||||
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
|
||||
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
|
||||
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
|
||||
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
|
||||
|
||||
[plugins]
|
||||
|
||||
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -12,7 +12,7 @@ kotlin {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.ktor.common")
|
||||
api internalProject("micro_utils.coroutines")
|
||||
api "io.ktor:ktor-client-core:$ktor_version"
|
||||
api libs.ktor.client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.websocket.ws
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readBytes
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -17,6 +18,7 @@ import kotlinx.serialization.DeserializationStrategy
|
||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
|
||||
): Flow<T> {
|
||||
val correctedUrl = url.asCorrectWebSocketUrl
|
||||
@@ -26,7 +28,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
do {
|
||||
val reconnect = try {
|
||||
safely {
|
||||
ws(correctedUrl) {
|
||||
ws(correctedUrl, requestBuilder) {
|
||||
for (received in incoming) {
|
||||
when (received) {
|
||||
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
|
||||
@@ -65,10 +67,12 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = createStandardWebsocketFlow(
|
||||
url,
|
||||
checkReconnection
|
||||
checkReconnection,
|
||||
requestBuilder
|
||||
) {
|
||||
serialFormat.decodeDefault(deserializer, it)
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
|
||||
expect suspend fun MPPFile.inputProvider(): InputProvider
|
@@ -1,16 +1,20 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import kotlinx.serialization.*
|
||||
|
||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||
|
||||
class UnifiedRequester(
|
||||
private val client: HttpClient = HttpClient(),
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
val client: HttpClient = HttpClient(),
|
||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) {
|
||||
suspend fun <ResultType> uniget(
|
||||
url: String,
|
||||
@@ -31,11 +35,66 @@ class UnifiedRequester(
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||
|
||||
suspend fun <ResultType> unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||
|
||||
suspend fun <BodyType, ResultType> unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
|
||||
|
||||
suspend fun <ResultType> unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): ResultType = client.unimultipart(
|
||||
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {}
|
||||
): ResultType = client.unimultipart(
|
||||
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
|
||||
)
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||
checkReconnection: (Throwable?) -> Boolean,
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat, requestBuilder)
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
|
||||
}
|
||||
|
||||
val defaultRequester = UnifiedRequester()
|
||||
@@ -69,3 +128,124 @@ suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
suspend fun <ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = submitFormWithBinaryData<StandardKtorSerialInputData>(
|
||||
url,
|
||||
formData = formData {
|
||||
append(
|
||||
"bytes",
|
||||
inputProvider,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, mimetype)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
}
|
||||
) {
|
||||
requestBuilder()
|
||||
}.let { serialFormat.decodeDefault(resultDeserializer, it) }
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
filename: String,
|
||||
otherData: BodyPair<BodyType>,
|
||||
inputProvider: InputProvider,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
filename,
|
||||
inputProvider,
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder = {
|
||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||
append(
|
||||
"data",
|
||||
InputProvider(serialized.size.toLong()) {
|
||||
ByteReadPacket(serialized)
|
||||
},
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
},
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
||||
suspend fun <ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
mppFile.filename.string,
|
||||
mppFile.inputProvider(),
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder,
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
|
||||
url: String,
|
||||
mppFile: MPPFile,
|
||||
otherData: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||
mimetype: String = "*/*",
|
||||
additionalParametersBuilder: FormBuilder.() -> Unit = {},
|
||||
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
|
||||
requestBuilder: HttpRequestBuilder.() -> Unit = {},
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
): ResultType = unimultipart(
|
||||
url,
|
||||
mppFile,
|
||||
resultDeserializer,
|
||||
mimetype,
|
||||
additionalParametersBuilder = {
|
||||
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
|
||||
append(
|
||||
"data",
|
||||
InputProvider(serialized.size.toLong()) {
|
||||
ByteReadPacket(serialized)
|
||||
},
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
|
||||
dataHeadersBuilder()
|
||||
}
|
||||
)
|
||||
additionalParametersBuilder()
|
||||
},
|
||||
dataHeadersBuilder,
|
||||
requestBuilder,
|
||||
serialFormat
|
||||
)
|
||||
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
expect suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||
): TemporalFileId
|
||||
|
||||
suspend fun UnifiedRequester.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||
): TemporalFileId = client.tempUpload(
|
||||
fullTempUploadDraftPath, file, onUpload
|
||||
)
|
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
|
||||
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
|
||||
InputProvider(it.size.toLong()) {
|
||||
ByteReadPacket(it)
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.*
|
||||
import org.w3c.xhr.*
|
||||
|
||||
suspend fun tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (Long, Long) -> Unit
|
||||
): TemporalFileId {
|
||||
val formData = FormData()
|
||||
val answer = CompletableDeferred<TemporalFileId>()
|
||||
|
||||
formData.append(
|
||||
"data",
|
||||
file
|
||||
)
|
||||
|
||||
val request = XMLHttpRequest()
|
||||
request.responseType = XMLHttpRequestResponseType.TEXT
|
||||
request.upload.onprogress = {
|
||||
onUpload(it.loaded.toLong(), it.total.toLong())
|
||||
}
|
||||
request.onload = {
|
||||
if (request.status == 200.toShort()) {
|
||||
answer.complete(TemporalFileId(request.responseText))
|
||||
} else {
|
||||
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||
}
|
||||
}
|
||||
request.onerror = {
|
||||
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||
}
|
||||
request.open("POST", fullTempUploadDraftPath, true)
|
||||
request.send(formData)
|
||||
|
||||
val handle = currentCoroutineContext().job.invokeOnCompletion {
|
||||
runCatching {
|
||||
request.abort()
|
||||
}
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
answer.await()
|
||||
}.also {
|
||||
handle.dispose()
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
|
||||
actual suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (uploaded: Long, count: Long) -> Unit
|
||||
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.client.request.forms.InputProvider
|
||||
import io.ktor.utils.io.streams.asInput
|
||||
|
||||
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
|
||||
inputStream().asInput()
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package dev.inmo.micro_utils.ktor.client
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.filename
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.onUpload
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import java.net.URLConnection
|
||||
|
||||
internal val MPPFile.mimeType: String
|
||||
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
|
||||
|
||||
actual suspend fun HttpClient.tempUpload(
|
||||
fullTempUploadDraftPath: String,
|
||||
file: MPPFile,
|
||||
onUpload: (Long, Long) -> Unit
|
||||
): TemporalFileId {
|
||||
val inputProvider = file.inputProvider()
|
||||
val fileId = submitFormWithBinaryData<String>(
|
||||
fullTempUploadDraftPath,
|
||||
formData = formData {
|
||||
append(
|
||||
"data",
|
||||
inputProvider,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, file.mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
onUpload(onUpload)
|
||||
}
|
||||
return TemporalFileId(fileId)
|
||||
}
|
@@ -10,8 +10,11 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
|
||||
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
||||
api internalProject("micro_utils.common")
|
||||
api libs.kt.serialization.cbor
|
||||
api libs.klock
|
||||
api libs.uuid
|
||||
api libs.ktor.io
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.utils.io.core.Input
|
||||
|
||||
expect fun MPPFile.input(): Input
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
const val DefaultTemporalFilesSubPath = "temp_upload"
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class TemporalFileId(val string: String)
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import io.ktor.utils.io.core.Input
|
||||
|
||||
actual fun MPPFile.input(): Input = ByteReadPacket(readBytes())
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.ktor.common
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import io.ktor.utils.io.core.Input
|
||||
import io.ktor.utils.io.streams.asInput
|
||||
|
||||
actual fun MPPFile.input(): Input = inputStream().asInput()
|
@@ -16,10 +16,10 @@ kotlin {
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-server:$ktor_version"
|
||||
api "io.ktor:ktor-server-cio:$ktor_version"
|
||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||
api "io.ktor:ktor-websockets:$ktor_version"
|
||||
api libs.ktor.server
|
||||
api libs.ktor.server.cio
|
||||
api libs.ktor.server.host.common
|
||||
api libs.ktor.websockets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,29 +2,31 @@ package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.application.featureOrNull
|
||||
import io.ktor.application.install
|
||||
import io.ktor.http.URLProtocol
|
||||
import io.ktor.http.cio.websocket.*
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.websocket.webSocket
|
||||
import io.ktor.routing.application
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
|
||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
||||
if (incoming.tryReceive() != null) {
|
||||
close()
|
||||
throw CorrectCloseException
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
converter: (T) -> StandardKtorSerialInputData
|
||||
protocol: URLProtocol = URLProtocol.WS,
|
||||
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
|
||||
) {
|
||||
webSocket(suburl) {
|
||||
application.apply {
|
||||
featureOrNull(io.ktor.websocket.WebSockets) ?: install(io.ktor.websocket.WebSockets)
|
||||
}
|
||||
webSocket(suburl, protocol.name) {
|
||||
safely {
|
||||
flow.collect {
|
||||
send(converter(it))
|
||||
converter(it) ?.let { data ->
|
||||
send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +36,24 @@ fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
protocol: URLProtocol = URLProtocol.WS,
|
||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||
) = includeWebsocketHandling(
|
||||
suburl,
|
||||
flow
|
||||
) {
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
}
|
||||
flow,
|
||||
protocol,
|
||||
converter = if (filter == null) {
|
||||
{
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
}
|
||||
} else {
|
||||
{
|
||||
if (filter(it)) {
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -1,28 +1,39 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.PartData
|
||||
import io.ktor.http.content.forEachPart
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.request.receiveMultipart
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.asStream
|
||||
import io.ktor.util.cio.writeChannel
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.websocket.WebSocketServerSession
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.io.File.createTempFile
|
||||
|
||||
class UnifiedRouter(
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||
) {
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>
|
||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||
serializer: SerializationStrategy<T>,
|
||||
protocol: URLProtocol = URLProtocol.WS,
|
||||
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
|
||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
|
||||
|
||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||
answerSerializer: SerializationStrategy<T>,
|
||||
@@ -81,6 +92,11 @@ class UnifiedRouter(
|
||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val default
|
||||
get() = defaultUnifiedRouter
|
||||
}
|
||||
}
|
||||
|
||||
val defaultUnifiedRouter = UnifiedRouter()
|
||||
@@ -104,6 +120,139 @@ suspend fun <T> ApplicationCall.uniload(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.uniloadMultipart(
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: Input? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
when (it.name) {
|
||||
"bytes" -> resultInput = it.provider()
|
||||
else -> onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
resultInput ?: error("Bytes has not been received")
|
||||
}
|
||||
|
||||
suspend fun <T> ApplicationCall.uniloadMultipart(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
|
||||
): Pair<Input, T> {
|
||||
var data: Optional<T>? = null
|
||||
val resultInput = uniloadMultipart(
|
||||
onFormItem,
|
||||
{
|
||||
if (it.name == "data") {
|
||||
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||
} else {
|
||||
onCustomFileItem(it)
|
||||
}
|
||||
},
|
||||
onBinaryContent
|
||||
)
|
||||
|
||||
val completeData = data ?: error("Data has not been received")
|
||||
return resultInput to (completeData.dataOrNull().let { it as T })
|
||||
}
|
||||
|
||||
suspend fun <T> ApplicationCall.uniloadMultipartFile(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: MPPFile? = null
|
||||
var data: Optional<T>? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
when (it.name) {
|
||||
"bytes" -> {
|
||||
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||
resultInput = MPPFile.createTempFile(
|
||||
name.nameWithoutExtension.let {
|
||||
var resultName = it
|
||||
while (resultName.length < 3) {
|
||||
resultName += "_"
|
||||
}
|
||||
resultName
|
||||
},
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
|
||||
else -> onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
val completeData = data ?: error("Data has not been received")
|
||||
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.uniloadMultipartFile(
|
||||
onFormItem: (PartData.FormItem) -> Unit = {},
|
||||
onCustomFileItem: (PartData.FileItem) -> Unit = {},
|
||||
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
|
||||
) = safely {
|
||||
val multipartData = receiveMultipart()
|
||||
|
||||
var resultInput: MPPFile? = null
|
||||
|
||||
multipartData.forEachPart {
|
||||
when (it) {
|
||||
is PartData.FormItem -> onFormItem(it)
|
||||
is PartData.FileItem -> {
|
||||
if (it.name == "bytes") {
|
||||
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
|
||||
resultInput = MPPFile.createTempFile(
|
||||
name.nameWithoutExtension.let {
|
||||
var resultName = it
|
||||
while (resultName.length < 3) {
|
||||
resultName += "_"
|
||||
}
|
||||
resultName
|
||||
},
|
||||
".${name.extension}"
|
||||
).apply {
|
||||
outputStream().use { fileStream ->
|
||||
it.provider().asStream().copyTo(fileStream)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onCustomFileItem(it)
|
||||
}
|
||||
}
|
||||
is PartData.BinaryItem -> onBinaryContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
resultInput ?: error("Bytes has not been received")
|
||||
}
|
||||
|
||||
suspend fun ApplicationCall.getParameterOrSendError(
|
||||
field: String
|
||||
) = parameters[field].also {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.*
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -9,16 +11,57 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
||||
block: Application.() -> Unit
|
||||
): TEngine {
|
||||
val env = applicationEngineEnvironment {
|
||||
): TEngine = embeddedServer(
|
||||
engine,
|
||||
applicationEngineEnvironment {
|
||||
module(block)
|
||||
connector {
|
||||
this@connector.host = host
|
||||
this@connector.port = port
|
||||
this.host = host
|
||||
this.port = port
|
||||
}
|
||||
}
|
||||
return embeddedServer(engine, env)
|
||||
additionalEngineEnvironmentConfigurator()
|
||||
},
|
||||
additionalConfigurationConfigurator
|
||||
)
|
||||
|
||||
/**
|
||||
* Create server with [CIO] server engine without starting of it
|
||||
*
|
||||
* @see ApplicationEngine.start
|
||||
*/
|
||||
fun createKtorServer(
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
||||
block: Application.() -> Unit
|
||||
): CIOApplicationEngine = createKtorServer(
|
||||
CIO,
|
||||
host,
|
||||
port,
|
||||
additionalEngineEnvironmentConfigurator,
|
||||
additionalConfigurationConfigurator,
|
||||
block
|
||||
)
|
||||
|
||||
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
|
||||
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
|
||||
configurators: List<KtorApplicationConfigurator>
|
||||
): TEngine = createKtorServer(
|
||||
engine,
|
||||
host,
|
||||
port,
|
||||
additionalEngineEnvironmentConfigurator,
|
||||
additionalConfigurationConfigurator
|
||||
) {
|
||||
configurators.forEach { it.apply { configure() } }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
|
||||
fun createKtorServer(
|
||||
host: String = "localhost",
|
||||
port: Int = Random.nextInt(1024, 65535),
|
||||
block: Application.() -> Unit
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, block)
|
||||
configurators: List<KtorApplicationConfigurator>,
|
||||
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
|
||||
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)
|
||||
|
@@ -0,0 +1,132 @@
|
||||
package dev.inmo.micro_utils.ktor.server
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.micro_utils.common.FileName
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
|
||||
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.content.PartData
|
||||
import io.ktor.http.content.streamProvider
|
||||
import io.ktor.request.receiveMultipart
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.post
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.FileTime
|
||||
|
||||
class TemporalFilesRoutingConfigurator(
|
||||
private val subpath: String = DefaultTemporalFilesSubPath,
|
||||
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
|
||||
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
interface TemporalFilesUtilizer {
|
||||
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
|
||||
|
||||
companion object : TemporalFilesUtilizer {
|
||||
class ByTimerUtilizer(
|
||||
private val removeMillis: Long,
|
||||
private val scope: CoroutineScope
|
||||
) : TemporalFilesUtilizer {
|
||||
override fun start(
|
||||
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||
filesMutex: Mutex,
|
||||
onNewFileFlow: Flow<TemporalFileId>
|
||||
): Job = scope.launchSafelyWithoutExceptions {
|
||||
while (isActive) {
|
||||
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
|
||||
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
|
||||
}
|
||||
if (filesWithCreationInfo.isEmpty()) {
|
||||
delay(removeMillis)
|
||||
continue
|
||||
}
|
||||
var min = filesWithCreationInfo.first()
|
||||
for (fileWithCreationInfo in filesWithCreationInfo) {
|
||||
if (fileWithCreationInfo.second < min.second) {
|
||||
min = fileWithCreationInfo
|
||||
}
|
||||
}
|
||||
delay(System.currentTimeMillis() - (min.second + removeMillis))
|
||||
filesMutex.withLock {
|
||||
filesMap.remove(min.first)
|
||||
} ?.delete()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(
|
||||
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||
filesMutex: Mutex,
|
||||
onNewFileFlow: Flow<TemporalFileId>
|
||||
): Job = Job()
|
||||
}
|
||||
}
|
||||
|
||||
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
|
||||
private val temporalFilesMutex = Mutex()
|
||||
private val filesFlow = MutableSharedFlow<TemporalFileId>()
|
||||
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
|
||||
|
||||
override fun Route.invoke() {
|
||||
post(subpath) {
|
||||
unifiedRouter.apply {
|
||||
val multipart = call.receiveMultipart()
|
||||
|
||||
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
|
||||
var part = multipart.readPart()
|
||||
|
||||
while (part != null) {
|
||||
if (part is PartData.FileItem) {
|
||||
break
|
||||
}
|
||||
part = multipart.readPart()
|
||||
}
|
||||
|
||||
part ?.let {
|
||||
if (it is PartData.FileItem) {
|
||||
val fileId = TemporalFileId(uuid4().toString())
|
||||
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
|
||||
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
|
||||
outputStream().use { outputStream ->
|
||||
it.streamProvider().use {
|
||||
it.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileInfo ?.also { (fileId, file) ->
|
||||
temporalFilesMutex.withLock {
|
||||
temporalFilesMap[fileId] = file
|
||||
}
|
||||
call.respond(fileId.string)
|
||||
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
|
||||
} ?: call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
|
||||
temporalFilesMutex.withLock {
|
||||
temporalFilesMap.remove(temporalFileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
|
||||
|
||||
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
|
||||
temporalFilesMap.remove(temporalFileId)
|
||||
}
|
||||
}
|
@@ -4,8 +4,8 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,16 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||
implementation libs.kt.stdlib
|
||||
implementation libs.kt.serialization
|
||||
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-java:$ktor_version"
|
||||
implementation libs.ktor.client
|
||||
implementation libs.ktor.client.java
|
||||
}
|
||||
|
||||
mainClassName="MainKt"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
|
||||
package dev.inmo.micro_utils.language_codes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
52
mime_types/mimes_generator/mime_generator.py
Normal file
52
mime_types/mimes_generator/mime_generator.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import pandas as pd
|
||||
import itertools
|
||||
|
||||
def fix_name(category, raw_name):
|
||||
splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+')
|
||||
out1 = ""
|
||||
for s in splitted:
|
||||
out1 += s.capitalize()
|
||||
|
||||
result = ""
|
||||
if out1[0].isdigit():
|
||||
result += category[0].capitalize()
|
||||
result += out1
|
||||
else:
|
||||
result += out1
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
df = pd.read_html(open('table.html', 'r'))
|
||||
mimes = []
|
||||
for row in df[0].iterrows():
|
||||
mime = row[1][1]
|
||||
mime_category = mime.split('/', 1)[0]
|
||||
mime_name = mime.split('/', 1)[1]
|
||||
mimes.append({
|
||||
'mime_category': mime_category,
|
||||
'mime_name': mime_name,
|
||||
})
|
||||
|
||||
# codegen
|
||||
|
||||
mimes.sort(key=lambda x: x['mime_category'])
|
||||
grouped = itertools.groupby(mimes, lambda x: x['mime_category'])
|
||||
code = ''
|
||||
code2 = 'internal val knownMimeTypes: Set<MimeType> = setOf(\n'
|
||||
code2 += ' KnownMimeTypes.Any,\n'
|
||||
for key, group in grouped:
|
||||
group_name = key.capitalize()
|
||||
code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name
|
||||
code += ' @Serializable(MimeTypeSerializer::class)\n object Any: %s ("%s/*")\n' % (group_name, key)
|
||||
for mime in group:
|
||||
name = fix_name(mime['mime_category'], mime['mime_name'])
|
||||
code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name'])
|
||||
code2 += ' KnownMimeTypes.%s.%s,\n' % (group_name, name)
|
||||
code += '}\n\n'
|
||||
code2 += ')\n'
|
||||
with open('out1.txt', 'w') as file:
|
||||
file.write(code)
|
||||
with open('out2.txt', 'w') as file:
|
||||
file.write(code2)
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
|
||||
package dev.inmo.micro_utils.mime_types
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@@ -26,7 +26,6 @@ kotlin {
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -4,7 +4,13 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
@@ -28,7 +34,6 @@ kotlin {
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@@ -4,7 +4,13 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
js (IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
@@ -17,7 +23,7 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
|
||||
implementation libs.kt.serialization
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
@@ -40,8 +46,8 @@ kotlin {
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
implementation "androidx.test.ext:junit:$test_ext_junit_version"
|
||||
implementation "androidx.test.espresso:espresso-core:$espresso_core"
|
||||
implementation libs.android.test.junit
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +56,6 @@ kotlin {
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
72
mppProjectWithSerializationAndCompose.gradle
Normal file
72
mppProjectWithSerializationAndCompose.gradle
Normal file
@@ -0,0 +1,72 @@
|
||||
project.version = "$version"
|
||||
project.group = "$group"
|
||||
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
js (IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
android {
|
||||
publishAllLibraryVariants()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
implementation libs.kt.serialization
|
||||
implementation compose.runtime
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-common')
|
||||
implementation kotlin('test-annotations-common')
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation compose.desktop.currentOs
|
||||
}
|
||||
}
|
||||
jvmTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation compose.web.core
|
||||
}
|
||||
}
|
||||
jsTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-js')
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
implementation libs.android.test.junit
|
||||
implementation libs.android.espresso
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
@@ -5,3 +5,13 @@ plugins {
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package dev.inmo.micro_utils.pagination
|
||||
|
||||
import dev.inmo.micro_utils.common.intersect
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -9,7 +10,7 @@ import kotlin.math.floor
|
||||
* If you want to request something, you should use [SimplePagination]. If you need to return some result including
|
||||
* pagination - [PaginationResult]
|
||||
*/
|
||||
interface Pagination {
|
||||
interface Pagination : ClosedRange<Int> {
|
||||
/**
|
||||
* Started with 0.
|
||||
* Number of page inside of pagination. Offset can be calculated as [page] * [size]
|
||||
@@ -20,6 +21,17 @@ interface Pagination {
|
||||
* Size of current page. Offset can be calculated as [page] * [size]
|
||||
*/
|
||||
val size: Int
|
||||
|
||||
override val start: Int
|
||||
get() = page * size
|
||||
override val endInclusive: Int
|
||||
get() = start + size - 1
|
||||
}
|
||||
|
||||
fun Pagination.intersect(
|
||||
other: Pagination
|
||||
): Pagination? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
PaginationByIndexes(it.first, it.second)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +44,7 @@ inline val Pagination.isFirstPage
|
||||
* First number in index of objects. It can be used as offset for databases or other data sources
|
||||
*/
|
||||
val Pagination.firstIndex: Int
|
||||
get() = page * size
|
||||
get() = start
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
@@ -41,7 +53,7 @@ val Pagination.firstIndex: Int
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
|
||||
*/
|
||||
val Pagination.lastIndexExclusive: Int
|
||||
get() = firstIndex + size
|
||||
get() = endInclusive + 1
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
@@ -50,7 +62,7 @@ val Pagination.lastIndexExclusive: Int
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
|
||||
*/
|
||||
val Pagination.lastIndex: Int
|
||||
get() = lastIndexExclusive - 1
|
||||
get() = endInclusive
|
||||
|
||||
/**
|
||||
* Calculates pages count for given [datasetSize]
|
||||
|
@@ -16,6 +16,16 @@ suspend fun <T> getAll(
|
||||
return results.toList()
|
||||
}
|
||||
|
||||
suspend fun <T, R> R.getAllBy(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
paginationMapper: R.(PaginationResult<T>) -> Pagination?,
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAll(
|
||||
initialPagination,
|
||||
{ paginationMapper(it) },
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
@@ -25,6 +35,14 @@ suspend fun <T> getAllWithNextPaging(
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithNextPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithNextPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
|
||||
suspend fun <T> getAllWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend (Pagination) -> PaginationResult<T>
|
||||
@@ -33,3 +51,11 @@ suspend fun <T> getAllWithCurrentPaging(
|
||||
{ it.currentPageIfNotEmpty() },
|
||||
block
|
||||
)
|
||||
|
||||
suspend fun <T, R> R.getAllByWithCurrentPaging(
|
||||
initialPagination: Pagination = FirstPagePagination(),
|
||||
block: suspend R.(Pagination) -> PaginationResult<T>
|
||||
): List<T> = getAllWithCurrentPaging(
|
||||
initialPagination,
|
||||
{ block(it) }
|
||||
)
|
||||
|
@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
|
||||
size.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
|
||||
is List<T> -> optionallyReverse(reverse)
|
||||
is Set<T> -> optionallyReverse(reverse)
|
||||
else -> if (reverse) {
|
||||
reversed()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
|
||||
reversed()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
|
||||
reversed().toSet()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
|
||||
Array(size) {
|
||||
get(lastIndex - it)
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
|
||||
* Example:
|
||||
*
|
||||
* * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|`
|
||||
* * `|__f__l_|` will be transformed to `|__f__l_|`
|
||||
* * `|__f__l_|` will be transformed to `|_f__l__|`
|
||||
*
|
||||
* @return Reversed version of this [Pagination]
|
||||
*/
|
||||
@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
|
||||
* Shortcut for [reverse]
|
||||
*/
|
||||
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
|
||||
|
||||
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
|
||||
reverse(objectsCount)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
|
||||
reverse(objectsCount)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ kotlin {
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
|
||||
api libs.jb.exposed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,8 +15,8 @@ kotlin {
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api "io.ktor:ktor-server:$ktor_version"
|
||||
api "io.ktor:ktor-server-host-common:$ktor_version"
|
||||
api libs.ktor.server
|
||||
api libs.ktor.server.host.common
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
@@ -69,8 +68,19 @@ publishing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign publishing.publications
|
||||
|
||||
if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
apply plugin: 'signing'
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
|
||||
sign publishing.publications
|
||||
}
|
||||
|
||||
task signAll {
|
||||
tasks.withType(Sign).forEach {
|
||||
dependsOn(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}
|
||||
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/MicroUtils/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"It is set of projects with micro tools for avoiding of routines coding","url":"https://github.com/InsanusMokrassar/MicroUtils/","vcsUrl":"https://github.com/InsanusMokrassar/MicroUtils.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/MicroUtils"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
|
@@ -10,10 +10,10 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
api internalProject("micro_utils.pagination.common")
|
||||
|
||||
api "com.benasher44:uuid:$uuidVersion"
|
||||
api libs.uuid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ kotlin {
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.core:core-ktx:$core_ktx_version"
|
||||
api libs.android.coreKtx
|
||||
api internalProject("micro_utils.common")
|
||||
api internalProject("micro_utils.coroutines")
|
||||
}
|
||||
|
@@ -13,12 +13,12 @@ interface VersionsRepo<T> : Repo {
|
||||
* By default, instance of this interface will check that version of table with name [tableName] is less than
|
||||
* [version] or is absent
|
||||
*
|
||||
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
|
||||
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
|
||||
* until database version will be equal to [version]
|
||||
* In case if [tableName] didn't found, will be called [onCreate]. Then in case if [tableName] have version less
|
||||
* than parameter [version] or null, it will increase version one-by-one until database version will be equal to
|
||||
* [version]
|
||||
*
|
||||
* @param version Current version of table
|
||||
* @param onCreate This callback will be called in case when table have no information about table
|
||||
* @param onCreate This callback will be called in case when repo have no information about table
|
||||
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
|
||||
* "to" will always be greater than "from"
|
||||
*/
|
||||
|
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
class CursorIterator(
|
||||
private val c: Cursor
|
||||
) : Iterator<Cursor> {
|
||||
private var i = 0
|
||||
|
||||
init {
|
||||
c.moveToFirst()
|
||||
}
|
||||
override fun hasNext(): Boolean {
|
||||
return i < c.count
|
||||
}
|
||||
|
||||
override fun next(): Cursor {
|
||||
i++
|
||||
return if (c.moveToNext()) {
|
||||
c
|
||||
} else {
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun Cursor.iterator(): CursorIterator = CursorIterator(this)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user