mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
ccc1dd7857 | |||
5d450e0632 | |||
cd3838f321 |
5
.github/workflows/dokka_push.yml
vendored
5
.github/workflows/dokka_push.yml
vendored
@@ -11,8 +11,11 @@ jobs:
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Build
|
||||
run: ./gradlew build && ./gradlew dokkaHtml
|
||||
run: ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
|
@@ -1,14 +1,17 @@
|
||||
|
||||
name: Build
|
||||
name: Publish package to GitHub Packages
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Fix android 32.0.0 dx
|
||||
continue-on-error: true
|
||||
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
@@ -19,6 +22,7 @@ jobs:
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew publishAllPublicationsToGiteaRepository
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
8
.space.kts
Normal file
8
.space.kts
Normal file
@@ -0,0 +1,8 @@
|
||||
job("Build and run tests") {
|
||||
container(displayName = "Run gradle build", image = "openjdk:11") {
|
||||
kotlinScript { api ->
|
||||
// here can be your complex logic
|
||||
api.gradlew("build")
|
||||
}
|
||||
}
|
||||
}
|
935
CHANGELOG.md
935
CHANGELOG.md
@@ -1,940 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.20.6
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`
|
||||
* Fixes in exposed key-values repos
|
||||
|
||||
## 0.20.5
|
||||
|
||||
* `Coroutines`:
|
||||
* Fixes in `SmartRWLocker`
|
||||
|
||||
## 0.20.4
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.9.0` -> `1.9.10`
|
||||
* `KSLog`: `1.2.0` -> `1.2.1`
|
||||
* `Compose`: `1.5.0` -> `1.5.1`
|
||||
* `UUID`: `0.8.0` -> `0.8.1`
|
||||
|
||||
## 0.20.3
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.4.3` -> `1.5.0`
|
||||
* `Exposed`: `0.42.1` -> `0.43.0`
|
||||
* `Ktor`: `2.3.3` -> `2.3.4`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Fixes in locks of caches
|
||||
|
||||
## 0.20.2
|
||||
|
||||
* All main repos uses `SmartRWLocker`
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.5.1` -> `1.6.0`
|
||||
* `Exposed`: `0.42.0` -> `0.42.1`
|
||||
* `Korlibs`: `4.0.9` -> `4.0.10`
|
||||
* `Androis SDK`: `33` -> `34`
|
||||
|
||||
## 0.20.1
|
||||
|
||||
* `SmallTextField`:
|
||||
* Module is initialized
|
||||
* `Pickers`:
|
||||
* Module is initialized
|
||||
* `Coroutines`:
|
||||
* Add `SmartSemaphore`
|
||||
* Add `SmartRWLocker`
|
||||
|
||||
## 0.20.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.8.22` -> `1.9.0`
|
||||
* `KSLog`: `1.1.1` -> `1.2.0`
|
||||
* `Exposed`: `0.41.1` -> `0.42.0`
|
||||
* `UUID`: `0.7.1` -> `0.8.0`
|
||||
* `Korlibs`: `4.0.3` -> `4.0.9`
|
||||
* `Ktor`: `2.3.2` -> `2.3.3`
|
||||
* `Okio`: `3.4.0` -> `3.5.0`
|
||||
|
||||
## 0.19.9
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.4.2` -> `3.4.3`
|
||||
* `Startup`:
|
||||
* Now it is possible to start application in synchronous way
|
||||
|
||||
## 0.19.8
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.2` -> `1.7.3`
|
||||
* `Kotlin`: `1.8.20` -> `1.8.22`
|
||||
* `Compose`: `1.4.1` -> `1.4.3`
|
||||
* `Okio`: `3.3.0` -> `3.4.0`
|
||||
* `RecyclerView`: `1.3.0` -> `1.3.1`
|
||||
* `Fragment`: `1.6.0` -> `1.6.1`
|
||||
* `Repos`:
|
||||
* Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo`
|
||||
* `Cache`:
|
||||
* All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos
|
||||
will be automatically invalidated on start of their work
|
||||
|
||||
## 0.19.7
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.7.1` -> `1.7.2`
|
||||
|
||||
## 0.19.6
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.4` -> `1.7.1`
|
||||
* `Ktor`: `2.3.1` -> `2.3.2`
|
||||
* `Compose`: `1.4.0` -> `1.4.1`
|
||||
|
||||
## 0.19.5
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Fixes in new type generation
|
||||
|
||||
## 0.19.4
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.4.1` -> `3.4.2`
|
||||
* `Android Fragments`: `1.5.7` -> `1.6.0`
|
||||
* `Koin`
|
||||
* `Generator`
|
||||
* Fixes in new generic generator part
|
||||
|
||||
## 0.19.3
|
||||
|
||||
* `Koin`
|
||||
* `Generator`
|
||||
* New getter methods now available with opportunity to use parameters
|
||||
* Old notation `*Single` and `*Factory` is deprecated since this release. With old
|
||||
will be generated new `single*` and `factory*` notations for new generations
|
||||
* Add opportunity to use generic-oriented koin definitions
|
||||
|
||||
## 0.19.2
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.3.0` -> `2.3.1`
|
||||
* `Koin`: `3.4.0` -> `3.4.1`
|
||||
* `Uuid`: `0.7.0` -> `0.7.1`
|
||||
|
||||
## 0.19.1
|
||||
|
||||
* `Versions`:
|
||||
* `Korlibs`: `4.0.1` -> `4.0.3`
|
||||
* `Kotlin Poet`: `1.13.2` -> `1.14.0`
|
||||
|
||||
## 0.19.0
|
||||
|
||||
* `Versions`:
|
||||
* `Korlibs`: `3.4.0` -> `4.0.1`
|
||||
|
||||
## 0.18.4
|
||||
|
||||
* `Koin`:
|
||||
* New extension `lazyInject`
|
||||
|
||||
## 0.18.3
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.5.0` -> `1.5.1`
|
||||
* `Android Cor Ktx`: `1.10.0` -> `1.10.1`
|
||||
|
||||
## 0.18.2
|
||||
|
||||
* `Startup`:
|
||||
* Now internal `Json` is fully customizable
|
||||
|
||||
## 0.18.1
|
||||
|
||||
* `Common`:
|
||||
* Add `MapDiff`
|
||||
* `Coroutines`:
|
||||
* Add `SmartMutex`
|
||||
|
||||
## 0.18.0
|
||||
|
||||
**ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED**
|
||||
|
||||
* `Versions`:
|
||||
* `Android Fragments`: `1.5.6` -> `1.5.7`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Now it is possible to take query parameters as list
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* New `WriteKeyValuesRepo.removeWithValue`
|
||||
* `Cache`:
|
||||
* Rename full caching factories functions to `fullyCached`
|
||||
|
||||
## 0.17.8
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.2.4` -> `2.3.0`
|
||||
|
||||
## 0.17.7
|
||||
|
||||
* `Versions`:
|
||||
* `Android CoreKtx`: `1.9.0` -> `1.10.0`
|
||||
* `Startup`:
|
||||
* Add support of `linuxX64` and `mingwX64` platforms
|
||||
|
||||
## 0.17.6
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.8.10` -> `1.8.20`
|
||||
* `KSLog`: `1.0.0` -> `1.1.1`
|
||||
* `Compose`: `1.3.1` -> `1.4.0`
|
||||
* `Koin`: `3.3.2` -> `3.4.0`
|
||||
* `RecyclerView`: `1.2.1` -> `1.3.0`
|
||||
* `Fragment`: `1.5.5` -> `1.5.6`
|
||||
* Experimentally (`!!!`) add `linuxX64` and `mingwX64` targets
|
||||
|
||||
## 0.17.5
|
||||
|
||||
* `Common`:
|
||||
* Conversations of number primitives with bounds care
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* By default, `getAll` for repos will take all the size of repo as page size
|
||||
* New extension for all built-in repos `maxPagePagination`
|
||||
* All the repos got `getAll` functions
|
||||
|
||||
## 0.17.4
|
||||
|
||||
* `Serialization`:
|
||||
* `Mapper`:
|
||||
* Module inited
|
||||
* `Versions`:
|
||||
* `Compose`: `1.3.1-rc02` -> `1.3.1`
|
||||
|
||||
## 0.17.3
|
||||
|
||||
* `Common`:
|
||||
* Add `fixed` extensions for `Float` and `Double`
|
||||
* New function `emptyDiff`
|
||||
* Now you may pass custom `comparisonFun` to all `diff` functions
|
||||
|
||||
## 0.17.2
|
||||
|
||||
* `FSM`:
|
||||
* `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default
|
||||
|
||||
## 0.17.1
|
||||
|
||||
* **Hotfix** for absence of jvm dependencies in android modules
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.2.3` -> `2.2.4`
|
||||
|
||||
## 0.17.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.7.20` -> `1.8.10`
|
||||
* `Serialization`: `1.4.1` -> `1.5.0`
|
||||
* `KSLog`: `0.5.4` -> `1.0.0`
|
||||
* `AppCompat`: `1.6.0` -> `1.6.1`
|
||||
|
||||
## 0.16.13
|
||||
|
||||
* `Repos`:
|
||||
* `Generator`:
|
||||
* Module has been created
|
||||
|
||||
## 0.16.12
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* `CommonExposedRepo.selectByIds` uses `foldRight` by default instead of raw foreach
|
||||
* `Koin`:
|
||||
* `Generator`:
|
||||
* Module has been created
|
||||
|
||||
## 0.16.11
|
||||
|
||||
* `LanguageCodes`:
|
||||
* In android and JVM targets now available `toJavaLocale` and from Java `Locale` conversations from/to
|
||||
`IetfLanguageCode`
|
||||
|
||||
## 0.16.10
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* New transformer type: `ReadCRUDFromKeyValueRepo`
|
||||
* New transformer type: `ReadKeyValueFromCRUDRepo`
|
||||
* `Pagination`:
|
||||
* New `paginate` extensions with `reversed` support for `List`/`Set`
|
||||
|
||||
## 0.16.9
|
||||
|
||||
* `Versions`:
|
||||
* `Koin`: `3.2.2` -> `3.3.2`
|
||||
* `AppCompat`: `1.5.1` -> `1.6.0`
|
||||
* `Ktor`:
|
||||
* `Client`
|
||||
* `HttpResponse.bodyOrNull` now retrieve callback to check if body should be received or null
|
||||
* New extension `HttpResponse.bodyOrNullOnNoContent`
|
||||
|
||||
## 0.16.8
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.2.2` -> `2.2.3`
|
||||
* `Ktor`:
|
||||
* `Client`
|
||||
* Fixes in `HttpClient.uniUpload`
|
||||
* `Server`
|
||||
* Fixes in `PartData.FileItem.download`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* New type of caches: `FallbackCacheRepo`
|
||||
* Fixes in `Write*` variants of cached repos
|
||||
* New type `ActionWrapper`
|
||||
* New `AutoRecache*` classes for all types of repos as `FallbackCacheRepo`s
|
||||
* `Common`:
|
||||
* New transformations for key-value and key-values vice-verse
|
||||
* Fixes in `FileReadKeyValueRepo`
|
||||
|
||||
## 0.16.7
|
||||
|
||||
* `Common`:
|
||||
* New extensions `ifTrue`/`ifFalse`/`alsoIfTrue`/`alsoIfFalse`/`letIfTrue`/`letIfFalse`
|
||||
* `Diff` now is serializable
|
||||
* Add `IndexedValue` serializer
|
||||
* `repeatOnFailure` extending: now you may pass any lambda to check if continue to try/do something
|
||||
* `Compose`:
|
||||
* New extension `MutableState.asState`
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* All the `Flow` conversations to compose `State`/`MutableState`/`SnapshotStateList`/`List` got several new
|
||||
parameters
|
||||
* `Flow.toMutableState` now is deprecated in favor to `asMutableComposeState`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* New type `FullCacheRepo`
|
||||
* New type `CommonCacheRepo`
|
||||
* `CacheRepo` got `invalidate` method. It will fully reload `FullCacheRepo` and just clear `CommonCacheRepo`
|
||||
* New extensions `KVCache.actualizeAll`
|
||||
|
||||
## 0.16.6
|
||||
|
||||
* `Startup`:
|
||||
* `Launcher`:
|
||||
* Improvements in `StartLauncherPlugin#start` methods
|
||||
* Add opportunity to pass second argument on `JVM` platform as log level
|
||||
* `Repos`:
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* All clients repos got opportunity to customize their flows
|
||||
* `Exposed`:
|
||||
* Extensions `eqOrIsNull` and `neqOrIsNotNull` for `Column`
|
||||
|
||||
## 0.16.5
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.2.1` -> `2.2.2`
|
||||
|
||||
## 0.16.4
|
||||
|
||||
* `Coroutines`:
|
||||
* Create `launchInCurrentThread`
|
||||
|
||||
## 0.16.3
|
||||
|
||||
* `Startup`:
|
||||
* `Launcher`:
|
||||
* All starting API have been moved into `StartLauncherPlugin` and do not require serialize/deserialize cycle for now
|
||||
|
||||
## 0.16.2
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.2.1` -> `1.2.2`
|
||||
* `Startup`:
|
||||
* Module become available on `JS` target
|
||||
|
||||
## 0.16.1
|
||||
|
||||
* `Coroutines`:
|
||||
* New `runCatchingSafely`/`safelyWithResult` with receivers
|
||||
* `SafeWrapper`:
|
||||
* Module inited
|
||||
|
||||
## 0.16.0
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.1.3` -> `2.2.1`
|
||||
* `Android Fragment`: `1.5.3` -> `1.5.5`
|
||||
|
||||
## 0.15.1
|
||||
|
||||
* `Startup`:
|
||||
* Inited :)
|
||||
* `Plugin`:
|
||||
* Inited :)
|
||||
* `Launcher`:
|
||||
* Inited :)
|
||||
|
||||
## 0.15.0
|
||||
|
||||
* `Repos`:
|
||||
* `CRUD`:
|
||||
* `Common`:
|
||||
* New method `ReadCRUDRepo#getIdsByPagination`
|
||||
* `Android`:
|
||||
* `AbstractAndroidCRUDRepo` got new abstract method `toId`
|
||||
* `Exposed`:
|
||||
* `CommonExposedRepo` new abstract property `asId`
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `KtorReadCRUDRepoClient` now requires `paginationIdType`
|
||||
* `LanguageCodes`:
|
||||
* Updates and fixes in generation
|
||||
* `MimeTypes`:
|
||||
* Updates and fixes in generation
|
||||
|
||||
## 0.14.4
|
||||
|
||||
* `Common`:
|
||||
* `JVM`:
|
||||
* New extension `downloadToTempFile`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Small fix in `handleUniUpload`
|
||||
* `ApplicationCall#uniloadMultipartFile` now uses `uniloadMultipart`
|
||||
* `Common`:
|
||||
* New extension `downloadToTempFile`
|
||||
* `Client`:
|
||||
* New extensions on top of `uniUpload`
|
||||
|
||||
## 0.14.3
|
||||
|
||||
* `Common`:
|
||||
* New type `Progress`
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* New universal `uniUpload` extension for `HttpClient`
|
||||
* `Server`:
|
||||
* New universal `handleUniUpload` extension for `ApplicationCall`
|
||||
* Add extensions `download` and `downloadToTemporalFile`
|
||||
|
||||
## 0.14.2
|
||||
|
||||
* `Versions`:
|
||||
* `Exposed`: `0.40.1` -> `0.41.1`
|
||||
|
||||
|
||||
## 0.14.1
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `3.3.1` -> `3.4.0`
|
||||
* `UUID`: `0.5.0` -> `0.6.0`
|
||||
|
||||
## 0.14.0
|
||||
|
||||
**ALL DEPRECATIONS HAVE BEEN REMOVED**
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.7.10` -> `1.7.20`
|
||||
* `Klock`: `3.3.0` -> `3.3.1`
|
||||
* `Compose`: `1.2.0` -> `1.2.1`
|
||||
* `Exposed`: `0.39.2` -> `0.40.1`
|
||||
|
||||
## 0.13.2
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `3.1.0` -> `3.3.0`
|
||||
* `Ktor`: `2.1.2` -> `2.1.3`
|
||||
|
||||
## 0.13.1
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* `AbstractExposedWriteCRUDRepo#createAndInsertId` now is optional and returns nullable value
|
||||
|
||||
## 0.13.0
|
||||
|
||||
**ALL DEPRECATIONS HAVE BEEN REMOVED**
|
||||
**A LOT OF KTOR METHODS RELATED TO UnifierRouter/UnifiedRequester HAVE BEEN REMOVED**
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* `AbstractExposedWriteCRUDRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `createAndInsertId`
|
||||
* Old `update` method has been deprecated and not recommended to override anymore in realizations
|
||||
* Old `insert` method now is `open` instead of `abstract` and can be omitted
|
||||
* `AbstractExposedKeyValueRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `insertKey`
|
||||
* Old `update` method has been deprecated and not recommended to override anymore
|
||||
* Old `insert` method now is `open` instead of `abstract` and can be omitted in realizations
|
||||
|
||||
## 0.12.17
|
||||
|
||||
* `Versions`:
|
||||
* `JB Compose`: `1.2.0-alpha01-dev774` -> `1.2.0-beta02`
|
||||
* `Ktor`: `2.1.1` -> `2.1.2`
|
||||
* `Koin`: `3.2.0` -> `3.2.2`
|
||||
|
||||
## 0.12.16
|
||||
|
||||
* `Coroutines`:
|
||||
* `Android`:
|
||||
* Add class `FlowOnHierarchyChangeListener`
|
||||
* Add `ViewGroup#setOnHierarchyChangeListenerRecursively(OnHierarchyChangeListener)`
|
||||
|
||||
## 0.12.15
|
||||
|
||||
* `Common`:
|
||||
* `applyDiff` will return `Diff` object since this release
|
||||
* `Android`:
|
||||
* New functions/extensions `findViewsByTag` and `findViewsByTagInActivity`
|
||||
* `Coroutines`:
|
||||
* Add `Flow` extensions `flatMap`, `flatMapNotNull` and `flatten`
|
||||
* Add `Flow` extensions `takeNotNull` and `filterNotNull`
|
||||
|
||||
## 0.12.14
|
||||
|
||||
* `Versions`:
|
||||
* `Android CoreKTX`: `1.8.0` -> `1.9.0`
|
||||
* `Android AppCompat`: `1.4.2` -> `1.5.1`
|
||||
* Android Compile SDK: 32 -> 33
|
||||
* Android Build Tools: 32.0.0 -> 33.0.0
|
||||
* `Common`:
|
||||
* `Android`:
|
||||
* Add `argumentOrNull`/`argumentOrThrow` delegates for fragments
|
||||
* `Coroutines`:
|
||||
* Rewrite `awaitFirstWithDeferred` onto `CompletableDeferred` instead of coroutines suspending
|
||||
|
||||
## 0.12.13
|
||||
|
||||
* `Coroutines`:
|
||||
* Add opportunity to use markers in actors (solution of [#160](https://github.com/InsanusMokrassar/MicroUtils/issues/160))
|
||||
* `Koin`:
|
||||
* Module inited :)
|
||||
* `Repos`:
|
||||
* `Android`:
|
||||
* Add typealias `KeyValueSPRepo` and opportunity to create shared preferences `KeyValue` repo with `KeyValueStore(...)` (fix of [#155](https://github.com/InsanusMokrassar/MicroUtils/issues/155))
|
||||
|
||||
## 0.12.12
|
||||
|
||||
* `Common`:
|
||||
* `Compose`:
|
||||
* `JS`:
|
||||
* Add `SkeletonAnimation` stylesheet
|
||||
|
||||
## 0.12.11
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Override `KeyValue` cache method `values`
|
||||
|
||||
## 0.12.10
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Hotfix in key values `get`
|
||||
|
||||
## 0.12.9
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `3.0.0` -> `3.1.0`
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Fixes in key values cache
|
||||
|
||||
## 0.12.8
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.1.0` -> `2.1.1`
|
||||
* `Compose`: `1.2.0-alpha01-dev764` -> `1.2.0-alpha01-dev774`
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* New extension `HttpClient#bodyOrNull` which returns `null` in case when server responded with `No Content` (204)
|
||||
* `Server`:
|
||||
* New extension `ApplicationCall#respondOrNoContent` which responds `No Content` (204) when passed data is null
|
||||
|
||||
## 0.12.7
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* Force `WriteCRUDCacheRepo` to subscribe on new and updated objects of parent repo
|
||||
* `Pagination`:
|
||||
* New function `changeResultsUnchecked(Pagination)`
|
||||
|
||||
## 0.12.6
|
||||
|
||||
* `MimeeTypes>`:
|
||||
* Fixed absence of `image/*` in known mime types
|
||||
|
||||
## 0.12.5
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Fixes in `paginate` extensions
|
||||
|
||||
## 0.12.4
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.7.0` -> `1.7.10`
|
||||
* `Compose`: `1.2.0-alpha01-dev755` -> `1.2.0-alpha01-dev764`
|
||||
|
||||
## 0.12.3
|
||||
|
||||
* `Repos`:
|
||||
* `Exposed`:
|
||||
* Add abstract exposed variants of `KeyValue` and `KeyValues` repos
|
||||
* Add new extension `Query#selectPaginated`
|
||||
|
||||
## 0.12.2
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.4.0-RC` -> `1.4.0`
|
||||
* `Compose`: `1.2.0-alpha01-dev753` -> `1.2.0-alpha01-dev755`
|
||||
|
||||
## 0.12.1
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.0.3` -> `2.1.0`
|
||||
|
||||
## 0.12.0
|
||||
|
||||
**OLD DEPRECATIONS HAVE BEEN REMOVED**
|
||||
|
||||
**MINIMAL ANDROID API HAS BEEN ENLARGED UP TO API 21 (Android 5.0)**
|
||||
|
||||
* `Versions`
|
||||
* `Kotlin`: `1.6.21` -> `1.7.0`
|
||||
* `Coroutines`: `1.6.3` -> `1.6.4`
|
||||
* `Exposed`: `0.38.2` -> `0.39.2`
|
||||
* `Compose`: `1.2.0-alpha01-dev729` -> `1.2.0-alpha01-dev753`
|
||||
* `Klock`: `2.7.0` -> `3.0.0`
|
||||
* `uuid`: `0.4.1` -> `0.5.0`
|
||||
* `Android Core KTX`: `1.7.0` -> `1.8.0`
|
||||
* `Android AppCompat`: `1.4.1` -> `1.4.2`
|
||||
* `Ktor`:
|
||||
* All previously standard functions related to work with binary data by default have been deprecated
|
||||
|
||||
## 0.11.14
|
||||
|
||||
* `Pagination`:
|
||||
* `PaginationResult` got new field `objectsNumber` which by default is a times between `pagesNumber` and `size`
|
||||
|
||||
## 0.11.13
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.6.3` -> `1.6.4`
|
||||
* `Compose`: `1.2.0-alpha01-dev629` -> `1.2.0-alpha01-dev731`
|
||||
|
||||
## 0.11.12
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* `JVM`:
|
||||
* Fixes in `ReadFileKeyValueRepo` methods (`values`/`keys`)
|
||||
|
||||
## 0.11.11
|
||||
|
||||
* `Crypto`:
|
||||
* `hmacSha256` has been deprecated
|
||||
* `Ktor`:
|
||||
* `Client`:
|
||||
* `BodyPair` has been deprecated
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* New interface `CacheRepo`
|
||||
* New interface `FullCacheRepo`
|
||||
* `actualize*` methods inside of full cache repos now open for overriding
|
||||
|
||||
## 0.11.10
|
||||
|
||||
* `Repos`:
|
||||
* `Cache`:
|
||||
* `KVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
|
||||
* `SimpleKVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
|
||||
* New `KVCache` subtype - `FullKVCache`
|
||||
* Add `Full*` variants of standard repos
|
||||
* Add `cached`/`caching` (for write repos) extensions for all standard types of repos
|
||||
|
||||
## 0.11.9
|
||||
|
||||
* `Versions`
|
||||
* `Coroutines`: `1.6.1` -> `1.6.3`
|
||||
* `Ktor`: `2.0.2` -> `2.0.3`
|
||||
* `Compose`: `1.2.0-alpha01-dev686` -> `1.2.0-alpha01-dev729`
|
||||
|
||||
## 0.11.8
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Fixes in `FileKeyValueRepo`
|
||||
|
||||
## 0.11.7
|
||||
|
||||
* `Common`:
|
||||
* New abstractions `SimpleMapper` and `SimpleSuspendableMapper`
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* Add mappers for `CRUDRepo`
|
||||
|
||||
## 0.11.6
|
||||
|
||||
* `FSM`:
|
||||
* `Common`
|
||||
* Several fixes related to the jobs handling
|
||||
|
||||
## 0.11.5
|
||||
|
||||
* `Coroutines`:
|
||||
* `Compose`:
|
||||
* Add extension `StateFlow#asMutableComposeListState` and `StateFlow#asComposeList`
|
||||
* Add extension `StateFlow#asMutableComposeState`/`StateFlow#asComposeState`
|
||||
|
||||
## 0.11.4
|
||||
|
||||
**THIS VERSION HAS BEEN BROKEN, DO NOT USE IT**
|
||||
|
||||
## 0.11.3
|
||||
|
||||
* `Ktor`:
|
||||
* Support of `WebSockets` has been improved
|
||||
* `Client`:
|
||||
* New extensions: `HttpClient#openBaseWebSocketFlow`, `HttpClient#openWebSocketFlow`, `HttpClient#openSecureWebSocketFlow`
|
||||
|
||||
## 0.11.2
|
||||
|
||||
* `Ktor`:
|
||||
* Support of `WebSockets` has been improved and added fixes inside of clients
|
||||
|
||||
## 0.11.1
|
||||
|
||||
* `Repos`
|
||||
* `Ktor`
|
||||
* In `configureReadKeyValueRepoRoutes` and `configureReadKeyValuesRepoRoutes` configurators fixed requiring of `reversed` property
|
||||
|
||||
## 0.11.0
|
||||
|
||||
* `Versions`
|
||||
* `UUID`: `0.4.0` -> `0.4.1`
|
||||
* `Ktor`
|
||||
* `Client`:
|
||||
* New extension fun `HttpResponse#throwOnUnsuccess`
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
* `Server`:
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
* `Repos`
|
||||
* `Ktor`:
|
||||
* Fully rewritten work with all declared repositories
|
||||
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
|
||||
|
||||
## 0.10.8
|
||||
|
||||
* `Common`
|
||||
* Add `Element.isOverflow*` extension properties
|
||||
|
||||
## 0.10.7
|
||||
|
||||
* `Pagination`:
|
||||
* Now it is possible to use `doForAll*` and `getForAll` functions in non suspend places
|
||||
|
||||
## 0.10.6
|
||||
|
||||
* `Versions`
|
||||
* `Ktor`: `2.0.1` -> `2.0.2`
|
||||
* `Common`
|
||||
* `JS`:
|
||||
* Add `ResizeObserver` functionality
|
||||
|
||||
## 0.10.5
|
||||
|
||||
* `Versions`
|
||||
* `Compose`: `1.2.0-alpha01-dev683` -> `1.2.0-alpha01-dev686`
|
||||
* `Repos`
|
||||
* `Android`:
|
||||
* New function `SharedPreferencesKeyValueRepo`
|
||||
* `FSM`
|
||||
* Add `StateHandlingErrorHandler` and opportunity to handle states handling errors
|
||||
|
||||
## 0.10.4
|
||||
|
||||
* `Versions`:
|
||||
* `Serialization`: `1.3.2` -> `1.3.3`
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.2.0-alpha01-dev682` -> `1.2.0-alpha01-dev683`
|
||||
* `Coroutines`:
|
||||
* Fixes in `AccumulatorFlow`
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* `Versions`:
|
||||
* `Compose`: `1.2.0-alpha01-dev675` -> `1.2.0-alpha01-dev682`
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `2.0.0` -> `2.0.1`
|
||||
* `Crypto`:
|
||||
* Add `hmacSha256`
|
||||
* Add `hex`
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.6.10` -> `1.6.21`
|
||||
* `Compose`: `1.1.1` -> `1.2.0-alpha01-dev675`
|
||||
* `Exposed`: `0.37.3` -> `0.38.2`
|
||||
* `Ktor`: `1.6.8` -> `2.0.0`
|
||||
* `Dokka`: `1.6.10` -> `1.6.21`
|
||||
|
||||
## 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`:
|
||||
|
@@ -10,7 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.appCompat.resources
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.android.smalltextfield")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,27 +0,0 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
|
||||
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
|
||||
initialVelocity: Float,
|
||||
animationSpec: DecayAnimationSpec<Float>,
|
||||
adjustTarget: ((Float) -> Float)?,
|
||||
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
|
||||
val adjustedTarget = adjustTarget?.invoke(targetValue)
|
||||
|
||||
return if (adjustedTarget != null) {
|
||||
animateTo(
|
||||
targetValue = adjustedTarget,
|
||||
initialVelocity = initialVelocity,
|
||||
block = block
|
||||
)
|
||||
} else {
|
||||
animateDecay(
|
||||
initialVelocity = initialVelocity,
|
||||
animationSpec = animationSpec,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,222 +0,0 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.center
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
|
||||
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
|
||||
}
|
||||
|
||||
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
|
||||
|
||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NumberPicker(
|
||||
number: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
range: IntRange? = null,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||
allowUseManualInput: Boolean = true,
|
||||
onStateChanged: (Int) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val numbersColumnHeight = 36.dp
|
||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||
|
||||
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||
|
||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||
if (range != null) {
|
||||
val offsetRange = remember(number, range) {
|
||||
val value = number
|
||||
val first = -(range.last - value) * halvedNumbersColumnHeightPx
|
||||
val last = -(range.first - value) * halvedNumbersColumnHeightPx
|
||||
first..last
|
||||
}
|
||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||
}
|
||||
}
|
||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||
val animatedStateValue = animatedStateValue(animatedOffset.value)
|
||||
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
|
||||
|
||||
val inputFieldShown = if (allowUseManualInput) {
|
||||
remember { mutableStateOf(false) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { deltaY ->
|
||||
if (inputFieldShown ?.value != true) {
|
||||
coroutineScope.launch {
|
||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
if (inputFieldShown ?.value != true) {
|
||||
coroutineScope.launch {
|
||||
val endValue = animatedOffset.fling(
|
||||
initialVelocity = velocity,
|
||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||
adjustTarget = { target ->
|
||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||
val coercedAnchors =
|
||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||
val base =
|
||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||
coercedPoint + base
|
||||
}
|
||||
).endState.value
|
||||
|
||||
onStateChanged(animatedStateValue(endValue))
|
||||
animatedOffset.snapTo(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val spacing = 4.dp
|
||||
|
||||
val upEnabled = range == null || range.first < number
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(number - 1)
|
||||
inputFieldShown ?.value = false
|
||||
},
|
||||
enabled = upEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val baseLabelModifier = Modifier.align(Alignment.Center)
|
||||
ProvideTextStyle(textStyle) {
|
||||
Text(
|
||||
text = (animatedStateValue - 1).toString(),
|
||||
modifier = baseLabelModifier
|
||||
.offset(y = -halvedNumbersColumnHeight)
|
||||
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||
)
|
||||
|
||||
if (inputFieldShown ?.value == true) {
|
||||
val currentValue = remember { mutableStateOf(number.toString()) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
SmallTextField(
|
||||
currentValue.value,
|
||||
{
|
||||
val asDigit = it.toIntOrNull()
|
||||
when {
|
||||
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
|
||||
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
|
||||
else -> { /* do nothing */ }
|
||||
}
|
||||
},
|
||||
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
|
||||
detectTapGestures {
|
||||
if (!checkContains(it)) {
|
||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||
inputFieldShown.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
currentValue.value.toIntOrNull() ?.let(onStateChanged)
|
||||
inputFieldShown.value = false
|
||||
},
|
||||
singleLine = true,
|
||||
textStyle = textStyle
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = animatedStateValue.toString(),
|
||||
modifier = baseLabelModifier
|
||||
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||
.clickable {
|
||||
if (inputFieldShown ?.value == false) {
|
||||
inputFieldShown.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = (animatedStateValue + 1).toString(),
|
||||
modifier = baseLabelModifier
|
||||
.offset(y = halvedNumbersColumnHeight)
|
||||
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
|
||||
val downEnabled = range == null || range.last > number
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(number + 1)
|
||||
inputFieldShown ?.value = false
|
||||
},
|
||||
enabled = downEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,156 +0,0 @@
|
||||
package dev.inmo.micro_utils.android.pickers
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.*
|
||||
|
||||
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun <T> SetPicker(
|
||||
current: T,
|
||||
dataList: List<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
arrowsColor: Color = MaterialTheme.colorScheme.primary,
|
||||
dataToString: @Composable (T) -> String = { it.toString() },
|
||||
onStateChanged: (T) -> Unit = {},
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
|
||||
textStyle.lineHeight.toDp()
|
||||
}
|
||||
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
|
||||
val halvedNumbersColumnHeight = numbersColumnHeight / 2
|
||||
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
|
||||
|
||||
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
|
||||
val lastIndex = dataList.size - 1
|
||||
|
||||
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
|
||||
|
||||
val animatedOffset = remember { Animatable(0f) }.apply {
|
||||
val offsetRange = remember(index, lastIndex) {
|
||||
val value = index
|
||||
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
|
||||
val last = value * halvedNumbersColumnHeightPx
|
||||
first..last
|
||||
}
|
||||
updateBounds(offsetRange.start, offsetRange.endInclusive)
|
||||
}
|
||||
val indexAnimatedOffset = if (animatedOffset.value > 0) {
|
||||
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||
} else {
|
||||
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
|
||||
}
|
||||
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
|
||||
val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset
|
||||
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { deltaY ->
|
||||
coroutineScope.launch {
|
||||
animatedOffset.snapTo(animatedOffset.value + deltaY)
|
||||
}
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
coroutineScope.launch {
|
||||
val endValue = animatedOffset.fling(
|
||||
initialVelocity = velocity,
|
||||
animationSpec = exponentialDecay(frictionMultiplier = 20f),
|
||||
adjustTarget = { target ->
|
||||
val coercedTarget = target % halvedNumbersColumnHeightPx
|
||||
val coercedAnchors =
|
||||
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
|
||||
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
|
||||
val base =
|
||||
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
|
||||
coercedPoint + base
|
||||
}
|
||||
).endState.value
|
||||
|
||||
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
|
||||
animatedOffset.snapTo(0f)
|
||||
}
|
||||
}
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val spacing = 4.dp
|
||||
|
||||
val upEnabled = index > 0
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(dataList.elementAt(index - 1))
|
||||
},
|
||||
enabled = upEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
Box(
|
||||
modifier = Modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProvideTextStyle(textStyle) {
|
||||
dataList.forEachIndexed { i, t ->
|
||||
val alpha = when {
|
||||
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
|
||||
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
val offset = when {
|
||||
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
|
||||
i == indexAnimatedOffset -> coercedAnimatedOffset
|
||||
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
|
||||
else -> return@forEachIndexed
|
||||
}
|
||||
Text(
|
||||
text = dataToString(t),
|
||||
modifier = Modifier
|
||||
.alpha(alpha)
|
||||
.offset(y = with(LocalDensity.current) { offset.toDp() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(spacing))
|
||||
|
||||
val downEnabled = index < lastIndex
|
||||
IconButton(
|
||||
{
|
||||
onStateChanged(dataList.elementAt(index + 1))
|
||||
},
|
||||
enabled = downEnabled
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,13 +10,13 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.recyclerView
|
||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
1
android/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.android.recyclerview"/>
|
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.android.compose.material3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,66 +0,0 @@
|
||||
package dev.inmo.micro_utils.android.smalltextfield
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SmallTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
textColor: Color = textStyle.color.takeOrElse {
|
||||
LocalContentColor.current
|
||||
},
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier = modifier,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle.copy(
|
||||
color = textColor
|
||||
),
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
cursorBrush = SolidColor(
|
||||
textStyle.color.takeOrElse {
|
||||
LocalContentColor.current
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
20
build.gradle
20
build.gradle
@@ -7,27 +7,20 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
classpath libs.buildscript.kt.ksp
|
||||
classpath libs.buildscript.jb.dokka
|
||||
classpath libs.buildscript.gh.release
|
||||
classpath libs.buildscript.android.gradle
|
||||
classpath libs.buildscript.android.dexcount
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
@@ -42,4 +35,3 @@ allprojects {
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
apply from: "./github_release.gradle"
|
||||
apply from: "./versions_plugin_setup.gradle"
|
||||
|
@@ -16,19 +16,6 @@ kotlin {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
api libs.android.fragment
|
||||
}
|
||||
dependsOn jvmMain
|
||||
}
|
||||
|
||||
linuxX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
mingwX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,16 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
|
||||
class DefaultDisposableEffectResult(
|
||||
private val onDispose: () -> Unit
|
||||
) : DisposableEffectResult {
|
||||
override fun dispose() {
|
||||
onDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DoNothing = DefaultDisposableEffectResult {}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
|
||||
/**
|
||||
* Converts current [MutableState] to immutable [State] using [derivedStateOf]
|
||||
*/
|
||||
fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value }
|
@@ -1,9 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composition
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Composition.linkWithElement(element: Element) {
|
||||
element.onRemoved { dispose() }
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.attributes.ATarget
|
||||
|
||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||
link,
|
||||
mode.targetStr,
|
||||
features
|
||||
)
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun <TElement : Element> renderComposableAndLinkToRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithElement(root)
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.css.*
|
||||
|
||||
object SkeletonAnimation : StyleSheet() {
|
||||
val skeletonKeyFrames: CSSNamedKeyframes by keyframes {
|
||||
to { backgroundPosition("-20% 0") }
|
||||
}
|
||||
|
||||
fun CSSBuilder.includeSkeletonStyle(
|
||||
duration: CSSSizeValue<out CSSUnitTime> = 2.s,
|
||||
timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut,
|
||||
iterationCount: Int? = null,
|
||||
direction: AnimationDirection = AnimationDirection.Normal,
|
||||
keyFrames: CSSNamedKeyframes = skeletonKeyFrames,
|
||||
hideChildren: Boolean = true,
|
||||
hideText: Boolean = hideChildren
|
||||
) {
|
||||
backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)")
|
||||
backgroundSize("200% 100%")
|
||||
backgroundPosition("180% 0")
|
||||
animation(keyFrames) {
|
||||
duration(duration)
|
||||
timingFunction(timingFunction)
|
||||
iterationCount(iterationCount)
|
||||
direction(direction)
|
||||
}
|
||||
if (hideText) {
|
||||
property("color", "${Color.transparent} !important")
|
||||
}
|
||||
|
||||
if (hideChildren) {
|
||||
child(self, universal) style {
|
||||
property("visibility", "hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val skeleton by style {
|
||||
includeSkeletonStyle()
|
||||
}
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,76 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
object ArgumentPropertyNullableDelegate {
|
||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? {
|
||||
val arguments = thisRef.arguments ?: return null
|
||||
val key = property.name
|
||||
return when (property.getter.returnType.classifier) {
|
||||
// Scalars
|
||||
String::class -> arguments.getString(key)
|
||||
Boolean::class -> arguments.getBoolean(key)
|
||||
Byte::class -> arguments.getByte(key)
|
||||
Char::class -> arguments.getChar(key)
|
||||
Double::class -> arguments.getDouble(key)
|
||||
Float::class -> arguments.getFloat(key)
|
||||
Int::class -> arguments.getInt(key)
|
||||
Long::class -> arguments.getLong(key)
|
||||
Short::class -> arguments.getShort(key)
|
||||
|
||||
// References
|
||||
Bundle::class -> arguments.getBundle(key)
|
||||
CharSequence::class -> arguments.getCharSequence(key)
|
||||
Parcelable::class -> arguments.getParcelable(key)
|
||||
|
||||
// Scalar arrays
|
||||
BooleanArray::class -> arguments.getBooleanArray(key)
|
||||
ByteArray::class -> arguments.getByteArray(key)
|
||||
CharArray::class -> arguments.getCharArray(key)
|
||||
DoubleArray::class -> arguments.getDoubleArray(key)
|
||||
FloatArray::class -> arguments.getFloatArray(key)
|
||||
IntArray::class -> arguments.getIntArray(key)
|
||||
LongArray::class -> arguments.getLongArray(key)
|
||||
ShortArray::class -> arguments.getShortArray(key)
|
||||
Array::class -> {
|
||||
val componentType = property.returnType.classifier ?.javaClass ?.componentType!!
|
||||
@Suppress("UNCHECKED_CAST") // Checked by reflection.
|
||||
when {
|
||||
Parcelable::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getParcelableArray(key)
|
||||
}
|
||||
String::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getStringArray(key)
|
||||
}
|
||||
CharSequence::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getCharSequenceArray(key)
|
||||
}
|
||||
Serializable::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getSerializable(key)
|
||||
}
|
||||
else -> {
|
||||
val valueType = componentType.canonicalName
|
||||
throw IllegalArgumentException(
|
||||
"Illegal value array type $valueType for key \"$key\""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Serializable::class -> arguments.getSerializable(key)
|
||||
else -> null
|
||||
} as? T
|
||||
}
|
||||
}
|
||||
|
||||
object ArgumentPropertyNonNullableDelegate {
|
||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||
return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!!
|
||||
}
|
||||
}
|
||||
|
||||
fun argumentOrNull() = ArgumentPropertyNullableDelegate
|
||||
fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate
|
@@ -1,61 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun findViewsByTag(viewGroup: ViewGroup, tag: Any?): List<View> {
|
||||
return viewGroup.children.flatMap {
|
||||
findViewsByTag(it, tag)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(viewGroup: ViewGroup, key: Int, tag: Any?): List<View> {
|
||||
return viewGroup.children.flatMap {
|
||||
findViewsByTag(it, key, tag)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(view: View, tag: Any?): List<View> {
|
||||
val result = mutableListOf<View>()
|
||||
if (view.tag == tag) {
|
||||
result.add(view)
|
||||
}
|
||||
if (view is ViewGroup) {
|
||||
result.addAll(findViewsByTag(view, tag))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(view: View, key: Int, tag: Any?): List<View> {
|
||||
val result = mutableListOf<View>()
|
||||
if (view.getTag(key) == tag) {
|
||||
result.add(view)
|
||||
}
|
||||
if (view is ViewGroup) {
|
||||
result.addAll(findViewsByTag(view, key, tag))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun Activity.findViewsByTag(tag: Any?) = rootView ?.let {
|
||||
findViewsByTag(it, tag)
|
||||
}
|
||||
|
||||
fun Activity.findViewsByTag(key: Int, tag: Any?) = rootView ?.let {
|
||||
findViewsByTag(it, key, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTag(tag: Any?) = view ?.let {
|
||||
findViewsByTag(it, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTag(key: Int, tag: Any?) = view ?.let {
|
||||
findViewsByTag(it, key, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTagInActivity(tag: Any?) = activity ?.findViewsByTag(tag)
|
||||
|
||||
fun Fragment.findViewsByTagInActivity(key: Int, tag: Any?) = activity ?.findViewsByTag(key, tag)
|
@@ -1,7 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
|
||||
val Activity.rootView: View?
|
||||
get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@RequiresOptIn(
|
||||
|
@@ -2,8 +2,6 @@
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private inline fun <T> getObject(
|
||||
additional: MutableList<T>,
|
||||
iterator: Iterator<T>
|
||||
@@ -16,29 +14,16 @@ private inline fun <T> getObject(
|
||||
/**
|
||||
* Diff object which contains information about differences between two [Iterable]s
|
||||
*
|
||||
* See tests for more info
|
||||
*
|
||||
* @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection
|
||||
* @param added The object which appear in new collection only. Indexes here show the index in the new collection
|
||||
* @param replaced Pair of old-new changes. First object has been presented in the old collection on its
|
||||
* [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in
|
||||
* case when some value has been replaced after adds or removes in original collection the object index will be changed
|
||||
*
|
||||
* @see calculateDiff
|
||||
*/
|
||||
@Serializable
|
||||
data class Diff<T> internal constructor(
|
||||
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
||||
val removed: List<IndexedValue<T>>,
|
||||
/**
|
||||
* Old-New values pairs
|
||||
*/
|
||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||
) {
|
||||
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||
}
|
||||
|
||||
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
val added: List<IndexedValue<T>>
|
||||
)
|
||||
|
||||
private inline fun <T> performChanges(
|
||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||
@@ -47,18 +32,17 @@ private inline fun <T> performChanges(
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
strictComparison: Boolean
|
||||
) {
|
||||
var i = -1
|
||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||
i++
|
||||
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||
changedList.addAll(
|
||||
potentialChanges.take(i).mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||
}
|
||||
)
|
||||
@@ -108,7 +92,7 @@ private inline fun <T> performChanges(
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> {
|
||||
var i = -1
|
||||
var j = -1
|
||||
@@ -136,60 +120,31 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
}
|
||||
|
||||
when {
|
||||
comparisonFun(oldObject, newObject) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||
})
|
||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
||||
potentiallyChangedObjects.clear()
|
||||
}
|
||||
else -> {
|
||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||
val previousOldsAdditionsSize = additionalInOld.size
|
||||
val previousNewsAdditionsSize = additionalInNew.size
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
potentiallyChangedObjects.add(null to null)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculating [Diff] object
|
||||
*
|
||||
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
|
||||
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(
|
||||
other,
|
||||
comparisonFun = if (strictComparison) {
|
||||
{ t1, t2 ->
|
||||
t1 === t2
|
||||
}
|
||||
} else {
|
||||
{ t1, t2 ->
|
||||
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
|
||||
}
|
||||
}
|
||||
)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(other, strictComparison)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
noinline comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(other, comparisonFun)
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||
|
||||
/**
|
||||
@@ -198,52 +153,3 @@ 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)
|
||||
|
||||
/**
|
||||
* Applies [diff] to [this] [MutableList]
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
diff: Diff<T>
|
||||
) {
|
||||
for (i in diff.removed.indices.sortedDescending()) {
|
||||
removeAt(diff.removed[i].index)
|
||||
}
|
||||
diff.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
diff.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
|
||||
* [Diff.replaced] values will be reversed too
|
||||
*/
|
||||
fun <T> Diff<T>.reversed() = Diff(
|
||||
removed = added,
|
||||
replaced = replaced.map { it.second to it.first },
|
||||
added = removed
|
||||
)
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("unused", "NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
@@ -23,18 +21,26 @@ import kotlinx.serialization.encoding.*
|
||||
sealed interface Either<T1, T2> {
|
||||
val optionalT1: Optional<T1>
|
||||
val optionalT2: Optional<T2>
|
||||
|
||||
val t1OrNull: T1?
|
||||
@Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1"))
|
||||
val t1: T1?
|
||||
get() = optionalT1.dataOrNull()
|
||||
val t2OrNull: T2?
|
||||
@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(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
@@ -45,6 +51,7 @@ class EitherSerializer<T1, T2>(
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
@@ -76,6 +83,7 @@ class EitherSerializer<T1, T2>(
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
@@ -97,7 +105,7 @@ class EitherSerializer<T1, T2>(
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
val t1: T1
|
||||
override val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = t1.optional
|
||||
override val optionalT2: Optional<T2> = Optional.absent()
|
||||
@@ -108,7 +116,7 @@ data class EitherFirst<T1, T2>(
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
val t2: T2
|
||||
override val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = Optional.absent()
|
||||
override val optionalT2: Optional<T2> = t2.optional
|
||||
|
@@ -1,43 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
|
||||
return if (this) {
|
||||
block()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
|
||||
return if (this) {
|
||||
null
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
|
||||
letIfTrue(block)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
|
||||
letIfFalse(block)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
|
||||
return if (this) {
|
||||
block()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
|
||||
return if (this) {
|
||||
null
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.PairSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> {
|
||||
private val originalSerializer = PairSerializer(Int.serializer(), subSerializer)
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = originalSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): IndexedValue<T> {
|
||||
val pair = originalSerializer.deserialize(decoder)
|
||||
return IndexedValue(
|
||||
pair.first,
|
||||
pair.second
|
||||
)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IndexedValue<T>) {
|
||||
originalSerializer.serialize(
|
||||
encoder,
|
||||
Pair(value.index, value.value)
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Contains diff based on the comparison of objects with the same [K].
|
||||
*
|
||||
* @param removed Contains map with keys removed from parent map
|
||||
* @param changed Contains map with keys values changed new map in comparison with old one
|
||||
* @param added Contains map with new keys and values
|
||||
*/
|
||||
data class MapDiff<K, V> @Warning(warning) constructor(
|
||||
val removed: Map<K, V>,
|
||||
val changed: Map<K, Pair<V, V>>,
|
||||
val added: Map<K, V>
|
||||
) {
|
||||
fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty()
|
||||
inline fun isNotEmpty() = !isEmpty()
|
||||
|
||||
companion object {
|
||||
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
|
||||
fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> createCompareFun(
|
||||
strictComparison: Boolean
|
||||
): (K, V, V) -> Boolean = if (strictComparison) {
|
||||
{ _, first, second -> first === second }
|
||||
} else {
|
||||
{ _, first, second -> first == second }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
||||
*
|
||||
* @param compareFun Will be used to determine changed values
|
||||
*/
|
||||
fun <K, V> Map<K, V>.diff(
|
||||
other: Map<K, V>,
|
||||
compareFun: (K, V, V) -> Boolean
|
||||
): MapDiff<K, V> {
|
||||
val removed: Map<K, V> = (keys - other.keys).associateWith {
|
||||
getValue(it)
|
||||
}
|
||||
val added: Map<K, V> = (other.keys - keys).associateWith {
|
||||
other.getValue(it)
|
||||
}
|
||||
val changed = keys.intersect(other.keys).mapNotNull {
|
||||
val old = getValue(it)
|
||||
val new = other.getValue(it)
|
||||
if (compareFun(it, old, new)) {
|
||||
return@mapNotNull null
|
||||
} else {
|
||||
it to (old to new)
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
return MapDiff(
|
||||
removed,
|
||||
changed,
|
||||
added
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
||||
*
|
||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
||||
* `equals` will be used
|
||||
*/
|
||||
fun <K, V> Map<K, V>.diff(
|
||||
other: Map<K, V>,
|
||||
strictComparison: Boolean = false
|
||||
): MapDiff<K, V> = diff(
|
||||
other,
|
||||
compareFun = createCompareFun(strictComparison)
|
||||
)
|
||||
|
||||
/**
|
||||
* Will apply [mapDiff] to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
mapDiff: MapDiff<K, V>
|
||||
) {
|
||||
mapDiff.apply {
|
||||
removed.keys.forEach { remove(it) }
|
||||
changed.forEach { (k, oldNew) ->
|
||||
put(k, oldNew.second)
|
||||
}
|
||||
added.forEach { (k, new) ->
|
||||
put(k, new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will apply changes with [from] map into [this] one
|
||||
*
|
||||
* @param compareFun Will be used to determine changed values
|
||||
*
|
||||
* @return [MapDiff] applied to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
from: Map<K, V>,
|
||||
compareFun: (K, V, V) -> Boolean
|
||||
): MapDiff<K, V> {
|
||||
return diff(from, compareFun).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will apply changes with [from] map into [this] one
|
||||
*
|
||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
||||
* `equals` will be used
|
||||
*
|
||||
* @return [MapDiff] applied to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
from: Map<K, V>,
|
||||
strictComparison: Boolean = false
|
||||
): MapDiff<K, V> = applyDiff(
|
||||
from,
|
||||
compareFun = createCompareFun(strictComparison)
|
||||
)
|
||||
|
||||
/**
|
||||
* Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the
|
||||
* [MapDiff.changed] values will be reversed too
|
||||
*/
|
||||
fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff(
|
||||
removed = added,
|
||||
changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first },
|
||||
added = removed
|
||||
)
|
@@ -1,53 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
interface SimpleMapper<T1, T2> {
|
||||
fun convertToT1(from: T2): T1
|
||||
fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleMapperImpl<T1, T2>(
|
||||
private val t1: (T2) -> T1,
|
||||
private val t2: (T1) -> T2,
|
||||
) : SimpleMapper<T1, T2> {
|
||||
override fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleMapper(
|
||||
noinline t1: (T2) -> T1,
|
||||
noinline t2: (T1) -> T2,
|
||||
) = SimpleMapperImpl(t1, t2)
|
||||
|
||||
interface SimpleSuspendableMapper<T1, T2> {
|
||||
suspend fun convertToT1(from: T2): T1
|
||||
suspend fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleSuspendableMapperImpl<T1, T2>(
|
||||
private val t1: suspend (T2) -> T1,
|
||||
private val t2: suspend (T1) -> T2,
|
||||
) : SimpleSuspendableMapper<T1, T2> {
|
||||
override suspend fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleSuspendableMapper(
|
||||
noinline t1: suspend (T2) -> T1,
|
||||
noinline t2: suspend (T1) -> T2,
|
||||
) = SimpleSuspendableMapperImpl(t1, t2)
|
@@ -41,18 +41,10 @@ data class Optional<T> internal constructor(
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
inline val <T : Any> T?.optionalOrAbsentIfNull
|
||||
get() = if (this == null) {
|
||||
Optional.absent<T>()
|
||||
} else {
|
||||
Optional.presented(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||
@OptIn(Warning::class)
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||
}
|
||||
|
||||
@@ -60,7 +52,6 @@ inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
@OptIn(Warning::class)
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||
}
|
||||
|
||||
@@ -68,7 +59,6 @@ inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
@OptIn(Warning::class)
|
||||
if (!dataPresented) { block() }
|
||||
}
|
||||
|
||||
@@ -76,22 +66,27 @@ inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||
@OptIn(Warning::class)
|
||||
if (!dataPresented) { block() } else null
|
||||
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() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
@Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse"))
|
||||
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
||||
|
@@ -1,28 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
|
||||
/**
|
||||
* Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort()
|
||||
/**
|
||||
* Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte()
|
||||
|
||||
/**
|
||||
* Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
||||
*/
|
||||
fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
|
||||
/**
|
||||
* Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()
|
||||
|
||||
/**
|
||||
* Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte()
|
@@ -1,37 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Progress private constructor(
|
||||
val of1: Double
|
||||
) {
|
||||
val of1Float
|
||||
get() = of1.toFloat()
|
||||
val of100
|
||||
get() = of1 * 100
|
||||
val of100Float
|
||||
get() = of100.toFloat()
|
||||
val of100Int
|
||||
get() = of100.toInt()
|
||||
|
||||
init {
|
||||
require(of1 in rangeOfValues) {
|
||||
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val rangeOfValues = 0.0 .. 1.0
|
||||
|
||||
val START = Progress(rangeOfValues.start)
|
||||
val COMPLETED = Progress(rangeOfValues.endInclusive)
|
||||
|
||||
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
|
||||
operator fun invoke(part: Number, total: Number) = Progress(
|
||||
part.toDouble() / total.toDouble()
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
@file:Suppress(
|
||||
"RemoveRedundantCallsOfConversionMethods",
|
||||
"RedundantVisibilityModifier",
|
||||
)
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.Byte
|
||||
import kotlin.Double
|
||||
import kotlin.Float
|
||||
import kotlin.Int
|
||||
import kotlin.Long
|
||||
import kotlin.Short
|
||||
import kotlin.Suppress
|
||||
|
||||
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
|
||||
|
||||
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
|
||||
|
||||
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()
|
@@ -1,27 +1,5 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
onFailure: (Throwable) -> Boolean,
|
||||
action: () -> R
|
||||
): Result<R> {
|
||||
do {
|
||||
runCatching {
|
||||
action()
|
||||
}.onFailure {
|
||||
if (!onFailure(it)) {
|
||||
return Result.failure(it)
|
||||
}
|
||||
}.onSuccess {
|
||||
return Result.success(it)
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
@@ -32,23 +10,12 @@ inline fun <R> repeatOnFailure(
|
||||
onEachFailure: (Throwable) -> Unit = {},
|
||||
action: (Int) -> R
|
||||
): Optional<R> {
|
||||
var i = 0
|
||||
val result = repeatOnFailure(
|
||||
{
|
||||
onEachFailure(it)
|
||||
if (i < times) {
|
||||
i++
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
repeat(times) {
|
||||
runCatching {
|
||||
action(it)
|
||||
}.onFailure(onEachFailure).onSuccess {
|
||||
return Optional.presented(it)
|
||||
}
|
||||
) {
|
||||
action(i)
|
||||
}
|
||||
return if (result.isSuccess) {
|
||||
Optional.presented(result.getOrThrow())
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
return Optional.absent()
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
val FixedSignsRange = 0 .. 100
|
||||
|
||||
expect fun Float.fixed(signs: Int): Float
|
||||
expect fun Double.fixed(signs: Int): Double
|
@@ -32,7 +32,7 @@ class DiffUtilsTests {
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
for ((i, v) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
@@ -54,8 +54,8 @@ class DiffUtilsTests {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in oldList.indices) {
|
||||
for ((i, _) in withIndex) {
|
||||
for (step in 0 until oldList.size) {
|
||||
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)
|
||||
@@ -73,83 +73,4 @@ class DiffUtilsTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleRemoveApplyWorks() {
|
||||
val oldList = (0 until 10).toList()
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val removedSublist = oldList.subList(i, i + count)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
val targetList = oldList - removedSublist
|
||||
|
||||
mutableOldList.applyDiff(targetList)
|
||||
|
||||
assertEquals(
|
||||
targetList,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleAddApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||
val mutable = oldList.toMutableList()
|
||||
mutable.addAll(i, addedSublist)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
|
||||
mutableOldList.applyDiff(mutable)
|
||||
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleChangesApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in oldList.indices) {
|
||||
for ((i, _) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
|
||||
val newList = if (step == 0) {
|
||||
i until oldList.size
|
||||
} else {
|
||||
i until oldList.size step step
|
||||
}
|
||||
newList.forEach { index ->
|
||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||
mutable[index] = it.value
|
||||
}
|
||||
}
|
||||
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
mutableOldList.applyDiff(mutable)
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,61 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
|
||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||
lateinit var observer: MutationObserver
|
||||
|
||||
observer = MutationObserver { _, _ ->
|
||||
fun checkIfRemoved(node: Node): Boolean {
|
||||
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
|
||||
}
|
||||
|
||||
if (checkIfRemoved(this)) {
|
||||
observer.disconnect()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previousIntersectionRatio = -1f
|
||||
val observer = IntersectionObserver { entries, observer ->
|
||||
entries.forEach {
|
||||
if (previousIntersectionRatio != it.intersectionRatio) {
|
||||
previousIntersectionRatio = it.intersectionRatio.toFloat()
|
||||
it.block(previousIntersectionRatio, observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(this)
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
|
||||
var previous = -1f
|
||||
onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio > 0 && previous == 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previous = -1f
|
||||
return onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio == 0f && previous != 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,124 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.DOMRectReadOnly
|
||||
import org.w3c.dom.Element
|
||||
|
||||
external interface IntersectionObserverOptions {
|
||||
/**
|
||||
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
|
||||
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
|
||||
* visible.
|
||||
*/
|
||||
var root: Element?
|
||||
|
||||
/**
|
||||
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
|
||||
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
|
||||
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
|
||||
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
var rootMargin: String?
|
||||
|
||||
/**
|
||||
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
|
||||
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
|
||||
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
|
||||
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
|
||||
*/
|
||||
var threshold: Array<Number>?
|
||||
}
|
||||
fun IntersectionObserverOptions(
|
||||
block: IntersectionObserverOptions.() -> Unit = {}
|
||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
||||
|
||||
external interface IntersectionObserverEntry {
|
||||
/**
|
||||
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
|
||||
* the documentation for Element.getBoundingClientRect().
|
||||
*/
|
||||
val boundingClientRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
*/
|
||||
val intersectionRatio: Number
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly representing the target's visible area.
|
||||
*/
|
||||
val intersectionRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
|
||||
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
|
||||
* then you know the transition is from intersecting to not-intersecting.
|
||||
*/
|
||||
val isIntersecting: Boolean
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
*/
|
||||
val rootBounds: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* The Element whose intersection with the root changed.
|
||||
*/
|
||||
val target: Element
|
||||
|
||||
/**
|
||||
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
|
||||
* IntersectionObserver's time origin.
|
||||
*/
|
||||
val time: Double
|
||||
}
|
||||
|
||||
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
|
||||
|
||||
/**
|
||||
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
|
||||
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
|
||||
*/
|
||||
external class IntersectionObserver(callback: IntersectionObserverCallback) {
|
||||
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
|
||||
|
||||
/**
|
||||
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
|
||||
* was passed to the constructor or its value is null, the top-level document's viewport is used.
|
||||
*/
|
||||
val root: Element
|
||||
|
||||
/**
|
||||
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
|
||||
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
|
||||
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
|
||||
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
val rootMargin: String
|
||||
|
||||
/**
|
||||
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
|
||||
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
|
||||
* crossed for that target. If no value was passed to the constructor, 0 is used.
|
||||
*/
|
||||
val thresholds: Array<Number>
|
||||
|
||||
/**
|
||||
* Stops the IntersectionObserver object from observing any target.
|
||||
*/
|
||||
fun disconnect()
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver a target element to observe.
|
||||
*/
|
||||
fun observe(targetElement: Element)
|
||||
|
||||
/**
|
||||
* Returns an array of IntersectionObserverEntry objects for all observed targets.
|
||||
*/
|
||||
fun takeRecords(): Array<IntersectionObserverEntry>
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver to stop observing a particular target element.
|
||||
*/
|
||||
fun unobserve(targetElement: Element)
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.Element
|
||||
|
||||
inline val Element.isOverflowWidth
|
||||
get() = scrollWidth > clientWidth
|
||||
|
||||
inline val Element.isOverflowHeight
|
||||
get() = scrollHeight > clientHeight
|
||||
|
||||
inline val Element.isOverflow
|
||||
get() = isOverflowHeight || isOverflowWidth
|
@@ -1,38 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventListener
|
||||
|
||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
||||
lateinit var observer: MutationObserver
|
||||
val listener = EventListener {
|
||||
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
|
||||
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
|
||||
val childrenGettingElement = elementsToCheck.removeFirst()
|
||||
for (i in 0 until childrenGettingElement.childElementCount) {
|
||||
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
|
||||
}
|
||||
}
|
||||
if (elementsToCheck.isEmpty()) {
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
if (options == null) {
|
||||
document.addEventListener(type, listener)
|
||||
} else {
|
||||
document.addEventListener(type, listener, options)
|
||||
}
|
||||
observer = onRemoved {
|
||||
if (options == null) {
|
||||
document.removeEventListener(type, listener)
|
||||
} else {
|
||||
document.removeEventListener(type, listener, options)
|
||||
}
|
||||
observer.disconnect()
|
||||
}
|
||||
return listener
|
||||
}
|
||||
|
||||
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
@@ -1,8 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
||||
window.open(link, target, features) ?.focus()
|
||||
}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.*
|
||||
import kotlin.js.Json
|
||||
import kotlin.js.json
|
||||
|
||||
external class ResizeObserver(
|
||||
callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit
|
||||
) {
|
||||
fun observe(target: Element, options: Json = definedExternally)
|
||||
|
||||
fun unobserve(target: Element)
|
||||
|
||||
fun disconnect()
|
||||
}
|
||||
|
||||
external interface ResizeObserverSize {
|
||||
val blockSize: Float
|
||||
val inlineSize: Float
|
||||
}
|
||||
|
||||
external interface ResizeObserverEntry {
|
||||
val borderBoxSize: Array<ResizeObserverSize>
|
||||
val contentBoxSize: Array<ResizeObserverSize>
|
||||
val devicePixelContentBoxSize: Array<ResizeObserverSize>
|
||||
val contentRect: DOMRectReadOnly
|
||||
val target: Element
|
||||
}
|
||||
|
||||
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
|
||||
target,
|
||||
json(
|
||||
"box" to options.box ?.name
|
||||
)
|
||||
)
|
||||
|
||||
class ResizeObserverObserveOptions(
|
||||
val box: Box? = null
|
||||
) {
|
||||
sealed interface Box {
|
||||
val name: String
|
||||
|
||||
object Content : Box {
|
||||
override val name: String
|
||||
get() = "content-box"
|
||||
}
|
||||
|
||||
object Border : Box {
|
||||
override val name: String
|
||||
get() = "border-box"
|
||||
}
|
||||
|
||||
object DevicePixelContent : Box {
|
||||
override val name: String
|
||||
get() = "device-pixel-content-box"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.dom.createElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.files.get
|
||||
|
||||
fun selectFile(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {},
|
||||
onFile: (MPPFile) -> Unit
|
||||
) {
|
||||
(document.createElement("input") {
|
||||
(this as HTMLInputElement).apply {
|
||||
type = "file"
|
||||
onchange = {
|
||||
runCatching {
|
||||
files ?.get(0) ?: error("File must not be null")
|
||||
}.onSuccess {
|
||||
onFile(it)
|
||||
}.onFailure {
|
||||
onFailure(it)
|
||||
}
|
||||
}
|
||||
inputSetup(this)
|
||||
}
|
||||
} as HTMLElement).click()
|
||||
}
|
||||
|
@@ -1,14 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
|
||||
fun triggerDownloadFile(filename: String, fileLink: String) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
hiddenElement.href = fileLink
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = filename
|
||||
hiddenElement.click()
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
|
||||
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()
|
@@ -1,20 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
|
||||
fun InputStream.downloadToTempFile(
|
||||
fileName: String = UUID.randomUUID().toString(),
|
||||
fileExtension: String? = ".temp",
|
||||
folder: File? = null
|
||||
) = File.createTempFile(
|
||||
fileName,
|
||||
fileExtension,
|
||||
folder
|
||||
).apply {
|
||||
outputStream().use {
|
||||
copyTo(it)
|
||||
}
|
||||
deleteOnExit()
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toFloat();
|
||||
|
||||
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toDouble();
|
@@ -1,36 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
actual typealias MPPFile = Path
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(toString())
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||
it.size()
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = {
|
||||
FileSystem.SYSTEM.read(this) {
|
||||
readByteArray()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
bytesAllocatorSync()
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Float.fixed(signs: Int): Float {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toDouble()
|
||||
}
|
||||
}
|
1
common/src/main/AndroidManifest.xml
Normal file
1
common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.common"/>
|
@@ -1,36 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
actual typealias MPPFile = Path
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(toString())
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||
it.size()
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = {
|
||||
FileSystem.SYSTEM.read(this) {
|
||||
readByteArray()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
bytesAllocatorSync()
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Float.fixed(signs: Int): Float {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toDouble()
|
||||
}
|
||||
}
|
@@ -10,19 +10,13 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines.android
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
}
|
||||
dependsOn(jvmMain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.coroutines")
|
||||
api project(":micro_utils.common.compose")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,60 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import dev.inmo.micro_utils.common.applyDiff
|
||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList]
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): SnapshotStateList<T> {
|
||||
val state = mutableStateListOf<T>()
|
||||
val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
|
||||
{
|
||||
withContext(useContextOnChange) {
|
||||
state.applyDiff(it)
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
state.applyDiff(it)
|
||||
}
|
||||
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* In fact, it is just classcast of [asMutableComposeListState] to [List]
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*
|
||||
* @return Changing in time [List] which follow [Flow] values
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asComposeList(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): List<T> = asMutableComposeListState(scope, useContextOnChange, onException)
|
||||
|
@@ -1,94 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.asState
|
||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
||||
import dev.inmo.micro_utils.coroutines.doInUI
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow]
|
||||
*
|
||||
* @param initial First value which will be passed to the result [MutableState]
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
fun <T> Flow<T>.asMutableComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): MutableState<T> {
|
||||
val state = mutableStateOf(initial)
|
||||
val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let {
|
||||
{
|
||||
withContext(useContextOnChange) {
|
||||
state.value = it
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
state.value = it
|
||||
}
|
||||
subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock)
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow].
|
||||
* This conversation will pass its [StateFlow.value] as the first value
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asMutableComposeState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException)
|
||||
|
||||
/**
|
||||
* Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state
|
||||
*
|
||||
* @param initial First value which will be passed to the result [State]
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
fun <T> Flow<T>.asComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): State<T> {
|
||||
val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
|
||||
return state.asState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asComposeState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): State<T> = asComposeState(value, scope, useContextOnChange, onException)
|
||||
|
@@ -1,14 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun Composition.linkWithJob(job: Job) {
|
||||
job.invokeOnCompletion {
|
||||
this@linkWithJob.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
@@ -1,26 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.linkWithElement
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(
|
||||
currentCoroutineContext()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(currentCoroutineContext())
|
||||
linkWithElement(root)
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<manifest/>
|
@@ -1,50 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
/**
|
||||
* [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener]
|
||||
*
|
||||
* @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this
|
||||
* [ViewGroup] too
|
||||
* @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow
|
||||
* @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow
|
||||
*/
|
||||
class FlowOnHierarchyChangeListener(
|
||||
private val recursive: Boolean = false,
|
||||
private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE),
|
||||
private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
|
||||
) : ViewGroup.OnHierarchyChangeListener {
|
||||
val onChildViewAdded = _onChildViewAdded.asSharedFlow()
|
||||
val onChildViewRemoved = _onChildViewRemoved.asSharedFlow()
|
||||
|
||||
/**
|
||||
* Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also
|
||||
* subscribe to [child] hierarchy changes.
|
||||
*
|
||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is
|
||||
* [Int.MAX_VALUE]
|
||||
*/
|
||||
override fun onChildViewAdded(parent: View, child: View) {
|
||||
_onChildViewAdded.tryEmit(parent to child)
|
||||
|
||||
if (recursive && child is ViewGroup) {
|
||||
child.setOnHierarchyChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just emit data into [onChildViewRemoved]
|
||||
*
|
||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is
|
||||
* [Int.MAX_VALUE]
|
||||
*/
|
||||
override fun onChildViewRemoved(parent: View, child: View) {
|
||||
_onChildViewRemoved.tryEmit(parent to child)
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.OnHierarchyChangeListener
|
||||
|
||||
/**
|
||||
* Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this].
|
||||
* This extension DO NOT guarantee that recursive subscription will happen after this method call
|
||||
*/
|
||||
fun ViewGroup.setOnHierarchyChangeListenerRecursively(
|
||||
listener: OnHierarchyChangeListener
|
||||
) {
|
||||
setOnHierarchyChangeListener(listener)
|
||||
(0 until childCount).forEach {
|
||||
(getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener)
|
||||
}
|
||||
}
|
@@ -6,12 +6,11 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
private sealed interface AccumulatorFlowStep<T>
|
||||
private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T>
|
||||
private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
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:
|
||||
@@ -27,12 +26,12 @@ class AccumulatorFlow<T>(
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<T>>()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep<T>> { step ->
|
||||
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.firstOrNull() === step.data) {
|
||||
if (activeData.first() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
@@ -43,7 +42,7 @@ class AccumulatorFlow<T>(
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it) }
|
||||
dataToSend.forEach { step.channel.send(it as Any) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,29 +58,24 @@ class AccumulatorFlow<T>(
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it)
|
||||
channel.send(it as Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||
val result = runCatchingSafely {
|
||||
for (data in channel) {
|
||||
val emitResult = runCatchingSafely {
|
||||
collector.emit(data)
|
||||
}
|
||||
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
}
|
||||
emitResult.getOrThrow()
|
||||
for (data in channel) {
|
||||
try {
|
||||
collector.emit(data as T)
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
} finally {
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
}
|
||||
}
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
result.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,19 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun <T> CoroutineScope.actor(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
block: suspend (T) -> Unit
|
||||
): Channel<T> {
|
||||
val channel = Channel<T>(channelCapacity)
|
||||
channel.consumeAsFlow().subscribe(this, block)
|
||||
launch {
|
||||
for (data in channel) {
|
||||
block(data)
|
||||
}
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
|
@@ -1,30 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
|
||||
fun <T> CoroutineScope.actorAsync(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
markerFactory: suspend (T) -> Any? = { null },
|
||||
block: suspend (T) -> Unit
|
||||
): Channel<T> {
|
||||
val channel = Channel<T>(channelCapacity)
|
||||
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
|
||||
return channel
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.safeActorAsync(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline markerFactory: suspend (T) -> Any? = { null },
|
||||
crossinline block: suspend (T) -> Unit
|
||||
): Channel<T> = actorAsync(
|
||||
channelCapacity,
|
||||
markerFactory
|
||||
) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
@@ -6,19 +6,23 @@ import kotlin.coroutines.*
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): Pair<Deferred<T>, T> {
|
||||
val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>()
|
||||
val scope = scope.LinkedSupervisorScope()
|
||||
forEach {
|
||||
scope.launch {
|
||||
resultDeferred.complete(it to it.await())
|
||||
scope.cancel()
|
||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||
scope.launch(SupervisorJob()) {
|
||||
val scope = this
|
||||
forEach {
|
||||
scope.launch {
|
||||
continuation.resume(it to it.await())
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultDeferred.await().also {
|
||||
if (cancelOnResult) {
|
||||
forEach {
|
||||
runCatchingSafely { it.cancel() }
|
||||
}.also {
|
||||
if (cancelOnResult) {
|
||||
forEach {
|
||||
try {
|
||||
it.cancel()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,39 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.js.JsName
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
inline fun <T, R> Flow<Flow<T>>.flatMap(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flow {
|
||||
collect {
|
||||
it.collect {
|
||||
emit(mapper(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsName("flatMapIterable")
|
||||
@JvmName("flatMapIterable")
|
||||
inline fun <T, R> Flow<Iterable<T>>.flatMap(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = map {
|
||||
it.asFlow()
|
||||
}.flatMap(mapper)
|
||||
|
||||
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flatMap(mapper).takeNotNull()
|
||||
|
||||
@JsName("flatMapNotNullIterable")
|
||||
@JvmName("flatMapNotNullIterable")
|
||||
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flatMap(mapper).takeNotNull()
|
||||
|
||||
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
|
||||
|
||||
@JsName("flattenIterable")
|
||||
@JvmName("flattenIterable")
|
||||
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }
|
@@ -1,6 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
|
||||
fun <T> Flow<T>.filterNotNull() = takeNotNull()
|
@@ -115,21 +115,10 @@ suspend inline fun <T> runCatchingSafely(
|
||||
safely(onException, block)
|
||||
}
|
||||
|
||||
suspend inline fun <T, R> T.runCatchingSafely(
|
||||
noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatching {
|
||||
safely(onException) { block() }
|
||||
}
|
||||
|
||||
suspend inline fun <T> safelyWithResult(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
suspend inline fun <T, R> T.safelyWithResult(
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
/**
|
||||
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||
* returning null at one time
|
||||
|
@@ -1,136 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the
|
||||
* [SmartMutex] state.
|
||||
*
|
||||
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||
* [Mutable] [SmartMutex]. Otherwise, [Immutable].
|
||||
*
|
||||
* Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its
|
||||
* [Mutable] creator
|
||||
*/
|
||||
sealed interface SmartMutex {
|
||||
val lockStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* * True - locked
|
||||
* * False - unlocked
|
||||
*/
|
||||
val isLocked: Boolean
|
||||
get() = lockStateFlow.value
|
||||
|
||||
/**
|
||||
* Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow]
|
||||
*/
|
||||
class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex
|
||||
|
||||
/**
|
||||
* Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||
* [Immutable] variant of [this] instance with [immutable] factory
|
||||
*
|
||||
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
|
||||
*/
|
||||
class Mutable(locked: Boolean = false) : SmartMutex {
|
||||
private val _lockStateFlow = MutableStateFlow<Boolean>(locked)
|
||||
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
|
||||
|
||||
private val internalChangesMutex = Mutex()
|
||||
|
||||
fun immutable() = Immutable(lockStateFlow)
|
||||
|
||||
/**
|
||||
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
|
||||
* wait for [isLocked] == false and then try to lock
|
||||
*/
|
||||
suspend fun lock() {
|
||||
do {
|
||||
waitUnlock()
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
if (_lockStateFlow.value) {
|
||||
true
|
||||
} else {
|
||||
_lockStateFlow.value = true
|
||||
false
|
||||
}
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to lock this [SmartMutex] immediataly
|
||||
*
|
||||
* @return True if lock was successful. False otherwise
|
||||
*/
|
||||
suspend fun tryLock(): Boolean {
|
||||
return if (!_lockStateFlow.value) {
|
||||
internalChangesMutex.withLock {
|
||||
if (!_lockStateFlow.value) {
|
||||
_lockStateFlow.value = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
|
||||
* [SmartMutex] - false
|
||||
*/
|
||||
suspend fun unlock(): Boolean {
|
||||
return if (_lockStateFlow.value) {
|
||||
internalChangesMutex.withLock {
|
||||
if (_lockStateFlow.value) {
|
||||
_lockStateFlow.value = false
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
lock()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||
* that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it }
|
@@ -1,104 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Composite mutex which works with next rules:
|
||||
*
|
||||
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
|
||||
* * [releaseRead] will just free up one permit in [readSemaphore]
|
||||
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
|
||||
* * [unlockWrite] will just unlock [writeMutex]
|
||||
*/
|
||||
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
||||
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
|
||||
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
|
||||
|
||||
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
||||
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
|
||||
|
||||
/**
|
||||
* Do lock in [readSemaphore] inside of [writeMutex] locking
|
||||
*/
|
||||
suspend fun acquireRead() {
|
||||
_readSemaphore.acquire()
|
||||
}
|
||||
|
||||
/**
|
||||
* Release one read permit in [readSemaphore]
|
||||
*/
|
||||
suspend fun releaseRead(): Boolean {
|
||||
return _readSemaphore.release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
|
||||
*/
|
||||
suspend fun lockWrite() {
|
||||
_writeMutex.lock()
|
||||
_readSemaphore.acquire(readPermits)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock [writeMutex]
|
||||
*/
|
||||
suspend fun unlockWrite(): Boolean {
|
||||
return _writeMutex.unlock().also {
|
||||
if (it) {
|
||||
_readSemaphore.release(readPermits)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
acquireRead()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
releaseRead()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
|
||||
* the fact that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
|
||||
|
||||
/**
|
||||
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
lockWrite()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
unlockWrite()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||
* that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()
|
@@ -1,168 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
|
||||
* [SmartSemaphore] state.
|
||||
*
|
||||
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
|
||||
*
|
||||
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
|
||||
* [Mutable] creator
|
||||
*/
|
||||
sealed interface SmartSemaphore {
|
||||
val permitsStateFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* * True - locked
|
||||
* * False - unlocked
|
||||
*/
|
||||
val freePermits: Int
|
||||
get() = permitsStateFlow.value
|
||||
|
||||
/**
|
||||
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
|
||||
*/
|
||||
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
|
||||
|
||||
/**
|
||||
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||
* [Immutable] variant of [this] instance with [immutable] factory
|
||||
*
|
||||
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
|
||||
*/
|
||||
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
|
||||
private val _freePermitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits)
|
||||
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
|
||||
|
||||
private val internalChangesMutex = Mutex(false)
|
||||
|
||||
fun immutable() = Immutable(permitsStateFlow)
|
||||
|
||||
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
|
||||
|
||||
/**
|
||||
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
|
||||
*/
|
||||
suspend fun acquire(permits: Int = 1) {
|
||||
var acquiredPermits = 0
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
try {
|
||||
do {
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
val requiredPermits = checkedPermits - acquiredPermits
|
||||
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true
|
||||
acquiredPermits += acquiring
|
||||
_freePermitsStateFlow.value -= acquiring
|
||||
|
||||
acquiredPermits != checkedPermits
|
||||
}
|
||||
if (shouldContinue) {
|
||||
waitRelease()
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
} catch (e: Throwable) {
|
||||
release(acquiredPermits)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
|
||||
* wait for [freePermits] == false and then try to lock
|
||||
*/
|
||||
suspend fun acquireByOne(permits: Int = 1) {
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
do {
|
||||
waitRelease(checkedPermits)
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
if (_freePermitsStateFlow.value < checkedPermits) {
|
||||
true
|
||||
} else {
|
||||
_freePermitsStateFlow.value -= checkedPermits
|
||||
false
|
||||
}
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to lock this [SmartSemaphore] immediataly
|
||||
*
|
||||
* @return True if lock was successful. False otherwise
|
||||
*/
|
||||
suspend fun tryAcquire(permits: Int = 1): Boolean {
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
return if (_freePermitsStateFlow.value < checkedPermits) {
|
||||
internalChangesMutex.withLock {
|
||||
if (_freePermitsStateFlow.value < checkedPermits) {
|
||||
_freePermitsStateFlow.value -= checkedPermits
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
|
||||
* [SmartSemaphore] - false
|
||||
*/
|
||||
suspend fun release(permits: Int = 1): Boolean {
|
||||
val checkedPermits = checkedPermits(permits)
|
||||
return if (_freePermitsStateFlow.value < this.permits) {
|
||||
internalChangesMutex.withLock {
|
||||
if (_freePermitsStateFlow.value < this.permits) {
|
||||
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
acquire(permits)
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
release(permits)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
|
||||
* the fact that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user