mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-11-04 14:14:51 +00:00 
			
		
		
		
	Compare commits
	
		
			124 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										208
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,211 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
@@ -165,4 +371,4 @@ All deprecations has been removed
 | 
			
		||||
 | 
			
		||||
## 0.1.0
 | 
			
		||||
 | 
			
		||||
Inited :)
 | 
			
		||||
Inited :)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								README.md
									
									
									
									
									
								
							@@ -1 +1,36 @@
 | 
			
		||||
# 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
 | 
			
		||||
* `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,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
									
								
								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"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
 | 
			
		||||
 | 
			
		||||
inline fun <T> CoroutineScope.safeActor(
 | 
			
		||||
    channelCapacity: Int = Channel.UNLIMITED,
 | 
			
		||||
    noinline onException: ExceptionHandler<Unit> = {},
 | 
			
		||||
    noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
 | 
			
		||||
    crossinline block: suspend (T) -> Unit
 | 
			
		||||
): Channel<T> = actor(
 | 
			
		||||
    channelCapacity
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -16,7 +16,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T> Flow<T>.subscribeSafely(
 | 
			
		||||
    scope: CoroutineScope,
 | 
			
		||||
    noinline onException: ExceptionHandler<Unit> = { throw it },
 | 
			
		||||
    noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
 | 
			
		||||
    noinline block: suspend (T) -> Unit
 | 
			
		||||
) = subscribe(scope) {
 | 
			
		||||
    safely(onException) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,123 @@
 | 
			
		||||
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 }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +1,14 @@
 | 
			
		||||
buildscript {
 | 
			
		||||
    repositories {
 | 
			
		||||
        mavenLocal()
 | 
			
		||||
        jcenter()
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
 | 
			
		||||
        classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    id "org.jetbrains.kotlin.multiplatform"
 | 
			
		||||
    id "org.jetbrains.kotlin.plugin.serialization"
 | 
			
		||||
    id "org.jetbrains.dokka" version "$dokka_version"
 | 
			
		||||
    id "com.android.library"
 | 
			
		||||
    id "org.jetbrains.dokka"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    mavenLocal()
 | 
			
		||||
    jcenter()
 | 
			
		||||
    google()
 | 
			
		||||
    mavenCentral()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +18,7 @@ kotlin {
 | 
			
		||||
        browser()
 | 
			
		||||
        nodejs()
 | 
			
		||||
    }
 | 
			
		||||
    android {}
 | 
			
		||||
 | 
			
		||||
    sourceSets {
 | 
			
		||||
        commonMain {
 | 
			
		||||
@@ -41,6 +30,57 @@ kotlin {
 | 
			
		||||
                        it != project
 | 
			
		||||
                        && it.hasProperty("kotlin")
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
 | 
			
		||||
                        && it.kotlin.sourceSets.any { it.name.contains("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
 | 
			
		||||
                    }
 | 
			
		||||
@@ -84,5 +124,11 @@ tasks.dokkaHtml {
 | 
			
		||||
        named("jvmMain") {
 | 
			
		||||
            sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        named("androidMain") {
 | 
			
		||||
            sourceRoots.setFrom(findSourcesWithName("androidMain", "commonMain"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$defaultAndroidSettingsPresetPath"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
dokka_version=1.4.0
 | 
			
		||||
 | 
			
		||||
org.gradle.jvmargs=-Xmx1024m
 | 
			
		||||
							
								
								
									
										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.4.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.3.0
 | 
			
		||||
version=0.4.15
 | 
			
		||||
android_code_version=19
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.7-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"
 | 
			
		||||
 
 | 
			
		||||
@@ -25,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) {
 | 
			
		||||
@@ -64,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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,32 +4,61 @@ import dev.inmo.micro_utils.ktor.common.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.request.get
 | 
			
		||||
import io.ktor.client.request.post
 | 
			
		||||
import kotlinx.serialization.DeserializationStrategy
 | 
			
		||||
import kotlinx.serialization.SerializationStrategy
 | 
			
		||||
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"/>
 | 
			
		||||
@@ -33,10 +33,11 @@ fun <T> Route.includeWebsocketHandling(
 | 
			
		||||
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,12 +3,87 @@ 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 kotlinx.serialization.DeserializationStrategy
 | 
			
		||||
import kotlinx.serialization.SerializationStrategy
 | 
			
		||||
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>,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 +22,12 @@ 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
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,4 +6,16 @@ interface MapperRepo<FromKey, FromValue, ToKey, 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,6 +36,7 @@ interface ReadOneToManyKeyValueRepo<Key, Value> : Repo {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
typealias ReadKeyValuesRepo<Key,Value> = ReadOneToManyKeyValueRepo<Key, Value>
 | 
			
		||||
 | 
			
		||||
interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
 | 
			
		||||
    val onNewValue: Flow<Pair<Key, Value>>
 | 
			
		||||
@@ -46,7 +48,13 @@ interface WriteOneToManyKeyValueRepo<Key, Value> : Repo {
 | 
			
		||||
    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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
typealias WriteKeyValuesRepo<Key,Value> = WriteOneToManyKeyValueRepo<Key, Value>
 | 
			
		||||
 | 
			
		||||
suspend inline fun <Key, Value, REPO : WriteOneToManyKeyValueRepo<Key, Value>> REPO.add(
 | 
			
		||||
    keysAndValues: List<Pair<Key, List<Value>>>
 | 
			
		||||
@@ -64,7 +72,24 @@ 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> WriteOneToManyKeyValueRepo<Key, Value>.remove(
 | 
			
		||||
    keysAndValues: List<Pair<Key, List<Value>>>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,16 +1,17 @@
 | 
			
		||||
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>>
 | 
			
		||||
@@ -18,7 +19,9 @@ interface WriteStandardKeyValueRepo<Key, Value> : Repo {
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
 | 
			
		||||
suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.set(
 | 
			
		||||
    vararg toSet: Pair<Key, Value>
 | 
			
		||||
@@ -32,4 +35,17 @@ suspend inline fun <Key, Value> WriteStandardKeyValueRepo<Key, Value>.unset(
 | 
			
		||||
    vararg k: Key
 | 
			
		||||
) = unset(k.toList())
 | 
			
		||||
 | 
			
		||||
interface StandardKeyValueRepo<Key, Value> : ReadStandardKeyValueRepo<Key, Value>, WriteStandardKeyValueRepo<Key, Value>
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,23 @@ open class MapperReadStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
    )
 | 
			
		||||
@@ -51,6 +68,21 @@ open class MapperReadStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
    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>
 | 
			
		||||
@@ -73,8 +105,27 @@ open class MapperWriteStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
            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>
 | 
			
		||||
@@ -82,3 +133,18 @@ open class MapperStandardKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
    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)
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,23 @@ open class MapperReadOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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())
 | 
			
		||||
 | 
			
		||||
@@ -49,6 +66,21 @@ open class MapperReadOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
    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>
 | 
			
		||||
@@ -75,9 +107,30 @@ open class MapperWriteOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
        }.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>
 | 
			
		||||
@@ -85,3 +138,18 @@ open class MapperOneToManyKeyValueRepo<FromKey, FromValue, ToKey, ToValue>(
 | 
			
		||||
    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
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.database.DatabaseErrorHandler
 | 
			
		||||
import android.database.sqlite.SQLiteDatabase
 | 
			
		||||
import android.database.sqlite.SQLiteOpenHelper
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.safely
 | 
			
		||||
import dev.inmo.micro_utils.repos.versions.*
 | 
			
		||||
import kotlin.coroutines.Continuation
 | 
			
		||||
import kotlin.coroutines.resume
 | 
			
		||||
import kotlin.coroutines.resumeWithException
 | 
			
		||||
 | 
			
		||||
class StandardSQLHelper(
 | 
			
		||||
    context: Context,
 | 
			
		||||
    name: String,
 | 
			
		||||
    factory: SQLiteDatabase.CursorFactory? = null,
 | 
			
		||||
    version: Int = 1,
 | 
			
		||||
    errorHandler: DatabaseErrorHandler? = null
 | 
			
		||||
) {
 | 
			
		||||
    val sqlOpenHelper = object : SQLiteOpenHelper(context, name, factory, version, errorHandler) {
 | 
			
		||||
        override fun onCreate(db: SQLiteDatabase?) {}
 | 
			
		||||
 | 
			
		||||
        override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
 | 
			
		||||
    }
 | 
			
		||||
    val versionsRepo: VersionsRepo<SQLiteOpenHelper> by lazy {
 | 
			
		||||
        StandardVersionsRepo(AndroidSQLStandardVersionsRepoProxy(sqlOpenHelper))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun <T> writableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.writableTransaction(block)
 | 
			
		||||
 | 
			
		||||
    suspend fun <T> readableTransaction(block: suspend SQLiteDatabase.() -> T): T = sqlOpenHelper.readableTransaction(block)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> SQLiteOpenHelper.writableTransaction(block: suspend SQLiteDatabase.() -> T): T {
 | 
			
		||||
    return writableDatabase.transaction(block)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <T> SQLiteOpenHelper.readableTransaction(block: suspend SQLiteDatabase.() -> T): T {
 | 
			
		||||
    return readableDatabase.transaction(block)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos.crud
 | 
			
		||||
 | 
			
		||||
import android.database.Cursor
 | 
			
		||||
import android.database.sqlite.SQLiteDatabase
 | 
			
		||||
import dev.inmo.micro_utils.pagination.Pagination
 | 
			
		||||
import dev.inmo.micro_utils.pagination.PaginationResult
 | 
			
		||||
import dev.inmo.micro_utils.pagination.createPaginationResult
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
 | 
			
		||||
val <T> T.asId: String
 | 
			
		||||
    get() = (this as? String) ?: this!!.toString()
 | 
			
		||||
 | 
			
		||||
abstract class AbstractAndroidCRUDRepo<ObjectType, IdType>(
 | 
			
		||||
    protected val helper: StandardSQLHelper
 | 
			
		||||
) : ReadStandardCRUDRepo<ObjectType, IdType> {
 | 
			
		||||
    protected abstract val tableName: String
 | 
			
		||||
    protected abstract val idColumnName: String
 | 
			
		||||
    protected abstract suspend fun Cursor.toObject(): ObjectType
 | 
			
		||||
    protected fun SQLiteDatabase.count(): Long = select(tableName).use {
 | 
			
		||||
        it.count
 | 
			
		||||
    }.toLong()
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(id: IdType): Boolean = helper.readableTransaction {
 | 
			
		||||
        select(
 | 
			
		||||
            tableName,
 | 
			
		||||
            null,
 | 
			
		||||
            "$idColumnName=?",
 | 
			
		||||
            arrayOf(id.asId)
 | 
			
		||||
        ).use {
 | 
			
		||||
            it.count > 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getById(id: IdType): ObjectType? = helper.readableTransaction {
 | 
			
		||||
        select(
 | 
			
		||||
            tableName,
 | 
			
		||||
            selection = "$idColumnName=?",
 | 
			
		||||
            selectionArgs = arrayOf(id.asId),
 | 
			
		||||
            limit = limitClause(1)
 | 
			
		||||
        ).use { c ->
 | 
			
		||||
            if (c.moveToFirst()) {
 | 
			
		||||
                c.toObject()
 | 
			
		||||
            } else {
 | 
			
		||||
                null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getByPagination(pagination: Pagination): PaginationResult<ObjectType> {
 | 
			
		||||
        return helper.readableTransaction {
 | 
			
		||||
            select(
 | 
			
		||||
                tableName,
 | 
			
		||||
                limit = pagination.limitClause()
 | 
			
		||||
            ).use {
 | 
			
		||||
                if (it.moveToFirst()) {
 | 
			
		||||
                    val resultList = mutableListOf(it.toObject())
 | 
			
		||||
                    while (it.moveToNext()) {
 | 
			
		||||
                        resultList.add(it.toObject())
 | 
			
		||||
                    }
 | 
			
		||||
                    resultList.createPaginationResult(pagination, count())
 | 
			
		||||
                } else {
 | 
			
		||||
                    emptyList<ObjectType>().createPaginationResult(pagination, 0)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,102 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos.crud
 | 
			
		||||
 | 
			
		||||
import android.content.ContentValues
 | 
			
		||||
import dev.inmo.micro_utils.common.mapNotNullA
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
 | 
			
		||||
abstract class AbstractMutableAndroidCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
    helper: StandardSQLHelper
 | 
			
		||||
) : WriteStandardCRUDRepo<ObjectType, IdType, InputValueType>,
 | 
			
		||||
    AbstractAndroidCRUDRepo<ObjectType, IdType>(helper) {
 | 
			
		||||
    protected val newObjectsChannel = MutableSharedFlow<ObjectType>(64)
 | 
			
		||||
    protected val updateObjectsChannel = MutableSharedFlow<ObjectType>(64)
 | 
			
		||||
    protected val deleteObjectsIdsChannel = MutableSharedFlow<IdType>(64)
 | 
			
		||||
    override val newObjectsFlow: Flow<ObjectType> = newObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    protected abstract suspend fun InputValueType.asContentValues(id: IdType? = null): ContentValues
 | 
			
		||||
 | 
			
		||||
    override suspend fun create(values: List<InputValueType>): List<ObjectType> {
 | 
			
		||||
        val indexes = helper.writableTransaction {
 | 
			
		||||
            values.map {
 | 
			
		||||
                insert(tableName, null, it.asContentValues())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return helper.readableTransaction {
 | 
			
		||||
            indexes.mapNotNullA { i ->
 | 
			
		||||
                select(
 | 
			
		||||
                    tableName,
 | 
			
		||||
                    selection = "$internalId=?",
 | 
			
		||||
                    selectionArgs = arrayOf(i.toString())
 | 
			
		||||
                ).use { c ->
 | 
			
		||||
                    if (c.moveToFirst()) {
 | 
			
		||||
                        c.toObject()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.also {
 | 
			
		||||
            it.forEach {
 | 
			
		||||
                newObjectsChannel.emit(it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun deleteById(ids: List<IdType>) {
 | 
			
		||||
        val deleted = mutableListOf<IdType>()
 | 
			
		||||
        helper.writableTransaction {
 | 
			
		||||
            ids.forEach { id ->
 | 
			
		||||
                delete(tableName, "$idColumnName=?", arrayOf(id.asId)).also {
 | 
			
		||||
                    if (it > 0) {
 | 
			
		||||
                        deleted.add(id)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        deleted.forEach {
 | 
			
		||||
            deleteObjectsIdsChannel.emit(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(id: IdType, value: InputValueType): ObjectType? {
 | 
			
		||||
        val asContentValues = value.asContentValues(id)
 | 
			
		||||
        if (asContentValues.keySet().isNotEmpty()) {
 | 
			
		||||
            helper.writableTransaction {
 | 
			
		||||
                update(
 | 
			
		||||
                    tableName,
 | 
			
		||||
                    asContentValues,
 | 
			
		||||
                    "$idColumnName=?",
 | 
			
		||||
                    arrayOf(id.asId)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return getById(id) ?.also {
 | 
			
		||||
            updateObjectsChannel.emit(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun update(values: List<UpdatedValuePair<IdType, InputValueType>>): List<ObjectType> {
 | 
			
		||||
        helper.writableTransaction {
 | 
			
		||||
            values.forEach { (id, value) ->
 | 
			
		||||
                update(
 | 
			
		||||
                    tableName,
 | 
			
		||||
                    value.asContentValues(id),
 | 
			
		||||
                    "$idColumnName=?",
 | 
			
		||||
                    arrayOf(id.asId)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return values.mapNotNullA {
 | 
			
		||||
            getById(it.first)
 | 
			
		||||
        }.also {
 | 
			
		||||
            it.forEach {
 | 
			
		||||
                updateObjectsChannel.emit(it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun count(): Long = helper.readableTransaction { select(tableName).use { it.count.toLong() } }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,153 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos.keyvalue
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import androidx.core.content.edit
 | 
			
		||||
import dev.inmo.micro_utils.pagination.Pagination
 | 
			
		||||
import dev.inmo.micro_utils.pagination.PaginationResult
 | 
			
		||||
import dev.inmo.micro_utils.pagination.utils.paginate
 | 
			
		||||
import dev.inmo.micro_utils.pagination.utils.reverse
 | 
			
		||||
import dev.inmo.micro_utils.repos.StandardKeyValueRepo
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
 | 
			
		||||
private val cache = HashMap<String, KeyValueStore<*>>()
 | 
			
		||||
 | 
			
		||||
fun <T : Any> Context.keyValueStore(
 | 
			
		||||
    name: String = "default",
 | 
			
		||||
    cacheValues: Boolean = false
 | 
			
		||||
): StandardKeyValueRepo<String, T> {
 | 
			
		||||
    return cache.getOrPut(name) {
 | 
			
		||||
        KeyValueStore<T>(this, name, cacheValues)
 | 
			
		||||
    } as KeyValueStore<T>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class KeyValueStore<T : Any> internal constructor (
 | 
			
		||||
    c: Context,
 | 
			
		||||
    preferencesName: String,
 | 
			
		||||
    useCache: Boolean = false
 | 
			
		||||
) : SharedPreferences.OnSharedPreferenceChangeListener, StandardKeyValueRepo<String, T> {
 | 
			
		||||
    private val sharedPreferences = c.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
 | 
			
		||||
 | 
			
		||||
    private val cachedData = if (useCache) {
 | 
			
		||||
        mutableMapOf<String, Any>()
 | 
			
		||||
    } else {
 | 
			
		||||
        null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val onNewValueChannel = MutableSharedFlow<Pair<String, T>>()
 | 
			
		||||
    private val _onValueRemovedFlow = MutableSharedFlow<String>()
 | 
			
		||||
 | 
			
		||||
    override val onNewValue: Flow<Pair<String, T>> = onNewValueChannel.asSharedFlow()
 | 
			
		||||
    override val onValueRemoved: Flow<String> = _onValueRemovedFlow.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        cachedData ?.let {
 | 
			
		||||
            sharedPreferences.all.forEach {
 | 
			
		||||
                if (it.value != null) {
 | 
			
		||||
                    cachedData[it.key] = it.value as Any
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            sharedPreferences.registerOnSharedPreferenceChangeListener(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSharedPreferenceChanged(sp: SharedPreferences, key: String) {
 | 
			
		||||
        val value = sp.all[key]
 | 
			
		||||
        cachedData ?: return
 | 
			
		||||
        if (value != null) {
 | 
			
		||||
            cachedData[key] = value
 | 
			
		||||
        } else {
 | 
			
		||||
            cachedData.remove(key)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun get(k: String): T? {
 | 
			
		||||
        return (cachedData ?. get(k) ?: sharedPreferences.all[k]) as? T
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult<T> {
 | 
			
		||||
        val resultPagination = if (reversed) pagination.reverse(count()) else pagination
 | 
			
		||||
        return sharedPreferences.all.values.paginate(
 | 
			
		||||
            resultPagination
 | 
			
		||||
        ).let {
 | 
			
		||||
            PaginationResult(
 | 
			
		||||
                it.page,
 | 
			
		||||
                it.pagesNumber,
 | 
			
		||||
                it.results.map { it as T }.let { if (reversed) it.reversed() else it },
 | 
			
		||||
                it.size
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult<String> {
 | 
			
		||||
        val resultPagination = if (reversed) pagination.reverse(count()) else pagination
 | 
			
		||||
        return sharedPreferences.all.keys.paginate(
 | 
			
		||||
            resultPagination
 | 
			
		||||
        ).let {
 | 
			
		||||
            PaginationResult(
 | 
			
		||||
                it.page,
 | 
			
		||||
                it.pagesNumber,
 | 
			
		||||
                it.results.let { if (reversed) it.reversed() else it },
 | 
			
		||||
                it.size
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun keys(v: T, pagination: Pagination, reversed: Boolean): PaginationResult<String> {
 | 
			
		||||
        val resultPagination = if (reversed) pagination.reverse(count()) else pagination
 | 
			
		||||
        return sharedPreferences.all.mapNotNull { (k, value) -> if (value == v) k else null }.paginate(
 | 
			
		||||
            resultPagination
 | 
			
		||||
        ).let {
 | 
			
		||||
            PaginationResult(
 | 
			
		||||
                it.page,
 | 
			
		||||
                it.pagesNumber,
 | 
			
		||||
                it.results.let { if (reversed) it.reversed() else it },
 | 
			
		||||
                it.size
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(key: String): Boolean = sharedPreferences.contains(key)
 | 
			
		||||
 | 
			
		||||
    override suspend fun count(): Long = sharedPreferences.all.size.toLong()
 | 
			
		||||
 | 
			
		||||
    override suspend fun set(toSet: Map<String, T>) {
 | 
			
		||||
        sharedPreferences.edit {
 | 
			
		||||
            toSet.forEach { (k, v) ->
 | 
			
		||||
                when(v) {
 | 
			
		||||
                    is Int -> putInt(k, v)
 | 
			
		||||
                    is Long -> putLong(k, v)
 | 
			
		||||
                    is Float -> putFloat(k, v)
 | 
			
		||||
                    is String -> putString(k, v)
 | 
			
		||||
                    is Boolean -> putBoolean(k, v)
 | 
			
		||||
                    is Set<*> -> putStringSet(k, v.map { (it as? String) ?: it.toString() }.toSet())
 | 
			
		||||
                    else -> error(
 | 
			
		||||
                        "Currently supported only primitive types and set for SharedPreferences KeyValue repos"
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        toSet.forEach { (k, v) ->
 | 
			
		||||
            onNewValueChannel.emit(k to v)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun unset(toUnset: List<String>) {
 | 
			
		||||
        sharedPreferences.edit {
 | 
			
		||||
            toUnset.forEach { remove(it) }
 | 
			
		||||
        }
 | 
			
		||||
        toUnset.forEach { _onValueRemovedFlow.emit(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun unsetWithValues(toUnset: List<T>) {
 | 
			
		||||
        val keysToRemove = sharedPreferences.all.mapNotNull { if (it.value in toUnset) it.key else null }
 | 
			
		||||
        sharedPreferences.edit {
 | 
			
		||||
            keysToRemove.map {
 | 
			
		||||
                remove(it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        keysToRemove.forEach {
 | 
			
		||||
            _onValueRemovedFlow.emit(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,237 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos.onetomany
 | 
			
		||||
 | 
			
		||||
import android.database.sqlite.SQLiteOpenHelper
 | 
			
		||||
import androidx.core.content.contentValuesOf
 | 
			
		||||
import dev.inmo.micro_utils.common.mapNotNullA
 | 
			
		||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
 | 
			
		||||
import dev.inmo.micro_utils.pagination.Pagination
 | 
			
		||||
import dev.inmo.micro_utils.pagination.PaginationResult
 | 
			
		||||
import dev.inmo.micro_utils.pagination.createPaginationResult
 | 
			
		||||
import dev.inmo.micro_utils.pagination.utils.reverse
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asSharedFlow
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import kotlinx.serialization.KSerializer
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
 | 
			
		||||
private val internalSerialFormat = Json {
 | 
			
		||||
    ignoreUnknownKeys = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class OneToManyAndroidRepo<Key, Value>(
 | 
			
		||||
    private val tableName: String,
 | 
			
		||||
    private val keySerializer: KSerializer<Key>,
 | 
			
		||||
    private val valueSerializer: KSerializer<Value>,
 | 
			
		||||
    private val helper: SQLiteOpenHelper
 | 
			
		||||
) : OneToManyKeyValueRepo<Key, Value> {
 | 
			
		||||
    private val _onNewValue: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
 | 
			
		||||
    override val onNewValue: Flow<Pair<Key, Value>> = _onNewValue.asSharedFlow()
 | 
			
		||||
    private val _onValueRemoved: MutableSharedFlow<Pair<Key, Value>> = MutableSharedFlow()
 | 
			
		||||
    override val onValueRemoved: Flow<Pair<Key, Value>> = _onValueRemoved.asSharedFlow()
 | 
			
		||||
    private val _onDataCleared = MutableSharedFlow<Key>()
 | 
			
		||||
    override val onDataCleared: Flow<Key> = _onDataCleared.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    private val idColumnName = "id"
 | 
			
		||||
    private val valueColumnName = "value"
 | 
			
		||||
 | 
			
		||||
    private fun Key.asId() = internalSerialFormat.encodeToString(keySerializer, this)
 | 
			
		||||
    private fun Value.asValue() = internalSerialFormat.encodeToString(valueSerializer, this)
 | 
			
		||||
    private fun String.asValue(): Value = internalSerialFormat.decodeFromString(valueSerializer, this)
 | 
			
		||||
    private fun String.asKey(): Key = internalSerialFormat.decodeFromString(keySerializer, this)
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        runBlocking(DatabaseCoroutineContext) {
 | 
			
		||||
            helper.writableTransaction {
 | 
			
		||||
                createTable(
 | 
			
		||||
                    tableName,
 | 
			
		||||
                    internalId to internalIdType,
 | 
			
		||||
                    idColumnName to ColumnType.Text.NOT_NULLABLE,
 | 
			
		||||
                    valueColumnName to ColumnType.Text.NULLABLE
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun add(toAdd: Map<Key, List<Value>>) {
 | 
			
		||||
        val added = mutableListOf<Pair<Key, Value>>()
 | 
			
		||||
        helper.writableTransaction {
 | 
			
		||||
            toAdd.forEach { (k, values) ->
 | 
			
		||||
                values.forEach { v ->
 | 
			
		||||
                    insert(
 | 
			
		||||
                        tableName,
 | 
			
		||||
                        null,
 | 
			
		||||
                        contentValuesOf(
 | 
			
		||||
                            idColumnName to k.asId(),
 | 
			
		||||
                            valueColumnName to v.asValue()
 | 
			
		||||
                        )
 | 
			
		||||
                    ).also {
 | 
			
		||||
                        if (it != -1L) {
 | 
			
		||||
                            added.add(k to v)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        added.forEach { _onNewValue.emit(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun clear(k: Key) {
 | 
			
		||||
        helper.writableTransaction {
 | 
			
		||||
            delete(tableName, "$idColumnName=?", arrayOf(k.asId()))
 | 
			
		||||
        }.also {
 | 
			
		||||
            if (it > 0) {
 | 
			
		||||
                _onDataCleared.emit(k)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun set(toSet: Map<Key, List<Value>>) {
 | 
			
		||||
        val (clearedKeys, inserted) = helper.writableTransaction {
 | 
			
		||||
            toSet.mapNotNull { (k, _) ->
 | 
			
		||||
                if (delete(tableName, "$idColumnName=?", arrayOf(k.asId())) > 0) {
 | 
			
		||||
                    k
 | 
			
		||||
                } else {
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            } to toSet.flatMap { (k, values) ->
 | 
			
		||||
                values.map { v ->
 | 
			
		||||
                    insert(
 | 
			
		||||
                        tableName,
 | 
			
		||||
                        null,
 | 
			
		||||
                        contentValuesOf(idColumnName to k.asId(), valueColumnName to v.asValue())
 | 
			
		||||
                    )
 | 
			
		||||
                    k to v
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        clearedKeys.forEach { _onDataCleared.emit(it) }
 | 
			
		||||
        inserted.forEach { newPair -> _onNewValue.emit(newPair) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(k: Key): Boolean = helper.readableTransaction {
 | 
			
		||||
        select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
 | 
			
		||||
            it.count > 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun contains(k: Key, v: Value): Boolean = helper.readableTransaction {
 | 
			
		||||
        select(
 | 
			
		||||
            tableName,
 | 
			
		||||
            selection = "$idColumnName=? AND $valueColumnName=?",
 | 
			
		||||
            selectionArgs = arrayOf(k.asId(), v.asValue()),
 | 
			
		||||
            limit = FirstPagePagination(1).limitClause()
 | 
			
		||||
        ).use {
 | 
			
		||||
            it.count > 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun count(): Long =helper.readableTransaction {
 | 
			
		||||
        select(
 | 
			
		||||
            tableName
 | 
			
		||||
        ).use {
 | 
			
		||||
            it.count
 | 
			
		||||
        }
 | 
			
		||||
    }.toLong()
 | 
			
		||||
 | 
			
		||||
    override suspend fun count(k: Key): Long = helper.readableTransaction {
 | 
			
		||||
        select(tableName, selection = "$idColumnName=?", selectionArgs = arrayOf(k.asId()), limit = FirstPagePagination(1).limitClause()).use {
 | 
			
		||||
            it.count
 | 
			
		||||
        }
 | 
			
		||||
    }.toLong()
 | 
			
		||||
 | 
			
		||||
    override suspend fun get(
 | 
			
		||||
        k: Key,
 | 
			
		||||
        pagination: Pagination,
 | 
			
		||||
        reversed: Boolean
 | 
			
		||||
    ): PaginationResult<Value> = count(k).let { count ->
 | 
			
		||||
        val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
 | 
			
		||||
        helper.readableTransaction {
 | 
			
		||||
            select(
 | 
			
		||||
                tableName,
 | 
			
		||||
                selection = "$idColumnName=?",
 | 
			
		||||
                selectionArgs = arrayOf(k.asId()),
 | 
			
		||||
                limit = resultPagination.limitClause()
 | 
			
		||||
            ).use { c ->
 | 
			
		||||
                mutableListOf<Value>().also {
 | 
			
		||||
                    if (c.moveToFirst()) {
 | 
			
		||||
                        do {
 | 
			
		||||
                            it.add(c.getString(valueColumnName).asValue())
 | 
			
		||||
                        } while (c.moveToNext())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.createPaginationResult(
 | 
			
		||||
            pagination,
 | 
			
		||||
            count
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun keys(
 | 
			
		||||
        pagination: Pagination,
 | 
			
		||||
        reversed: Boolean
 | 
			
		||||
    ): PaginationResult<Key> = count().let { count ->
 | 
			
		||||
        val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
 | 
			
		||||
        helper.readableTransaction {
 | 
			
		||||
            select(
 | 
			
		||||
                tableName,
 | 
			
		||||
                limit = resultPagination.limitClause()
 | 
			
		||||
            ).use { c ->
 | 
			
		||||
                mutableListOf<Key>().also {
 | 
			
		||||
                    if (c.moveToFirst()) {
 | 
			
		||||
                        do {
 | 
			
		||||
                            it.add(c.getString(idColumnName).asKey())
 | 
			
		||||
                        } while (c.moveToNext())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.createPaginationResult(
 | 
			
		||||
            pagination,
 | 
			
		||||
            count
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun keys(
 | 
			
		||||
        v: Value,
 | 
			
		||||
        pagination: Pagination,
 | 
			
		||||
        reversed: Boolean
 | 
			
		||||
    ): PaginationResult<Key> = count().let { count ->
 | 
			
		||||
        val resultPagination = pagination.let { if (reversed) pagination.reverse(count) else pagination }
 | 
			
		||||
        helper.readableTransaction {
 | 
			
		||||
            select(
 | 
			
		||||
                tableName,
 | 
			
		||||
                selection = "$valueColumnName=?",
 | 
			
		||||
                selectionArgs = arrayOf(v.asValue()),
 | 
			
		||||
                limit = resultPagination.limitClause()
 | 
			
		||||
            ).use { c ->
 | 
			
		||||
                mutableListOf<Key>().also {
 | 
			
		||||
                    if (c.moveToFirst()) {
 | 
			
		||||
                        do {
 | 
			
		||||
                            it.add(c.getString(idColumnName).asKey())
 | 
			
		||||
                        } while (c.moveToNext())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.createPaginationResult(
 | 
			
		||||
            pagination,
 | 
			
		||||
            count
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun remove(toRemove: Map<Key, List<Value>>) {
 | 
			
		||||
        helper.writableTransaction {
 | 
			
		||||
            toRemove.flatMap { (k, vs) ->
 | 
			
		||||
                vs.mapNotNullA { v ->
 | 
			
		||||
                    if (delete(tableName, "$idColumnName=? AND $valueColumnName=?", arrayOf(k.asId(), v.asValue())) > 0) {
 | 
			
		||||
                        k to v
 | 
			
		||||
                    } else {
 | 
			
		||||
                        null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.forEach { (k, v) ->
 | 
			
		||||
            _onValueRemoved.emit(k to v)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
package dev.inmo.micro_utils.repos.versions
 | 
			
		||||
 | 
			
		||||
import android.database.sqlite.SQLiteOpenHelper
 | 
			
		||||
import androidx.core.content.contentValuesOf
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will create [VersionsRepo] based on [SQLiteOpenHelper] with table inside of [database]
 | 
			
		||||
 */
 | 
			
		||||
@Suppress("NOTHING_TO_INLINE")
 | 
			
		||||
inline fun versionsRepo(database: SQLiteOpenHelper): VersionsRepo<SQLiteOpenHelper> = StandardVersionsRepo(
 | 
			
		||||
    AndroidSQLStandardVersionsRepoProxy(database)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class AndroidSQLStandardVersionsRepoProxy(
 | 
			
		||||
    override val database: SQLiteOpenHelper
 | 
			
		||||
) : StandardVersionsRepoProxy<SQLiteOpenHelper> {
 | 
			
		||||
    private val tableName: String = "AndroidSQLStandardVersionsRepo"
 | 
			
		||||
    private val tableNameColumnName = "tableName"
 | 
			
		||||
    private val tableVersionColumnName = "version"
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        runBlocking(DatabaseCoroutineContext) {
 | 
			
		||||
            database.writableTransaction {
 | 
			
		||||
                createTable(
 | 
			
		||||
                    tableName,
 | 
			
		||||
                    tableNameColumnName to ColumnType.Text.NOT_NULLABLE,
 | 
			
		||||
                    tableVersionColumnName to ColumnType.Numeric.INTEGER()
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getTableVersion(tableName: String): Int? = database.writableTransaction {
 | 
			
		||||
        select(
 | 
			
		||||
            this@AndroidSQLStandardVersionsRepoProxy.tableName,
 | 
			
		||||
            selection = "$tableNameColumnName=?",
 | 
			
		||||
            selectionArgs = arrayOf(tableName),
 | 
			
		||||
            limit = limitClause(1)
 | 
			
		||||
        ).use {
 | 
			
		||||
            if (it.moveToFirst()) {
 | 
			
		||||
                it.getInt(tableVersionColumnName)
 | 
			
		||||
            } else {
 | 
			
		||||
                null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun updateTableVersion(tableName: String, version: Int) {
 | 
			
		||||
        database.writableTransaction {
 | 
			
		||||
            val updated = update(
 | 
			
		||||
                this@AndroidSQLStandardVersionsRepoProxy.tableName,
 | 
			
		||||
                contentValuesOf(tableVersionColumnName to version),
 | 
			
		||||
                "$tableNameColumnName=?",
 | 
			
		||||
                arrayOf(tableName)
 | 
			
		||||
            ) > 0
 | 
			
		||||
            if (!updated) {
 | 
			
		||||
                insert(this@AndroidSQLStandardVersionsRepoProxy.tableName, null, contentValuesOf(tableNameColumnName to tableName, tableVersionColumnName to version))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
@file:Suppress("NOTHING_TO_INLINE")
 | 
			
		||||
 | 
			
		||||
package dev.inmo.micro_utils.repos.versions
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.database.sqlite.SQLiteOpenHelper
 | 
			
		||||
import androidx.core.content.contentValuesOf
 | 
			
		||||
import dev.inmo.micro_utils.repos.*
 | 
			
		||||
import dev.inmo.micro_utils.repos.keyvalue.keyValueStore
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will create [VersionsRepo] based on [T], but versions will be stored in [StandardKeyValueRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @receiver Will be used to create [KeyValueBasedVersionsRepoProxy] via [keyValueStore] and pass it to [StandardVersionsRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @see [KeyValueBasedVersionsRepoProxy]
 | 
			
		||||
 * @see [keyValueStore]
 | 
			
		||||
 */
 | 
			
		||||
inline fun <T> Context.versionsKeyValueRepo(
 | 
			
		||||
    database: T
 | 
			
		||||
): VersionsRepo<T> = StandardVersionsRepo(
 | 
			
		||||
    KeyValueBasedVersionsRepoProxy(
 | 
			
		||||
        keyValueStore("SPVersionsRepo"),
 | 
			
		||||
        database
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
/**
 | 
			
		||||
 * Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @receiver Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @see [keyValueStore]
 | 
			
		||||
 */
 | 
			
		||||
inline fun Context.versionsKeyValueRepoForSQL(
 | 
			
		||||
    database: SQLiteOpenHelper
 | 
			
		||||
) = versionsKeyValueRepo(database)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will create [VersionsRepo] based on [SQLiteOpenHelper], but versions will be stored in [StandardKeyValueRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @param context Will be used to create [StandardKeyValueRepo] via [keyValueStore] and pass it to [StandardVersionsRepo]
 | 
			
		||||
 *
 | 
			
		||||
 * @see [keyValueStore]
 | 
			
		||||
 */
 | 
			
		||||
inline fun versionsRepo(
 | 
			
		||||
    context: Context,
 | 
			
		||||
    database: SQLiteOpenHelper
 | 
			
		||||
) = context.versionsKeyValueRepoForSQL(database)
 | 
			
		||||
@@ -2,7 +2,6 @@ package dev.inmo.micro_utils.repos.exposed
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.repos.UpdatedValuePair
 | 
			
		||||
import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo
 | 
			
		||||
import kotlinx.coroutines.channels.BroadcastChannel
 | 
			
		||||
import kotlinx.coroutines.flow.*
 | 
			
		||||
import org.jetbrains.exposed.sql.*
 | 
			
		||||
import org.jetbrains.exposed.sql.statements.InsertStatement
 | 
			
		||||
@@ -25,8 +24,10 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
    override val updatedObjectsFlow: Flow<ObjectType> = updateObjectsChannel.asSharedFlow()
 | 
			
		||||
    override val deletedObjectsIdsFlow: Flow<IdType> = deleteObjectsIdsChannel.asSharedFlow()
 | 
			
		||||
 | 
			
		||||
    @Deprecated("Will be removed in near major update. Override open fun with the same name instead")
 | 
			
		||||
    abstract val InsertStatement<Number>.asObject: ObjectType
 | 
			
		||||
    abstract val selectByIds: SqlExpressionBuilder.(List<out IdType>) -> Op<Boolean>
 | 
			
		||||
    protected open fun InsertStatement<Number>.asObject(value: InputValueType): ObjectType = asObject
 | 
			
		||||
    abstract val selectByIds: SqlExpressionBuilder.(List<IdType>) -> Op<Boolean>
 | 
			
		||||
 | 
			
		||||
    protected abstract fun insert(value: InputValueType, it: InsertStatement<Number>)
 | 
			
		||||
    protected abstract fun update(id: IdType, value: InputValueType, it: UpdateStatement)
 | 
			
		||||
@@ -34,7 +35,7 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
    protected open suspend fun onBeforeCreate(value: List<InputValueType>) {}
 | 
			
		||||
    private fun createWithoutNotification(value: InputValueType): ObjectType {
 | 
			
		||||
        return transaction(database) {
 | 
			
		||||
            insert { insert(value, it) }.asObject
 | 
			
		||||
            insert { insert(value, it) }.asObject(value)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -42,10 +43,8 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
        onBeforeCreate(values)
 | 
			
		||||
        return transaction(db = database) {
 | 
			
		||||
            values.map { value -> createWithoutNotification(value) }
 | 
			
		||||
        }.also {
 | 
			
		||||
            it.forEach {
 | 
			
		||||
                newObjectsChannel.emit(it)
 | 
			
		||||
            }
 | 
			
		||||
        }.onEach {
 | 
			
		||||
            newObjectsChannel.emit(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -83,13 +82,9 @@ abstract class AbstractExposedWriteCRUDRepo<ObjectType, IdType, InputValueType>(
 | 
			
		||||
        return (
 | 
			
		||||
            transaction(db = database) {
 | 
			
		||||
                values.map { (id, value) -> updateWithoutNotification(id, value) }
 | 
			
		||||
            }.filter {
 | 
			
		||||
                it != null
 | 
			
		||||
            } as List<ObjectType>
 | 
			
		||||
        ).also {
 | 
			
		||||
            it.forEach {
 | 
			
		||||
                updateObjectsChannel.emit(it)
 | 
			
		||||
            }
 | 
			
		||||
            }.filterNotNull()
 | 
			
		||||
        ).onEach {
 | 
			
		||||
            updateObjectsChannel.emit(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    protected open suspend fun onBeforeDelete(ids: List<IdType>) {}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user