mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
171 Commits
Author | SHA1 | Date | |
---|---|---|---|
c0523469fa | |||
9706524572 | |||
3174b84367 | |||
6cccd5ff6c | |||
972857268d | |||
f3cd92cc5e | |||
502a49644c | |||
51719b5868 | |||
17821bd094 | |||
6a89ffaa8a | |||
d5a8d0f4d4 | |||
9739bd871e | |||
632d2545d4 | |||
ecfa273a81 | |||
a36828116e | |||
14aa9ca26c | |||
069e51f2ff | |||
a15cbdfb1a | |||
4af8114eda | |||
90dc84e900 | |||
67c595b440 | |||
830b7aee56 | |||
1890608cb3 | |||
bd396959a9 | |||
d5fe19f0a5 | |||
46bfb09415 | |||
a60cb596d1 | |||
6f9d5e2d5f | |||
80bc226ee1 | |||
12e37184e1 | |||
25e9345d02 | |||
ccc4d030c3 | |||
90c0817b6d | |||
527f7bbafe | |||
765a32729f | |||
de783f77a2 | |||
de4c8d104c | |||
88c8c28f45 | |||
57b36826d1 | |||
f81a2f309b | |||
5fc760f4a5 | |||
091cb38339 | |||
3ae9b3e576 | |||
b03b4cbeec | |||
f2c1b3c76a | |||
f3bec34882 | |||
d6aa9fe9c2 | |||
2d5304a770 | |||
88f2c16c82 | |||
490c318d1c | |||
8beaf61a08 | |||
8b61c984eb | |||
e38094df58 | |||
c25e3f5867 | |||
f78e81d175 | |||
3837ae237d | |||
2b6ef8b4ff | |||
6cc0eefb3e | |||
ab11e28bf7 | |||
26d5f5a5f5 | |||
74ae91cba6 | |||
70509c7edd | |||
5a69bd6c63 | |||
091bb1394f | |||
b82c3864a0 | |||
49ee38a936 | |||
c201866c51 | |||
023f841fb5 | |||
76102e9ab3 | |||
b2fc5e2a4d | |||
55aacb8753 | |||
8702846216 | |||
3347a6d189 | |||
47b3b42949 | |||
e985631621 | |||
e15034bfa4 | |||
e5dbcd25dd | |||
3714c02c12 | |||
fa636b4146 | |||
3d825aebc3 | |||
629884a396 | |||
e7a0fa4e8f | |||
1a41f37a9d | |||
07a65e0bb5 | |||
615f7f99c3 | |||
3de5558ed4 | |||
6bbe3a271f | |||
8bee29f683 | |||
c40f0fdcb9 | |||
30d1453a12 | |||
75fa88b00d | |||
0f817ad212 | |||
d7b46ae0d4 | |||
b6be14ecca | |||
33dbfc6f69 | |||
fcacdcd544 | |||
dd2fc5a86f | |||
0f8a6f6bde | |||
1efd94181d | |||
71ff0232aa | |||
63921cd984 | |||
051e03bed3 | |||
a051394f4f | |||
b872babe45 | |||
5a9cabc4bd | |||
3ba630684a | |||
498cd12f94 | |||
062848f2e4 | |||
d4b4547718 | |||
22cd440dd7 | |||
6fc64526d4 | |||
08075dfafe | |||
efcb25622e | |||
5ebf29d1fb | |||
b7d0ce3c97 | |||
e20929aec4 | |||
f74167cc65 | |||
c55cd5342e | |||
237d89d611 | |||
aff9b52f1c | |||
d8cc6ed06e | |||
dffafa54c3 | |||
776a3af497 | |||
5b3d9e8d64 | |||
90b7d74a0c | |||
5b596c76e0 | |||
c59e601e2e | |||
5ce71ee6f6 | |||
97dadf517a | |||
a739e874bf | |||
c73cb14615 | |||
d9464f7b90 | |||
aeee41680e | |||
c914f8c44a | |||
abfda3627c | |||
5b5bfa02db | |||
0fdb072385 | |||
7b4c9d59b0 | |||
bd5923716f | |||
64cc42a23b | |||
325f178763 | |||
fc8d0e52ef | |||
e58348907e | |||
eaba9173ae | |||
2f42b30f87 | |||
35913b95be | |||
8023fa1b76 | |||
4cbe2d1d61 | |||
740036e8d9 | |||
273a7aa9a4 | |||
dccb479c3c | |||
6f5c6e5ebe | |||
8495cc6263 | |||
d242d8dcf4 | |||
864d576e70 | |||
f0127b018e | |||
5eb48a58bf | |||
b690f68c7f | |||
7f19b83828 | |||
98a1ec82db | |||
63313ff964 | |||
210e32bed4 | |||
5b325a8ff9 | |||
4582e0c817 | |||
e2d1c5d6a1 | |||
4019ad7d31 | |||
e7df21e91a | |||
b2e30c9f6d | |||
0b27b5cc06 | |||
347e9c32fe | |||
cc18f58e4c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,5 +10,6 @@ build/
|
||||
out/
|
||||
|
||||
secret.gradle
|
||||
local.properties
|
||||
|
||||
publishing.sh
|
||||
|
20
.travis.yml
20
.travis.yml
@@ -1,13 +1,27 @@
|
||||
language: java
|
||||
language: android
|
||||
install: true
|
||||
|
||||
os: linux
|
||||
dist: trusty
|
||||
jdk: oraclejdk8
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-30.0.2
|
||||
- android-30
|
||||
- add-on
|
||||
- extra
|
||||
|
||||
before_script:
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2"
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30"
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
||||
- state: test
|
||||
script: ./gradlew allTests
|
||||
# Tests are temporarily disabled on public travis due to the problems of launching
|
||||
# - state: test
|
||||
# script: ./gradlew allTests
|
||||
|
301
CHANGELOG.md
301
CHANGELOG.md
@@ -1,5 +1,304 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.17
|
||||
|
||||
* `Common`
|
||||
* Functionality for decode/encode base 64 to/from `ByteArray`/`String` have been added
|
||||
* `Serialization`
|
||||
* `Base 64`
|
||||
* Project has been initiated
|
||||
|
||||
## 0.4.16
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* New `safelyWithoutExceptions` function may accept `onException` parameter with nullable result
|
||||
* Old `safelyWithoutExceptions` now using `defaultSafelyWithoutExceptionHandler` to handle exceptions "like in
|
||||
`safely`", but it is expected that `defaultSafelyWithoutExceptionHandler` will not throw any exception
|
||||
|
||||
## 0.4.15
|
||||
|
||||
* `Coroutines`:
|
||||
* `safely`:
|
||||
* `SafelyExceptionHandlerKey` has been deprecated
|
||||
* `SafelyExceptionHandler` has been deprecated
|
||||
* `ContextSafelyExceptionHandlerKey` has been added
|
||||
* `ContextSafelyExceptionHandler` has been added
|
||||
* `safelyWithContextExceptionHandler` has been added
|
||||
|
||||
## 0.4.14
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.20` -> `1.4.21`
|
||||
* `Ktor`: `1.4.3` -> `1.5.0`
|
||||
* `Klock`: `2.0.1` -> `2.0.2`
|
||||
* `Coroutines`:
|
||||
* Add global variable `defaultSafelyExceptionHandler`
|
||||
* Add `SafelyExceptionHandlerKey` and `SafelyExceptionHandler` classes to be able to overwrite
|
||||
`defaultSafelyExceptionHandler` using context of coroutine
|
||||
|
||||
## 0.4.13
|
||||
|
||||
* `Common`
|
||||
* `Android`
|
||||
* Add expand/collapse functionality for horizontal expand/collapse
|
||||
|
||||
## 0.4.12
|
||||
|
||||
* `Coroutines`
|
||||
* `JVM`
|
||||
* Update `launchSynchronously` signature
|
||||
* `Selector`
|
||||
* Project created
|
||||
|
||||
## 0.4.11
|
||||
|
||||
* `Common`
|
||||
* Add `clamp` function
|
||||
|
||||
## 0.4.10
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `2.0.0` -> `2.0.1`
|
||||
* `Repo`
|
||||
* Repo `WriteStandardKeyValueRepo` got new method `unsetWithValues`
|
||||
|
||||
## 0.4.9
|
||||
|
||||
* `Versions`:
|
||||
* `Ktor`: `1.4.2` -> `1.4.3`
|
||||
* `Coroutines`:
|
||||
* `launchSynchronously` has been added in JVM
|
||||
* `Repo`
|
||||
* `Common`
|
||||
* In repos different usages of `BroadcastChannel`s has been replaced with `MutableSharedFlow`
|
||||
* `Exposed`
|
||||
* `asObject` open fun has been added in CRUD realization
|
||||
|
||||
## 0.4.8
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.4.1` -> `1.4.2`
|
||||
* `UUID`: `0.2.2` -> `0.2.3`
|
||||
* `Pagination`
|
||||
* Add `PaginatedIterable` and `PaginatedIterator`
|
||||
|
||||
## 0.4.7
|
||||
|
||||
* `Ktor`
|
||||
* `Client`
|
||||
* New class `UnifiedRequester`
|
||||
* `Server`
|
||||
* New class `UnifiedRouter`
|
||||
* `Repos`
|
||||
* `Ktor`
|
||||
* `Client`
|
||||
* Rewriting of all clients on new `UnifiedRequester`
|
||||
* `Server`
|
||||
* Rewriting of all clients on new `UnifiedRouter`
|
||||
|
||||
## 0.4.6
|
||||
|
||||
* `Common`
|
||||
* New annotation `Warning` has been added
|
||||
* `Pagination`
|
||||
* `Common`
|
||||
* `Pagination` got new extension: `Pagination#isFirstPage`
|
||||
* `Coroutines`:
|
||||
* New extension `FlowCollector#invoke` has been added
|
||||
* `Repos`
|
||||
* `Common`
|
||||
* `JVM` (and `Android` since `Android API 26`):
|
||||
* `FileStandardKeyValueRepo` has been added
|
||||
* Add several `typealias`es for each type of repos
|
||||
|
||||
## 0.4.5
|
||||
|
||||
* `Android`
|
||||
* `Alerts`
|
||||
* `Common`
|
||||
* Project has been created
|
||||
* `RecyclerView`
|
||||
* Project has been created
|
||||
* `Common`
|
||||
* Annotation `PreviewFeature` has been added
|
||||
* `Android`
|
||||
* Added tools to work with visibility in more comfortable way
|
||||
* Added tools to work with disabled/enabled state in more comfortable way
|
||||
* Added tools to work with expanded/collapsed state in more comfortable way (in preview mode)
|
||||
|
||||
## 0.4.4
|
||||
|
||||
* `Versions`:
|
||||
* `Klock`: `1.12.1` -> `2.0.0`
|
||||
* `Commons`:
|
||||
* Update left items functionality to include work with `GridLayoutManager`
|
||||
* `Repos`:
|
||||
* Add interface `VersionsRepo`
|
||||
* Add default realization of `VersionsRepo` named `StandardVersionsRepo` which use `StandardVersionsRepoProxy`
|
||||
to get access to some end-store
|
||||
* Add default realization of `StandardVersionsRepoProxy` based on `KeyValue` repos
|
||||
* Add realizations of `StandardVersionsRepoProxy` for exposed and android (`SQL` and `SharedPreferences`)
|
||||
* `Commons`:
|
||||
* In Android fully reworked transactions functions
|
||||
* Now `DatabaseCoroutineContext` is a shortcut for `Dispatchers.IO`
|
||||
|
||||
## 0.4.3
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `1.4.10` -> `1.4.20`
|
||||
* `Common`:
|
||||
* Two new extensions for Android:
|
||||
* `Resources#getSp`
|
||||
* `Resources#getDp`
|
||||
|
||||
## 0.4.2
|
||||
|
||||
* `Repos`:
|
||||
* Add `WriteOneToManyKeyValueRepo#set` function and extensions
|
||||
|
||||
## 0.4.1
|
||||
|
||||
* `Repos`:
|
||||
* Fixed error in `ExposedKeyValueRepo` related to negative size of shared flow
|
||||
* Fixed error in `ExposedKeyValueRepo` related to avoiding of table initiation
|
||||
|
||||
## 0.4.0
|
||||
|
||||
* `Repos`:
|
||||
* `ReadOneToManyKeyValueRepo` got `keys` method with value parameter
|
||||
* All implementations inside of this library has been updated
|
||||
* `ReadStandardKeyValueRepo` got `keys` method with value parameter
|
||||
* All implementations inside of this library has been updated
|
||||
* New extensions `withMapper`
|
||||
|
||||
## 0.3.3
|
||||
|
||||
* `Coroutines`:
|
||||
* New extension `Flow<T>#plus`
|
||||
|
||||
## 0.3.2
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.4.1` -> `1.4.2`
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* New inline function `mapper` for simplier creating of `MapperRepo` objects
|
||||
* Extensions `withMapper` for keyvalue repos and onetomany repos
|
||||
|
||||
## 0.3.1
|
||||
|
||||
**ANDROID PACKAGES**
|
||||
|
||||
* `Android`:
|
||||
* `RecyclerView`:
|
||||
* Library has been created
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.common-android`
|
||||
* `Coroutines`
|
||||
* Now available package `dev.inmo:micro_utils.coroutines-android`
|
||||
* `Ktor`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.ktor.common-android`
|
||||
* `Client`
|
||||
* Now available package `dev.inmo:micro_utils.ktor.client-android`
|
||||
* `MimeTypes`
|
||||
* Now available package `dev.inmo:micro_utils.mime_types-android`
|
||||
* `Pagination`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.pagination.common-android`
|
||||
* `Ktor`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.pagination.ktor.common-android`
|
||||
* `Repos`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.repos.common-android`
|
||||
* Now it is possible to use default realizations of repos abstractions natively on android
|
||||
* `Inmemory`
|
||||
* Now available package `dev.inmo:micro_utils.repos.inmemory-android`
|
||||
* `Ktor`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.repos.ktor.common-android`
|
||||
* `Common`
|
||||
* Now available package `dev.inmo:micro_utils.repos.ktor.client-android`
|
||||
|
||||
## 0.3.0
|
||||
|
||||
All deprecations has been removed
|
||||
|
||||
* `Repos`:
|
||||
* `Common`:
|
||||
* `KeyValue` and `OneToMany` repos lost their deprecated methods
|
||||
* `OneToMany` write repos got additional extensions for mutation of repo
|
||||
* `KeyValue` write repos got additional extensions for mutation of repo
|
||||
* New interface `MapperRepo` and new classes which are using this:
|
||||
* `KeyValue`
|
||||
* `MapperReadStandardKeyValueRepo`
|
||||
* `MapperWriteStandardKeyValueRepo`
|
||||
* `MapperStandardKeyValueRepo`
|
||||
* `OneToMany`
|
||||
* `MapperReadOneToManyKeyValueRepo`
|
||||
* `MapperWriteOneToManyKeyValueRepo`
|
||||
* `MapperOneToManyKeyValueRepo`
|
||||
* `Exposed`:
|
||||
* CRUD realizations replaced their channels to shared flows
|
||||
|
||||
## 0.2.7
|
||||
|
||||
* `Versions`:
|
||||
* `Coroutines`: `1.4.0` -> `1.4.1`
|
||||
* `Repos`:
|
||||
* `WriteStandardKeyValueRepo` got new methods `set` and `unset` with collections
|
||||
* All standard realizations of repos got collections methods realizations
|
||||
* All old usages of `BroadcastFlow` and `BroadcastChannel` has been replaced with `MutableSharedFlow`
|
||||
* `Ktor`:
|
||||
* `Server`:
|
||||
* Fixed incorrect answer for `keyvalue`
|
||||
|
||||
## 0.2.6
|
||||
|
||||
* `Pagination`
|
||||
* Fixes in function `List#paginate`
|
||||
* Extension property `Pagination#lastIndexExclusive`
|
||||
|
||||
## 0.2.5
|
||||
|
||||
* `Coroutines`
|
||||
* Function `safelyWithoutExceptions`
|
||||
* Extension `CoroutineScope#safeActor`
|
||||
|
||||
## 0.2.4
|
||||
|
||||
* `Versions`
|
||||
* `Serialization`: `1.0.0` -> `1.0.1`
|
||||
* `Common`
|
||||
* Full rework of `DiffUtils`
|
||||
* Data class `Diff` has been added
|
||||
* Extension `Iterable#calculateDiff` has been added
|
||||
* Extension `Iterable#calculateStrictDiff` as replacement for `Iterable#calculateDiff` with
|
||||
`strictComparison` mode enabled
|
||||
* Functions `Diff` (as analog of `Iterable#calculateDiff`) and `StrictDiff` (as analog of
|
||||
`Iterable#calculateStrictDiff`)
|
||||
* `Coroutines`
|
||||
* `BroadcastFlow` now is deprecated
|
||||
* `BroadcastStateFlow` now is deprecated
|
||||
* New extensions for `Flow`s:
|
||||
* `Flow#subscribe`
|
||||
* `Flow#subscribeSafely`
|
||||
* `Flow#subscribeSafelyWithoutExceptions`
|
||||
|
||||
## 0.2.3
|
||||
|
||||
* `Versions`
|
||||
* `Coroutines`: `1.3.9` -> `1.4.0`
|
||||
* `Exposed`: `0.27.1` -> `0.28.1`
|
||||
* `Common`
|
||||
* `K/JS`
|
||||
* Add several extensions for `Element` objects to detect that object is on screen viewport
|
||||
* Add several extensions for `Element` objects to detect object visibility
|
||||
* `Coroutines`
|
||||
* `BroadcastStateFlow` now use different strategy for getting of state and implements `replayCache`
|
||||
|
||||
## 0.2.2
|
||||
|
||||
* `Repos`
|
||||
@@ -88,4 +387,4 @@
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Inited :)
|
||||
Inited :)
|
||||
|
38
README.md
38
README.md
@@ -1 +1,37 @@
|
||||
# MicroUtils
|
||||
# MicroUtils
|
||||
|
||||
This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies:
|
||||
|
||||
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)
|
||||
* [`Kotlin Serialization`](https://github.com/Kotlin/kotlinx.serialization)
|
||||
* [`Kotlin Exposed`](https://github.com/JetBrains/Exposed)
|
||||
* [`Ktor`](https://ktor.io)
|
||||
|
||||
<details>
|
||||
<summary> <b>Android environment</b> </summary>
|
||||
|
||||
You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUtils/blob/master/gradle.properties#L24-L34">properties file</a> to get information about current project dependencies, compile and build tools for `Android` target.
|
||||
|
||||
</details>
|
||||
|
||||
## Projects
|
||||
|
||||
* `common` contains common tools for platform which usually are absent out-of-the-box when you starting project
|
||||
* `selector` contains tools to use `Selector` interface with things like `RecyclerView` in android or other selection needs
|
||||
* `coroutines` is a module for `Kotlin Coroutines` with different things like subscribing on flows (`onEach` + `launchIn` shortcut :) )
|
||||
* `ktor` is a set of modules for `client`s and `server`s
|
||||
* `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types
|
||||
* `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination
|
||||
* `serialization` is a collection of projects with serializers for `kotlinx.serialization`
|
||||
* `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories
|
||||
|
||||
## Complex modules structure
|
||||
|
||||
Most of complex modules are built with next hierarchy:
|
||||
|
||||
* `common` submodule for `API` things which are common for all platforms
|
||||
* `exposed` submodule contains realizations for exposed tables
|
||||
* `ktor` submodule is usually unavailable directly, because it contains its own submodules for clients and servers
|
||||
* `common` part contains routes which are common for clients and servers
|
||||
* `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests
|
||||
* `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object
|
||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
17
android/alerts/common/build.gradle
Normal file
17
android/alerts/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppAndroidProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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"/>
|
@@ -0,0 +1,55 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package dev.inmo.micro_utils.android.alerts.common
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
||||
typealias AlertDialogCallback = (DialogInterface) -> Unit
|
||||
|
||||
inline fun Context.createAlertDialogTemplate(
|
||||
title: String? = null,
|
||||
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<String, AlertDialogCallback?>? = null
|
||||
): AlertDialog.Builder {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
|
||||
title ?.let {
|
||||
builder.setTitle(title)
|
||||
}
|
||||
|
||||
positivePair ?. let {
|
||||
builder.setPositiveButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||
}
|
||||
negativePair ?. let {
|
||||
builder.setNegativeButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||
}
|
||||
neutralPair ?. let {
|
||||
builder.setNeutralButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
inline fun Context.createAlertDialogTemplateWithResources(
|
||||
title: Int? = null,
|
||||
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<Int, AlertDialogCallback?>? = null
|
||||
): AlertDialog.Builder = createAlertDialogTemplate(
|
||||
title ?.let { getString(it) },
|
||||
positivePair ?.let { getString(it.first) to it.second },
|
||||
neutralPair ?.let { getString(it.first) to it.second },
|
||||
negativePair ?.let { getString(it.first) to it.second }
|
||||
)
|
||||
|
||||
inline fun AlertDialog.setDismissChecker(noinline checker: () -> Boolean) : AlertDialog {
|
||||
setOnDismissListener {
|
||||
if (!checker()) {
|
||||
show()
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package dev.inmo.micro_utils.android.alerts.common
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
|
||||
inline fun <T: View> Context.createCustomViewAlertDialog(
|
||||
title: String? = null,
|
||||
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true,
|
||||
viewCreator: (Context) -> T
|
||||
): AlertDialog = createAlertDialogTemplate(
|
||||
title, positivePair, neutralPair, negativePair
|
||||
).apply {
|
||||
setView(viewCreator(this@createCustomViewAlertDialog))
|
||||
}.create().apply {
|
||||
if (show) show()
|
||||
}
|
||||
|
||||
inline fun <T: View> Context.createCustomViewAlertDialogWithResources(
|
||||
title: Int? = null,
|
||||
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true,
|
||||
viewCreator: (Context) -> T
|
||||
): AlertDialog = createCustomViewAlertDialog(
|
||||
title ?.let { getString(it) },
|
||||
positivePair ?.let { getString(it.first) to it.second },
|
||||
neutralPair ?.let { getString(it.first) to it.second },
|
||||
negativePair ?.let { getString(it.first) to it.second },
|
||||
show,
|
||||
viewCreator
|
||||
)
|
@@ -0,0 +1,45 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package dev.inmo.micro_utils.android.alerts.common
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
inline fun Context.createSimpleTextAlertDialog(
|
||||
text: String,
|
||||
title: String? = null,
|
||||
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true
|
||||
): AlertDialog = createAlertDialogTemplate(
|
||||
title,
|
||||
positivePair,
|
||||
neutralPair,
|
||||
negativePair
|
||||
).apply {
|
||||
setMessage(text)
|
||||
}.create().apply {
|
||||
if (show) {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Context.createSimpleTextAlertDialog(
|
||||
@StringRes
|
||||
text: Int,
|
||||
@StringRes
|
||||
title: Int? = null,
|
||||
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true
|
||||
): AlertDialog = createSimpleTextAlertDialog(
|
||||
getString(text),
|
||||
title ?.let { getString(it) },
|
||||
positivePair ?.let { getString(it.first) to it.second },
|
||||
neutralPair ?.let { getString(it.first) to it.second },
|
||||
negativePair ?.let { getString(it.first) to it.second },
|
||||
show
|
||||
)
|
18
android/alerts/recyclerview/build.gradle
Normal file
18
android/alerts/recyclerview/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppAndroidProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.android.alerts.common")
|
||||
api internalProject("micro_utils.android.recyclerview")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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"/>
|
@@ -0,0 +1,65 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||
import dev.inmo.micro_utils.android.recyclerview.*
|
||||
|
||||
data class AlertAction(
|
||||
val title: String,
|
||||
val callback: (DialogInterface) -> Unit
|
||||
)
|
||||
|
||||
private class ActionViewHolder(
|
||||
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||
private lateinit var action: AlertAction
|
||||
private val textView: TextView
|
||||
get() = itemView.findViewById(android.R.id.text1)
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
action.callback(dialogInterfaceGetter())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(item: AlertAction) {
|
||||
action = item
|
||||
textView.text = item.title
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionsRecyclerViewAdapter(
|
||||
data: List<AlertAction>,
|
||||
private val dialogInterfaceGetter: () -> DialogInterface
|
||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||
parent, dialogInterfaceGetter
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.createActionsAlertDialog(
|
||||
actions: List<AlertAction>,
|
||||
title: Int? = null,
|
||||
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true
|
||||
): AlertDialog {
|
||||
lateinit var dialogInterface: DialogInterface
|
||||
|
||||
return createRecyclerViewDialog(
|
||||
title, positivePair, neutralPair, negativePair, show
|
||||
) {
|
||||
ActionsRecyclerViewAdapter(
|
||||
actions
|
||||
) {
|
||||
dialogInterface
|
||||
}
|
||||
}.also { dialogInterface = it }
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||
import dev.inmo.micro_utils.android.alerts.common.createCustomViewAlertDialogWithResources
|
||||
|
||||
fun Context.createRecyclerViewDialog(
|
||||
title: Int? = null,
|
||||
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||
show: Boolean = true,
|
||||
layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this),
|
||||
marginOfRecyclerView: Int = 8, // dp
|
||||
recyclerViewSetUp: RecyclerView.() -> Unit = {},
|
||||
adapterFactory: () -> RecyclerView.Adapter<*>
|
||||
): AlertDialog {
|
||||
val recyclerView = RecyclerView(this).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView)
|
||||
}
|
||||
this.layoutManager = layoutManager
|
||||
adapter = adapterFactory()
|
||||
recyclerViewSetUp()
|
||||
}
|
||||
|
||||
return createCustomViewAlertDialogWithResources(
|
||||
title,
|
||||
positivePair,
|
||||
neutralPair,
|
||||
negativePair,
|
||||
show
|
||||
) {
|
||||
recyclerView
|
||||
}
|
||||
}
|
22
android/recyclerview/build.gradle
Normal file
22
android/recyclerview/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppAndroidProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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"/>
|
@@ -0,0 +1,22 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
abstract class AbstractStandardViewHolder<T>(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
viewId: Int,
|
||||
onViewInflated: ((View) -> Unit)? = null
|
||||
) : AbstractViewHolder<T>(
|
||||
inflater.inflate(viewId, container, false).also {
|
||||
onViewInflated ?.invoke(it)
|
||||
}
|
||||
) {
|
||||
constructor(
|
||||
container: ViewGroup,
|
||||
viewId: Int,
|
||||
onViewInflated: ((View) -> Unit)? = null
|
||||
) : this(LayoutInflater.from(container.context), container, viewId, onViewInflated)
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class AbstractViewHolder<in T>(
|
||||
view: View
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
abstract fun onBind(item: T)
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
|
||||
val Context.recyclerViewItemsDecoration
|
||||
get() = DividerItemDecoration(this, LinearLayout.VERTICAL)
|
||||
|
@@ -0,0 +1,53 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import androidx.recyclerview.widget.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun RecyclerView.LayoutManager.findLastVisibleItemPositionGetter(): (() -> Int)? = when (this) {
|
||||
is LinearLayoutManager -> ::findLastVisibleItemPosition
|
||||
is GridLayoutManager -> ::findLastVisibleItemPosition
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun RecyclerView.lastVisibleItemFlow(
|
||||
completingScope: CoroutineScope
|
||||
): Flow<Int> {
|
||||
val lastVisibleElementFun: () -> Int = layoutManager ?.findLastVisibleItemPositionGetter() ?: error("Currently supported only linear and grid layout manager")
|
||||
val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
|
||||
addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
lastVisibleFlow.value = lastVisibleElementFun()
|
||||
}
|
||||
}.also { scrollListener ->
|
||||
lastVisibleFlow.onCompletion {
|
||||
removeOnScrollListener(scrollListener)
|
||||
}.launchIn(completingScope)
|
||||
}
|
||||
)
|
||||
return lastVisibleFlow.asStateFlow()
|
||||
}
|
||||
|
||||
inline fun Flow<Int>.mapLeftItems(
|
||||
crossinline countGetter: () -> Int
|
||||
): Flow<Int> = map { countGetter() - it }
|
||||
|
||||
inline fun Flow<Int>.mapRequireFilling(
|
||||
minimalLeftItems: Int,
|
||||
crossinline countGetter: () -> Int
|
||||
): Flow<Int> = mapLeftItems(countGetter).mapNotNull {
|
||||
if (it < minimalLeftItems) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun RecyclerView.mapRequireFilling(
|
||||
minimalLeftItems: Int,
|
||||
completingScope: CoroutineScope,
|
||||
crossinline countGetter: () -> Int
|
||||
): Flow<Int> = lastVisibleItemFlow(completingScope).mapRequireFilling(minimalLeftItems, countGetter)
|
@@ -0,0 +1,68 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
|
||||
abstract class RecyclerViewAdapter<T>(
|
||||
val data: List<T>
|
||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||
var emptyView: View? = null
|
||||
set(value) {
|
||||
field = value
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
init {
|
||||
registerAdapterDataObserver(
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeChanged(positionStart, itemCount)
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
checkEmpty()
|
||||
}
|
||||
}
|
||||
)
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onBindViewHolder(holder: AbstractViewHolder<T>, position: Int) {
|
||||
holder.onBind(data[position])
|
||||
}
|
||||
|
||||
private fun checkEmpty() {
|
||||
emptyView ?. let {
|
||||
if (data.isEmpty()) {
|
||||
it.visibility = View.VISIBLE
|
||||
} else {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +1,19 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +22,7 @@ allprojects {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
}
|
||||
}
|
||||
|
@@ -12,4 +12,7 @@ function assert_success() {
|
||||
export RELEASE_MODE=true
|
||||
project="$1"
|
||||
|
||||
assert_success ./gradlew clean "$project:clean" "$project:build" "$project:publishToMavenLocal" "$project:bintrayUpload"
|
||||
assert_success ./gradlew clean
|
||||
assert_success ./gradlew "$project:build"
|
||||
assert_success ./gradlew "$project:publishToMavenLocal"
|
||||
assert_success ./gradlew "$project:bintrayUpload"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@RequiresOptIn(
|
||||
"It is possible, that behaviour of this thing will be changed or removed in future releases",
|
||||
RequiresOptIn.Level.WARNING
|
||||
)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
)
|
||||
annotation class PreviewFeature
|
||||
|
||||
@RequiresOptIn(
|
||||
"This thing is marked as warned. See message of warn to get more info",
|
||||
RequiresOptIn.Level.WARNING
|
||||
)
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.CONSTRUCTOR,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
)
|
||||
annotation class Warning(val message: String)
|
@@ -0,0 +1,77 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.experimental.and
|
||||
|
||||
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
private const val BASE64_MASK: Byte = 0x3f
|
||||
private const val BASE64_PAD = '='
|
||||
private val BASE64_INVERSE_ALPHABET = IntArray(256) {
|
||||
BASE64_ALPHABET.indexOf(it.toChar())
|
||||
}
|
||||
|
||||
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
|
||||
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
|
||||
|
||||
typealias EncodedBase64String = String
|
||||
typealias EncodedByteArray = ByteArray
|
||||
|
||||
fun String.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
||||
fun String.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
||||
|
||||
fun ByteArray.encodeBase64String(): EncodedBase64String = buildString {
|
||||
var i = 0
|
||||
while (this@encodeBase64String.size > i) {
|
||||
val read = kotlin.math.min(3, this@encodeBase64String.size - i)
|
||||
val data = ByteArray(3) {
|
||||
if (it < read) {
|
||||
this@encodeBase64String[it + i]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
val padSize = (data.size - read) * 8 / 6
|
||||
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
|
||||
((data[1].toInt() and 0xFF) shl 8) or
|
||||
(data[2].toInt() and 0xFF)
|
||||
|
||||
for (index in data.size downTo padSize) {
|
||||
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
|
||||
append(char.toBase64())
|
||||
}
|
||||
|
||||
repeat(padSize) { append(BASE64_PAD) }
|
||||
|
||||
i += read
|
||||
}
|
||||
}
|
||||
fun ByteArray.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
||||
|
||||
fun EncodedBase64String.decodeBase64() = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
||||
fun EncodedBase64String.decodeBase64String() = decodeBase64().decodeToString()
|
||||
|
||||
fun EncodedByteArray.decodeBase64(): ByteArray {
|
||||
val result = mutableListOf<Byte>()
|
||||
val data = ByteArray(4)
|
||||
(0 until size step 4).forEach { i ->
|
||||
var read = 0
|
||||
for (j in 0 until 4) {
|
||||
if (j + i < size) {
|
||||
data[j] = get(j + i)
|
||||
read++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val chunk = data.foldIndexed(0) { index, result, current ->
|
||||
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
|
||||
}
|
||||
for (index in data.size - 2 downTo (data.size - read)) {
|
||||
val origin = (chunk shr (8 * index)) and 0xff
|
||||
result.add(origin.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
fun EncodedByteArray.decodeBase64String() = decodeBase64().decodeToString()
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||
return when {
|
||||
this < min -> min
|
||||
this > max -> max
|
||||
else -> this
|
||||
}
|
||||
}
|
@@ -1,10 +1,151 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T> Iterable<T>.syncWith(
|
||||
other: Iterable<T>,
|
||||
removed: (List<T>) -> Unit = {},
|
||||
added: (List<T>) -> Unit = {}
|
||||
) {
|
||||
removed(filter { it !in other })
|
||||
added(other.filter { it !in this })
|
||||
private inline fun <T> getObject(
|
||||
additional: MutableList<T>,
|
||||
iterator: Iterator<T>
|
||||
): T? = when {
|
||||
additional.isNotEmpty() -> additional.removeFirst()
|
||||
iterator.hasNext() -> iterator.next()
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff object which contains information about differences between two [Iterable]s
|
||||
*
|
||||
* @see calculateDiff
|
||||
*/
|
||||
data class Diff<T> internal constructor(
|
||||
val removed: List<IndexedValue<T>>,
|
||||
/**
|
||||
* Old-New values pairs
|
||||
*/
|
||||
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>?>>,
|
||||
additionalsInOld: MutableList<T>,
|
||||
additionalsInNew: MutableList<T>,
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
strictComparison: Boolean
|
||||
) {
|
||||
var i = -1
|
||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||
i++
|
||||
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 {
|
||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||
}
|
||||
)
|
||||
val newPotentials = potentialChanges.drop(i).take(potentialChanges.size - i)
|
||||
when {
|
||||
oldOneEqualToNewObject -> {
|
||||
newPotentials.first().second ?.let { addedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
addedList.add(newOne!!)
|
||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
||||
}
|
||||
}
|
||||
newOneEqualToOldObject -> {
|
||||
newPotentials.first().first ?.let { removedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
removedList.add(oldOne!!)
|
||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
potentialChanges.clear()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (potentialChanges.isNotEmpty() && potentialChanges.last().let { it.first == null && it.second == null }) {
|
||||
potentialChanges.dropLast(1).forEach { (old, new) ->
|
||||
when {
|
||||
old != null && new != null -> changedList.add(old to new)
|
||||
old != null -> removedList.add(old)
|
||||
new != null -> addedList.add(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
var i = -1
|
||||
var j = -1
|
||||
|
||||
val additionalInOld = mutableListOf<T>()
|
||||
val additionalInNew = mutableListOf<T>()
|
||||
|
||||
val oldIterator = iterator()
|
||||
val newIterator = other.iterator()
|
||||
|
||||
val potentiallyChangedObjects = mutableListOf<Pair<IndexedValue<T>?, IndexedValue<T>?>>()
|
||||
val changedObjects = mutableListOf<Pair<IndexedValue<T>, IndexedValue<T>>>()
|
||||
val addedObjects = mutableListOf<IndexedValue<T>>()
|
||||
val removedObjects = mutableListOf<IndexedValue<T>>()
|
||||
|
||||
while (true) {
|
||||
i++
|
||||
j++
|
||||
|
||||
val oldObject = getObject(additionalInOld, oldIterator)
|
||||
val newObject = getObject(additionalInNew, newIterator)
|
||||
|
||||
if (oldObject == null && newObject == null) {
|
||||
break
|
||||
}
|
||||
|
||||
when {
|
||||
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, strictComparison)
|
||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
potentiallyChangedObjects.add(null to null)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode enabled
|
||||
*/
|
||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||
other: Iterable<T>
|
||||
) = calculateDiff(other, strictComparison = true)
|
||||
|
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
|
||||
class Base64Text {
|
||||
|
||||
val testText = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"""
|
||||
val base64Text = """TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpTZWQgdXQgcGVyc3BpY2lhdGlzIHVuZGUgb21uaXMgaXN0ZSBuYXR1cyBlcnJvciBzaXQgdm9sdXB0YXRlbSBhY2N1c2FudGl1bSBkb2xvcmVtcXVlIGxhdWRhbnRpdW0sIHRvdGFtIHJlbSBhcGVyaWFtLCBlYXF1ZSBpcHNhIHF1YWUgYWIgaWxsbyBpbnZlbnRvcmUgdmVyaXRhdGlzIGV0IHF1YXNpIGFyY2hpdGVjdG8gYmVhdGFlIHZpdGFlIGRpY3RhIHN1bnQgZXhwbGljYWJvLiBOZW1vIGVuaW0gaXBzYW0gdm9sdXB0YXRlbSBxdWlhIHZvbHVwdGFzIHNpdCBhc3Blcm5hdHVyIGF1dCBvZGl0IGF1dCBmdWdpdCwgc2VkIHF1aWEgY29uc2VxdXVudHVyIG1hZ25pIGRvbG9yZXMgZW9zIHF1aSByYXRpb25lIHZvbHVwdGF0ZW0gc2VxdWkgbmVzY2l1bnQuIE5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCwgcXVpIGRvbG9yZW0gaXBzdW0gcXVpYSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIsIGFkaXBpc2NpIHZlbGl0LCBzZWQgcXVpYSBub24gbnVtcXVhbSBlaXVzIG1vZGkgdGVtcG9yYSBpbmNpZHVudCB1dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hbSBhbGlxdWFtIHF1YWVyYXQgdm9sdXB0YXRlbS4gVXQgZW5pbSBhZCBtaW5pbWEgdmVuaWFtLCBxdWlzIG5vc3RydW0gZXhlcmNpdGF0aW9uZW0gdWxsYW0gY29ycG9yaXMgc3VzY2lwaXQgbGFib3Jpb3NhbSwgbmlzaSB1dCBhbGlxdWlkIGV4IGVhIGNvbW1vZGkgY29uc2VxdWF0dXI/IFF1aXMgYXV0ZW0gdmVsIGV1bSBpdXJlIHJlcHJlaGVuZGVyaXQgcXVpIGluIGVhIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIHF1YW0gbmloaWwgbW9sZXN0aWFlIGNvbnNlcXVhdHVyLCB2ZWwgaWxsdW0gcXVpIGRvbG9yZW0gZXVtIGZ1Z2lhdCBxdW8gdm9sdXB0YXMgbnVsbGEgcGFyaWF0dXI/"""
|
||||
@Test
|
||||
fun decodeEncode() {
|
||||
val encoded = testText.encodeBase64String()
|
||||
assertEquals(base64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(testText, decoded)
|
||||
}
|
||||
|
||||
val urlTestText = "https://example.com?isitexample=1"
|
||||
val urlTestBase64Text = "aHR0cHM6Ly9leGFtcGxlLmNvbT9pc2l0ZXhhbXBsZT0x"
|
||||
@Test
|
||||
fun decodeEncodeUrl() {
|
||||
val encoded = urlTestText.encodeBase64String()
|
||||
assertEquals(urlTestBase64Text, encoded)
|
||||
val decoded = encoded.decodeBase64String()
|
||||
assertEquals(urlTestText, decoded)
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.math.floor
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DiffUtilsTests {
|
||||
@Test
|
||||
fun testThatSimpleRemoveWorks() {
|
||||
val oldList = (0 until 10).toList()
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val removedSublist = oldList.subList(i, i + count)
|
||||
oldList.calculateDiff(oldList - removedSublist).apply {
|
||||
assertEquals(
|
||||
removedSublist.mapIndexed { j, o -> IndexedValue(i + j, o) },
|
||||
removed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleAddWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||
val mutable = oldList.toMutableList()
|
||||
mutable.addAll(i, addedSublist)
|
||||
oldList.calculateDiff(mutable).apply {
|
||||
assertEquals(
|
||||
addedSublist.mapIndexed { j, o -> IndexedValue(i + j, o) },
|
||||
added
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleChangesWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.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)
|
||||
).map { index ->
|
||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||
mutable[index] = it.value
|
||||
}
|
||||
}
|
||||
oldList.calculateDiff(mutable).apply {
|
||||
assertEquals(
|
||||
changes,
|
||||
replaced
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.DOMRect
|
||||
import org.w3c.dom.Element
|
||||
|
||||
val DOMRect.isOnScreenByLeftEdge: Boolean
|
||||
get() = left >= 0 && left <= window.innerWidth
|
||||
inline val Element.isOnScreenByLeftEdge
|
||||
get() = getBoundingClientRect().isOnScreenByLeftEdge
|
||||
|
||||
val DOMRect.isOnScreenByRightEdge: Boolean
|
||||
get() = right >= 0 && right <= window.innerWidth
|
||||
inline val Element.isOnScreenByRightEdge
|
||||
get() = getBoundingClientRect().isOnScreenByRightEdge
|
||||
|
||||
internal val DOMRect.isOnScreenHorizontally: Boolean
|
||||
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
|
||||
|
||||
|
||||
val DOMRect.isOnScreenByTopEdge: Boolean
|
||||
get() = top >= 0 && top <= window.innerHeight
|
||||
inline val Element.isOnScreenByTopEdge
|
||||
get() = getBoundingClientRect().isOnScreenByTopEdge
|
||||
|
||||
val DOMRect.isOnScreenByBottomEdge: Boolean
|
||||
get() = bottom >= 0 && bottom <= window.innerHeight
|
||||
inline val Element.isOnScreenByBottomEdge
|
||||
get() = getBoundingClientRect().isOnScreenByBottomEdge
|
||||
|
||||
internal val DOMRect.isOnScreenVertically: Boolean
|
||||
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
|
||||
|
||||
|
||||
val DOMRect.isOnScreenFully: Boolean
|
||||
get() = isOnScreenByLeftEdge && isOnScreenByTopEdge && isOnScreenByRightEdge && isOnScreenByBottomEdge
|
||||
val Element.isOnScreenFully: Boolean
|
||||
get() = getBoundingClientRect().isOnScreenFully
|
||||
|
||||
val DOMRect.isOnScreen: Boolean
|
||||
get() = isOnScreenFully || (isOnScreenHorizontally && isOnScreenVertically)
|
||||
inline val Element.isOnScreen: Boolean
|
||||
get() = getBoundingClientRect().isOnScreen
|
@@ -0,0 +1,48 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.css.CSSStyleDeclaration
|
||||
|
||||
sealed class Visibility
|
||||
object Visible : Visibility()
|
||||
object Invisible : Visibility()
|
||||
object Gone : Visibility()
|
||||
|
||||
var CSSStyleDeclaration.visibilityState: Visibility
|
||||
get() = when {
|
||||
display == "none" -> Gone
|
||||
visibility == "hidden" -> Invisible
|
||||
else -> Visible
|
||||
}
|
||||
set(value) {
|
||||
when (value) {
|
||||
Visible -> {
|
||||
if (display == "none") {
|
||||
display = "initial"
|
||||
}
|
||||
visibility = "visible"
|
||||
}
|
||||
Invisible -> {
|
||||
if (display == "none") {
|
||||
display = "initial"
|
||||
}
|
||||
visibility = "hidden"
|
||||
}
|
||||
Gone -> {
|
||||
display = "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
inline var Element.visibilityState: Visibility
|
||||
get() = window.getComputedStyle(this).visibilityState
|
||||
set(value) {
|
||||
window.getComputedStyle(this).visibilityState = value
|
||||
}
|
||||
|
||||
inline val Element.isVisible: Boolean
|
||||
get() = visibilityState == Visible
|
||||
inline val Element.isInvisible: Boolean
|
||||
get() = visibilityState == Invisible
|
||||
inline val Element.isGone: Boolean
|
||||
get() = visibilityState == Gone
|
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"/>
|
@@ -0,0 +1,13 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
inline fun Resources.getSp(
|
||||
resId: Int
|
||||
) = getDimension(resId) / displayMetrics.scaledDensity
|
||||
|
||||
inline fun Resources.getDp(
|
||||
resId: Int
|
||||
) = getDimension(resId) * displayMetrics.density
|
@@ -0,0 +1,140 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.Transformation
|
||||
|
||||
private fun View.performExpand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
onMeasured: View.() -> Unit,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
measure(targetWidth, targetHeight)
|
||||
onMeasured()
|
||||
show()
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
super.applyTransformation(interpolatedTime, t)
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean = true
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
private fun View.performCollapse(
|
||||
duration: Long = 500,
|
||||
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||
) {
|
||||
val a: Animation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||
if (interpolatedTime == 1f) {
|
||||
gone()
|
||||
} else {
|
||||
onPerformAnimation(interpolatedTime, t)
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun willChangeBounds(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
a.duration = duration
|
||||
|
||||
startAnimation(a)
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expand(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredHeight = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredHeight = this.measuredHeight
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.expandHorizontally(
|
||||
duration: Long = 500,
|
||||
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
) {
|
||||
var measuredWidth = 0
|
||||
performExpand(
|
||||
duration,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
{
|
||||
measuredWidth = this.measuredWidth
|
||||
}
|
||||
) { interpolatedTime, _ ->
|
||||
layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.collapse(duration: Long = 500) {
|
||||
val initialHeight: Int = measuredHeight
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
fun View.collapseHorizontally(duration: Long = 500) {
|
||||
val initialWidth: Int = measuredWidth
|
||||
performCollapse(duration) { interpolatedTime, _ ->
|
||||
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewFeature
|
||||
inline val View.isCollapsed
|
||||
get() = visibility == View.GONE
|
||||
|
||||
@PreviewFeature
|
||||
inline val View.isExpanded
|
||||
get() = !isCollapsed
|
||||
|
||||
/**
|
||||
* @return true in case of expanding
|
||||
*/
|
||||
@PreviewFeature
|
||||
fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
expand(duration)
|
||||
true
|
||||
} else {
|
||||
collapse(duration)
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true in case of expanding
|
||||
*/
|
||||
@PreviewFeature
|
||||
fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||
expandHorizontally(duration)
|
||||
true
|
||||
} else {
|
||||
collapseHorizontally(duration)
|
||||
false
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
|
||||
inline fun <T, R> Iterable<T>.mapNotNullA(transform: (T) -> R?): List<R> = map(transform).filter { it != null } as List<R>
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
|
||||
inline fun <T, R> Array<T>.mapNotNullA(mapper: (T) -> R?): List<R> = map(mapper).filter { it != null } as List<R>
|
@@ -0,0 +1,34 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
inline val View.enabled
|
||||
get() = isEnabled
|
||||
|
||||
inline val View.disabled
|
||||
get() = !enabled
|
||||
|
||||
fun View.disable() {
|
||||
if (this is ViewGroup) {
|
||||
(0 until childCount).forEach { getChildAt(it).disable() }
|
||||
}
|
||||
isEnabled = false
|
||||
}
|
||||
|
||||
fun View.enable() {
|
||||
if (this is ViewGroup) {
|
||||
(0 until childCount).forEach { getChildAt(it).enable() }
|
||||
}
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
fun View.toggleEnabledState(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
enable()
|
||||
} else {
|
||||
disable()
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.view.View
|
||||
|
||||
inline val View.gone
|
||||
get() = visibility == View.GONE
|
||||
inline fun View.gone() {
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
inline val View.hidden
|
||||
get() = visibility == View.INVISIBLE
|
||||
inline fun View.hide() {
|
||||
visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
inline val View.shown
|
||||
get() = visibility == View.VISIBLE
|
||||
inline fun View.show() {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
||||
if (isShown) {
|
||||
if (goneOnHide) {
|
||||
gone()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
@@ -12,5 +13,10 @@ kotlin {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,3 +17,15 @@ fun <T> CoroutineScope.actor(
|
||||
return channel
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.safeActor(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
crossinline block: suspend (T) -> Unit
|
||||
): Channel<T> = actor(
|
||||
channelCapacity
|
||||
) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,21 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun <T> BroadcastFlow(
|
||||
internalChannelSize: Int = Channel.BUFFERED
|
||||
): BroadcastFlow<T> {
|
||||
val channel = BroadcastChannel<T>(internalChannelSize)
|
||||
|
||||
return BroadcastFlow(
|
||||
channel,
|
||||
channel.asFlow()
|
||||
)
|
||||
}
|
||||
|
||||
class BroadcastFlow<T> internal constructor(
|
||||
private val channel: BroadcastChannel<T>,
|
||||
private val flow: Flow<T>
|
||||
): Flow<T> by flow, SendChannel<T> by channel
|
@@ -1,33 +0,0 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class BroadcastStateFlow<T> internal constructor(
|
||||
parentFlow: Flow<T>,
|
||||
private val stateGetter: () -> T
|
||||
) : StateFlow<T>, Flow<T> by parentFlow {
|
||||
override val value: T
|
||||
get() = stateGetter()
|
||||
}
|
||||
|
||||
fun <T> BroadcastChannel<T>.asStateFlow(value: T, scope: CoroutineScope): StateFlow<T> = asFlow().let {
|
||||
var state: T = value
|
||||
it.onEach { state = it }.launchIn(scope)
|
||||
BroadcastStateFlow(it) {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> BroadcastChannel<T?>.asStateFlow(scope: CoroutineScope): StateFlow<T?> = asStateFlow(null, scope)
|
||||
|
||||
fun <T> broadcastStateFlow(initial: T, scope: CoroutineScope, channelSize: Int = Channel.BUFFERED) = BroadcastChannel<T>(
|
||||
channelSize
|
||||
).let {
|
||||
it to it.asStateFlow(initial, scope)
|
||||
}
|
||||
|
||||
fun <T> broadcastStateFlow(scope: CoroutineScope, channelSize: Int = Channel.BUFFERED) = broadcastStateFlow<T?>(null, scope, channelSize)
|
||||
|
@@ -0,0 +1,5 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
|
||||
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)
|
@@ -0,0 +1,37 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend (T) -> Unit) = onEach(block).launchIn(scope)
|
||||
|
||||
/**
|
||||
* Use [subscribe], but all [block]s will be called inside of [safely] function.
|
||||
* Use [onException] to set up your reaction for [Throwable]s
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafely(
|
||||
scope: CoroutineScope,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeSafely(
|
||||
scope,
|
||||
{},
|
||||
block
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.merge
|
||||
|
||||
inline operator fun <T> Flow<T>.plus(other: Flow<T>) = merge(this, other)
|
@@ -1,23 +1,156 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||
*/
|
||||
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||
|
||||
/**
|
||||
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||
*/
|
||||
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||
try {
|
||||
defaultSafelyExceptionHandler(it)
|
||||
} catch (e: Throwable) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key for [SafelyExceptionHandler] which can be used in [CoroutineContext.get] to get current default
|
||||
* [SafelyExceptionHandler]
|
||||
*/
|
||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||
class SafelyExceptionHandlerKey<T> : CoroutineContext.Key<SafelyExceptionHandler<T>>
|
||||
|
||||
/**
|
||||
* Shortcut for creating instance of [SafelyExceptionHandlerKey]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||
inline fun <T> safelyExceptionHandlerKey() = SafelyExceptionHandlerKey<T>()
|
||||
|
||||
/**
|
||||
* Wrapper for [ExceptionHandler] which can be used in [CoroutineContext] to set local (for [CoroutineContext]) default
|
||||
* [ExceptionHandler]. To get it use [CoroutineContext.get] with key [SafelyExceptionHandlerKey]
|
||||
*
|
||||
* @see SafelyExceptionHandlerKey
|
||||
* @see ExceptionHandler
|
||||
*/
|
||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandler", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
||||
class SafelyExceptionHandler<T>(
|
||||
val handler: ExceptionHandler<T>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*> = safelyExceptionHandlerKey<T>()
|
||||
}
|
||||
|
||||
/**
|
||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandler
|
||||
*/
|
||||
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||
|
||||
/**
|
||||
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||
*
|
||||
* @see safelyWithContextExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
class ContextSafelyExceptionHandler(
|
||||
val handler: ExceptionHandler<Unit>
|
||||
) : CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<*>
|
||||
get() = ContextSafelyExceptionHandlerKey
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if
|
||||
* exists
|
||||
*
|
||||
* @see ContextSafelyExceptionHandler
|
||||
* @see ContextSafelyExceptionHandlerKey
|
||||
*/
|
||||
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||
|
||||
/**
|
||||
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
|
||||
* already contains [ContextSafelyExceptionHandler], [ContextSafelyExceptionHandler.handler] will be used BEFORE
|
||||
* [contextExceptionHandler] in case of exception.
|
||||
*
|
||||
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
|
||||
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
|
||||
*/
|
||||
suspend fun <T> safelyWithContextExceptionHandler(
|
||||
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler ->
|
||||
ContextSafelyExceptionHandler {
|
||||
oldHandler(it)
|
||||
contextExceptionHandler(it)
|
||||
}
|
||||
} ?: ContextSafelyExceptionHandler(contextExceptionHandler)
|
||||
return withContext(contextSafelyExceptionHandler) {
|
||||
safely(safelyExceptionHandler, block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
||||
*
|
||||
* Priorities of [ExceptionHandler]s:
|
||||
*
|
||||
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
|
||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||
* * [defaultSafelyExceptionHandler]
|
||||
*
|
||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||
* exception will be available for catching
|
||||
*
|
||||
* @see defaultSafelyExceptionHandler
|
||||
* @see safelyWithoutExceptions
|
||||
* @see safelyWithContextExceptionHandler
|
||||
*/
|
||||
suspend inline fun <T> safely(
|
||||
noinline onException: ExceptionHandler<T> = { throw it },
|
||||
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T {
|
||||
return try {
|
||||
supervisorScope(block)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||
onException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||
* result from exception (instead of throwing it)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline onException: ExceptionHandler<T?>,
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safely(onException, block)
|
||||
|
||||
/**
|
||||
* Shortcut for [safely] without exception handler (instead of this you will always receive null as a result)
|
||||
*/
|
||||
suspend inline fun <T> safelyWithoutExceptions(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T? = safelyWithoutExceptions(
|
||||
{
|
||||
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||
null
|
||||
},
|
||||
block
|
||||
)
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||
var throwable: Throwable? = null
|
||||
var result: T? = null
|
||||
val objectToSynchronize = java.lang.Object()
|
||||
val launchCallback = {
|
||||
launch {
|
||||
safely(
|
||||
{
|
||||
throwable = it
|
||||
}
|
||||
) {
|
||||
result = block()
|
||||
}
|
||||
synchronized(objectToSynchronize) {
|
||||
objectToSynchronize.notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized(objectToSynchronize) {
|
||||
launchCallback()
|
||||
objectToSynchronize.wait()
|
||||
}
|
||||
throw throwable ?: return result!!
|
||||
}
|
||||
|
||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
@@ -0,0 +1,38 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.test.Test
|
||||
|
||||
class HandleSafelyCoroutineContextTest {
|
||||
@Test
|
||||
fun testHandleSafelyCoroutineContext() {
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
var contextHandlerHappen = false
|
||||
var localHandlerHappen = false
|
||||
var defaultHandlerHappen = false
|
||||
defaultSafelyExceptionHandler = {
|
||||
defaultHandlerHappen = true
|
||||
throw it
|
||||
}
|
||||
val contextHandler: ExceptionHandler<Unit> = {
|
||||
contextHandlerHappen = true
|
||||
}
|
||||
val checkJob = scope.launch {
|
||||
safelyWithContextExceptionHandler(contextHandler) {
|
||||
safely(
|
||||
{
|
||||
localHandlerHappen = true
|
||||
}
|
||||
) {
|
||||
error("That must happen :)")
|
||||
}
|
||||
println(coroutineContext)
|
||||
error("That must happen too:)")
|
||||
}
|
||||
}
|
||||
launchSynchronously { checkJob.join() }
|
||||
assert(contextHandlerHappen)
|
||||
assert(localHandlerHappen)
|
||||
assert(defaultHandlerHappen)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LaunchSynchronouslyTest {
|
||||
@Test
|
||||
fun testRunInCoroutine() {
|
||||
(0 .. 10000).forEach {
|
||||
assertEquals(it, launchSynchronously { it })
|
||||
}
|
||||
}
|
||||
}
|
1
coroutines/src/main/AndroidManifest.xml
Normal file
1
coroutines/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines"/>
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.Main,
|
||||
block
|
||||
)
|
40
defaultAndroidSettings
Normal file
40
defaultAndroidSettings
Normal file
@@ -0,0 +1,40 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
}
|
134
dokka/build.gradle
Normal file
134
dokka/build.gradle
Normal file
@@ -0,0 +1,134 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
id "org.jetbrains.dokka"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(BOTH) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
android {}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
|
||||
project.parent.subprojects.forEach {
|
||||
if (
|
||||
it != project
|
||||
&& it.hasProperty("kotlin")
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") }
|
||||
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") }
|
||||
) {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SourceDirectorySet> findSourcesWithName(String... approximateNames) {
|
||||
return parent.subprojects
|
||||
.findAll { it != project && it.hasProperty("kotlin") }
|
||||
.collectMany { it.kotlin.sourceSets }
|
||||
.findAll { sourceSet ->
|
||||
approximateNames.any { nameToFilter ->
|
||||
sourceSet.name.contains(nameToFilter)
|
||||
}
|
||||
}.collect { it.kotlin }
|
||||
}
|
||||
|
||||
tasks.dokkaHtml {
|
||||
dokkaSourceSets {
|
||||
configureEach {
|
||||
skipDeprecated.set(true)
|
||||
|
||||
sourceLink {
|
||||
localDirectory.set(file("./"))
|
||||
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
||||
remoteLineSuffix.set("#L")
|
||||
}
|
||||
}
|
||||
|
||||
named("commonMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("commonMain"))
|
||||
}
|
||||
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
}
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
|
||||
}
|
||||
|
||||
named("androidMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("androidMain", "commonMain"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
1
dokka/src/main/AndroidManifest.xml
Normal file
1
dokka/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.dokka"/>
|
@@ -10,17 +10,21 @@ allprojects {
|
||||
}
|
||||
|
||||
internalProject = { name ->
|
||||
if (releaseMode) {
|
||||
"$group:$name:$version"
|
||||
} else {
|
||||
projectByName("$name")
|
||||
}
|
||||
// if (releaseMode) {
|
||||
// "$group:$name:$version"
|
||||
// } else {
|
||||
// projectByName("$name")
|
||||
// }
|
||||
projectByName("$name")
|
||||
}
|
||||
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
|
||||
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
||||
|
@@ -3,20 +3,42 @@ org.gradle.parallel=true
|
||||
kotlin.js.generate.externals=true
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.js=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
kotlin_version=1.4.10
|
||||
kotlin_coroutines_version=1.3.9
|
||||
kotlin_serialisation_core_version=1.0.0
|
||||
kotlin_exposed_version=0.27.1
|
||||
kotlin_version=1.4.21
|
||||
kotlin_coroutines_version=1.4.2
|
||||
kotlin_serialisation_core_version=1.0.1
|
||||
kotlin_exposed_version=0.28.1
|
||||
|
||||
ktor_version=1.4.1
|
||||
ktor_version=1.5.0
|
||||
|
||||
klockVersion=1.12.1
|
||||
klockVersion=2.0.2
|
||||
|
||||
gradle_bintray_plugin_version=1.8.5
|
||||
github_release_plugin_version=2.2.12
|
||||
|
||||
uuidVersion=0.2.2
|
||||
uuidVersion=0.2.3
|
||||
|
||||
# ANDROID
|
||||
|
||||
core_ktx_version=1.3.2
|
||||
androidx_recycler_version=1.1.0
|
||||
appcompat_version=1.2.0
|
||||
|
||||
android_minSdkVersion=19
|
||||
android_compileSdkVersion=30
|
||||
android_buildToolsVersion=30.0.2
|
||||
dexcount_version=2.0.0
|
||||
junit_version=4.12
|
||||
test_ext_junit_version=1.1.2
|
||||
espresso_core=3.3.0
|
||||
|
||||
# Dokka
|
||||
|
||||
dokka_version=1.4.20
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.2.2
|
||||
version=0.4.17
|
||||
android_code_version=21
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
@@ -4,7 +4,8 @@ import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.websocket.ws
|
||||
import io.ktor.http.cio.websocket.*
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readBytes
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
@@ -24,7 +25,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
val producerScope = this@channelFlow
|
||||
do {
|
||||
val reconnect = try {
|
||||
safely ({ throw it }) {
|
||||
safely {
|
||||
ws(correctedUrl) {
|
||||
for (received in incoming) {
|
||||
when (received) {
|
||||
@@ -63,10 +64,11 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||
url: String,
|
||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = createStandardWebsocketFlow(
|
||||
url,
|
||||
checkReconnection
|
||||
) {
|
||||
standardKtorSerialFormat.decodeDefault(deserializer, it)
|
||||
serialFormat.decodeDefault(deserializer, it)
|
||||
}
|
||||
|
@@ -8,27 +8,57 @@ import kotlinx.serialization.*
|
||||
|
||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||
|
||||
class UnifiedRequester(
|
||||
private val client: HttpClient = HttpClient(),
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) {
|
||||
suspend fun <ResultType> uniget(
|
||||
url: String,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
): ResultType = client.get<StandardKtorSerialInputData>(
|
||||
url
|
||||
).let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
|
||||
fun <T> encodeUrlQueryValue(
|
||||
serializationStrategy: SerializationStrategy<T>,
|
||||
value: T
|
||||
) = serialFormat.encodeHex(
|
||||
serializationStrategy,
|
||||
value
|
||||
)
|
||||
|
||||
suspend fun <BodyType, ResultType> unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = client.post<StandardKtorSerialInputData>(url) {
|
||||
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
}.let {
|
||||
serialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
|
||||
fun <T> createStandardWebsocketFlow(
|
||||
url: String,
|
||||
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||
}
|
||||
|
||||
val defaultRequester = UnifiedRequester()
|
||||
|
||||
suspend fun <ResultType> HttpClient.uniget(
|
||||
url: String,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = get<StandardKtorSerialInputData>(
|
||||
url
|
||||
).let {
|
||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
) = defaultRequester.uniget(url, resultDeserializer)
|
||||
|
||||
|
||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = standardKtorSerialFormat.encodeHex(
|
||||
this,
|
||||
value
|
||||
)
|
||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = defaultRequester.encodeUrlQueryValue(this, value)
|
||||
|
||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||
url: String,
|
||||
bodyInfo: BodyPair<BodyType>,
|
||||
resultDeserializer: DeserializationStrategy<ResultType>
|
||||
) = post<StandardKtorSerialInputData>(url) {
|
||||
body = standardKtorSerialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||
}.let {
|
||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
||||
}
|
||||
) = defaultRequester.unipost(url, bodyInfo, resultDeserializer)
|
||||
|
1
ktor/client/src/main/AndroidManifest.xml
Normal file
1
ktor/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.ktor.client"/>
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
@@ -3,16 +3,28 @@ package dev.inmo.micro_utils.ktor.common
|
||||
typealias QueryParam = Pair<String, String?>
|
||||
typealias QueryParams = Map<String, String?>
|
||||
|
||||
/**
|
||||
* Create query part which includes key=value pairs separated with &
|
||||
*/
|
||||
val QueryParams.asUrlQuery: String
|
||||
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
|
||||
|
||||
/**
|
||||
* Create query part which includes key=value pairs separated with &
|
||||
*/
|
||||
val List<QueryParam>.asUrlQuery: String
|
||||
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
|
||||
|
||||
/**
|
||||
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||
*/
|
||||
fun String.includeQueryParams(
|
||||
queryParams: QueryParams
|
||||
): String = "$this${if(queryParams.isNotEmpty()) "${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" else ""}"
|
||||
|
||||
/**
|
||||
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||
*/
|
||||
fun String.includeQueryParams(
|
||||
queryParams: List<QueryParam>
|
||||
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
|
||||
|
1
ktor/common/src/main/AndroidManifest.xml
Normal file
1
ktor/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.ktor.common"/>
|
@@ -22,23 +22,22 @@ fun <T> Route.includeWebsocketHandling(
|
||||
converter: (T) -> StandardKtorSerialInputData
|
||||
) {
|
||||
webSocket(suburl) {
|
||||
// println("connected")
|
||||
safely {
|
||||
flow.collect {
|
||||
send(converter(it))
|
||||
}
|
||||
}
|
||||
// println("disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>
|
||||
serializer: SerializationStrategy<T>,
|
||||
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||
) = includeWebsocketHandling(
|
||||
suburl,
|
||||
flow
|
||||
) {
|
||||
standardKtorSerialFormat.encodeDefault(serializer, it)
|
||||
serialFormat.encodeDefault(serializer, it)
|
||||
}
|
||||
|
@@ -3,13 +3,88 @@ package dev.inmo.micro_utils.ktor.server
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import dev.inmo.micro_utils.ktor.common.*
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.util.toByteArray
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.*
|
||||
|
||||
class UnifiedRouter(
|
||||
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||
) {
|
||||
fun <T> Route.includeWebsocketHandling(
|
||||
suburl: String,
|
||||
flow: Flow<T>,
|
||||
serializer: SerializationStrategy<T>
|
||||
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||
|
||||
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||
answerSerializer: SerializationStrategy<T>,
|
||||
answer: T
|
||||
) {
|
||||
call.respondBytes (
|
||||
serialFormat.encodeDefault(answerSerializer, answer),
|
||||
serialFormatContentType
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> PipelineContext<*, ApplicationCall>.uniload(
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = safely {
|
||||
serialFormat.decodeDefault(
|
||||
deserializer,
|
||||
call.receive()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun PipelineContext<*, ApplicationCall>.getParameterOrSendError(
|
||||
field: String
|
||||
) = call.parameters[field].also {
|
||||
if (it == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Request must contains $field")
|
||||
}
|
||||
}
|
||||
|
||||
fun PipelineContext<*, ApplicationCall>.getQueryParameter(
|
||||
field: String
|
||||
) = call.request.queryParameters[field]
|
||||
|
||||
suspend fun PipelineContext<*, ApplicationCall>.getQueryParameterOrSendError(
|
||||
field: String
|
||||
) = getQueryParameter(field).also {
|
||||
if (it == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValue(
|
||||
field: String,
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = getQueryParameter(field) ?.let {
|
||||
serialFormat.decodeHex(
|
||||
deserializer,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValueOrSendError(
|
||||
field: String,
|
||||
deserializer: DeserializationStrategy<T>
|
||||
) = decodeUrlQueryValue(field, deserializer).also {
|
||||
if (it == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val defaultUnifiedRouter = UnifiedRouter()
|
||||
|
||||
suspend fun <T> ApplicationCall.unianswer(
|
||||
answerSerializer: SerializationStrategy<T>,
|
||||
answer: T
|
||||
|
@@ -1,60 +0,0 @@
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
project.publishing.publications.all {
|
||||
// rename artifacts
|
||||
groupId "${project.group}"
|
||||
if (it.name.contains('kotlinMultiplatform')) {
|
||||
artifactId = "${project.name}"
|
||||
} else {
|
||||
artifactId = "${project.name}-$name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications.all {
|
||||
artifact javadocsJar
|
||||
|
||||
pom {
|
||||
description = ""
|
||||
name = "${project.name}"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror"
|
||||
|
||||
scm {
|
||||
developerConnection = "scm:git:[fetch=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git[push=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
url = "ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
}
|
||||
|
||||
developers {
|
||||
|
||||
developer {
|
||||
id = "InsanusMokrassar"
|
||||
name = "Aleksei Ovsiannikov"
|
||||
email = "ovsyannikov.alexey95@gmail.com"
|
||||
}
|
||||
|
||||
|
||||
developer {
|
||||
id = "000Sanya"
|
||||
name = "Syrov Aleksandr"
|
||||
email = "000sanya.000sanya@gmail.com"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
licenses {
|
||||
|
||||
license {
|
||||
name = "Apache Software License 2.0"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
1
mime_types/src/main/AndroidManifest.xml
Normal file
1
mime_types/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.mime_types"/>
|
26
mppAndroidProject
Normal file
26
mppAndroidProject
Normal file
@@ -0,0 +1,26 @@
|
||||
project.version = "$version"
|
||||
project.group = "$group"
|
||||
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
publishAllLibraryVariants()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-common')
|
||||
implementation kotlin('test-annotations-common')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
@@ -9,6 +9,9 @@ kotlin {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
android {
|
||||
publishAllLibraryVariants()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
@@ -34,5 +37,14 @@ kotlin {
|
||||
implementation kotlin('test-junit')
|
||||
}
|
||||
}
|
||||
androidTest {
|
||||
dependencies {
|
||||
implementation kotlin('test-junit')
|
||||
implementation "androidx.test.ext:junit:$test_ext_junit_version"
|
||||
implementation "androidx.test.espresso:espresso-core:$espresso_core"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "$defaultAndroidSettingsPresetPath"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
@@ -22,12 +22,27 @@ interface Pagination {
|
||||
val size: Int
|
||||
}
|
||||
|
||||
/**
|
||||
* Logical shortcut for comparison that page is 0
|
||||
*/
|
||||
inline val Pagination.isFirstPage
|
||||
get() = page == 0
|
||||
|
||||
/**
|
||||
* First number in index of objects. It can be used as offset for databases or other data sources
|
||||
*/
|
||||
val Pagination.firstIndex: Int
|
||||
get() = page * size
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
*
|
||||
* [[firstIndex], [lastIndex]]; That means, that for [Pagination] with [Pagination.size] == 10 and [Pagination.page] == 1
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19. Here [Pagination.lastIndexExclusive] == 20
|
||||
*/
|
||||
val Pagination.lastIndexExclusive: Int
|
||||
get() = firstIndex + size
|
||||
|
||||
/**
|
||||
* Last number in index of objects. In fact, one [Pagination] object represent data in next range:
|
||||
*
|
||||
@@ -35,7 +50,7 @@ val Pagination.firstIndex: Int
|
||||
* you will retrieve [Pagination.firstIndex] == 10 and [Pagination.lastIndex] == 19.
|
||||
*/
|
||||
val Pagination.lastIndex: Int
|
||||
get() = firstIndex + size - 1
|
||||
get() = lastIndexExclusive - 1
|
||||
|
||||
/**
|
||||
* Calculates pages count for given [datasetSize]
|
||||
|
@@ -21,7 +21,12 @@ fun <T> Iterable<T>.paginate(with: Pagination): PaginationResult<T> {
|
||||
}
|
||||
|
||||
fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> {
|
||||
return subList(with.firstIndex, with.lastIndex + 1).createPaginationResult(
|
||||
val firstIndex = maxOf(with.firstIndex, 0)
|
||||
val lastIndex = minOf(with.lastIndexExclusive, size)
|
||||
if (firstIndex > lastIndex) {
|
||||
return emptyPaginationResult()
|
||||
}
|
||||
return subList(firstIndex, lastIndex).createPaginationResult(
|
||||
with,
|
||||
size.toLong()
|
||||
)
|
||||
|
@@ -0,0 +1,41 @@
|
||||
package dev.inmo.micro_utils.pagination.utils
|
||||
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
|
||||
class PaginatedIterator<T>(
|
||||
pageSize: Int,
|
||||
private val countGetter: () -> Long,
|
||||
private val paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||
) : Iterator<T> {
|
||||
private var pagination = FirstPagePagination(pageSize)
|
||||
private val currentStack = mutableListOf<T>()
|
||||
override fun hasNext(): Boolean = currentStack.isNotEmpty() || (countGetter() < pagination.lastIndexExclusive)
|
||||
|
||||
override fun next(): T {
|
||||
if (currentStack.isEmpty()) {
|
||||
val resultPagination = paginationResultGetter.invoke(pagination)
|
||||
currentStack.addAll(resultPagination.results)
|
||||
require(currentStack.isNotEmpty()) { "There is no elements left" }
|
||||
pagination = resultPagination.nextPage()
|
||||
}
|
||||
return currentStack.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
class PaginatedIterable<T>(
|
||||
private val pageSize: Int,
|
||||
private val countGetter: () -> Long,
|
||||
private val paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||
) : Iterable<T> {
|
||||
override fun iterator(): Iterator<T> = PaginatedIterator(pageSize, countGetter, paginationResultGetter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will make iterable using incoming [countGetter] and [paginationResultGetter]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> makeIterable(
|
||||
noinline countGetter: () -> Long,
|
||||
pageSize: Int = defaultMediumPageSize,
|
||||
noinline paginationResultGetter: Pagination.() -> PaginationResult<T>
|
||||
): Iterable<T> = PaginatedIterable(pageSize, countGetter, paginationResultGetter)
|
1
pagination/common/src/main/AndroidManifest.xml
Normal file
1
pagination/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.pagination.common"/>
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
1
pagination/ktor/common/src/main/AndroidManifest.xml
Normal file
1
pagination/ktor/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.pagination.ktor.common"/>
|
@@ -1 +1 @@
|
||||
{"bintrayConfig":{"repo":"MicroUtils","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/MicroUtils","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror","vcsUrl":"ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}]},"type":"Multiplatform"}
|
||||
{"bintrayConfig":{"repo":"MicroUtils","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/MicroUtils","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"","url":"https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror","vcsUrl":"ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}]}}
|
119
publish.gradle
119
publish.gradle
@@ -1,59 +1,76 @@
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
apply from: "$publishMavenPath"
|
||||
|
||||
bintray {
|
||||
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
filesSpec {
|
||||
from "${buildDir}/publications/"
|
||||
eachFile {
|
||||
String directorySubname = it.getFile().parentFile.name
|
||||
if (it.getName() == "module.json") {
|
||||
if (directorySubname == "kotlinMultiplatform") {
|
||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
|
||||
} else {
|
||||
it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
|
||||
}
|
||||
} else {
|
||||
if (directorySubname == "kotlinMultiplatform" && it.getName() == "pom-default.xml") {
|
||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.pom")
|
||||
} else {
|
||||
it.exclude()
|
||||
}
|
||||
}
|
||||
}
|
||||
into "${project.group}".replace(".", "/")
|
||||
}
|
||||
|
||||
publish = true
|
||||
override = true
|
||||
|
||||
pkg {
|
||||
repo = "MicroUtils"
|
||||
name = "${project.name}"
|
||||
vcsUrl = "https://github.com/InsanusMokrassar/MicroUtils"
|
||||
licenses = ["Apache-2.0"]
|
||||
version {
|
||||
name = "${project.version}"
|
||||
released = new Date()
|
||||
vcsTag = "${project.version}"
|
||||
gpg {
|
||||
sign = true
|
||||
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
|
||||
}
|
||||
}
|
||||
}
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
}
|
||||
task sourceJar (type : Jar) {
|
||||
classifier = 'sources'
|
||||
}
|
||||
|
||||
bintrayUpload.doFirst {
|
||||
publications = publishing.publications.collect {
|
||||
afterEvaluate {
|
||||
project.publishing.publications.all {
|
||||
// rename artifacts
|
||||
groupId "${project.group}"
|
||||
if (it.name.contains('kotlinMultiplatform')) {
|
||||
null
|
||||
artifactId = "${project.name}"
|
||||
artifact sourceJar
|
||||
} else {
|
||||
it.name
|
||||
artifactId = "${project.name}-$name"
|
||||
}
|
||||
} - null
|
||||
}
|
||||
}
|
||||
|
||||
bintrayUpload.dependsOn publishToMavenLocal
|
||||
publishing {
|
||||
publications.all {
|
||||
artifact javadocsJar
|
||||
|
||||
pom {
|
||||
description = ""
|
||||
name = "${project.name}"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror"
|
||||
|
||||
scm {
|
||||
developerConnection = "scm:git:[fetch=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git[push=]ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
url = "ssh://git@git.inmo.dev:8322/InsanusMokrassar/MicroUtils_mirror.git"
|
||||
}
|
||||
|
||||
developers {
|
||||
|
||||
developer {
|
||||
id = "InsanusMokrassar"
|
||||
name = "Aleksei Ovsiannikov"
|
||||
email = "ovsyannikov.alexey95@gmail.com"
|
||||
}
|
||||
|
||||
|
||||
developer {
|
||||
id = "000Sanya"
|
||||
name = "Syrov Aleksandr"
|
||||
email = "000sanya.000sanya@gmail.com"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
licenses {
|
||||
|
||||
license {
|
||||
name = "Apache Software License 2.0"
|
||||
url = "https://git.inmo.dev/InsanusMokrassar/MicroUtils_mirror/src/master/LICENSE"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "bintray"
|
||||
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/MicroUtils/${project.name}/;publish=1;override=1")
|
||||
credentials {
|
||||
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
@@ -15,5 +16,18 @@ kotlin {
|
||||
api "com.benasher44:uuid:$uuidVersion"
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api internalProject("micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.core:core-ktx:$core_ktx_version"
|
||||
api internalProject("micro_utils.common")
|
||||
api internalProject("micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
interface MapperRepo<FromKey, FromValue, ToKey, ToValue> {
|
||||
suspend fun FromKey.toOutKey() = this as ToKey
|
||||
suspend fun FromValue.toOutValue() = this as ToValue
|
||||
|
||||
suspend fun ToKey.toInnerKey() = this as FromKey
|
||||
suspend fun ToValue.toInnerValue() = this as FromValue
|
||||
}
|
||||
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> mapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
) = object : MapperRepo<FromKey, FromValue, ToKey, ToValue> {
|
||||
override suspend fun FromKey.toOutKey(): ToKey = keyFromToTo()
|
||||
override suspend fun FromValue.toOutValue(): ToValue = valueFromToTo()
|
||||
override suspend fun ToKey.toInnerKey(): FromKey = keyToToFrom()
|
||||
override suspend fun ToValue.toInnerValue(): FromValue = valueToToFrom()
|
||||
}
|
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||
suspend fun get(k: Key, pagination: Pagination, reversed: Boolean = false): PaginationResult<Value>
|
||||
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
||||
suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
||||
suspend fun contains(k: Key): Boolean
|
||||
suspend fun contains(k: Key, v: Value): Boolean
|
||||
suspend fun count(k: Key): Long
|
||||
@@ -35,59 +36,75 @@ interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("ReadOneToManyKeyValueRepo", "dev.inmo.micro_utils.repos.ReadOneToManyKeyValueRepo"))
|
||||
typealias OneToManyReadKeyValueRepo<Key, Value> = ReadOneToManyKeyValueRepo<Key, Value>
|
||||
typealias ReadKeyValuesRepo<Key,Value> = ReadOneToManyKeyValueRepo<Key, Value>
|
||||
|
||||
interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
|
||||
val onNewValue: Flow<Pair<Key, Value>>
|
||||
val onValueRemoved: Flow<Pair<Key, Value>>
|
||||
val onDataCleared: Flow<Key>
|
||||
|
||||
suspend fun add(toAdd: Map<Key, List<Value>>) = toAdd.forEach { (k, values) ->
|
||||
values.forEach { v ->
|
||||
add(k, v)
|
||||
}
|
||||
}
|
||||
@Deprecated("Will be extracted as extension for other add method")
|
||||
suspend fun add(k: Key, v: Value)
|
||||
suspend fun add(toAdd: Map<Key, List<Value>>)
|
||||
|
||||
suspend fun remove(toRemove: Map<Key, List<Value>>) = toRemove.forEach { (k, values) ->
|
||||
values.forEach { v ->
|
||||
remove(k, v)
|
||||
}
|
||||
}
|
||||
@Deprecated("Will be extracted as extension for other remove method")
|
||||
suspend fun remove(k: Key, v: Value)
|
||||
suspend fun remove(toRemove: Map<Key, List<Value>>)
|
||||
|
||||
suspend fun clear(k: Key)
|
||||
|
||||
suspend fun set(toSet: Map<Key, List<Value>>) {
|
||||
toSet.keys.forEach { key -> clear(key) }
|
||||
add(toSet)
|
||||
}
|
||||
}
|
||||
@Deprecated("Renamed", ReplaceWith("WriteOneToManyKeyValueRepo", "dev.inmo.micro_utils.repos.WriteOneToManyKeyValueRepo"))
|
||||
typealias OneToManyWriteKeyValueRepo<Key, Value> = WriteOneToManyKeyValueRepo<Key, Value>
|
||||
typealias WriteKeyValuesRepo<Key,Value> = WriteOneToManyKeyValueRepo<Key, Value>
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||
keysAndValues: List<Pair<Key, List<Value>>>
|
||||
) = add(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||
vararg keysAndValues: Pair<Key, List<Value>>
|
||||
) = add(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.add(
|
||||
k: Key, v: List<Value>
|
||||
) = add(mapOf(k to v))
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.add(
|
||||
k: Key, vararg v: Value
|
||||
) = add(k, v.toList())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.set(
|
||||
keysAndValues: List<Pair<Key, List<Value>>>
|
||||
) = set(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.set(
|
||||
vararg keysAndValues: Pair<Key, List<Value>>
|
||||
) = set(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
||||
k: Key, v: List<Value>
|
||||
) = set(mapOf(k to v))
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.set(
|
||||
k: Key, vararg v: Value
|
||||
) = set(k, v.toList())
|
||||
|
||||
interface OneToManyKeyValueRepo<Key, Value> : ReadOneToManyKeyValueRepo<Key, Value>, WriteOneToManyKeyValueRepo<Key, Value>
|
||||
typealias KeyValuesRepo<Key,Value> = OneToManyKeyValueRepo<Key, Value>
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||
k: Key,
|
||||
vararg v: Value
|
||||
) = add(mapOf(k to v.toList()))
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||
keysAndValues: List<Pair<Key, List<Value>>>
|
||||
) = add(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
|
||||
vararg keysAndValues: Pair<Key, List<Value>>
|
||||
) = add(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.remove(
|
||||
k: Key,
|
||||
vararg v: Value
|
||||
) = remove(mapOf(k to v.toList()))
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.remove(
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||
keysAndValues: List<Pair<Key, List<Value>>>
|
||||
) = remove(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.remove(
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||
vararg keysAndValues: Pair<Key, List<Value>>
|
||||
) = remove(keysAndValues.toMap())
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||
k: Key,
|
||||
v: List<Value>
|
||||
) = remove(mapOf(k to v))
|
||||
|
||||
suspend inline fun <Key, Value> WriteOneToManyKeyValueRepo<Key, Value>.remove(
|
||||
k: Key,
|
||||
vararg v: Value
|
||||
) = remove(k, v.toList())
|
||||
|
@@ -10,6 +10,7 @@ interface ReadStandardCRUDRepo<ObjectType, IdType> : Repo {
|
||||
suspend fun contains(id: IdType): Boolean
|
||||
suspend fun count(): Long
|
||||
}
|
||||
typealias ReadCRUDRepo<ObjectType, IdType> = ReadStandardCRUDRepo<ObjectType, IdType>
|
||||
|
||||
typealias UpdatedValuePair<IdType, ValueType> = Pair<IdType, ValueType>
|
||||
val <IdType> UpdatedValuePair<IdType, *>.id
|
||||
@@ -27,6 +28,7 @@ interface WriteStandardCRUDRepo<ObjectType, IdType, InputValueType> : Repo {
|
||||
suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType>
|
||||
suspend fun deleteById(ids: List<IdType>)
|
||||
}
|
||||
typealias WriteCRUDRepo<ObjectType, IdType, InputValueType> = WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||
|
||||
suspend fun <ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>.create(
|
||||
vararg values: InputValueType
|
||||
@@ -39,4 +41,5 @@ suspend fun <ObjectType, IdType, InputValueType> WriteStandardCRUDRepo<ObjectTyp
|
||||
) = deleteById(ids.toList())
|
||||
|
||||
interface StandardCRUDRepo<ObjectType, IdType, InputValueType> : ReadStandardCRUDRepo<ObjectType, IdType>,
|
||||
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||
WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>
|
||||
typealias CRUDRepo<ObjectType, IdType, InputValueType> = StandardCRUDRepo<ObjectType, IdType, InputValueType>
|
@@ -1,23 +1,51 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ReadStandardKeyValueRepo<Key, Value> : Repo {
|
||||
suspend fun get(k: Key): Value?
|
||||
suspend fun values(pagination: Pagination, reversed: Boolean = false): PaginationResult<Value>
|
||||
suspend fun keys(pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
||||
suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean = false): PaginationResult<Key>
|
||||
suspend fun contains(key: Key): Boolean
|
||||
suspend fun count(): Long
|
||||
}
|
||||
typealias ReadKeyValueRepo<Key,Value> = ReadStandardKeyValueRepo<Key, Value>
|
||||
|
||||
interface WriteStandardKeyValueRepo<Key, Value> : Repo {
|
||||
val onNewValue: Flow<Pair<Key, Value>>
|
||||
val onValueRemoved: Flow<Key>
|
||||
|
||||
suspend fun set(k: Key, v: Value)
|
||||
suspend fun unset(k: Key)
|
||||
suspend fun set(toSet: Map<Key, Value>)
|
||||
suspend fun unset(toUnset: List<Key>)
|
||||
suspend fun unsetWithValues(toUnset: List<Value>)
|
||||
}
|
||||
typealias WriteKeyValueRepo<Key,Value> = WriteStandardKeyValueRepo<Key, Value>
|
||||
|
||||
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value>
|
||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.set(
|
||||
vararg toSet: Pair<Key, Value>
|
||||
) = set(toSet.toMap())
|
||||
|
||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.set(
|
||||
k: Key, v: Value
|
||||
) = set(k to v)
|
||||
|
||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unset(
|
||||
vararg k: Key
|
||||
) = unset(k.toList())
|
||||
|
||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unsetWithValues(
|
||||
vararg v: Value
|
||||
) = unsetWithValues(v.toList())
|
||||
|
||||
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value> {
|
||||
override suspend fun unsetWithValues(toUnset: List<Value>) = toUnset.forEach { v ->
|
||||
doWithPagination {
|
||||
keys(v, it).also {
|
||||
unset(it.results)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
typealias KeyValueRepo<Key,Value> = StandardKeyValueRepo<Key, Value>
|
||||
|
@@ -0,0 +1,150 @@
|
||||
package dev.inmo.micro_utils.repos.mappers
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
open class MapperReadStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: ReadStandardKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : ReadStandardKeyValueRepo<FromKey, FromValue>, MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper {
|
||||
override suspend fun get(k: FromKey): FromValue? = to.get(
|
||||
k.toOutKey()
|
||||
) ?.toInnerValue()
|
||||
|
||||
override suspend fun values(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromValue> = to.values(
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerValue() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromKey> = to.keys(
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerKey() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
v: FromValue,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromKey> = to.keys(
|
||||
v.toOutValue(),
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerKey() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun contains(key: FromKey): Boolean = to.contains(
|
||||
key.toOutKey()
|
||||
)
|
||||
|
||||
override suspend fun count(): Long = to.count()
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> ReadStandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): ReadStandardKeyValueRepo<FromKey, FromValue> = MapperReadStandardKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> ReadStandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): ReadStandardKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
||||
|
||||
open class MapperWriteStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: WriteStandardKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : WriteStandardKeyValueRepo<FromKey, FromValue>, MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper {
|
||||
override val onNewValue: Flow<Pair<FromKey, FromValue>> = to.onNewValue.map { (k, v) ->
|
||||
k.toInnerKey() to v.toInnerValue()
|
||||
}
|
||||
override val onValueRemoved: Flow<FromKey> = to.onValueRemoved.map { k ->
|
||||
k.toInnerKey()
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<FromKey, FromValue>) = to.set(
|
||||
toSet.map { (k, v) ->
|
||||
k.toOutKey() to v.toOutValue()
|
||||
}.toMap()
|
||||
)
|
||||
|
||||
override suspend fun unset(toUnset: List<FromKey>) = to.unset(
|
||||
toUnset.map { k ->
|
||||
k.toOutKey()
|
||||
}
|
||||
)
|
||||
|
||||
override suspend fun unsetWithValues(toUnset: List<FromValue>) = to.unsetWithValues(
|
||||
toUnset.map { it.toOutValue() }
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> WriteStandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): WriteStandardKeyValueRepo<FromKey, FromValue> = MapperWriteStandardKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> WriteStandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): WriteStandardKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
||||
|
||||
open class MapperStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: StandardKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : StandardKeyValueRepo<FromKey, FromValue>,
|
||||
MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper,
|
||||
ReadStandardKeyValueRepo<FromKey, FromValue> by MapperReadStandardKeyValueRepo(to, mapper),
|
||||
WriteStandardKeyValueRepo<FromKey, FromValue> by MapperWriteStandardKeyValueRepo(to, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> StandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): StandardKeyValueRepo<FromKey, FromValue> = MapperStandardKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> StandardKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): StandardKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
@@ -0,0 +1,155 @@
|
||||
package dev.inmo.micro_utils.repos.mappers
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.PaginationResult
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
open class MapperReadOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: ReadOneToManyKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : ReadOneToManyKeyValueRepo<FromKey, FromValue>, MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper {
|
||||
override suspend fun get(
|
||||
k: FromKey,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromValue> = to.get(
|
||||
k.toOutKey(),
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerValue() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromKey> = to.keys(
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerKey() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
v: FromValue,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<FromKey> = to.keys(
|
||||
v.toOutValue(),
|
||||
pagination,
|
||||
reversed
|
||||
).let {
|
||||
PaginationResult(
|
||||
it.page,
|
||||
it.pagesNumber,
|
||||
it.results.map { it.toInnerKey() },
|
||||
it.size
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun contains(k: FromKey): Boolean = to.contains(k.toOutKey())
|
||||
override suspend fun contains(k: FromKey, v: FromValue): Boolean = to.contains(k.toOutKey(), v.toOutValue())
|
||||
|
||||
override suspend fun count(): Long = to.count()
|
||||
override suspend fun count(k: FromKey): Long = to.count(k.toOutKey())
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> ReadOneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): ReadOneToManyKeyValueRepo<FromKey, FromValue> = MapperReadOneToManyKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> ReadOneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): ReadOneToManyKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
||||
|
||||
open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: WriteOneToManyKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : WriteOneToManyKeyValueRepo<FromKey, FromValue>, MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper {
|
||||
override val onNewValue: Flow<Pair<FromKey, FromValue>> = to.onNewValue.map { (k, v) ->
|
||||
k.toInnerKey() to v.toInnerValue()
|
||||
}
|
||||
override val onValueRemoved: Flow<Pair<FromKey, FromValue>> = to.onValueRemoved.map { (k, v) ->
|
||||
k.toInnerKey() to v.toInnerValue()
|
||||
}
|
||||
override val onDataCleared: Flow<FromKey> = to.onDataCleared.map { k ->
|
||||
k.toInnerKey()
|
||||
}
|
||||
|
||||
override suspend fun add(toAdd: Map<FromKey, List<FromValue>>) = to.add(
|
||||
toAdd.map { (k, v) ->
|
||||
k.toOutKey() to v.map { it.toOutValue() }
|
||||
}.toMap()
|
||||
)
|
||||
|
||||
override suspend fun remove(toRemove: Map<FromKey, List<FromValue>>) = to.remove(
|
||||
toRemove.map { (k, v) ->
|
||||
k.toOutKey() to v.map { it.toOutValue() }
|
||||
}.toMap()
|
||||
)
|
||||
|
||||
override suspend fun set(toSet: Map<FromKey, List<FromValue>>) {
|
||||
to.set(
|
||||
toSet.map { (k, vs) -> k.toOutKey() to vs.map { v -> v.toOutValue() } }.toMap()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clear(k: FromKey) = to.clear(k.toOutKey())
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> WriteOneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): WriteOneToManyKeyValueRepo<FromKey, FromValue> = MapperWriteOneToManyKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> WriteOneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): WriteOneToManyKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
||||
|
||||
open class MapperOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
|
||||
private val to: OneToManyKeyValueRepo<ToKey, ToValue>,
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
) : OneToManyKeyValueRepo<FromKey, FromValue>,
|
||||
MapperRepo<FromKey, FromValue, ToKey, ToValue> by mapper,
|
||||
ReadOneToManyKeyValueRepo<FromKey, FromValue> by MapperReadOneToManyKeyValueRepo(to, mapper),
|
||||
WriteOneToManyKeyValueRepo<FromKey, FromValue> by MapperWriteOneToManyKeyValueRepo(to, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <FromKey, FromValue, ToKey, ToValue> OneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
mapper: MapperRepo<FromKey, FromValue, ToKey, ToValue>
|
||||
): OneToManyKeyValueRepo<FromKey, FromValue> = MapperOneToManyKeyValueRepo(this, mapper)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified FromKey, reified FromValue, reified ToKey, reified ToValue> OneToManyKeyValueRepo<ToKey, ToValue>.withMapper(
|
||||
crossinline keyFromToTo: suspend FromKey.() -> ToKey = { this as ToKey },
|
||||
crossinline valueFromToTo: suspend FromValue.() -> ToValue = { this as ToValue },
|
||||
crossinline keyToToFrom: suspend ToKey.() -> FromKey = { this as FromKey },
|
||||
crossinline valueToToFrom: suspend ToValue.() -> FromValue = { this as FromValue },
|
||||
): OneToManyKeyValueRepo<FromKey, FromValue> = withMapper(
|
||||
mapper(keyFromToTo, valueFromToTo, keyToToFrom, valueToToFrom)
|
||||
)
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.repos.versions
|
||||
|
||||
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.set
|
||||
|
||||
class KeyValueBasedVersionsRepoProxy<T>(
|
||||
private val keyValueStore: StandardKeyValueRepo<String, Int>,
|
||||
override val database: T
|
||||
) : StandardVersionsRepoProxy<T> {
|
||||
override suspend fun getTableVersion(tableName: String): Int? = keyValueStore.get(tableName)
|
||||
|
||||
override suspend fun updateTableVersion(tableName: String, version: Int) { keyValueStore.set(tableName, version) }
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.micro_utils.repos.versions
|
||||
|
||||
import dev.inmo.micro_utils.repos.Repo
|
||||
|
||||
interface StandardVersionsRepoProxy<T> : Repo {
|
||||
val database: T
|
||||
|
||||
suspend fun getTableVersion(tableName: String): Int?
|
||||
suspend fun updateTableVersion(tableName: String, version: Int)
|
||||
}
|
||||
|
||||
class StandardVersionsRepo<T>(
|
||||
private val proxy: StandardVersionsRepoProxy<T>
|
||||
) : VersionsRepo<T> {
|
||||
override suspend fun setTableVersion(
|
||||
tableName: String,
|
||||
version: Int,
|
||||
onCreate: suspend T.() -> Unit,
|
||||
onUpdate: suspend T.(from: Int, to: Int) -> Unit
|
||||
) {
|
||||
var savedVersion = proxy.getTableVersion(tableName)
|
||||
if (savedVersion == null) {
|
||||
proxy.database.onCreate()
|
||||
proxy.updateTableVersion(tableName, version)
|
||||
} else {
|
||||
while (savedVersion != null && savedVersion < version) {
|
||||
val newVersion = savedVersion + 1
|
||||
|
||||
proxy.database.onUpdate(savedVersion, newVersion)
|
||||
|
||||
proxy.updateTableVersion(tableName, newVersion)
|
||||
savedVersion = newVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.micro_utils.repos.versions
|
||||
|
||||
import dev.inmo.micro_utils.repos.Repo
|
||||
|
||||
/**
|
||||
* This interface has been created due to requirement to work with different versions of databases and make some
|
||||
* migrations between versions
|
||||
*
|
||||
* @param T It is a type of database, which will be used by this repo to retrieve current table version and update it
|
||||
*/
|
||||
interface VersionsRepo<T> : Repo {
|
||||
/**
|
||||
* By default, instance of this interface will check that version of table with name [tableName] is less than
|
||||
* [version] or is absent
|
||||
*
|
||||
* * In case if [tableName] didn't found, will be called [onCreate] and version of table will be set up to [version]
|
||||
* * In case if [tableName] have version less than parameter [version], it will increase version one-by-one
|
||||
* until database version will be equal to [version]
|
||||
*
|
||||
* @param version Current version of table
|
||||
* @param onCreate This callback will be called in case when table have no information about table
|
||||
* @param onUpdate This callback will be called after **iterative** changing of version. It is expected that parameter
|
||||
* "to" will always be greater than "from"
|
||||
*/
|
||||
suspend fun setTableVersion(
|
||||
tableName: String,
|
||||
version: Int,
|
||||
onCreate: suspend T.() -> Unit = {},
|
||||
onUpdate: suspend T.(from: Int, to: Int) -> Unit = { _, _ ->}
|
||||
)
|
||||
}
|
@@ -0,0 +1,183 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.common.Warning
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.pagination.utils.reverse
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardWatchEventKinds.*
|
||||
|
||||
private inline val String.isAbsolute
|
||||
get() = startsWith(File.separator)
|
||||
|
||||
class FileReadStandardKeyValueRepo(
|
||||
private val folder: File
|
||||
) : ReadStandardKeyValueRepo<String, File> {
|
||||
init {
|
||||
folder.mkdirs()
|
||||
}
|
||||
|
||||
override suspend fun get(k: String): File? {
|
||||
val file = File(folder, k)
|
||||
if (file.exists()) {
|
||||
return file
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<File> {
|
||||
val count = count()
|
||||
val resultPagination = if (reversed) pagination.reverse(count) else pagination
|
||||
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult()
|
||||
if (reversed) {
|
||||
filesPaths.reverse()
|
||||
}
|
||||
return filesPaths.map { File(folder, it) }.createPaginationResult(
|
||||
resultPagination,
|
||||
count
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
|
||||
val count = count()
|
||||
val resultPagination = if (reversed) pagination.reverse(count) else pagination
|
||||
val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult()
|
||||
if (reversed) {
|
||||
filesPaths.reverse()
|
||||
}
|
||||
return filesPaths.toList().createPaginationResult(
|
||||
resultPagination,
|
||||
count
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun keys(
|
||||
v: File,
|
||||
pagination: Pagination,
|
||||
reversed: Boolean
|
||||
): PaginationResult<String> {
|
||||
val resultPagination = if (reversed) pagination.reverse(1L) else pagination
|
||||
return if (resultPagination.isFirstPage) {
|
||||
val fileSubpath = v.absolutePath.removePrefix(folder.absolutePath)
|
||||
if (fileSubpath == v.absolutePath) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(fileSubpath)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}.createPaginationResult(resultPagination, 1L)
|
||||
}
|
||||
|
||||
override suspend fun contains(key: String): Boolean {
|
||||
return File(folder, key).exists()
|
||||
}
|
||||
|
||||
override suspend fun count(): Long = folder.list() ?.size ?.toLong() ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Files watching will not correctly works on Android with version of API lower than API 26
|
||||
*/
|
||||
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
||||
class FileWriteStandardKeyValueRepo(
|
||||
private val folder: File,
|
||||
filesChangedProcessingScope: CoroutineScope? = null
|
||||
) : WriteStandardKeyValueRepo<String, File> {
|
||||
private val _onNewValue = MutableSharedFlow<Pair<String, File>>()
|
||||
override val onNewValue: Flow<Pair<String, File>> = _onNewValue.asSharedFlow()
|
||||
private val _onValueRemoved = MutableSharedFlow<String>()
|
||||
override val onValueRemoved: Flow<String> = _onValueRemoved.asSharedFlow()
|
||||
|
||||
init {
|
||||
folder.mkdirs()
|
||||
filesChangedProcessingScope ?.let {
|
||||
it.launch {
|
||||
try {
|
||||
val folderPath = folder.toPath()
|
||||
while (isActive) {
|
||||
val key = try {
|
||||
folderPath.register(FileSystems.getDefault().newWatchService(), ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE)
|
||||
} catch (e: Exception) {
|
||||
// add verbose way to show that file watching is not working
|
||||
return@launch
|
||||
}
|
||||
|
||||
for (event in key.pollEvents()) {
|
||||
val relativeFilePath = (event.context() as? Path) ?: continue
|
||||
val file = relativeFilePath.toFile()
|
||||
val relativePath = file.toRelativeString(folder)
|
||||
|
||||
when (event.kind()) {
|
||||
ENTRY_CREATE, ENTRY_MODIFY -> {
|
||||
launch {
|
||||
_onNewValue.emit(relativePath to file)
|
||||
}
|
||||
}
|
||||
ENTRY_DELETE -> {
|
||||
launch {
|
||||
_onValueRemoved.emit(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key.isValid || folder.exists()) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// add verbose way to notify that this functionality is disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun set(toSet: Map<String, File>) {
|
||||
supervisorScope {
|
||||
toSet.map { (filename, fileSource) ->
|
||||
launch {
|
||||
val file = File(folder, filename)
|
||||
|
||||
file.delete()
|
||||
fileSource.copyTo(file, overwrite = true)
|
||||
_onNewValue.emit(filename to file)
|
||||
}
|
||||
}
|
||||
}.joinAll()
|
||||
}
|
||||
|
||||
override suspend fun unset(toUnset: List<String>) {
|
||||
toUnset.forEach {
|
||||
val file = File(folder, it)
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
_onValueRemoved.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unsetWithValues(toUnset: List<File>) {
|
||||
val keys = toUnset.mapNotNull { v ->
|
||||
val key = v.absolutePath.removePrefix(folder.absolutePath)
|
||||
if (key != v.absolutePath) {
|
||||
key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
unset(keys)
|
||||
}
|
||||
}
|
||||
|
||||
@Warning("Files watching will not correctly works on Android Platform with version of API lower than API 26")
|
||||
class FileStandardKeyValueRepo(
|
||||
folder: File,
|
||||
filesChangedProcessingScope: CoroutineScope? = null
|
||||
) : StandardKeyValueRepo<String, File>,
|
||||
WriteStandardKeyValueRepo<String, File> by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope),
|
||||
ReadStandardKeyValueRepo<String, File> by FileReadStandardKeyValueRepo(folder)
|
1
repos/common/src/main/AndroidManifest.xml
Normal file
1
repos/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.repos.common"/>
|
@@ -0,0 +1,46 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
sealed class ColumnType(
|
||||
typeName: String,
|
||||
nullable: Boolean
|
||||
) {
|
||||
open val asType: String = "$typeName${if (!nullable) " not null" else ""}"
|
||||
sealed class Text(
|
||||
nullable: Boolean
|
||||
) : ColumnType("text", nullable) {
|
||||
object NULLABLE : Text(true)
|
||||
object NOT_NULLABLE : Text(false)
|
||||
}
|
||||
sealed class Numeric(
|
||||
typeName: String,
|
||||
autoincrement: Boolean = false,
|
||||
primaryKey: Boolean = false,
|
||||
nullable: Boolean = false
|
||||
) : ColumnType(
|
||||
typeName,
|
||||
nullable
|
||||
) {
|
||||
override val asType: String = "${super.asType}${if (primaryKey) " primary key" else ""}${if (autoincrement) " autoincrement" else ""}"
|
||||
|
||||
class INTEGER(
|
||||
autoincrement: Boolean = false,
|
||||
primaryKey: Boolean = false,
|
||||
nullable: Boolean = false
|
||||
) : Numeric(
|
||||
"integer",
|
||||
autoincrement,
|
||||
primaryKey,
|
||||
nullable
|
||||
)
|
||||
class DOUBLE(autoincrement: Boolean = false, primaryKey: Boolean = false, nullable: Boolean = false) : Numeric(
|
||||
"double",
|
||||
autoincrement,
|
||||
primaryKey,
|
||||
nullable
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return asType
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "SimplifiableCall")
|
||||
fun contentValuesOfNotNull(vararg pairs: Pair<String, Any?>?) = contentValuesOf(
|
||||
*(pairs.filter { it != null } as List<Pair<String, Any?>>).toTypedArray()
|
||||
)
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
inline fun <T> Cursor.map(
|
||||
block: (Cursor) -> T
|
||||
): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
if (moveToFirst()) {
|
||||
do {
|
||||
result.add(block(this))
|
||||
} while (moveToNext())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun Cursor.firstOrNull(): Cursor? = if (moveToFirst()) {
|
||||
this
|
||||
} else {
|
||||
null
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
val DatabaseCoroutineContext: CoroutineContext = Dispatchers.IO
|
@@ -0,0 +1,64 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
|
||||
fun createTableQuery(
|
||||
tableName: String,
|
||||
vararg columnsToTypes: Pair<String, ColumnType>
|
||||
) = "create table $tableName (${columnsToTypes.joinToString(", ") { "${it.first} ${it.second}" }});"
|
||||
|
||||
fun SQLiteDatabase.createTable(
|
||||
tableName: String,
|
||||
vararg columnsToTypes: Pair<String, ColumnType>,
|
||||
onInit: (SQLiteDatabase.() -> Unit)? = null
|
||||
): Boolean {
|
||||
val existing = rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'", null).use {
|
||||
it.count > 0
|
||||
}
|
||||
return if (existing) {
|
||||
false
|
||||
// TODO:: add upgrade opportunity
|
||||
} else {
|
||||
execSQL(createTableQuery(tableName, *columnsToTypes))
|
||||
onInit ?.invoke(this)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun Cursor.getString(columnName: String) = getString(
|
||||
getColumnIndex(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getLong(columnName: String) = getLong(
|
||||
getColumnIndex(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getInt(columnName: String) = getInt(
|
||||
getColumnIndex(columnName)
|
||||
)
|
||||
|
||||
fun Cursor.getDouble(columnName: String) = getDouble(
|
||||
getColumnIndex(columnName)
|
||||
)
|
||||
|
||||
fun SQLiteDatabase.select(
|
||||
table: String,
|
||||
columns: Array<String>? = null,
|
||||
selection: String? = null,
|
||||
selectionArgs: Array<String>? = null,
|
||||
groupBy: String? = null,
|
||||
having: String? = null,
|
||||
orderBy: String? = null,
|
||||
limit: String? = null
|
||||
) = query(
|
||||
table, columns, selection, selectionArgs, groupBy, having, orderBy, limit
|
||||
)
|
||||
|
||||
fun makePlaceholders(count: Int): String {
|
||||
return (0 until count).joinToString { "?" }
|
||||
}
|
||||
|
||||
fun makeStringPlaceholders(count: Int): String {
|
||||
return (0 until count).joinToString { "\"?\"" }
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.firstIndex
|
||||
|
||||
fun limitClause(size: Long, since: Long? = null) = "${since ?.let { "$it, " } ?: ""}$size"
|
||||
fun limitClause(size: Int, since: Int? = null) = limitClause(size.toLong(), since ?.toLong())
|
||||
fun Pagination.limitClause() = limitClause(size, firstIndex)
|
@@ -0,0 +1,87 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import dev.inmo.micro_utils.coroutines.safely
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
private object ContextsPool {
|
||||
private val contexts = mutableListOf<CoroutineContext>()
|
||||
private val mutex = Mutex(locked = false)
|
||||
private val freeContexts = mutableListOf<CoroutineContext>()
|
||||
|
||||
suspend fun acquireContext(): CoroutineContext {
|
||||
return mutex.withLock {
|
||||
freeContexts.removeFirstOrNull() ?: Executors.newSingleThreadExecutor().asCoroutineDispatcher().also {
|
||||
contexts.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun freeContext(context: CoroutineContext) {
|
||||
return mutex.withLock {
|
||||
if (context in contexts && context !in freeContexts) {
|
||||
freeContexts.add(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> use(block: suspend (CoroutineContext) -> T): T = acquireContext().let {
|
||||
try {
|
||||
block(it)
|
||||
} finally {
|
||||
freeContext(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionContext(
|
||||
val databaseContext: CoroutineContext
|
||||
): CoroutineContext.Element {
|
||||
override val key: CoroutineContext.Key<TransactionContext> = TransactionContext
|
||||
|
||||
companion object : CoroutineContext.Key<TransactionContext>
|
||||
}
|
||||
|
||||
suspend fun <T> SQLiteDatabase.transaction(block: suspend SQLiteDatabase.() -> T): T {
|
||||
return coroutineContext[TransactionContext] ?.let {
|
||||
withContext(it.databaseContext) {
|
||||
block()
|
||||
}
|
||||
} ?: ContextsPool.use { context ->
|
||||
withContext(TransactionContext(context) + context) {
|
||||
beginTransaction()
|
||||
safely(
|
||||
{
|
||||
endTransaction()
|
||||
throw it
|
||||
}
|
||||
) {
|
||||
block().also {
|
||||
setTransactionSuccessful()
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> SQLiteDatabase.inlineTransaction(block: SQLiteDatabase.() -> T): T {
|
||||
return when {
|
||||
inTransaction() -> block()
|
||||
else -> {
|
||||
beginTransaction()
|
||||
try {
|
||||
block().also {
|
||||
setTransactionSuccessful()
|
||||
}
|
||||
} finally {
|
||||
endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.repos
|
||||
|
||||
val internalId = "_id"
|
||||
val internalIdType = ColumnType.Numeric.INTEGER(
|
||||
autoincrement = true,
|
||||
primaryKey = true
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user