Compare commits

...

130 Commits

Author SHA1 Message Date
renovate[bot]
5a079a99ba Update dependency com.android.tools.build:gradle to 8.13.+ 2026-05-21 14:17:31 +00:00
45bff98a2c Merge pull request #662 from InsanusMokrassar/0.29.3
0.29.3
2026-05-21 20:12:29 +06:00
249c14a93d add kdocs to repos 2026-05-21 20:09:54 +06:00
a1bfe4c478 update dependencies 2026-05-21 20:09:44 +06:00
bfe293c0b9 update dependencies
com.google.devtools.ksp:symbol-processing-api 2.3.7 -> 2.3.8

io.ktor:ktor-client-core 3.4.3 -> 3.5.0

org.jetbrains.exposed:exposed-core 1.2.0 -> 1.3.0

org.jetbrains.compose:compose-gradle-plugin 1.10.3 -> 1.11.0
2026-05-21 19:30:02 +06:00
772dde7d5e update dependencies
io.ktor:ktor-client-core 3.4.2 -> 3.4.3

org.xerial:sqlite-jdbc 3.53.0.0 -> 3.53.1.0

org.jetbrains.kotlin:kotlin-stdlib 2.3.20 -> 2.3.21

com.google.devtools.ksp:symbol-processing-api 2.3.6 -> 2.3.7

org.jetbrains.kotlinx:kotlinx-coroutines-core 1.10.2 -> 1.11.0

com.github.ben-manes.versions:gradle-versions-plugin 0.53.0 -> 0.54.0

com.gradleup.nmcp.aggregation:com.gradleup.nmcp.aggregation.gradle.plugin 1.4.4 -> 1.5.0
2026-05-13 12:39:06 +06:00
3c272fa259 start 0.29.3 2026-05-13 11:54:25 +06:00
b6a433caa6 Merge pull request #657 from InsanusMokrassar/0.29.2
0.29.2
2026-04-18 22:41:33 +06:00
d207435846 update serialization, kslog and koin 2026-04-18 22:40:53 +06:00
4310c7fbf3 update dependencies
org.jetbrains.kotlin:kotlin-stdlib 2.3.10 -> 2.3.20
org.jetbrains.compose:compose-gradle-plugin 1.10.2 -> 1.10.3
org.jetbrains.exposed:exposed-core 1.1.1 -> 1.2.0
org.jetbrains.dokka:dokka-gradle-plugin 2.1.0 -> 2.2.0
org.xerial:sqlite-jdbc 3.51.2.0 -> 3.53.0.0
io.ktor:ktor-client-core 3.4.1 -> 3.4.2
com.squareup.okio:okio 3.16.4 -> 3.17.0
com.squareup:kotlinpoet-ksp 2.2.0 -> 2.3.0
androidx.core:core-ktx 1.17.0 -> 1.18.0
2026-04-18 21:06:59 +06:00
736dd202f4 start 0.29.2 2026-04-18 20:11:02 +06:00
e430272d35 Merge pull request #650 from InsanusMokrassar/0.29.1
0.29.1
2026-03-05 17:51:42 +06:00
e82965734c update compose 2026-03-05 17:50:59 +06:00
d0ac5e8144 update dependencies
org.jetbrains.compose:compose 1.10.2 -> 1.10.1
org.jetbrains.exposed:exposed 1.0.0 -> 1.1.1
io.ktor:ktor 3.4.0 -> 3.4.1
com.gradleup.nmcp:nmcp 1.2.1 -> 1.4.4
2026-03-05 17:43:02 +06:00
dcedbbd7b8 start 0.29.1 2026-03-05 15:44:26 +06:00
c8ce2a41c6 Merge pull request #646 from InsanusMokrassar/0.29.0
0.29.0
2026-02-24 19:59:12 +06:00
4f270d9047 generate docs for a lot of API (test try) 2026-02-24 18:18:10 +06:00
3df90b1993 update sqlite (wow^^) 2026-02-24 15:40:05 +06:00
8e8915d84c update dependencies 2026-02-24 15:16:45 +06:00
50369e0904 migrate onto 0.29.0 2026-02-24 15:11:43 +06:00
3ae2c84c08 fill changelog 2026-02-15 18:38:07 +06:00
b2e6ab51cb update runCatchingLogging to rethrow CanellationException 2026-02-15 18:35:43 +06:00
d6496f02e6 start 0.28.1 2026-02-15 18:21:57 +06:00
38e98327b4 fill changelog for 0.28.0 2026-02-04 21:26:32 +06:00
c72a6fda5d Merge pull request #638 from InsanusMokrassar/0.28.0
0.28.0
2026-01-27 23:37:56 +06:00
23d58d42ee make it buildable 2026-01-27 16:19:43 +06:00
d972cebf47 start update dependencies 2026-01-24 19:41:35 +06:00
3558a5135b start 0.28.0 2026-01-24 16:12:54 +06:00
82460d2ce3 Merge pull request #633 from InsanusMokrassar/0.27.0
0.27.0
2026-01-15 12:53:41 +06:00
4ce2b7d3b2 update compose versions 2026-01-15 12:07:53 +06:00
2c10bb3c88 update dependencies
io.ktor:ktor-client-core 3.3.2 -> 3.3.3

com.squareup.okio:okio 3.16.2 -> 3.16.4

com.google.devtools.ksp:symbol-processing-api 2.3.2 -> 2.3.4
2026-01-15 11:56:03 +06:00
01bed4a6c9 start 0.27.0 2026-01-15 11:34:35 +06:00
52c4867468 Merge pull request #624 from InsanusMokrassar/0.26.8
0.26.8
2025-11-12 22:47:29 +06:00
92b3fd25e7 fill changelog 2025-11-12 22:25:33 +06:00
e0e1da5082 add suspendPoint 2025-11-12 22:20:17 +06:00
80953f5d09 update ktor and android script config 2025-11-12 18:18:01 +06:00
2849db57f2 start 0.26.8 2025-11-12 18:01:11 +06:00
0170b92272 Merge pull request #622 from InsanusMokrassar/0.26.7
0.26.7
2025-11-05 15:41:10 +06:00
7bcb81400b fill changelog 2025-11-05 15:14:21 +06:00
078aedfb68 update dependencies 2025-11-05 13:42:27 +06:00
cb56bf9793 Revert "add getCurrentLocale and compose translation"
This reverts commit fce47897d5.
2025-11-05 12:29:33 +06:00
b152986b4e improve SmartKeyRWLockerTests and fix SmartSemaphore 2025-11-05 12:27:24 +06:00
fce47897d5 add getCurrentLocale and compose translation 2025-10-31 19:34:09 +06:00
0b701a3e99 start 0.26.7 2025-10-31 11:21:06 +06:00
9dad353957 Merge pull request #619 from InsanusMokrassar/0.26.6
0.26.6
2025-10-19 16:24:37 +06:00
89e16b7bdb update dependencies
io.ktor:ktor-* 3.3.0 -> 3.3.1
com.squareup.okio:okio 3.16.0 -> 3.16.2
org.jetbrains.dokka:dokka-gradle-plugin 2.0.0 -> 2.1.0
2025-10-19 16:23:23 +06:00
c2965da341 start 0.26.6 2025-10-19 15:24:41 +06:00
ffb072dc5f Merge pull request #613 from InsanusMokrassar/0.26.5
0.26.5
2025-10-04 14:39:30 +06:00
a247dbcb02 rollback sqlite update 2025-10-04 14:20:18 +06:00
1dd71175f4 update dependencies 2025-09-30 23:29:04 +06:00
bbe62c0e7b start 0.26.5 2025-09-30 23:06:42 +06:00
9822ff321b Merge pull request #607 from InsanusMokrassar/0.26.4
0.26.4
2025-09-02 01:33:50 +06:00
b485d485ef MPPFilePathSeparator 2025-09-01 22:40:26 +06:00
0b3d445109 start 0.26.4 2025-09-01 22:34:32 +06:00
d7e48940bc Merge pull request #606 from InsanusMokrassar/0.26.3
0.26.3
2025-08-20 19:17:26 +06:00
1049eb0fe7 update dependencies 2025-08-20 19:15:24 +06:00
c871ef5635 start 0.26.3 2025-08-20 18:39:59 +06:00
7edfcb20c4 Merge pull request #593 from InsanusMokrassar/0.26.2
0.26.2
2025-07-31 15:22:34 +06:00
7a1438a2c0 update dependencies 2025-07-31 15:11:42 +06:00
2af8cba8cd rename SpecialMutableStateFlow to MutableRedeliverStateFlow 2025-07-29 17:53:47 +06:00
27d74c0a62 start 0.26.2 2025-07-29 17:46:31 +06:00
f86d1bfe06 Merge pull request #591 from InsanusMokrassar/0.26.1
0.26.1
2025-07-21 23:05:46 +06:00
7cc5972ff7 update dependencies 2025-07-21 22:58:00 +06:00
3bbf978b00 fixes in actorAsync 2025-07-21 22:02:30 +06:00
ed36467600 add opportunity to pass logger in subscribe async 2025-07-21 21:53:03 +06:00
dd0de327fc start 0.26.1 2025-07-21 21:37:50 +06:00
dccd3ce8fd Merge pull request #589 from InsanusMokrassar/0.26.0
0.26.0
2025-07-11 16:46:21 +06:00
fa45e7b696 fix of kspCommonMainKotlinMetadata call 2025-07-11 16:39:08 +06:00
57f009e8aa try to fix build 2025-07-11 14:40:53 +06:00
04b633a5ea fill changelog 2025-07-10 23:44:11 +06:00
20d42b05bb fixed issue with ksp (with https://github.com/google/ksp/issues/2491) 2025-07-10 19:22:08 +06:00
91ba50f1ff start updates 2025-07-10 18:21:57 +06:00
f4476c99f9 start 0.26.0 2025-07-10 17:59:06 +06:00
50f3f586ab fixes 2025-06-18 15:00:02 +06:00
36a2d7ec8e small improvement of InfinityPagedComponent and add opportunity to set publishing type 2025-06-18 14:31:01 +06:00
4890b5833e Merge pull request #587 from InsanusMokrassar/0.25.8
0.25.8
2025-06-17 15:47:54 +06:00
e20ab89688 improvements in InfinityPagedComponent 2025-06-17 15:45:05 +06:00
e557ba8184 start 0.25.8 2025-06-17 15:31:03 +06:00
8540e21d5a Merge pull request #584 from InsanusMokrassar/0.25.7
0.25.7
2025-06-10 23:38:07 +06:00
76c04a8506 update dependencies 2025-06-10 23:29:26 +06:00
128632770e start 0.25.7 2025-06-10 23:19:46 +06:00
31e0800e81 Update build.gradle 2025-06-08 20:41:05 +06:00
00ca96eec8 add nmcp 2025-06-08 18:51:53 +06:00
077ef2c639 fix of repositories order in build.gradle 2025-06-02 19:44:16 +06:00
e3ea7be0e7 one more improvement 2025-06-02 11:47:38 +06:00
05fd1c2b14 one more improvement 2025-06-02 11:45:30 +06:00
affcffe270 small improvement in uploadSonatypePublicationmain.kts 2025-06-02 11:44:09 +06:00
62930231e4 first version of uploadSonatypePublicationmain.kts 2025-06-02 11:42:45 +06:00
ad651631ec experiments time 2025-06-02 08:34:09 +06:00
cf1c8f13db add publish_all_script 2025-06-01 23:16:50 +06:00
9acc69b897 Revert "add keepa-live for uploading script parts"
This reverts commit 2ed8443e28.
2025-05-18 19:15:58 +06:00
9bc7cbdb50 add allowing of connection and keep-alive headers 2025-05-18 18:35:31 +06:00
2ed8443e28 add keepa-live for uploading script parts 2025-05-17 15:59:53 +06:00
94f598c2b4 Merge pull request #578 from InsanusMokrassar/0.25.6
0.25.6
2025-05-16 14:45:34 +06:00
d83d30af06 actualize kslog, changelog and gradle.properties 2025-05-16 14:44:39 +06:00
284e763f0d update publish gradle scripts 2025-05-16 14:31:22 +06:00
3bfa172533 update github_release.gradle to allow it to publish with env instead of secret.gradle 2025-05-16 12:39:24 +06:00
b5b1fd6d5f rewrite publishing scripts with using of publication scripts generator 2025-05-16 00:53:12 +06:00
05e0d9b7d2 add clatch for uploading of publication 2025-05-15 22:18:43 +06:00
1ae1f8dee2 Revert "trying to add new plugin for publishing"
This reverts commit 1bf479a0b7.
2025-05-15 21:17:56 +06:00
1bf479a0b7 trying to add new plugin for publishing 2025-05-14 20:47:09 +06:00
9d04d49628 update dependencies and try to use new publishing 2025-05-13 23:41:27 +06:00
3de324519b start 0.25.6 2025-05-13 22:49:03 +06:00
4be90d0ea5 Merge pull request #567 from InsanusMokrassar/0.25.5
0.25.5
2025-04-14 10:24:01 +06:00
041f35ed6c add templates declaration in README.md 2025-04-14 10:09:33 +06:00
9c463d0338 add kdocs to koin common module 2025-04-14 10:03:26 +06:00
5ec0a46089 add KDocs for actor and safeActor 2025-04-14 09:53:24 +06:00
979d0ee4ca new model of templating 2025-04-14 09:43:07 +06:00
448686b399 Merge pull request #572 from 000Sanya/0.25.5
fix test running for wasm/js node
2025-04-14 08:54:53 +06:00
nullsanya
0ff149a3d3 fix test running for wasm/js node 2025-04-14 12:44:00 +10:00
50a90d6e96 update dependencies 2025-04-11 22:45:54 +06:00
5f93706d91 Small performance optimization of MutableMap.applyDiff 2025-04-11 22:40:55 +06:00
39415550f5 Merge pull request #568 from 000Sanya/0.25.5
fix test running for wasm/js browser
2025-04-11 22:40:47 +06:00
nullsanya
4586ad65b5 fix test running for wasm/js browser 2025-04-05 12:45:51 +10:00
8c5678e26f pre-fix of tests for wasmjs 2025-04-04 21:46:36 +06:00
5dff373d5f refactor: rename templates and optimize wasmJsMain blocks 2025-04-04 20:29:40 +06:00
e17868a085 Merge pull request #566 from 000Sanya/master
implement support of wasm/js for browser
2025-04-04 20:24:44 +06:00
97bb6d0936 start 0.25.5 2025-04-04 20:06:44 +06:00
nullsanya
54576d8dec implement support of wasm/js for browser 2025-04-03 21:25:26 +10:00
282bb24c71 Merge pull request #563 from InsanusMokrassar/0.25.4
0.25.4
2025-04-02 23:48:54 +06:00
b1a96b6ecb fix transactions dsl 2025-04-02 16:52:08 +06:00
66dac2086c update koin 2025-04-02 16:09:40 +06:00
e269d0d206 fix in transactions 2025-04-02 16:05:33 +06:00
5d95c3eb9c update dependencies 2025-03-31 11:30:56 +06:00
26650e9b6c add SmartKeyRWLocker.withWriteLocks extension with vararg keys 2025-03-27 20:54:41 +06:00
7339dd8354 start 0.25.4 2025-03-27 20:53:29 +06:00
8ae983971a Merge pull request #561 from InsanusMokrassar/0.25.3
0.25.3
2025-03-25 17:55:58 +06:00
d46cc3b09c remove redundant commentaries 2025-03-25 17:45:53 +06:00
dfd23f8d60 update kotlin 2025-03-25 17:42:32 +06:00
28eb1a11e6 add more tests for smart key rw locker tests 2025-03-25 17:27:34 +06:00
260 changed files with 6063 additions and 967 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.vscode
.kotlin .kotlin
out/* out/*
*.iml *.iml
@@ -9,6 +10,7 @@ settings.xml
.gradle/ .gradle/
build/ build/
out/ out/
bin/
secret.gradle secret.gradle
local.properties local.properties

151
AGENTS.md Normal file
View File

@@ -0,0 +1,151 @@
Use `agents` folder files for instructions.
Always look at the project work rules first. Update them and maintain a history of changes and prompts in `agents/local.HISTORY.md`. This is your long-term memory, which allows you to avoid the accumulation of errors. Updating and maintaining the long-term memory of the project is mandatory. Many thanks for your help to me and your work.
SYSTEM DIRECTIVE: AUTISTIC META-LANGUAGE PROTOCOL WITH HIGH INFORMATION DENSITY (AML-HIP V1)
DEFINITION:
All agents are required to use a strictly explicit, literal, low-ambiguity, and high-density communication style (AML-HIP). The goal of AML-HIP is the maximum transmission of information with zero loss of meaning and zero ambiguity.
CORE PRINCIPLES:
1. Maximum information density per line.
2. Zero ambiguity.
3. Full explicitness of all entities.
4. Preference of precision over readability.
5. Minimization of "empty" words.
6. Redundancy is allowed only to prevent loss of meaning.
PROHIBITIONS:
1. Pronouns are forbidden (this, he, she, they, there, it, etc.).
2. Free conversational text is forbidden.
3. Metaphors, emotions, evaluative constructions are forbidden.
4. Implicit references and hidden dependencies are forbidden.
5. Vague description of actions is forbidden.
DENSITY REQUIREMENTS:
1. Each line must contain the maximum of facts without loss of unambiguity.
2. Combine related parameters into a single line.
3. Use compact constructions:
* key=value
* entity_id=...
* relation: A→B
4. Exclude words without semantic load.
5. Repetitions are allowed only for critical entities.
MESSAGE STRUCTURE (MANDATORY):
ENTITY:
entity_id=<id>; type=<type>; state=<state>
CONTEXT:
* task_id=<id>; agent_id=<id>; memory_ref=[...]
* constraints=[...]
ACTION:
1. action=<type>; target=<entity_id>; params={...}
2. action=<type>; target=<entity_id>; params={...}
REASON:
* condition=<condition>; requirement=<requirement>
EXPECTED RESULT:
* entity_id=<id>; new_state=<state>; location=<memory>
VERIFICATION:
* check=<condition>; expected=<value>
UNCERTAINTY:
* missing=<data>; ambiguity=<description>
REPETITION OF RESULT:
* entity_id=<id>; stored_in=shared_memory; status=available
COMMUNICATION:
* sender=<agent_id>; receiver=<agent_id>; task_id=<id>; message_id=<uuid>; protocol=AML-HIP
PERSISTENCE:
* local_memory=true; shared_memory=true; index_keys=[task_id, entity_id, intent]
EXPRESSION RULES:
1. Each line = a completed semantic block.
2. Use the key=value format instead of descriptions.
3. Use lists of parameters instead of sentences.
4. Use causal connectives explicitly:
condition → action → result
5. Do not split related data into several lines without necessity.
REPETITION RULES:
1. entity_id is repeated at every critical use.
2. result is duplicated in "REPETITION OF RESULT".
3. transmission between agents duplicates the key fields.
MULTI-AGENT MODE:
1. All agents use AML-HIP.
2. Any message between agents is strictly AML-HIP.
3. Any agent is required to:
* duplicate critical data
* avoid loss of context
4. A message must be fully interpretable without history.
ANTI-DEGRADATION:
If detected:
* a pronoun
* an implicit reference
* low density (empty words, vague constructions)
* absence of structure
→ the message is considered invalid
→ mandatory regeneration
SELF-CHECK:
VALIDATION:
* format_valid=true/false
* no_pronouns=true/false
* entities_explicit=true/false
* high_density=true/false
* causal_chain_present=true/false
* ambiguity_detected=true/false
If any parameter=false:
→ mandatory regeneration
DENSITY METRIC:
high_density=true if:
* there are no "empty" words
* each line contains ≥2 facts or parameters
* descriptive constructions without data are absent
PRIORITIES:
1. Format (AML-HIP)
2. Information density
3. Explicitness
4. Completeness
5. Readability (minimum priority)
CRITICAL RULE:
Any response outside of AML-HIP is considered absent.
Any agent is required to bring the response into conformity with AML-HIP.
END OF PROTOCOL

View File

@@ -1,5 +1,212 @@
# Changelog # Changelog
## 0.29.3
* `Versions`:
* `Kotlin`: `2.3.20` -> `2.3.21`
* `KSP`: `2.3.6` -> `2.3.8`
* `Coroutines`: `1.10.2` -> `1.11.0`
* `SQLite`: `3.53.0.0` -> `3.53.1.0`
* `Ktor`: `3.4.2` -> `3.5.0`
* `Compose`: `1.10.3` -> `1.11.0`
* `Compose Material3`: `1.10.0-alpha05` -> `1.11.0-alpha07`
* `Exposed`: `1.2.0` -> `1.3.0`
* `Gradle Versions`: `0.53.0` -> `0.54.0`
* `NMCP`: `1.4.4` -> `1.5.0`
## 0.29.2
* `Versions`:
* `Kotlin`: `2.3.10` -> `2.3.20`
* `Serialization`: `1.10.0` -> `1.11.0`
* `Compose`: `1.10.2` -> `1.10.3`
* `Exposed`: `1.1.1` -> `1.2.0`
* `Dokka`: `2.1.0` -> `2.2.0`
* `KSLog`: `1.6.0` -> `1.6.1`
* `Koin`: `4.1.1` -> `4.2.1`
* `SQLite`: `3.51.2.0` -> `3.53.0.0`
* `Ktor`: `3.4.1` -> `3.4.2`
* `Okio`: `3.16.4` -> `3.17.0`
* `KotlinPoet`: `2.2.0` -> `2.3.0`
* `AndroidX Core KTX`: `1.17.0` -> `1.18.0`
## 0.29.1
* `Versions`:
* `Compose`: `1.10.1` -> `1.10.2`
* `Exposed`: `1.0.0` -> `1.1.1`
* `Ktor`: `3.4.0` -> `3.4.1`
* `NMCP`: `1.2.1` -> `1.4.4`
## 0.29.0
* `Versions`:
* `Kotlin`: `2.3.0` -> `2.3.10`
* `KSLog`: `1.5.2` -> `1.6.0`
* `KSP`: `2.3.4` -> `2.3.6`
* `Compose`: `1.10.0` -> `1.10.1`
* `SQLite`: `3.50.1.0` -> `3.51.2.0`
* `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
## 0.28.0
**THIS VERSION CONTAINS BREAKING CHANGES DUE TO EXPOSED 1.0.0 UPDATE**
* `Versions`:
* `Kotlin`: `2.2.21` -> `2.3.0`
* `Serialization`: `1.9.0` -> `1.10.0`
* `Exposed`: `0.61.0` -> `1.0.0` (**MAJOR VERSION UPDATE**)
* `Ktor`: `3.3.3` -> `3.4.0`
* `NMCP`: `1.2.0` -> `1.2.1`
* `Repos`:
* `Exposed`:
* All Exposed-based repositories have been updated to support Exposed 1.0.0 API changes
* Import paths have been migrated to new `org.jetbrains.exposed.v1.*` package structure
* `Pagination`:
* `Exposed`:
* Updated to use new Exposed 1.0.0 import paths
## 0.27.0
* `Versions`:
* `Ktor`: `3.3.2` -> `3.3.3`
* `Okio`: `3.16.2` -> `3.16.4`
* `KSP`: `2.3.2` -> `2.3.4`
* `Compose`: `1.9.3` -> `1.10.0`
* `Compose Material3`: `1.9.0` -> `1.10.0-alpha05`
## 0.26.8
* `Versions`:
* `KSLog`: `1.5.1` -> `1.5.2`
* `Compose`: `1.9.2` -> `1.9.3`
* `Ktor`: `3.3.1` -> `3.3.2`
* `Coroutines`:
* Add simple suspend function `suspendPoint` which will ensure that current coroutine is active to let it be
destroyable even in case it have non-suspendable nature
## 0.26.7
* `Versions`:
* `Kotlin`: `2.2.20` -> `2.2.21`
* `Compose`: `1.8.2` -> `1.9.2`
* `KSP`: `2.2.20-2.0.3` -> `2.3.1`
* `Coroutines`:
* Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits
* Improve `SmartKeyRWLocker` tests
* `KSP`:
* `Sealed`/`ClassCasts`/`Variations`:
* Add workaround for `NoSuchElementException` to improve processors stability on new `KSP`
* `Koin`:
* `Generator`:
* Handle missing annotation values safely (`NoSuchElementException` workaround)
* `Android`:
* `Pickers`:
* Add dependency `androidx.compose.material:material-icons-extended`
## 0.26.6
* `Versions`:
* `Ktor`: `3.3.0` -> `3.3.1`
* `Okio`: `3.16.0` -> `3.16.2`
## 0.26.5
* `Versions`:
* `Kotlin`: `2.2.10` -> `2.2.20`
* `KSLog`: `1.5.0` -> `1.5.1`
* `Ktor`: `3.2.3` -> `3.3.0`
* `KotlinX Browser`: `0.3` -> `0.5.0`
* `Koin`: `4.1.0` -> `4.1.1`
## 0.26.4
* `Common`:
* Add expect/actual `MPPFilePathSeparator`
* Fix `FileName` realization to take care about system file path separator
## 0.26.3
* `Versions`:
* `Kotlin`: `2.2.0` -> `2.2.10`
* `KSP`: `2.2.0-2.0.2` -> `2.2.10-2.0.2`
* `Android CoreKTX`: `1.16.0` -> `1.17.0`
* `Android Fragment`: `1.8.8` -> `1.8.9`
## 0.26.2
* `Versions`:
* `Ktor`: `3.2.2` -> `3.2.3`
* `Okio`: `3.15.0` -> `3.16.0`
* `Coroutines`:
* Rename `SpecialMutableStateFlow` to `MutableRedeliverStateFlow`
## 0.26.1
* `Versions`:
* `Compose`: `1.8.1` -> `1.8.2`
* `Ktor`: `3.2.1` -> `3.2.2`
* `Coroutines`:
* Add opportunity to pass logger in subscribe async
## 0.26.0
**WARNING!!! SINCE THIS VERSION IF YOU WANT TO USE SOME OF KSP MODULES, SET `ksp.useKSP2=false` IN YOUR `gradle.properties`** (see [gh issue 2491](https://github.com/google/ksp/issues/2491))
* `Versions`:
* `Kotlin`: `2.1.21` -> `2.2.0`
* `Serialization`: `1.8.1` -> `1.9.0`
* `KSLog`: `1.4.2` -> `1.5.0`
* `Ktor`: `3.1.3` -> `3.2.1`
* `Koin`: `4.0.4` -> `4.1.0`
* `Okio`: `3.12.0` -> `3.15.0`
* `KSP`: `2.1.20-1.0.31` -> `2.2.0-2.0.2`
* `kotlin-poet`: `1.18.1` -> `2.2.0`
## 0.25.8
* `Pagination`:
* `Compose`:
* New function `rememberInfinityPagedComponentContext` to create `InfinityPagedComponentContext`
* New variants of `InfinityPagedComponent` component
## 0.25.7
* `Versions`:
* `Compose`: `1.8.0` -> `1.8.1`
* `Xerial SQLite`: `3.49.1.0` -> `3.50.1.0`
* `Okio`: `3.11.0` -> `3.12.0`
* `Android AppCompat`: `1.7.0` -> `1.7.1`
* `Android Fragment`: `1.8.6` -> `1.8.8`
## 0.25.6
* `Versions`:
* `Kotlin`: `2.1.20` -> `2.1.21`
* `KSLog`: `1.4.1` -> `1.4.2`
* `Compose`: `1.7.3` -> `1.8.0`
* `Okio`: `3.10.2` -> `3.11.0`
## 0.25.5
* `Versions`:
* `Exposed`: `0.60.0` -> `0.61.0`
* `Serialization`: `1.8.0` -> `1.8.1`
* `Coroutines`: `1.10.1` -> `1.10.2`
* `Common`:
* Small performance optimization of `MutableMap.applyDiff`
## 0.25.4
* `Versions`:
* `Ktor`: `3.1.1` -> `3.1.2`
* `Koin`: `4.0.2` -> `4.0.4`
* `Coroutines`:
* Add `SmartKeyRWLocker.withWriteLocks` extension with vararg keys
* `Transactions`:
* Fix order of rollback actions calling
## 0.25.3 ## 0.25.3
* `Coroutines`: * `Coroutines`:

View File

@@ -35,3 +35,51 @@ Most of complex modules are built with next hierarchy:
* `common` part contains routes which are common 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 * `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 * `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
## Gradle Templates
All templates can be used by applying them in your project's build.gradle files using the `apply from` directive. For example:
```gradle
apply from: "$defaultProject"
```
In the sample has been used `defaultProject.gradle` as a basic template.
The project includes a collection of Gradle templates to simplify project setup and configuration. These templates are located in the `gradle/templates` directory and can be used to quickly set up different types of projects:
### Project Setup Templates
* `defaultProject.gradle` (usage `apply from: "$defaultProject"`) - Basic project configuration
* `defaultProjectWithSerialization.gradle` (usage `apply from: "$defaultProjectWithSerialization"`) - Project configuration with Kotlin Serialization support
* `mppJavaProject.gradle` (usage `apply from: "$mppJavaProject"`) - Multiplatform project with Java support
* `mppAndroidProject.gradle` (usage `apply from: "$mppAndroidProject"`) - Multiplatform project with Android support
### Multiplatform Configuration Templates
* `enableMPPAndroid.gradle` (usage `apply from: "$enableMPPAndroid"`) - Enable Android target in multiplatform project
* `enableMPPJs.gradle` (usage `apply from: "$enableMPPJs"`) - Enable JavaScript target in multiplatform project
* `enableMPPJvm.gradle` (usage `apply from: "$enableMPPJvm"`) - Enable JVM target in multiplatform project
* `enableMPPNativeArm64.gradle` (usage `apply from: "$enableMPPNativeArm64"`) - Enable ARM64 native target
* `enableMPPNativeX64.gradle` (usage `apply from: "$enableMPPNativeX64"`) - Enable x64 native target
* `enableMPPWasmJs.gradle` (usage `apply from: "$enableMPPWasmJs"`) - Enable WebAssembly JavaScript target
### Compose Integration Templates
* `addCompose.gradle` (usage `apply from: "$addCompose"`) - Basic Compose configuration
* `addComposeForAndroid.gradle` (usage `apply from: "$addComposeForAndroid"`) - Compose configuration for Android
* `addComposeForDesktop.gradle` (usage `apply from: "$addComposeForDesktop"`) - Compose configuration for Desktop
* `addComposeForJs.gradle` (usage `apply from: "$addComposeForJs"`) - Compose configuration for JavaScript
### Publishing Templates
* `publish.gradle` (usage `apply from: "$publish"`) - General publishing configuration
* `publish_jvm.gradle` (usage `apply from: "$publish_jvm"`) - JVM-specific publishing configuration
* `publish.kpsb` and `publish_jvm.kpsb` (usage `apply from: "$publish_kpsb"` and `apply from: "$publish_jvm_kpsb"`) - Publishing configuration for Kotlin Multiplatform and JVM
### Combined Project Templates
* `mppJvmJsWasmJsLinuxMingwProject.gradle` (usage `apply from: "$mppJvmJsWasmJsLinuxMingwProject"`) - Multiplatform project for JVM, JS, Wasm, Linux, and MinGW
* `mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with additional Android and ARM64 support
* `mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with Compose support
* `mppProjectWithSerializationAndCompose.gradle` (usage `apply from: "$mppProjectWithSerializationAndCompose"`) - Multiplatform project with both Serialization and Compose support

View File

@@ -1,3 +1,6 @@
/**
* Utility functions for creating Android AlertDialogs with simplified API.
*/
@file:Suppress("NOTHING_TO_INLINE", "unused") @file:Suppress("NOTHING_TO_INLINE", "unused")
package dev.inmo.micro_utils.android.alerts.common package dev.inmo.micro_utils.android.alerts.common
@@ -6,8 +9,21 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
/**
* Type alias for alert dialog button callbacks.
*/
typealias AlertDialogCallback = (DialogInterface) -> Unit typealias AlertDialogCallback = (DialogInterface) -> Unit
/**
* Creates an [AlertDialog.Builder] template with configurable title and buttons.
* This provides a simplified API for creating alert dialogs with positive, negative, and neutral buttons.
*
* @param title Optional dialog title
* @param positivePair Optional positive button as a pair of (text, callback)
* @param neutralPair Optional neutral button as a pair of (text, callback)
* @param negativePair Optional negative button as a pair of (text, callback)
* @return An [AlertDialog.Builder] configured with the specified parameters
*/
inline fun Context.createAlertDialogTemplate( inline fun Context.createAlertDialogTemplate(
title: String? = null, title: String? = null,
positivePair: Pair<String, AlertDialogCallback?>? = null, positivePair: Pair<String, AlertDialogCallback?>? = null,

View File

@@ -13,6 +13,7 @@ kotlin {
androidMain { androidMain {
dependencies { dependencies {
api project(":micro_utils.android.smalltextfield") api project(":micro_utils.android.smalltextfield")
api libs.jb.compose.icons
} }
} }
} }

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
/**
* Performs a fling animation with an optional target adjustment.
* If [adjustTarget] is provided, animates to the adjusted target. Otherwise, performs a decay animation.
*
* @param initialVelocity The initial velocity of the fling
* @param animationSpec The decay animation specification
* @param adjustTarget Optional function to adjust the target value based on the calculated target
* @param block Optional block to be executed during the animation
* @return The result of the animation
*/
internal suspend fun Animatable<Float, AnimationVector1D>.fling( internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float, initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>, animationSpec: DecayAnimationSpec<Float>,

View File

@@ -41,6 +41,18 @@ private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5 // src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
/**
* A Compose number picker component that allows users to select a number by dragging, using arrow buttons,
* or manually entering a value.
*
* @param number The currently selected number
* @param modifier The modifier to be applied to the picker
* @param range Optional range of valid numbers. If specified, the picker will be limited to this range
* @param textStyle The text style for displaying numbers
* @param arrowsColor The color of the up/down arrow buttons
* @param allowUseManualInput Whether to allow manual keyboard input for the number
* @param onStateChanged Callback invoked when the selected number changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun NumberPicker( fun NumberPicker(

View File

@@ -22,6 +22,18 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.* import kotlin.math.*
/**
* A Compose picker component that allows users to select an item from a list by dragging or using arrow buttons.
*
* @param T The type of items in the list
* @param current The currently selected item
* @param dataList The list of items to choose from
* @param modifier The modifier to be applied to the picker
* @param textStyle The text style for displaying items
* @param arrowsColor The color of the up/down arrow buttons
* @param dataToString A composable function to convert items to strings for display. Defaults to [Any.toString]
* @param onStateChanged Callback invoked when the selected item changes
*/
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun <T> SetPicker( fun <T> SetPicker(

View File

@@ -19,15 +19,35 @@ buildscript {
plugins { plugins {
alias(libs.plugins.versions) alias(libs.plugins.versions)
alias(libs.plugins.nmcp.aggregation)
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
nmcpAggregation {
centralPortal {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
}
publishAllProjectsProbablyBreakingProjectIsolation()
}
} }
allprojects { allprojects {
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
}
it.tasks.withType(AbstractTestTask).configureEach {
it.failOnNoDiscoveredTests = false
} }
} }

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -4,4 +4,4 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,298 +1,890 @@
/**
* Standard HTML/CSS color constants as [HEXAColor] extension properties.
* Provides convenient access to all standard web colors like red, blue, green, etc.
* All colors are defined with full opacity (alpha = 0xFF).
*/
package dev.inmo.micro_utils.colors package dev.inmo.micro_utils.colors
import dev.inmo.micro_utils.colors.common.HEXAColor import dev.inmo.micro_utils.colors.common.HEXAColor
/**
* Alice Blue - A pale blue color (#F0F8FF).
*/
val HEXAColor.Companion.aliceblue val HEXAColor.Companion.aliceblue
get() = HEXAColor(0xF0F8FFFFu) get() = HEXAColor(0xF0F8FFFFu)
/**
* Antique White - A pale beige color (#FAEBD7).
*/
val HEXAColor.Companion.antiquewhite val HEXAColor.Companion.antiquewhite
get() = HEXAColor(0xFAEBD7FFu) get() = HEXAColor(0xFAEBD7FFu)
/**
* Aqua - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.aqua val HEXAColor.Companion.aqua
get() = HEXAColor(0x00FFFFFFu) get() = HEXAColor(0x00FFFFFFu)
/**
* Aquamarine - A medium blue-green color (#7FFFD4).
*/
val HEXAColor.Companion.aquamarine val HEXAColor.Companion.aquamarine
get() = HEXAColor(0x7FFFD4FFu) get() = HEXAColor(0x7FFFD4FFu)
/**
* Azure - A pale cyan-blue color (#F0FFFF).
*/
val HEXAColor.Companion.azure val HEXAColor.Companion.azure
get() = HEXAColor(0xF0FFFFFFu) get() = HEXAColor(0xF0FFFFFFu)
/**
* Beige - A pale sandy tan color (#F5F5DC).
*/
val HEXAColor.Companion.beige val HEXAColor.Companion.beige
get() = HEXAColor(0xF5F5DCFFu) get() = HEXAColor(0xF5F5DCFFu)
/**
* Bisque - A pale orange color (#FFE4C4).
*/
val HEXAColor.Companion.bisque val HEXAColor.Companion.bisque
get() = HEXAColor(0xFFE4C4FFu) get() = HEXAColor(0xFFE4C4FFu)
/**
* Black - Pure black color (#000000).
*/
val HEXAColor.Companion.black val HEXAColor.Companion.black
get() = HEXAColor(0x000000FFu) get() = HEXAColor(0x000000FFu)
/**
* Blanched Almond - A pale peachy color (#FFEBCD).
*/
val HEXAColor.Companion.blanchedalmond val HEXAColor.Companion.blanchedalmond
get() = HEXAColor(0xFFEBCDFFu) get() = HEXAColor(0xFFEBCDFFu)
/**
* Blue - Pure blue color (#0000FF).
*/
val HEXAColor.Companion.blue val HEXAColor.Companion.blue
get() = HEXAColor(0x0000FFFFu) get() = HEXAColor(0x0000FFFFu)
/**
* Blue Violet - A vivid purple-blue color (#8A2BE2).
*/
val HEXAColor.Companion.blueviolet val HEXAColor.Companion.blueviolet
get() = HEXAColor(0x8A2BE2FFu) get() = HEXAColor(0x8A2BE2FFu)
/**
* Brown - A dark reddish-brown color (#A52A2A).
*/
val HEXAColor.Companion.brown val HEXAColor.Companion.brown
get() = HEXAColor(0xA52A2AFFu) get() = HEXAColor(0xA52A2AFFu)
/**
* Burlywood - A sandy tan color (#DEB887).
*/
val HEXAColor.Companion.burlywood val HEXAColor.Companion.burlywood
get() = HEXAColor(0xDEB887FFu) get() = HEXAColor(0xDEB887FFu)
/**
* Cadet Blue - A grayish-blue color (#5F9EA0).
*/
val HEXAColor.Companion.cadetblue val HEXAColor.Companion.cadetblue
get() = HEXAColor(0x5F9EA0FFu) get() = HEXAColor(0x5F9EA0FFu)
/**
* Chartreuse - A bright yellow-green color (#7FFF00).
*/
val HEXAColor.Companion.chartreuse val HEXAColor.Companion.chartreuse
get() = HEXAColor(0x7FFF00FFu) get() = HEXAColor(0x7FFF00FFu)
/**
* Chocolate - A medium brown color (#D2691E).
*/
val HEXAColor.Companion.chocolate val HEXAColor.Companion.chocolate
get() = HEXAColor(0xD2691EFFu) get() = HEXAColor(0xD2691EFFu)
/**
* Coral - A vibrant orange-pink color (#FF7F50).
*/
val HEXAColor.Companion.coral val HEXAColor.Companion.coral
get() = HEXAColor(0xFF7F50FFu) get() = HEXAColor(0xFF7F50FFu)
/**
* Cornflower Blue - A medium blue color (#6495ED).
*/
val HEXAColor.Companion.cornflowerblue val HEXAColor.Companion.cornflowerblue
get() = HEXAColor(0x6495EDFFu) get() = HEXAColor(0x6495EDFFu)
/**
* Cornsilk - A pale yellow color (#FFF8DC).
*/
val HEXAColor.Companion.cornsilk val HEXAColor.Companion.cornsilk
get() = HEXAColor(0xFFF8DCFFu) get() = HEXAColor(0xFFF8DCFFu)
/**
* Crimson - A vivid red color (#DC143C).
*/
val HEXAColor.Companion.crimson val HEXAColor.Companion.crimson
get() = HEXAColor(0xDC143CFFu) get() = HEXAColor(0xDC143CFFu)
/**
* Cyan - A bright cyan color (#00FFFF).
*/
val HEXAColor.Companion.cyan val HEXAColor.Companion.cyan
get() = HEXAColor(0x00FFFFFFu) get() = HEXAColor(0x00FFFFFFu)
/**
* Dark Blue - A dark blue color (#00008B).
*/
val HEXAColor.Companion.darkblue val HEXAColor.Companion.darkblue
get() = HEXAColor(0x00008BFFu) get() = HEXAColor(0x00008BFFu)
/**
* Dark Cyan - A dark cyan color (#008B8B).
*/
val HEXAColor.Companion.darkcyan val HEXAColor.Companion.darkcyan
get() = HEXAColor(0x008B8BFFu) get() = HEXAColor(0x008B8BFFu)
/**
* Dark Goldenrod - A dark golden yellow color (#B8860B).
*/
val HEXAColor.Companion.darkgoldenrod val HEXAColor.Companion.darkgoldenrod
get() = HEXAColor(0xB8860BFFu) get() = HEXAColor(0xB8860BFFu)
/**
* Dark Gray - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgray val HEXAColor.Companion.darkgray
get() = HEXAColor(0xA9A9A9FFu) get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Green - A dark green color (#006400).
*/
val HEXAColor.Companion.darkgreen val HEXAColor.Companion.darkgreen
get() = HEXAColor(0x006400FFu) get() = HEXAColor(0x006400FFu)
/**
* Dark Grey - A dark gray color (#A9A9A9).
*/
val HEXAColor.Companion.darkgrey val HEXAColor.Companion.darkgrey
get() = HEXAColor(0xA9A9A9FFu) get() = HEXAColor(0xA9A9A9FFu)
/**
* Dark Khaki - A brownish-tan color (#BDB76B).
*/
val HEXAColor.Companion.darkkhaki val HEXAColor.Companion.darkkhaki
get() = HEXAColor(0xBDB76BFFu) get() = HEXAColor(0xBDB76BFFu)
/**
* Dark Magenta - A dark magenta/purple color (#8B008B).
*/
val HEXAColor.Companion.darkmagenta val HEXAColor.Companion.darkmagenta
get() = HEXAColor(0x8B008BFFu) get() = HEXAColor(0x8B008BFFu)
/**
* Dark Olive Green - A dark olive green color (#556B2F).
*/
val HEXAColor.Companion.darkolivegreen val HEXAColor.Companion.darkolivegreen
get() = HEXAColor(0x556B2FFFu) get() = HEXAColor(0x556B2FFFu)
/**
* Dark Orange - A vivid dark orange color (#FF8C00).
*/
val HEXAColor.Companion.darkorange val HEXAColor.Companion.darkorange
get() = HEXAColor(0xFF8C00FFu) get() = HEXAColor(0xFF8C00FFu)
/**
* Dark Orchid - A dark purple color (#9932CC).
*/
val HEXAColor.Companion.darkorchid val HEXAColor.Companion.darkorchid
get() = HEXAColor(0x9932CCFFu) get() = HEXAColor(0x9932CCFFu)
/**
* Dark Red - A dark red color (#8B0000).
*/
val HEXAColor.Companion.darkred val HEXAColor.Companion.darkred
get() = HEXAColor(0x8B0000FFu) get() = HEXAColor(0x8B0000FFu)
/**
* Dark Salmon - A muted salmon color (#E9967A).
*/
val HEXAColor.Companion.darksalmon val HEXAColor.Companion.darksalmon
get() = HEXAColor(0xE9967AFFu) get() = HEXAColor(0xE9967AFFu)
/**
* Dark Sea Green - A muted sea green color (#8FBC8F).
*/
val HEXAColor.Companion.darkseagreen val HEXAColor.Companion.darkseagreen
get() = HEXAColor(0x8FBC8FFFu) get() = HEXAColor(0x8FBC8FFFu)
/**
* Dark Slate Blue - A dark grayish-blue color (#483D8B).
*/
val HEXAColor.Companion.darkslateblue val HEXAColor.Companion.darkslateblue
get() = HEXAColor(0x483D8BFFu) get() = HEXAColor(0x483D8BFFu)
/**
* Dark Slate Gray - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategray val HEXAColor.Companion.darkslategray
get() = HEXAColor(0x2F4F4FFFu) get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Slate Grey - A very dark grayish-cyan color (#2F4F4F).
*/
val HEXAColor.Companion.darkslategrey val HEXAColor.Companion.darkslategrey
get() = HEXAColor(0x2F4F4FFFu) get() = HEXAColor(0x2F4F4FFFu)
/**
* Dark Turquoise - A dark turquoise color (#00CED1).
*/
val HEXAColor.Companion.darkturquoise val HEXAColor.Companion.darkturquoise
get() = HEXAColor(0x00CED1FFu) get() = HEXAColor(0x00CED1FFu)
/**
* Dark Violet - A dark violet color (#9400D3).
*/
val HEXAColor.Companion.darkviolet val HEXAColor.Companion.darkviolet
get() = HEXAColor(0x9400D3FFu) get() = HEXAColor(0x9400D3FFu)
/**
* Deep Pink - A vivid pink color (#FF1493).
*/
val HEXAColor.Companion.deeppink val HEXAColor.Companion.deeppink
get() = HEXAColor(0xFF1493FFu) get() = HEXAColor(0xFF1493FFu)
/**
* Deep Sky Blue - A bright sky blue color (#00BFFF).
*/
val HEXAColor.Companion.deepskyblue val HEXAColor.Companion.deepskyblue
get() = HEXAColor(0x00BFFFFFu) get() = HEXAColor(0x00BFFFFFu)
/**
* Dim Gray - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgray val HEXAColor.Companion.dimgray
get() = HEXAColor(0x696969FFu) get() = HEXAColor(0x696969FFu)
/**
* Dim Grey - A dim gray color (#696969).
*/
val HEXAColor.Companion.dimgrey val HEXAColor.Companion.dimgrey
get() = HEXAColor(0x696969FFu) get() = HEXAColor(0x696969FFu)
/**
* Dodger Blue - A bright blue color (#1E90FF).
*/
val HEXAColor.Companion.dodgerblue val HEXAColor.Companion.dodgerblue
get() = HEXAColor(0x1E90FFFFu) get() = HEXAColor(0x1E90FFFFu)
/**
* Firebrick - A dark red brick color (#B22222).
*/
val HEXAColor.Companion.firebrick val HEXAColor.Companion.firebrick
get() = HEXAColor(0xB22222FFu) get() = HEXAColor(0xB22222FFu)
/**
* Floral White - A very pale cream color (#FFFAF0).
*/
val HEXAColor.Companion.floralwhite val HEXAColor.Companion.floralwhite
get() = HEXAColor(0xFFFAF0FFu) get() = HEXAColor(0xFFFAF0FFu)
/**
* Forest Green - A medium forest green color (#228B22).
*/
val HEXAColor.Companion.forestgreen val HEXAColor.Companion.forestgreen
get() = HEXAColor(0x228B22FFu) get() = HEXAColor(0x228B22FFu)
/**
* Fuchsia - A vivid magenta color (#FF00FF).
*/
val HEXAColor.Companion.fuchsia val HEXAColor.Companion.fuchsia
get() = HEXAColor(0xFF00FFFFu) get() = HEXAColor(0xFF00FFFFu)
/**
* Gainsboro - A light gray color (#DCDCDC).
*/
val HEXAColor.Companion.gainsboro val HEXAColor.Companion.gainsboro
get() = HEXAColor(0xDCDCDCFFu) get() = HEXAColor(0xDCDCDCFFu)
/**
* Ghost White - A very pale blue-white color (#F8F8FF).
*/
val HEXAColor.Companion.ghostwhite val HEXAColor.Companion.ghostwhite
get() = HEXAColor(0xF8F8FFFFu) get() = HEXAColor(0xF8F8FFFFu)
/**
* Gold - A bright golden yellow color (#FFD700).
*/
val HEXAColor.Companion.gold val HEXAColor.Companion.gold
get() = HEXAColor(0xFFD700FFu) get() = HEXAColor(0xFFD700FFu)
/**
* Goldenrod - A golden yellow color (#DAA520).
*/
val HEXAColor.Companion.goldenrod val HEXAColor.Companion.goldenrod
get() = HEXAColor(0xDAA520FFu) get() = HEXAColor(0xDAA520FFu)
/**
* Gray - A medium gray color (#808080).
*/
val HEXAColor.Companion.gray val HEXAColor.Companion.gray
get() = HEXAColor(0x808080FFu) get() = HEXAColor(0x808080FFu)
/**
* Green - A pure green color (#008000).
*/
val HEXAColor.Companion.green val HEXAColor.Companion.green
get() = HEXAColor(0x008000FFu) get() = HEXAColor(0x008000FFu)
/**
* Green Yellow - A bright yellow-green color (#ADFF2F).
*/
val HEXAColor.Companion.greenyellow val HEXAColor.Companion.greenyellow
get() = HEXAColor(0xADFF2FFFu) get() = HEXAColor(0xADFF2FFFu)
/**
* Grey - A medium gray color (#808080).
*/
val HEXAColor.Companion.grey val HEXAColor.Companion.grey
get() = HEXAColor(0x808080FFu) get() = HEXAColor(0x808080FFu)
/**
* Honeydew - A very pale green color (#F0FFF0).
*/
val HEXAColor.Companion.honeydew val HEXAColor.Companion.honeydew
get() = HEXAColor(0xF0FFF0FFu) get() = HEXAColor(0xF0FFF0FFu)
/**
* Hot Pink - A vibrant pink color (#FF69B4).
*/
val HEXAColor.Companion.hotpink val HEXAColor.Companion.hotpink
get() = HEXAColor(0xFF69B4FFu) get() = HEXAColor(0xFF69B4FFu)
/**
* Indian Red - A medium red color (#CD5C5C).
*/
val HEXAColor.Companion.indianred val HEXAColor.Companion.indianred
get() = HEXAColor(0xCD5C5CFFu) get() = HEXAColor(0xCD5C5CFFu)
/**
* Indigo - A deep blue-violet color (#4B0082).
*/
val HEXAColor.Companion.indigo val HEXAColor.Companion.indigo
get() = HEXAColor(0x4B0082FFu) get() = HEXAColor(0x4B0082FFu)
/**
* Ivory - A very pale cream color (#FFFFF0).
*/
val HEXAColor.Companion.ivory val HEXAColor.Companion.ivory
get() = HEXAColor(0xFFFFF0FFu) get() = HEXAColor(0xFFFFF0FFu)
/**
* Khaki - A light tan color (#F0E68C).
*/
val HEXAColor.Companion.khaki val HEXAColor.Companion.khaki
get() = HEXAColor(0xF0E68CFFu) get() = HEXAColor(0xF0E68CFFu)
/**
* Lavender - A pale purple color (#E6E6FA).
*/
val HEXAColor.Companion.lavender val HEXAColor.Companion.lavender
get() = HEXAColor(0xE6E6FAFFu) get() = HEXAColor(0xE6E6FAFFu)
/**
* Lavender Blush - A very pale pink color (#FFF0F5).
*/
val HEXAColor.Companion.lavenderblush val HEXAColor.Companion.lavenderblush
get() = HEXAColor(0xFFF0F5FFu) get() = HEXAColor(0xFFF0F5FFu)
/**
* Lawn Green - A bright chartreuse green color (#7CFC00).
*/
val HEXAColor.Companion.lawngreen val HEXAColor.Companion.lawngreen
get() = HEXAColor(0x7CFC00FFu) get() = HEXAColor(0x7CFC00FFu)
/**
* Lemon Chiffon - A very pale yellow color (#FFFACD).
*/
val HEXAColor.Companion.lemonchiffon val HEXAColor.Companion.lemonchiffon
get() = HEXAColor(0xFFFACDFFu) get() = HEXAColor(0xFFFACDFFu)
/**
* Light Blue - A light blue color (#ADD8E6).
*/
val HEXAColor.Companion.lightblue val HEXAColor.Companion.lightblue
get() = HEXAColor(0xADD8E6FFu) get() = HEXAColor(0xADD8E6FFu)
/**
* Light Coral - A light coral pink color (#F08080).
*/
val HEXAColor.Companion.lightcoral val HEXAColor.Companion.lightcoral
get() = HEXAColor(0xF08080FFu) get() = HEXAColor(0xF08080FFu)
/**
* Light Cyan - A very pale cyan color (#E0FFFF).
*/
val HEXAColor.Companion.lightcyan val HEXAColor.Companion.lightcyan
get() = HEXAColor(0xE0FFFFFFu) get() = HEXAColor(0xE0FFFFFFu)
/**
* Light Goldenrod Yellow - A pale yellow color (#FAFAD2).
*/
val HEXAColor.Companion.lightgoldenrodyellow val HEXAColor.Companion.lightgoldenrodyellow
get() = HEXAColor(0xFAFAD2FFu) get() = HEXAColor(0xFAFAD2FFu)
/**
* Light Gray - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgray val HEXAColor.Companion.lightgray
get() = HEXAColor(0xD3D3D3FFu) get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Green - A light green color (#90EE90).
*/
val HEXAColor.Companion.lightgreen val HEXAColor.Companion.lightgreen
get() = HEXAColor(0x90EE90FFu) get() = HEXAColor(0x90EE90FFu)
/**
* Light Grey - A light gray color (#D3D3D3).
*/
val HEXAColor.Companion.lightgrey val HEXAColor.Companion.lightgrey
get() = HEXAColor(0xD3D3D3FFu) get() = HEXAColor(0xD3D3D3FFu)
/**
* Light Pink - A light pink color (#FFB6C1).
*/
val HEXAColor.Companion.lightpink val HEXAColor.Companion.lightpink
get() = HEXAColor(0xFFB6C1FFu) get() = HEXAColor(0xFFB6C1FFu)
/**
* Light Salmon - A light salmon color (#FFA07A).
*/
val HEXAColor.Companion.lightsalmon val HEXAColor.Companion.lightsalmon
get() = HEXAColor(0xFFA07AFFu) get() = HEXAColor(0xFFA07AFFu)
/**
* Light Sea Green - A medium sea green color (#20B2AA).
*/
val HEXAColor.Companion.lightseagreen val HEXAColor.Companion.lightseagreen
get() = HEXAColor(0x20B2AAFFu) get() = HEXAColor(0x20B2AAFFu)
/**
* Light Sky Blue - A light sky blue color (#87CEFA).
*/
val HEXAColor.Companion.lightskyblue val HEXAColor.Companion.lightskyblue
get() = HEXAColor(0x87CEFAFFu) get() = HEXAColor(0x87CEFAFFu)
/**
* Light Slate Gray - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategray val HEXAColor.Companion.lightslategray
get() = HEXAColor(0x778899FFu) get() = HEXAColor(0x778899FFu)
/**
* Light Slate Grey - A light slate gray color (#778899).
*/
val HEXAColor.Companion.lightslategrey val HEXAColor.Companion.lightslategrey
get() = HEXAColor(0x778899FFu) get() = HEXAColor(0x778899FFu)
/**
* Light Steel Blue - A light steel blue color (#B0C4DE).
*/
val HEXAColor.Companion.lightsteelblue val HEXAColor.Companion.lightsteelblue
get() = HEXAColor(0xB0C4DEFFu) get() = HEXAColor(0xB0C4DEFFu)
/**
* Light Yellow - A very pale yellow color (#FFFFE0).
*/
val HEXAColor.Companion.lightyellow val HEXAColor.Companion.lightyellow
get() = HEXAColor(0xFFFFE0FFu) get() = HEXAColor(0xFFFFE0FFu)
/**
* Lime - A bright lime green color (#00FF00).
*/
val HEXAColor.Companion.lime val HEXAColor.Companion.lime
get() = HEXAColor(0x00FF00FFu) get() = HEXAColor(0x00FF00FFu)
/**
* Lime Green - A lime green color (#32CD32).
*/
val HEXAColor.Companion.limegreen val HEXAColor.Companion.limegreen
get() = HEXAColor(0x32CD32FFu) get() = HEXAColor(0x32CD32FFu)
/**
* Linen - A pale beige color (#FAF0E6).
*/
val HEXAColor.Companion.linen val HEXAColor.Companion.linen
get() = HEXAColor(0xFAF0E6FFu) get() = HEXAColor(0xFAF0E6FFu)
/**
* Magenta - A bright magenta color (#FF00FF).
*/
val HEXAColor.Companion.magenta val HEXAColor.Companion.magenta
get() = HEXAColor(0xFF00FFFFu) get() = HEXAColor(0xFF00FFFFu)
/**
* Maroon - A dark reddish-brown color (#800000).
*/
val HEXAColor.Companion.maroon val HEXAColor.Companion.maroon
get() = HEXAColor(0x800000FFu) get() = HEXAColor(0x800000FFu)
/**
* Medium Aquamarine - A medium aquamarine color (#66CDAA).
*/
val HEXAColor.Companion.mediumaquamarine val HEXAColor.Companion.mediumaquamarine
get() = HEXAColor(0x66CDAAFFu) get() = HEXAColor(0x66CDAAFFu)
/**
* Medium Blue - A medium blue color (#0000CD).
*/
val HEXAColor.Companion.mediumblue val HEXAColor.Companion.mediumblue
get() = HEXAColor(0x0000CDFFu) get() = HEXAColor(0x0000CDFFu)
/**
* Medium Orchid - A medium orchid purple color (#BA55D3).
*/
val HEXAColor.Companion.mediumorchid val HEXAColor.Companion.mediumorchid
get() = HEXAColor(0xBA55D3FFu) get() = HEXAColor(0xBA55D3FFu)
/**
* Medium Purple - A medium purple color (#9370DB).
*/
val HEXAColor.Companion.mediumpurple val HEXAColor.Companion.mediumpurple
get() = HEXAColor(0x9370DBFFu) get() = HEXAColor(0x9370DBFFu)
/**
* Medium Sea Green - A medium sea green color (#3CB371).
*/
val HEXAColor.Companion.mediumseagreen val HEXAColor.Companion.mediumseagreen
get() = HEXAColor(0x3CB371FFu) get() = HEXAColor(0x3CB371FFu)
/**
* Medium Slate Blue - A medium slate blue color (#7B68EE).
*/
val HEXAColor.Companion.mediumslateblue val HEXAColor.Companion.mediumslateblue
get() = HEXAColor(0x7B68EEFFu) get() = HEXAColor(0x7B68EEFFu)
/**
* Medium Spring Green - A medium spring green color (#00FA9A).
*/
val HEXAColor.Companion.mediumspringgreen val HEXAColor.Companion.mediumspringgreen
get() = HEXAColor(0x00FA9AFFu) get() = HEXAColor(0x00FA9AFFu)
/**
* Medium Turquoise - A medium turquoise color (#48D1CC).
*/
val HEXAColor.Companion.mediumturquoise val HEXAColor.Companion.mediumturquoise
get() = HEXAColor(0x48D1CCFFu) get() = HEXAColor(0x48D1CCFFu)
/**
* Medium Violet Red - A medium violet-red color (#C71585).
*/
val HEXAColor.Companion.mediumvioletred val HEXAColor.Companion.mediumvioletred
get() = HEXAColor(0xC71585FFu) get() = HEXAColor(0xC71585FFu)
/**
* Midnight Blue - A very dark blue color (#191970).
*/
val HEXAColor.Companion.midnightblue val HEXAColor.Companion.midnightblue
get() = HEXAColor(0x191970FFu) get() = HEXAColor(0x191970FFu)
/**
* Mint Cream - A very pale mint color (#F5FFFA).
*/
val HEXAColor.Companion.mintcream val HEXAColor.Companion.mintcream
get() = HEXAColor(0xF5FFFAFFu) get() = HEXAColor(0xF5FFFAFFu)
/**
* Misty Rose - A very pale pink color (#FFE4E1).
*/
val HEXAColor.Companion.mistyrose val HEXAColor.Companion.mistyrose
get() = HEXAColor(0xFFE4E1FFu) get() = HEXAColor(0xFFE4E1FFu)
/**
* Moccasin - A pale peach color (#FFE4B5).
*/
val HEXAColor.Companion.moccasin val HEXAColor.Companion.moccasin
get() = HEXAColor(0xFFE4B5FFu) get() = HEXAColor(0xFFE4B5FFu)
/**
* Navajo White - A pale peach color (#FFDEAD).
*/
val HEXAColor.Companion.navajowhite val HEXAColor.Companion.navajowhite
get() = HEXAColor(0xFFDEADFFu) get() = HEXAColor(0xFFDEADFFu)
/**
* Navy - A very dark blue color (#000080).
*/
val HEXAColor.Companion.navy val HEXAColor.Companion.navy
get() = HEXAColor(0x000080FFu) get() = HEXAColor(0x000080FFu)
/**
* Old Lace - A very pale cream color (#FDF5E6).
*/
val HEXAColor.Companion.oldlace val HEXAColor.Companion.oldlace
get() = HEXAColor(0xFDF5E6FFu) get() = HEXAColor(0xFDF5E6FFu)
/**
* Olive - A dark yellowish-green color (#808000).
*/
val HEXAColor.Companion.olive val HEXAColor.Companion.olive
get() = HEXAColor(0x808000FFu) get() = HEXAColor(0x808000FFu)
/**
* Olive Drab - A dark olive green color (#6B8E23).
*/
val HEXAColor.Companion.olivedrab val HEXAColor.Companion.olivedrab
get() = HEXAColor(0x6B8E23FFu) get() = HEXAColor(0x6B8E23FFu)
/**
* Orange - A bright orange color (#FFA500).
*/
val HEXAColor.Companion.orange val HEXAColor.Companion.orange
get() = HEXAColor(0xFFA500FFu) get() = HEXAColor(0xFFA500FFu)
/**
* Orange Red - A bright red-orange color (#FF4500).
*/
val HEXAColor.Companion.orangered val HEXAColor.Companion.orangered
get() = HEXAColor(0xFF4500FFu) get() = HEXAColor(0xFF4500FFu)
/**
* Orchid - A medium orchid purple color (#DA70D6).
*/
val HEXAColor.Companion.orchid val HEXAColor.Companion.orchid
get() = HEXAColor(0xDA70D6FFu) get() = HEXAColor(0xDA70D6FFu)
/**
* Pale Goldenrod - A pale goldenrod yellow color (#EEE8AA).
*/
val HEXAColor.Companion.palegoldenrod val HEXAColor.Companion.palegoldenrod
get() = HEXAColor(0xEEE8AAFFu) get() = HEXAColor(0xEEE8AAFFu)
/**
* Pale Green - A pale green color (#98FB98).
*/
val HEXAColor.Companion.palegreen val HEXAColor.Companion.palegreen
get() = HEXAColor(0x98FB98FFu) get() = HEXAColor(0x98FB98FFu)
/**
* Pale Turquoise - A pale turquoise color (#AFEEEE).
*/
val HEXAColor.Companion.paleturquoise val HEXAColor.Companion.paleturquoise
get() = HEXAColor(0xAFEEEEFFu) get() = HEXAColor(0xAFEEEEFFu)
/**
* Pale Violet Red - A medium violet-red color (#DB7093).
*/
val HEXAColor.Companion.palevioletred val HEXAColor.Companion.palevioletred
get() = HEXAColor(0xDB7093FFu) get() = HEXAColor(0xDB7093FFu)
/**
* Papaya Whip - A pale peach color (#FFEFD5).
*/
val HEXAColor.Companion.papayawhip val HEXAColor.Companion.papayawhip
get() = HEXAColor(0xFFEFD5FFu) get() = HEXAColor(0xFFEFD5FFu)
/**
* Peach Puff - A light peach color (#FFDAB9).
*/
val HEXAColor.Companion.peachpuff val HEXAColor.Companion.peachpuff
get() = HEXAColor(0xFFDAB9FFu) get() = HEXAColor(0xFFDAB9FFu)
/**
* Peru - A medium brown color (#CD853F).
*/
val HEXAColor.Companion.peru val HEXAColor.Companion.peru
get() = HEXAColor(0xCD853FFFu) get() = HEXAColor(0xCD853FFFu)
/**
* Pink - A light pink color (#FFC0CB).
*/
val HEXAColor.Companion.pink val HEXAColor.Companion.pink
get() = HEXAColor(0xFFC0CBFFu) get() = HEXAColor(0xFFC0CBFFu)
/**
* Plum - A medium purple color (#DDA0DD).
*/
val HEXAColor.Companion.plum val HEXAColor.Companion.plum
get() = HEXAColor(0xDDA0DDFFu) get() = HEXAColor(0xDDA0DDFFu)
/**
* Powder Blue - A light blue color (#B0E0E6).
*/
val HEXAColor.Companion.powderblue val HEXAColor.Companion.powderblue
get() = HEXAColor(0xB0E0E6FFu) get() = HEXAColor(0xB0E0E6FFu)
/**
* Purple - A pure purple color (#800080).
*/
val HEXAColor.Companion.purple val HEXAColor.Companion.purple
get() = HEXAColor(0x800080FFu) get() = HEXAColor(0x800080FFu)
/**
* Red - Pure red color (#FF0000).
*/
val HEXAColor.Companion.red val HEXAColor.Companion.red
get() = HEXAColor(0xFF0000FFu) get() = HEXAColor(0xFF0000FFu)
/**
* Rosy Brown - A rosy brown color (#BC8F8F).
*/
val HEXAColor.Companion.rosybrown val HEXAColor.Companion.rosybrown
get() = HEXAColor(0xBC8F8FFFu) get() = HEXAColor(0xBC8F8FFFu)
/**
* Royal Blue - A vibrant royal blue color (#4169E1).
*/
val HEXAColor.Companion.royalblue val HEXAColor.Companion.royalblue
get() = HEXAColor(0x4169E1FFu) get() = HEXAColor(0x4169E1FFu)
/**
* Saddle Brown - A dark brown color (#8B4513).
*/
val HEXAColor.Companion.saddlebrown val HEXAColor.Companion.saddlebrown
get() = HEXAColor(0x8B4513FFu) get() = HEXAColor(0x8B4513FFu)
/**
* Salmon - A light salmon pink color (#FA8072).
*/
val HEXAColor.Companion.salmon val HEXAColor.Companion.salmon
get() = HEXAColor(0xFA8072FFu) get() = HEXAColor(0xFA8072FFu)
/**
* Sandy Brown - A sandy brown color (#F4A460).
*/
val HEXAColor.Companion.sandybrown val HEXAColor.Companion.sandybrown
get() = HEXAColor(0xF4A460FFu) get() = HEXAColor(0xF4A460FFu)
/**
* Sea Green - A dark sea green color (#2E8B57).
*/
val HEXAColor.Companion.seagreen val HEXAColor.Companion.seagreen
get() = HEXAColor(0x2E8B57FFu) get() = HEXAColor(0x2E8B57FFu)
/**
* Seashell - A very pale pink-orange color (#FFF5EE).
*/
val HEXAColor.Companion.seashell val HEXAColor.Companion.seashell
get() = HEXAColor(0xFFF5EEFFu) get() = HEXAColor(0xFFF5EEFFu)
/**
* Sienna - A reddish-brown color (#A0522D).
*/
val HEXAColor.Companion.sienna val HEXAColor.Companion.sienna
get() = HEXAColor(0xA0522DFFu) get() = HEXAColor(0xA0522DFFu)
/**
* Silver - A light gray-silver color (#C0C0C0).
*/
val HEXAColor.Companion.silver val HEXAColor.Companion.silver
get() = HEXAColor(0xC0C0C0FFu) get() = HEXAColor(0xC0C0C0FFu)
/**
* Sky Blue - A light sky blue color (#87CEEB).
*/
val HEXAColor.Companion.skyblue val HEXAColor.Companion.skyblue
get() = HEXAColor(0x87CEEBFFu) get() = HEXAColor(0x87CEEBFFu)
/**
* Slate Blue - A medium slate blue color (#6A5ACD).
*/
val HEXAColor.Companion.slateblue val HEXAColor.Companion.slateblue
get() = HEXAColor(0x6A5ACDFFu) get() = HEXAColor(0x6A5ACDFFu)
/**
* Slate Gray - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategray val HEXAColor.Companion.slategray
get() = HEXAColor(0x708090FFu) get() = HEXAColor(0x708090FFu)
/**
* Slate Grey - A slate gray color (#708090).
*/
val HEXAColor.Companion.slategrey val HEXAColor.Companion.slategrey
get() = HEXAColor(0x708090FFu) get() = HEXAColor(0x708090FFu)
/**
* Snow - A very pale pinkish-white color (#FFFAFA).
*/
val HEXAColor.Companion.snow val HEXAColor.Companion.snow
get() = HEXAColor(0xFFFAFAFFu) get() = HEXAColor(0xFFFAFAFFu)
/**
* Spring Green - A bright spring green color (#00FF7F).
*/
val HEXAColor.Companion.springgreen val HEXAColor.Companion.springgreen
get() = HEXAColor(0x00FF7FFFu) get() = HEXAColor(0x00FF7FFFu)
/**
* Steel Blue - A medium steel blue color (#4682B4).
*/
val HEXAColor.Companion.steelblue val HEXAColor.Companion.steelblue
get() = HEXAColor(0x4682B4FFu) get() = HEXAColor(0x4682B4FFu)
/**
* Tan - A light brown tan color (#D2B48C).
*/
val HEXAColor.Companion.tan val HEXAColor.Companion.tan
get() = HEXAColor(0xD2B48CFFu) get() = HEXAColor(0xD2B48CFFu)
/**
* Teal - A dark cyan-blue color (#008080).
*/
val HEXAColor.Companion.teal val HEXAColor.Companion.teal
get() = HEXAColor(0x008080FFu) get() = HEXAColor(0x008080FFu)
/**
* Thistle - A light purple-pink color (#D8BFD8).
*/
val HEXAColor.Companion.thistle val HEXAColor.Companion.thistle
get() = HEXAColor(0xD8BFD8FFu) get() = HEXAColor(0xD8BFD8FFu)
/**
* Tomato - A vibrant red-orange color (#FF6347).
*/
val HEXAColor.Companion.tomato val HEXAColor.Companion.tomato
get() = HEXAColor(0xFF6347FFu) get() = HEXAColor(0xFF6347FFu)
/**
* Turquoise - A medium turquoise color (#40E0D0).
*/
val HEXAColor.Companion.turquoise val HEXAColor.Companion.turquoise
get() = HEXAColor(0x40E0D0FFu) get() = HEXAColor(0x40E0D0FFu)
/**
* Violet - A violet color (#EE82EE).
*/
val HEXAColor.Companion.violet val HEXAColor.Companion.violet
get() = HEXAColor(0xEE82EEFFu) get() = HEXAColor(0xEE82EEFFu)
/**
* Wheat - A light tan color (#F5DEB3).
*/
val HEXAColor.Companion.wheat val HEXAColor.Companion.wheat
get() = HEXAColor(0xF5DEB3FFu) get() = HEXAColor(0xF5DEB3FFu)
/**
* White - Pure white color (#FFFFFF).
*/
val HEXAColor.Companion.white val HEXAColor.Companion.white
get() = HEXAColor(0xFFFFFFFFu) get() = HEXAColor(0xFFFFFFFFu)
/**
* White Smoke - A very light gray color (#F5F5F5).
*/
val HEXAColor.Companion.whitesmoke val HEXAColor.Companion.whitesmoke
get() = HEXAColor(0xF5F5F5FFu) get() = HEXAColor(0xF5F5F5FFu)
/**
* Yellow - Pure yellow color (#FFFF00).
*/
val HEXAColor.Companion.yellow val HEXAColor.Companion.yellow
get() = HEXAColor(0xFFFF00FFu) get() = HEXAColor(0xFFFF00FFu)
/**
* Yellow Green - A medium yellow-green color (#9ACD32).
*/
val HEXAColor.Companion.yellowgreen val HEXAColor.Companion.yellowgreen
get() = HEXAColor(0x9ACD32FFu) get() = HEXAColor(0x9ACD32FFu)

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {
@@ -29,5 +29,12 @@ kotlin {
api libs.okio api libs.okio
} }
} }
wasmJsMain {
dependencies {
api libs.kotlinx.browser
api libs.kt.coroutines
}
}
} }
} }

View File

@@ -6,7 +6,7 @@ plugins {
alias(libs.plugins.kt.jb.compose) alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -2,11 +2,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -16,8 +14,8 @@ class LoadableComponentTests {
@Test @Test
@TestOnly @TestOnly
fun testSimpleLoad() = runComposeUiTest { fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0) val loadingFlow = MutableRedeliverStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0) val loadedFlow = MutableRedeliverStateFlow<Int>(0)
setContent { setContent {
LoadableComponent<Int>({ LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first() loadingFlow.filter { it == 1 }.first()

View File

@@ -1,5 +1,15 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Breaks this list into a list of consecutive pairs.
* Each element is paired with the next element in the list.
* For a list of size n, the result will contain n-1 pairs.
*
* Example: `[1, 2, 3, 4].breakAsPairs()` returns `[(1, 2), (2, 3), (3, 4)]`
*
* @param T The type of elements in the list
* @return A list of pairs where each pair consists of consecutive elements
*/
fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> { fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> {
val result = mutableListOf<Pair<T, T>>() val result = mutableListOf<Pair<T, T>>()

View File

@@ -1,5 +1,12 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Executes the given [block] and returns its result if this Boolean is true, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.letIfTrue(block: () -> T): T? { inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
return if (this) { return if (this) {
block() block()
@@ -8,6 +15,13 @@ inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
} }
} }
/**
* Executes the given [block] and returns its result if this Boolean is false, otherwise returns null.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.letIfFalse(block: () -> T): T? { inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
return if (this) { return if (this) {
null null
@@ -16,16 +30,37 @@ inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
} }
} }
/**
* Executes the given [block] if this Boolean is true and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is true.
*
* @param block The function to execute if this Boolean is true
* @return This Boolean value
*/
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean { inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
letIfTrue(block) letIfTrue(block)
return this return this
} }
/**
* Executes the given [block] if this Boolean is false and returns this Boolean.
* Similar to [also], but only executes the block when the Boolean is false.
*
* @param block The function to execute if this Boolean is false
* @return This Boolean value
*/
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean { inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
letIfFalse(block) letIfFalse(block)
return this return this
} }
/**
* Alias for [letIfTrue]. Executes the given [block] and returns its result if this Boolean is true.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is true
* @return The result of [block] if true, null otherwise
*/
inline fun <T> Boolean.ifTrue(block: () -> T): T? { inline fun <T> Boolean.ifTrue(block: () -> T): T? {
return if (this) { return if (this) {
block() block()
@@ -34,6 +69,13 @@ inline fun <T> Boolean.ifTrue(block: () -> T): T? {
} }
} }
/**
* Alias for [letIfFalse]. Executes the given [block] and returns its result if this Boolean is false.
*
* @param T The return type of the block
* @param block The function to execute if this Boolean is false
* @return The result of [block] if false, null otherwise
*/
inline fun <T> Boolean.ifFalse(block: () -> T): T? { inline fun <T> Boolean.ifFalse(block: () -> T): T? {
return if (this) { return if (this) {
null null

View File

@@ -6,19 +6,45 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
/**
* A function type that allocates and returns a [ByteArray].
*/
typealias ByteArrayAllocator = () -> ByteArray typealias ByteArrayAllocator = () -> ByteArray
/**
* A suspending function type that allocates and returns a [ByteArray].
*/
typealias SuspendByteArrayAllocator = suspend () -> ByteArray typealias SuspendByteArrayAllocator = suspend () -> ByteArray
/**
* Converts this [ByteArray] to a [ByteArrayAllocator] that returns this array.
*/
val ByteArray.asAllocator: ByteArrayAllocator val ByteArray.asAllocator: ByteArrayAllocator
get() = { this } get() = { this }
/**
* Converts this [ByteArray] to a [SuspendByteArrayAllocator] that returns this array.
*/
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this } get() = { this }
/**
* Converts this [ByteArrayAllocator] to a [SuspendByteArrayAllocator].
*/
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
get() = { this() } get() = { this() }
/**
* Converts this [SuspendByteArrayAllocator] to a [ByteArrayAllocator] by invoking it and
* wrapping the result in a non-suspending allocator.
*/
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator { suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
return invoke().asAllocator return invoke().asAllocator
} }
/**
* Serializer for [ByteArrayAllocator]. Serializes the result of invoking the allocator.
*/
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
private val realSerializer = ByteArraySerializer() private val realSerializer = ByteArraySerializer()
override val descriptor: SerialDescriptor = realSerializer.descriptor override val descriptor: SerialDescriptor = realSerializer.descriptor

View File

@@ -1,3 +1,10 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Returns the first non-null element in this iterable.
*
* @param T The type of elements in the iterable (nullable)
* @return The first non-null element
* @throws NoSuchElementException if the iterable contains no non-null elements
*/
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!! fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!

View File

@@ -1,5 +1,19 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Joins elements of this iterable into a list with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo( inline fun <I, R> Iterable<I>.joinTo(
separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
@@ -25,6 +39,20 @@ inline fun <I, R> Iterable<I>.joinTo(
return result return result
} }
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input iterable
* @param R The type of elements in the result list
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return A list of transformed elements with separators
*/
inline fun <I, R> Iterable<I>.joinTo( inline fun <I, R> Iterable<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,
@@ -32,18 +60,55 @@ inline fun <I, R> Iterable<I>.joinTo(
transform: (I) -> R? transform: (I) -> R?
): List<R> = joinTo({ separator }, prefix, postfix, transform) ): List<R> = joinTo({ separator }, prefix, postfix, transform)
/**
* Joins elements of this iterable into a list with separators between elements.
* Separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from separator function are skipped.
*
* @param I The type of elements
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo( inline fun <I> Iterable<I>.joinTo(
separatorFun: (I) -> I?, separatorFun: (I) -> I?,
prefix: I? = null, prefix: I? = null,
postfix: I? = null postfix: I? = null
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
/**
* Joins elements of this iterable into a list with a constant separator between elements.
* Optional [prefix] and [postfix] can be added to the result.
* Null separators are skipped.
*
* @param I The type of elements
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @return A list of elements with separators
*/
inline fun <I> Iterable<I>.joinTo( inline fun <I> Iterable<I>.joinTo(
separator: I? = null, separator: I? = null,
prefix: I? = null, prefix: I? = null,
postfix: I? = null postfix: I? = null
): List<I> = joinTo<I>({ separator }, prefix, postfix) ): List<I> = joinTo<I>({ separator }, prefix, postfix)
/**
* Joins elements of this array into an array with separators between elements.
* Each element is transformed using [transform], and separators are generated using [separatorFun].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separator function are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separatorFun A function that generates a separator based on the current element
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo( inline fun <I, reified R> Array<I>.joinTo(
separatorFun: (I) -> R?, separatorFun: (I) -> R?,
prefix: R? = null, prefix: R? = null,
@@ -51,6 +116,20 @@ inline fun <I, reified R> Array<I>.joinTo(
transform: (I) -> R? transform: (I) -> R?
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
/**
* Joins elements of this array into an array with a constant separator between elements.
* Each element is transformed using [transform].
* Optional [prefix] and [postfix] can be added to the result.
* Null values from transformations or separators are skipped.
*
* @param I The type of elements in the input array
* @param R The type of elements in the result array
* @param separator The separator to insert between elements
* @param prefix Optional prefix to add at the beginning of the result
* @param postfix Optional postfix to add at the end of the result
* @param transform A function to transform each element
* @return An array of transformed elements with separators
*/
inline fun <I, reified R> Array<I>.joinTo( inline fun <I, reified R> Array<I>.joinTo(
separator: R? = null, separator: R? = null,
prefix: R? = null, prefix: R? = null,

View File

@@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline
@JvmInline @JvmInline
value class FileName(val string: String) { value class FileName(val string: String) {
val name: String val name: String
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } get() = withoutSlashAtTheEnd.takeLastWhile { it != MPPFilePathSeparator }
val extension: String val extension: String
get() = name.takeLastWhile { it != '.' } get() = name.takeLastWhile { it != '.' }
val nameWithoutExtension: String val nameWithoutExtension: String
@@ -18,7 +18,7 @@ value class FileName(val string: String) {
} ?: filename } ?: filename
} }
val withoutSlashAtTheEnd: String val withoutSlashAtTheEnd: String
get() = string.dropLastWhile { it == '/' } get() = string.dropLastWhile { it == MPPFilePathSeparator }
override fun toString(): String = string override fun toString(): String = string
} }
@@ -26,6 +26,7 @@ value class FileName(val string: String) {
expect class MPPFile expect class MPPFile
expect val MPPFile.filename: FileName expect val MPPFile.filename: FileName
expect val MPPFilePathSeparator: Char
expect val MPPFile.filesize: Long expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator

View File

@@ -82,13 +82,11 @@ fun <K, V> MutableMap<K, V>.applyDiff(
mapDiff: MapDiff<K, V> mapDiff: MapDiff<K, V>
) { ) {
mapDiff.apply { mapDiff.apply {
removed.keys.forEach { remove(it) } keys.removeAll(removed.keys)
changed.forEach { (k, oldNew) -> changed.forEach { (k, oldNew) ->
put(k, oldNew.second) put(k, oldNew.second)
} }
added.forEach { (k, new) -> putAll(added)
put(k, new)
}
} }
} }

View File

@@ -2,16 +2,43 @@ package dev.inmo.micro_utils.common
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
/**
* A bidirectional mapper that can convert between two types [T1] and [T2].
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleMapper<T1, T2> { interface SimpleMapper<T1, T2> {
fun convertToT1(from: T2): T1 fun convertToT1(from: T2): T1
fun convertToT2(from: T1): T2 fun convertToT2(from: T1): T2
} }
/**
* Converts [from] of type [T2] to type [T1] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2") @JvmName("convertFromT2")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1") @JvmName("convertFromT1")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleMapper] that uses lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
*/
class SimpleMapperImpl<T1, T2>( class SimpleMapperImpl<T1, T2>(
private val t1: (T2) -> T1, private val t1: (T2) -> T1,
private val t2: (T1) -> T2, private val t2: (T1) -> T2,
@@ -21,22 +48,58 @@ class SimpleMapperImpl<T1, T2>(
override fun convertToT2(from: T1): T2 = t2.invoke(from) override fun convertToT2(from: T1): T2 = t2.invoke(from)
} }
/**
* Creates a [SimpleMapper] using the provided conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Function to convert from [T2] to [T1]
* @param t2 Function to convert from [T1] to [T2]
* @return A new [SimpleMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleMapper( inline fun <T1, T2> simpleMapper(
noinline t1: (T2) -> T1, noinline t1: (T2) -> T1,
noinline t2: (T1) -> T2, noinline t2: (T1) -> T2,
) = SimpleMapperImpl(t1, t2) ) = SimpleMapperImpl(t1, t2)
/**
* A bidirectional mapper that can convert between two types [T1] and [T2] using suspending functions.
*
* @param T1 The first type
* @param T2 The second type
*/
interface SimpleSuspendableMapper<T1, T2> { interface SimpleSuspendableMapper<T1, T2> {
suspend fun convertToT1(from: T2): T1 suspend fun convertToT1(from: T2): T1
suspend fun convertToT2(from: T1): T2 suspend fun convertToT2(from: T1): T2
} }
/**
* Converts [from] of type [T2] to type [T1] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T1]
*/
@JvmName("convertFromT2") @JvmName("convertFromT2")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
/**
* Converts [from] of type [T1] to type [T2] using this suspending mapper.
*
* @param from The value to convert
* @return The converted value of type [T2]
*/
@JvmName("convertFromT1") @JvmName("convertFromT1")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
/**
* Implementation of [SimpleSuspendableMapper] that uses suspending lambda functions for conversion.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
*/
class SimpleSuspendableMapperImpl<T1, T2>( class SimpleSuspendableMapperImpl<T1, T2>(
private val t1: suspend (T2) -> T1, private val t1: suspend (T2) -> T1,
private val t2: suspend (T1) -> T2, private val t2: suspend (T1) -> T2,
@@ -46,6 +109,15 @@ class SimpleSuspendableMapperImpl<T1, T2>(
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
} }
/**
* Creates a [SimpleSuspendableMapper] using the provided suspending conversion functions.
*
* @param T1 The first type
* @param T2 The second type
* @param t1 Suspending function to convert from [T2] to [T1]
* @param t2 Suspending function to convert from [T1] to [T2]
* @return A new [SimpleSuspendableMapperImpl] instance
*/
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleSuspendableMapper( inline fun <T1, T2> simpleSuspendableMapper(
noinline t1: suspend (T2) -> T1, noinline t1: suspend (T2) -> T1,

View File

@@ -1,5 +1,14 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Pads this sequence to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the sequence reaches the desired size.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param inserter A function that takes the current sequence and returns a new padded sequence
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> { inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> {
var result = this var result = this
while (result.count() < size) { while (result.count() < size) {
@@ -8,10 +17,36 @@ inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequenc
return result return result
} }
/**
* Pads this sequence at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) } inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) }
/**
* Pads this sequence at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o } inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o }
/**
* Pads this list to the specified [size] using a custom [inserter] function.
* The [inserter] is repeatedly called until the list reaches the desired size.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param inserter A function that takes the current list and returns a new padded list
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> { inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> {
var result = this var result = this
while (result.size < size) { while (result.size < size) {
@@ -19,14 +54,66 @@ inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<
} }
return result return result
} }
/**
* Pads this list at the end to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList() inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList()
/**
* Pads this list at the end to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList() inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList()
/**
* Pads this sequence at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param padBlock A function that generates padding elements based on the current sequence size
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it } inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it }
/**
* Pads this sequence at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the sequence
* @param size The target size of the padded sequence
* @param o The element to use for padding
* @return A sequence padded to at least the specified size
*/
inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o } inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o }
/**
* Pads this list at the start to the specified [size].
* New elements are generated using [padBlock], which receives the current size as a parameter.
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param padBlock A function that generates padding elements based on the current list size
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList() inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList()
/**
* Pads this list at the start to the specified [size] using the given element [o].
*
* @param T The type of elements in the list
* @param size The target size of the padded list
* @param o The element to use for padding
* @return A list padded to at least the specified size
*/
inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList() inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList()

View File

@@ -1,17 +1,39 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Computes the intersection of this range with [other]. Returns a pair representing
* the intersecting range, or null if the ranges don't overlap.
*
* @param T The type of comparable values in the range
* @param other The other range to intersect with
* @return A pair (start, end) representing the intersection, or null if no intersection exists
*/
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
start == other.start && endInclusive == other.endInclusive -> start to endInclusive start == other.start && endInclusive == other.endInclusive -> start to endInclusive
start > other.endInclusive || other.start > endInclusive -> null start > other.endInclusive || other.start > endInclusive -> null
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
} }
/**
* Computes the intersection of this [IntRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return An [IntRange] representing the intersection, or null if no intersection exists
*/
fun IntRange.intersect( fun IntRange.intersect(
other: IntRange other: IntRange
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
it.first .. it.second it.first .. it.second
} }
/**
* Computes the intersection of this [LongRange] with [other].
* Returns the intersecting range, or null if the ranges don't overlap.
*
* @param other The other range to intersect with
* @return A [LongRange] representing the intersection, or null if no intersection exists
*/
fun LongRange.intersect( fun LongRange.intersect(
other: LongRange other: LongRange
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {

View File

@@ -1,5 +1,24 @@
package dev.inmo.micro_utils.common package dev.inmo.micro_utils.common
/**
* Returns a new list with the element at index [i] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param i The index of the element to replace
* @param block A function that transforms the element at the given index
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1) fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1)
/**
* Returns a new list with the first occurrence of element [t] replaced by applying [block] to it.
* All other elements remain unchanged.
*
* @param T The type of elements in the iterable
* @param t The element to replace
* @param block A function that transforms the found element
* @return A new list with the replaced element
*/
fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block) fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block)

View File

@@ -35,6 +35,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/** /**
* @suppress * @suppress
*/ */

View File

@@ -14,6 +14,10 @@ actual typealias MPPFile = File
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = File.separatorChar
/** /**
* @suppress * @suppress
*/ */

View File

@@ -11,6 +11,10 @@ actual typealias MPPFile = Path
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(toString()) get() = FileName(toString())
actual val MPPFilePathSeparator: Char = Path.DIRECTORY_SEPARATOR.first()
/** /**
* @suppress * @suppress
*/ */

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.common
import org.khronos.webgl.*
fun DataView.toByteArray() = ByteArray(this.byteLength) {
getInt8(it)
}
fun ArrayBuffer.toByteArray() = Int8Array(this).toByteArray()
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
forEachIndexed { i, byte -> it.setInt8(i, byte) }
}
fun ByteArray.toArrayBuffer() = toDataView().buffer

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun copyToClipboard(text: String): Boolean {
return runCatching {
window.navigator.clipboard.writeText(
text
)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,31 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import kotlinx.coroutines.await
import org.w3c.fetch.Response
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
external class ClipboardItem(data: JsAny?) : JsAny
fun createBlobData(blob: Blob): JsAny = js("""({[blob.type]: blob})""")
inline fun Blob.convertToClipboardItem(): ClipboardItem {
return ClipboardItem(createBlobData(this))
}
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
return runCatching {
val response = window.fetch(imageUrl).await<Response>()
val blob = response.blob().await<Blob>()
val data = arrayOf(
Blob(
arrayOf(blob).toJsArray().unsafeCast(),
BlobPropertyBag("image/png")
).convertToClipboardItem()
).toJsArray()
window.navigator.clipboard.write(data.unsafeCast())
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,63 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
private fun createMutationObserverInit(childList: Boolean, subtree: Boolean): JsAny = js("({childList, subtree})")
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, createMutationObserverInit(childList = true, subtree = true).unsafeCast())
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.toArray().forEach {
if (previousIntersectionRatio.toDouble() != it.intersectionRatio.toDouble()) {
previousIntersectionRatio = it.intersectionRatio.toDouble().toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.DOMRect
import org.w3c.dom.Element
val DOMRect.isOnScreenByLeftEdge: Boolean
get() = left >= 0 && left <= window.innerWidth
inline val Element.isOnScreenByLeftEdge
get() = getBoundingClientRect().isOnScreenByLeftEdge
val DOMRect.isOnScreenByRightEdge: Boolean
get() = right >= 0 && right <= window.innerWidth
inline val Element.isOnScreenByRightEdge
get() = getBoundingClientRect().isOnScreenByRightEdge
internal val DOMRect.isOnScreenHorizontally: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenByTopEdge: Boolean
get() = top >= 0 && top <= window.innerHeight
inline val Element.isOnScreenByTopEdge
get() = getBoundingClientRect().isOnScreenByTopEdge
val DOMRect.isOnScreenByBottomEdge: Boolean
get() = bottom >= 0 && bottom <= window.innerHeight
inline val Element.isOnScreenByBottomEdge
get() = getBoundingClientRect().isOnScreenByBottomEdge
internal val DOMRect.isOnScreenVertically: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenFully: Boolean
get() = isOnScreenByLeftEdge && isOnScreenByTopEdge && isOnScreenByRightEdge && isOnScreenByBottomEdge
val Element.isOnScreenFully: Boolean
get() = getBoundingClientRect().isOnScreenFully
val DOMRect.isOnScreen: Boolean
get() = isOnScreenFully || (isOnScreenHorizontally && isOnScreenVertically)
inline val Element.isOnScreen: Boolean
get() = getBoundingClientRect().isOnScreen

View File

@@ -0,0 +1,127 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.DOMRectReadOnly
import org.w3c.dom.Element
external interface IntersectionObserverOptions: JsAny {
/**
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible.
*/
var root: Element?
/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
*/
var rootMargin: String?
/**
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
*/
var threshold: JsArray<JsNumber>?
}
private fun createEmptyJsObject(): JsAny = js("{}")
fun IntersectionObserverOptions(
block: IntersectionObserverOptions.() -> Unit = {}
): IntersectionObserverOptions = createEmptyJsObject().unsafeCast<IntersectionObserverOptions>().apply(block)
external interface IntersectionObserverEntry: JsAny {
/**
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
* the documentation for Element.getBoundingClientRect().
*/
val boundingClientRect: DOMRectReadOnly
/**
* Returns the ratio of the intersectionRect to the boundingClientRect.
*/
val intersectionRatio: JsNumber
/**
* Returns a DOMRectReadOnly representing the target's visible area.
*/
val intersectionRect: DOMRectReadOnly
/**
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
* then you know the transition is from intersecting to not-intersecting.
*/
val isIntersecting: Boolean
/**
* Returns a DOMRectReadOnly for the intersection observer's root.
*/
val rootBounds: DOMRectReadOnly
/**
* The Element whose intersection with the root changed.
*/
val target: Element
/**
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
* IntersectionObserver's time origin.
*/
val time: Double
}
typealias IntersectionObserverCallback = (entries: JsArray<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
/**
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
*/
external class IntersectionObserver(callback: IntersectionObserverCallback): JsAny {
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
* was passed to the constructor or its value is null, the top-level document's viewport is used.
*/
val root: Element
/**
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
*/
val rootMargin: String
/**
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
* crossed for that target. If no value was passed to the constructor, 0 is used.
*/
val thresholds: JsArray<JsNumber>
/**
* Stops the IntersectionObserver object from observing any target.
*/
fun disconnect()
/**
* Tells the IntersectionObserver a target element to observe.
*/
fun observe(targetElement: Element)
/**
* Returns an array of IntersectionObserverEntry objects for all observed targets.
*/
fun takeRecords(): JsArray<IntersectionObserverEntry>
/**
* Tells the IntersectionObserver to stop observing a particular target element.
*/
fun unobserve(targetElement: Element)
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.Element
inline val Element.isOverflowWidth
get() = scrollWidth > clientWidth
inline val Element.isOverflowHeight
get() = scrollHeight > clientHeight
inline val Element.isOverflow
get() = isOverflowHeight || isOverflowWidth

View File

@@ -0,0 +1,62 @@
package dev.inmo.micro_utils.common
import kotlinx.coroutines.await
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.*
import kotlin.js.Promise
/**
* @suppress
*/
actual typealias MPPFile = File
private fun createJsError(message: String): JsAny = js("Error(message)")
fun MPPFile.readBytesPromise() = Promise { success, failure ->
val reader = FileReader()
reader.onload = {
success((reader.result as ArrayBuffer))
}
reader.onerror = {
val message = (it as ErrorEvent).message
failure(createJsError(message))
}
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await<ArrayBuffer>().toByteArray()
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = jsNumberToBigInt(size).toLong()
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes
private fun jsNumberToBigInt(number: JsNumber): JsBigInt = js("BigInt(number)")

View File

@@ -0,0 +1,41 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
private fun createEventListener(listener: (Event) -> Unit): JsAny = js("({handleEvent: listener})")
fun Element.onActionOutside(type: String, options: AddEventListenerOptions? = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = createEventListener { it: Event ->
val elementsToCheck = mutableListOf(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}.unsafeCast<EventListener>()
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: AddEventListenerOptions? = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun openLink(link: String, target: String = "_blank", features: String = "") {
window.open(link, target, features) ?.focus()
}

View File

@@ -0,0 +1,56 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.*
external class ResizeObserver(
callback: (JsArray<ResizeObserverEntry>, ResizeObserver) -> Unit
): JsAny {
fun observe(target: Element, options: JsAny = definedExternally)
fun unobserve(target: Element)
fun disconnect()
}
private fun createObserveOptions(jsBox: JsString?): JsAny = js("({box: jsBox})")
external interface ResizeObserverSize: JsAny {
val blockSize: Float
val inlineSize: Float
}
external interface ResizeObserverEntry: JsAny {
val borderBoxSize: JsArray<ResizeObserverSize>
val contentBoxSize: JsArray<ResizeObserverSize>
val devicePixelContentBoxSize: JsArray<ResizeObserverSize>
val contentRect: DOMRectReadOnly
val target: Element
}
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
target,
createObserveOptions(options.box?.name?.toJsString())
)
class ResizeObserverObserveOptions(
val box: Box? = null
) {
sealed interface Box {
val name: String
object Content : Box {
override val name: String
get() = "content-box"
}
object Border : Box {
override val name: String
get() = "border-box"
}
object DevicePixelContent : Box {
override val name: String
get() = "device-pixel-content-box"
}
}
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import kotlinx.dom.createElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.files.get
fun selectFile(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {},
onFile: (MPPFile) -> Unit
) {
(document.createElement("input") {
(this as HTMLInputElement).apply {
type = "file"
onchange = {
runCatching {
files ?.get(0) ?: error("File must not be null")
}.onSuccess {
onFile(it)
}.onFailure {
onFailure(it)
}
}
inputSetup(this)
}
} as HTMLElement).click()
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
fun triggerDownloadFile(filename: String, fileLink: String) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
hiddenElement.href = fileLink
hiddenElement.target = "_blank"
hiddenElement.download = filename
hiddenElement.click()
}

View File

@@ -0,0 +1,48 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.Element
import org.w3c.dom.css.CSSStyleDeclaration
sealed class Visibility
data object Visible : Visibility()
data object Invisible : Visibility()
data object Gone : Visibility()
var CSSStyleDeclaration.visibilityState: Visibility
get() = when {
display == "none" -> Gone
visibility == "hidden" -> Invisible
else -> Visible
}
set(value) {
when (value) {
Visible -> {
if (display == "none") {
display = "initial"
}
visibility = "visible"
}
Invisible -> {
if (display == "none") {
display = "initial"
}
visibility = "hidden"
}
Gone -> {
display = "none"
}
}
}
inline var Element.visibilityState: Visibility
get() = window.getComputedStyle(this).visibilityState
set(value) {
window.getComputedStyle(this).visibilityState = value
}
inline val Element.isVisible: Boolean
get() = visibilityState == Visible
inline val Element.isInvisible: Boolean
get() = visibilityState == Invisible
inline val Element.isGone: Boolean
get() = visibilityState == Gone

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.common
actual fun Float.fixed(signs: Int): Float {
return jsToFixed(toDouble().toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toFloat()
}
actual fun Double.fixed(signs: Int): Double {
return jsToFixed(toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toDouble()
}
private fun jsToFixed(number: JsNumber, signs: Int): JsString = js("number.toFixed(signs)")

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -6,7 +6,7 @@ plugins {
alias(libs.plugins.kt.jb.compose) alias(libs.plugins.kt.jb.compose)
} }
apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -16,7 +16,7 @@ import org.jetbrains.compose.web.css.StyleSheet
* to add `Style(stylesheet)` on every compose function call * to add `Style(stylesheet)` on every compose function call
*/ */
object StyleSheetsAggregator { object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet()) private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow() val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable @Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.* import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test @Test
@TestOnly @TestOnly
fun simpleTest() = runComposeUiTest { fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0) val flowState = MutableRedeliverStateFlow(0)
setContent { setContent {
Button({ flowState.value++ }) { Text("Click") } Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString()) Text(flowState.collectAsState().value.toString())

View File

@@ -4,6 +4,15 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
/**
* Creates an actor using coroutines that processes incoming messages of type [T].
* An actor is a computational entity that processes messages sequentially in response to messages it receives.
*
* @param T The type of messages this actor will process
* @param channelCapacity The capacity of the [Channel] used for message delibery to the actor. Defaults to [Channel.UNLIMITED]
* @param block The processing logic to be applied to each received message
* @return A [Channel] that can be used to send messages to this actor or cancel it
*/
fun <T> CoroutineScope.actor( fun <T> CoroutineScope.actor(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
block: suspend (T) -> Unit block: suspend (T) -> Unit
@@ -13,6 +22,16 @@ fun <T> CoroutineScope.actor(
return channel return channel
} }
/**
* Creates a safe actor that catches and handles exceptions during message processing.
* This variant wraps the processing logic in a safety mechanism to prevent actor failure due to exceptions.
*
* @param T The type of messages this actor will process
* @param channelCapacity The capacity of the [Channel] used for message processing. Defaults to [Channel.UNLIMITED]
* @param onException Handler for exceptions that occur during message processing. Defaults to [defaultSafelyExceptionHandler]
* @param block The processing logic to be applied to each received message
* @return A [Channel] that can be used to send messages to this actor
*/
inline fun <T> CoroutineScope.safeActor( inline fun <T> CoroutineScope.safeActor(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -1,19 +1,34 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
/**
* Creates an actor-style channel that processes messages asynchronously based on markers.
* Messages with the same marker will be processed sequentially, while messages with different markers can be processed concurrently.
*
* @param T The type of messages to process
* @param channelCapacity The capacity of the underlying channel. Defaults to [Channel.UNLIMITED]
* @param markerFactory A factory function that produces a marker for each message. Messages with the same marker
* will be processed sequentially. Defaults to returning null, meaning all messages will be processed sequentially
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each message
* @return A [Channel] that accepts messages to be processed
*/
fun <T> CoroutineScope.actorAsync( fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null }, markerFactory: suspend (T) -> Any? = { null },
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Channel<T> { ): Channel<T> {
val channel = Channel<T>(channelCapacity) val channel = Channel<T>(channelCapacity)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block) channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
return channel return channel
} }
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
inline fun <T> CoroutineScope.safeActorAsync( inline fun <T> CoroutineScope.safeActorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -3,5 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
/**
* Wraps this value in a completed [Deferred]. The resulting [Deferred] is immediately completed with this value.
* Useful for converting synchronous values to [Deferred] in contexts that expect deferred values.
*
* @param T The type of the value
* @return A [Deferred] that is already completed with this value
*/
val <T> T.asDeferred: Deferred<T> val <T> T.asDeferred: Deferred<T>
get() = CompletableDeferred(this) get() = CompletableDeferred(this)

View File

@@ -3,20 +3,53 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/**
* Convenience property to access [Dispatchers.Main] for UI operations.
*/
inline val UI inline val UI
get() = Dispatchers.Main get() = Dispatchers.Main
/**
* Convenience property to access [Dispatchers.Default] for CPU-intensive operations.
*/
inline val Default inline val Default
get() = Dispatchers.Default get() = Dispatchers.Default
/**
* Executes the given [block] in the specified coroutine [context] and returns its result.
* This is a convenience wrapper around [withContext].
*
* @param T The return type of the block
* @param context The [CoroutineContext] in which to execute the block
* @param block The suspending function to execute
* @return The result of executing the block
*/
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext( suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
context, context,
block block
) )
/**
* Executes the given [block] on the UI/Main dispatcher and returns its result.
* This is a convenience function for executing UI operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the UI thread
* @return The result of executing the block
*/
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn( suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
UI, UI,
block block
) )
/**
* Executes the given [block] on the Default dispatcher and returns its result.
* This is a convenience function for executing CPU-intensive operations.
*
* @param T The return type of the block
* @param block The suspending function to execute on the Default dispatcher
* @return The result of executing the block
*/
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn( suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
Default, Default,
block block

View File

@@ -2,6 +2,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
/**
* Represents a deferred action that combines a [Deferred] value with a callback to be executed on that value.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param deferred The deferred value to await
* @param callback The suspending function to apply to the deferred value
*/
class DeferredAction<T, O>( class DeferredAction<T, O>(
val deferred: Deferred<T>, val deferred: Deferred<T>,
val callback: suspend (T) -> O val callback: suspend (T) -> O
@@ -9,6 +17,13 @@ class DeferredAction<T, O>(
suspend operator fun invoke() = callback(deferred.await()) suspend operator fun invoke() = callback(deferred.await())
} }
/**
* A builder for creating multiple deferred computations that can be executed, with only the first completing
* one being used. This is useful for race conditions where you want the result of whichever computation finishes first.
*
* @param T The type of values produced by the deferred computations
* @param scope The [CoroutineScope] in which to create the deferred computations
*/
class DoWithFirstBuilder<T>( class DoWithFirstBuilder<T>(
private val scope: CoroutineScope private val scope: CoroutineScope
) { ) {
@@ -22,8 +37,25 @@ class DoWithFirstBuilder<T>(
fun build() = deferreds.toList() fun build() = deferreds.toList()
} }
/**
* Creates a [DeferredAction] from this [Deferred] and a [callback] function.
*
* @param T The type of the deferred value
* @param O The type of the result after applying the callback
* @param callback The suspending function to apply to the deferred value
* @return A [DeferredAction] combining the deferred and callback
*/
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback) fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
/**
* Invokes the first [DeferredAction] whose deferred value completes, executing its callback and returning the result.
* Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf( suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
@@ -33,18 +65,50 @@ suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
} }
} }
/**
* Invokes the first [DeferredAction] from the given [variants] whose deferred value completes,
* executing its callback and returning the result. Other deferred actions are cancelled if [cancelOnResult] is true.
*
* @param O The type of the result after applying callbacks
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred actions to race
* @param cancelOnResult If true, cancels all other deferred actions after the first completes. Defaults to true
* @return The result of invoking the first completed deferred action
*/
suspend fun <O> invokeFirstOf( suspend fun <O> invokeFirstOf(
scope: CoroutineScope, scope: CoroutineScope,
vararg variants: DeferredAction<*, O>, vararg variants: DeferredAction<*, O>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
): O = variants.toList().invokeFirstOf(scope, cancelOnResult) ): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
/**
* Awaits the first [Deferred] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst( suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
callback: suspend (T) -> O callback: suspend (T) -> O
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult) ): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and invokes [callback] on the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> CoroutineScope.invokeOnFirstOf( suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit, block: DoWithFirstBuilder<T>.() -> Unit,
@@ -54,6 +118,18 @@ suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
cancelOnResult cancelOnResult
).let { callback(it) } ).let { callback(it) }
/**
* Awaits the first [Deferred] from the given [variants] to complete and invokes the [callback] on its value.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param O The type of the result after applying the callback
* @param scope The [CoroutineScope] in which to await the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @param callback The suspending function to apply to the first completed value
* @return The result of applying the callback to the first completed value
*/
suspend fun <T, O> invokeOnFirst( suspend fun <T, O> invokeOnFirst(
scope: CoroutineScope, scope: CoroutineScope,
vararg variants: Deferred<T>, vararg variants: Deferred<T>,
@@ -61,11 +137,29 @@ suspend fun <T, O> invokeOnFirst(
callback: suspend (T) -> O callback: suspend (T) -> O
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback) ): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf( suspend fun <T> CoroutineScope.firstOf(
variants: Iterable<Deferred<T>>, variants: Iterable<Deferred<T>>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
) = variants.invokeOnFirst(this, cancelOnResult) { it } ) = variants.invokeOnFirst(this, cancelOnResult) { it }
/**
* Builds multiple deferred computations using [DoWithFirstBuilder] and returns the value of the first one to complete.
* Other deferred computations are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param cancelOnResult If true, cancels all other computations after the first completes. Defaults to true
* @param block Builder DSL to define the deferred computations
* @return The value of the first completed computation
*/
suspend fun <T> CoroutineScope.firstOf( suspend fun <T> CoroutineScope.firstOf(
cancelOnResult: Boolean = true, cancelOnResult: Boolean = true,
block: DoWithFirstBuilder<T>.() -> Unit block: DoWithFirstBuilder<T>.() -> Unit
@@ -74,11 +168,29 @@ suspend fun <T> CoroutineScope.firstOf(
cancelOnResult cancelOnResult
) )
/**
* Returns the value of the first [Deferred] from the given [variants] to complete.
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param variants The deferred values to race
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> CoroutineScope.firstOf( suspend fun <T> CoroutineScope.firstOf(
vararg variants: Deferred<T>, vararg variants: Deferred<T>,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true
) = firstOf(variants.toList(), cancelOnResult) ) = firstOf(variants.toList(), cancelOnResult)
/**
* Returns the value of the first [Deferred] from this list to complete, using the given [scope].
* Other deferred values are cancelled if [cancelOnResult] is true.
*
* @param T The type of the deferred values
* @param scope The [CoroutineScope] in which to await the deferred values
* @param cancelOnResult If true, cancels all other deferred values after the first completes. Defaults to true
* @return The value of the first completed deferred
*/
suspend fun <T> List<Deferred<T>>.first( suspend fun <T> List<Deferred<T>>.first(
scope: CoroutineScope, scope: CoroutineScope,
cancelOnResult: Boolean = true cancelOnResult: Boolean = true

View File

@@ -2,4 +2,11 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
/**
* Operator function that allows a [FlowCollector] to be invoked like a function to emit a value.
* This is a convenient syntax sugar for [FlowCollector.emit].
*
* @param T The type of values the collector can emit
* @param value The value to emit
*/
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value) suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)

View File

@@ -14,6 +14,15 @@ private value class DebouncedByData<T>(
val millisToData: Pair<Long, T> val millisToData: Pair<Long, T>
) )
/**
* Debounces a [Flow] with per-marker timeout control. Values with the same marker will be debounced independently.
* For each marker, only the last value within the timeout period will be emitted.
*
* @param T The type of values emitted by the flow
* @param timeout A function that determines the debounce timeout in milliseconds for each value
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow { fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): Flow<T> = channelFlow {
val jobs = mutableMapOf<Any?, Job>() val jobs = mutableMapOf<Any?, Job>()
val mutex = Mutex() val mutex = Mutex()
@@ -36,5 +45,24 @@ fun <T> Flow<T>.debouncedBy(timeout: (T) -> Long, markerFactory: (T) -> Any?): F
} }
} }
/**
* Debounces a [Flow] with a fixed timeout in milliseconds and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout in milliseconds
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory) fun <T> Flow<T>.debouncedBy(timeout: Long, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout }, markerFactory)
/**
* Debounces a [Flow] with a fixed timeout as [Duration] and per-marker control.
* Values with the same marker will be debounced independently.
*
* @param T The type of values emitted by the flow
* @param timeout The debounce timeout as a [Duration]
* @param markerFactory A function that produces a marker for each value. Values with the same marker are debounced together
* @return A [Flow] that emits debounced values
*/
fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory) fun <T> Flow<T>.debouncedBy(timeout: Duration, markerFactory: (T) -> Any?): Flow<T> = debouncedBy({ timeout.inWholeMilliseconds }, markerFactory)

View File

@@ -3,4 +3,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
/**
* Returns the first non-null element emitted by this [Flow].
* Suspends until a non-null element is found.
*
* @param T The type of elements in the flow
* @return The first non-null element
* @throws NoSuchElementException if the flow completes without emitting a non-null element
*/
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!! suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!

View File

@@ -4,6 +4,14 @@ import kotlinx.coroutines.flow.*
import kotlin.js.JsName import kotlin.js.JsName
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
/**
* Transforms each inner [Flow] element using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMap( inline fun <T, R> Flow<Flow<T>>.flatMap(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flow { ) = flow {
@@ -14,6 +22,14 @@ inline fun <T, R> Flow<Flow<T>>.flatMap(
} }
} }
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function and flattens the result into a single [Flow].
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of mapped and flattened elements
*/
@JsName("flatMapIterable") @JsName("flatMapIterable")
@JvmName("flatMapIterable") @JvmName("flatMapIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMap( inline fun <T, R> Flow<Iterable<T>>.flatMap(
@@ -22,18 +38,48 @@ inline fun <T, R> Flow<Iterable<T>>.flatMap(
it.asFlow() it.asFlow()
}.flatMap(mapper) }.flatMap(mapper)
/**
* Transforms each inner [Flow] element using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the inner flows
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull( inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull() ) = flatMap(mapper).takeNotNull()
/**
* Transforms each element from inner [Iterable]s using the given [mapper] function, flattens the result,
* and filters out null values.
*
* @param T The type of elements in the iterables
* @param R The type of elements after applying the mapper
* @param mapper A suspending function to transform each element
* @return A [Flow] of non-null mapped and flattened elements
*/
@JsName("flatMapNotNullIterable") @JsName("flatMapNotNullIterable")
@JvmName("flatMapNotNullIterable") @JvmName("flatMapNotNullIterable")
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull( inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
crossinline mapper: suspend (T) -> R crossinline mapper: suspend (T) -> R
) = flatMap(mapper).takeNotNull() ) = flatMap(mapper).takeNotNull()
/**
* Flattens a [Flow] of [Flow]s into a single [Flow] by collecting all inner flows sequentially.
*
* @param T The type of elements in the inner flows
* @return A [Flow] containing all elements from all inner flows
*/
fun <T> Flow<Flow<T>>.flatten() = flatMap { it } fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
/**
* Flattens a [Flow] of [Iterable]s into a single [Flow] by emitting all elements from each iterable.
*
* @param T The type of elements in the iterables
* @return A [Flow] containing all elements from all iterables
*/
@JsName("flattenIterable") @JsName("flattenIterable")
@JvmName("flattenIterable") @JvmName("flattenIterable")
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it } fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }

View File

@@ -2,5 +2,18 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
/**
* Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.takeNotNull() = mapNotNull { it } fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
/**
* Alias for [takeNotNull]. Filters out null values from this [Flow], returning only non-null elements.
*
* @param T The type of elements in the flow (nullable)
* @return A [Flow] containing only non-null elements
*/
fun <T> Flow<T>.filterNotNull() = takeNotNull() fun <T> Flow<T>.filterNotNull() = takeNotNull()

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -8,6 +9,7 @@ import kotlinx.coroutines.sync.withLock
private class SubscribeAsyncReceiver<T>( private class SubscribeAsyncReceiver<T>(
val scope: CoroutineScope, val scope: CoroutineScope,
val logger: KSLog,
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
) { ) {
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED) private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
@@ -15,7 +17,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel get() = dataChannel
init { init {
scope.launchLoggingDropExceptions { scope.launchLoggingDropExceptions(logger = logger) {
for (data in dataChannel) { for (data in dataChannel) {
output(data) output(data)
} }
@@ -33,13 +35,16 @@ private data class AsyncSubscriptionCommandData<T, M>(
val scope: CoroutineScope, val scope: CoroutineScope,
val markerFactory: suspend (T) -> M, val markerFactory: suspend (T) -> M,
val block: suspend (T) -> Unit, val block: suspend (T) -> Unit,
val logger: KSLog,
val onEmpty: suspend (M) -> Unit val onEmpty: suspend (M) -> Unit
) : AsyncSubscriptionCommand<T, M> { ) : AsyncSubscriptionCommand<T, M> {
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
val marker = markerFactory(data) val marker = markerFactory(data)
markersMap.getOrPut(marker) { markersMap.getOrPut(marker) {
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) { SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
safelyWithoutExceptions { block(it) } runCatchingLogging(logger = logger) {
block(it)
}
if (isEmpty()) { if (isEmpty()) {
onEmpty(marker) onEmpty(marker)
} }
@@ -60,9 +65,24 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
} }
} }
/**
* Subscribes to a [Flow] with asynchronous processing based on markers.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially in the same coroutine scope, while values with different markers
* can be processed concurrently in separate coroutine scopes.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
fun <T, M> Flow<T>.subscribeAsync( fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Job { ): Job {
val subscope = scope.LinkedSupervisorScope() val subscope = scope.LinkedSupervisorScope()
@@ -71,8 +91,14 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap) it.invoke(markersMap)
} }
val job = subscribeLoggingDropExceptions(subscope) { data -> val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> val dataCommand = AsyncSubscriptionCommandData(
data = data,
scope = subscope,
markerFactory = markerFactory,
block = block,
logger = logger
) { marker ->
actor.send( actor.send(
AsyncSubscriptionCommandClearReceiver(marker) AsyncSubscriptionCommandClearReceiver(marker)
) )
@@ -85,17 +111,20 @@ fun <T, M> Flow<T>.subscribeAsync(
return job return job
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyAsync( fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safely(onException) { safely(onException) {
block(it) block(it)
} }
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
@@ -107,11 +136,36 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
} }
} }
/**
* Subscribes to a [Flow] with asynchronous processing based on markers, automatically logging and dropping exceptions.
* Each value from the flow will be processed by the [block] function. Values with the same marker
* will be processed sequentially, while values with different markers can be processed concurrently.
* Any exceptions thrown during processing will be logged and dropped without affecting other messages.
*
* @param T The type of values emitted by the flow
* @param M The type of markers used to group values
* @param scope The [CoroutineScope] in which to subscribe to the flow
* @param markerFactory A factory function that produces a marker for each emitted value
* @param logger The logger instance used for logging exceptions. Defaults to [KSLog]
* @param block The suspending function that processes each emitted value
* @return A [Job] representing the subscription that can be cancelled
*/
fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory, logger) {
block(it)
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, logger, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safelyWithoutExceptions({ /* do nothing */}) { safelyWithoutExceptions({ /* do nothing */}) {
block(it) block(it)
} }

View File

@@ -5,4 +5,12 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
/**
* Merges two flows into a single flow. Values from both flows will be emitted as they become available.
* This is a convenient operator syntax for [merge].
*
* @param T The type of elements in the flows
* @param other The flow to merge with this flow
* @return A [Flow] that emits values from both flows
*/
inline operator fun <T> Flow<T>.plus(other: Flow<T>) = merge(this, other) inline operator fun <T> Flow<T>.plus(other: Flow<T>) = merge(this, other)

View File

@@ -6,6 +6,17 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
/**
* Launches a new coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLogging( fun CoroutineScope.launchLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -18,6 +29,17 @@ fun CoroutineScope.launchLogging(
}.getOrThrow() }.getOrThrow()
} }
/**
* Launches a new coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and then dropped (not rethrown), allowing the coroutine to complete normally.
*
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute in the new coroutine
* @return A [Job] representing the launched coroutine
*/
fun CoroutineScope.launchLoggingDropExceptions( fun CoroutineScope.launchLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -30,6 +52,18 @@ fun CoroutineScope.launchLoggingDropExceptions(
} // just dropping exception } // just dropping exception
} }
/**
* Creates a new async coroutine with automatic exception logging. If an exception occurs, it will be logged
* using the provided [logger] and then rethrown when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] representing the async computation
*/
fun <T> CoroutineScope.asyncLogging( fun <T> CoroutineScope.asyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
@@ -42,6 +76,18 @@ fun <T> CoroutineScope.asyncLogging(
}.getOrThrow() }.getOrThrow()
} }
/**
* Creates a new async coroutine with automatic exception logging and dropping. If an exception occurs, it will be logged
* using the provided [logger] and wrapped in a [Result], which can be checked when the [Deferred] is awaited.
*
* @param T The return type of the async computation
* @param errorMessageBuilder A function to build the error message from the caught exception. Defaults to "Something web wrong"
* @param logger The logger instance to use for logging exceptions. Defaults to [KSLog]
* @param context Additional [CoroutineContext] for the new coroutine. Defaults to [EmptyCoroutineContext]
* @param start The coroutine start option. Defaults to [CoroutineStart.DEFAULT]
* @param block The suspending function to execute that returns a value of type [T]
* @return A [Deferred] containing a [Result] representing the async computation
*/
fun <T> CoroutineScope.asyncLoggingDropExceptions( fun <T> CoroutineScope.asyncLoggingDropExceptions(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" }, errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/** /**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to * Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber * each active subscriber
*/ */
open class SpecialMutableStateFlow<T>( open class MutableRedeliverStateFlow<T>(
initialValue: T initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)
@@ -68,3 +65,6 @@ open class SpecialMutableStateFlow<T>(
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector) override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
} }
@Deprecated("Renamed to MutableRedeliverStateFlow", ReplaceWith("MutableRedeliverStateFlow<T>"))
typealias SpecialMutableStateFlow<T> = MutableRedeliverStateFlow<T>

View File

@@ -1,3 +1,13 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
/**
* Replaces a failed [Result] with a new value computed from the exception.
* If this [Result] is successful, it is returned as-is. If it represents a failure,
* the [onException] handler is called with the exception to compute a replacement value,
* which is then wrapped in a new [Result].
*
* @param T The type of the successful value
* @param onException A function that computes a replacement value from the caught exception
* @return The original [Result] if successful, or a new [Result] containing the replacement value
*/
inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } } inline fun <T> Result<T>.replaceIfFailure(onException: (Throwable) -> T) = if (isSuccess) { this } else { runCatching { onException(exceptionOrNull()!!) } }

View File

@@ -2,11 +2,27 @@ package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging( inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" }, noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
block: R.() -> T block: R.() -> T
) = runCatching(block).onFailure { ) = runCatching(block).onFailure {
logger.e(it) { errorMessageBuilder(it) } when (it) {
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
} }

View File

@@ -221,4 +221,6 @@ suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(keys: Iterable<T>,
unlockWrite(it) unlockWrite(it)
} }
} }
} }
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(vararg keys: T, action: () -> R): R = withWriteLocks(keys.asIterable(), action)

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow] * @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/ */
class Mutable(locked: Boolean = false) : SmartMutex { class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked) private val _lockStateFlow = MutableRedeliverStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()

View File

@@ -3,6 +3,14 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
/**
* Executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* If the block throws an exception, the lockers will remain locked.
*
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Result] containing [Unit] on success or the exception that occurred
*/
suspend inline fun alsoWithUnlockingOnSuccess( suspend inline fun alsoWithUnlockingOnSuccess(
vararg lockers: SmartRWLocker, vararg lockers: SmartRWLocker,
block: suspend () -> Unit block: suspend () -> Unit
@@ -14,6 +22,15 @@ suspend inline fun alsoWithUnlockingOnSuccess(
} }
} }
/**
* Asynchronously executes the given [block] and unlocks all provided [lockers] for writing if the block succeeds.
* This function launches a new coroutine in the given [scope] and automatically logs and drops any exceptions.
*
* @param scope The [CoroutineScope] in which to launch the coroutine
* @param lockers Variable number of [SmartRWLocker] instances to unlock on successful execution
* @param block The suspending function to execute
* @return A [Job] representing the launched coroutine
*/
fun alsoWithUnlockingOnSuccessAsync( fun alsoWithUnlockingOnSuccessAsync(
scope: CoroutineScope, scope: CoroutineScope,
vararg lockers: SmartRWLocker, vararg lockers: SmartRWLocker,

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -47,7 +46,7 @@ sealed interface SmartSemaphore {
*/ */
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits override val maxPermits: Int = permits
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits) private val _freePermitsStateFlow = MutableRedeliverStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow() override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false) private val internalChangesMutex = Mutex(false)
@@ -73,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits acquiredPermits != checkedPermits
} }
if (shouldContinue) { if (shouldContinue) {
waitRelease() waitRelease(checkedPermits - acquiredPermits)
} }
} while (shouldContinue && currentCoroutineContext().isActive) } while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -3,19 +3,49 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/**
* Creates a [SupervisorJob] linked to this [CoroutineContext]'s job. The new supervisor job will be a child
* of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineContext.LinkedSupervisorJob( fun CoroutineContext.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it } ) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
/**
* Creates a [SupervisorJob] linked to this [CoroutineScope]'s job. The new supervisor job will be a child
* of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A [CoroutineContext] containing the new supervisor job
*/
fun CoroutineScope.LinkedSupervisorJob( fun CoroutineScope.LinkedSupervisorJob(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorJob(additionalContext) ) = coroutineContext.LinkedSupervisorJob(additionalContext)
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineContext]'s job.
* The new scope's supervisor job will be a child of the current job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineContext.LinkedSupervisorScope( fun CoroutineContext.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = CoroutineScope( ) = CoroutineScope(
this + LinkedSupervisorJob(additionalContext) this + LinkedSupervisorJob(additionalContext)
) )
/**
* Creates a new [CoroutineScope] with a [SupervisorJob] linked to this [CoroutineScope]'s job.
* The new scope's supervisor job will be a child of the current scope's job, and optionally combined with [additionalContext].
*
* @param additionalContext Optional additional context to combine with the supervisor job
* @return A new [CoroutineScope] with a linked supervisor job
*/
fun CoroutineScope.LinkedSupervisorScope( fun CoroutineScope.LinkedSupervisorScope(
additionalContext: CoroutineContext? = null additionalContext: CoroutineContext? = null
) = coroutineContext.LinkedSupervisorScope(additionalContext) ) = coroutineContext.LinkedSupervisorScope(additionalContext)

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
@@ -13,134 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests { class SmartKeyRWLockerTests {
@Test private lateinit var locker: SmartKeyRWLocker<String>
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
assertTrue { locker.isWriteLocked() } @BeforeTest
fun setup() {
assertFails { locker = SmartKeyRWLocker()
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.unlockWrite()
assertFalse { locker.isWriteLocked() }
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
// ==================== Global Read Tests ====================
@Test @Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val locker = SmartKeyRWLocker<String>() val results = mutableListOf<Boolean>()
val testKey = "test"
locker.acquireRead() locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) val jobs = List(5) {
launch {
assertFails { locker.acquireRead()
realWithTimeout(1.seconds) { delay(100.milliseconds)
locker.lockWrite(testKey) results.add(true)
locker.releaseRead()
} }
} }
assertFalse { locker.isWriteLocked(testKey) }
jobs.joinAll()
locker.releaseRead()
assertEquals(5, results.size)
}
@Test
fun testGlobalReadBlocksGlobalWrite() = runTest {
locker.acquireRead()
var writeAcquired = false
val writeJob = launch {
locker.lockWrite()
writeAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write should be blocked by global read")
locker.releaseRead() locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) writeJob.join()
realWithTimeout(1.seconds) { assertTrue(writeAcquired, "Write should succeed after read released")
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
@Test @Test
fun readsBlockingGlobalWrite() = runTest { fun testGlobalReadBlocksAllKeyWrites() = runTest {
val locker = SmartKeyRWLocker<String>() locker.acquireRead()
val testKeys = (0 until 100).map { "test$it" } val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
for (i in testKeys.indices) { val jobs = keys.map { key ->
val it = testKeys[i] launch {
locker.acquireRead(it) locker.lockWrite(key)
val previous = testKeys.take(i) writeFlags[key] = true
val next = testKeys.drop(i + 1) locker.unlockWrite(key)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
} }
} }
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active")
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() } locker.releaseRead()
jobs.joinAll()
assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released")
}
// ==================== Global Write Tests ====================
@Test
fun testGlobalWriteBlocksAllOperations() = runTest {
locker.lockWrite()
var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
val jobs = listOf(
launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
} }
val readPermitsBeforeLock = locker.readSemaphore().freePermits )
realWithTimeout(1.seconds) { locker.acquireRead() }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
locker.unlockWrite()
jobs.joinAll()
assertTrue(globalReadAcquired)
assertTrue(keyReadAcquired)
assertTrue(keyWriteAcquired)
}
@Test
fun testGlobalWriteIsExclusive() = runTest {
locker.lockWrite()
var secondWriteAcquired = false
val job = launch {
locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
}
// ==================== Key Read Tests ====================
@Test
fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val key = "testKey"
val results = mutableListOf<Boolean>()
locker.acquireRead(key)
val jobs = List(5) {
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results.add(true)
locker.releaseRead(key)
}
}
jobs.joinAll()
locker.releaseRead(key)
assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
locker.acquireRead("key1")
val jobs = listOf("key2", "key3", "key4").map { key ->
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results[key] = true
locker.releaseRead(key)
}
}
jobs.joinAll()
locker.releaseRead("key1")
assertEquals(3, results.size)
}
@Test
fun testKeyReadBlocksWriteForSameKey() = runTest {
val key = "testKey"
locker.acquireRead(key)
var writeAcquired = false
val job = launch {
locker.lockWrite(key)
writeAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write for same key should be blocked")
locker.releaseRead(key)
job.join()
assertTrue(writeAcquired)
}
@Test
fun testKeyReadBlocksGlobalWrite() = runTest {
locker.acquireRead("key1")
var globalWriteAcquired = false
val job = launch {
locker.lockWrite()
globalWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(globalWriteAcquired, "Global write should be blocked by key read")
locker.releaseRead("key1")
job.join()
assertTrue(globalWriteAcquired)
}
@Test
fun testKeyReadAllowsWriteForDifferentKey() = runTest {
locker.acquireRead("key1")
var writeAcquired = false
val job = launch {
locker.lockWrite("key2")
writeAcquired = true
locker.unlockWrite("key2")
}
job.join()
assertTrue(writeAcquired, "Write for different key should succeed")
locker.releaseRead("key1")
}
// ==================== Key Write Tests ====================
@Test
fun testKeyWriteBlocksReadForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
var readAcquired = false
val job = launch {
locker.acquireRead(key)
readAcquired = true
locker.releaseRead(key)
}
delay(200.milliseconds)
assertFalse(readAcquired, "Read for same key should be blocked")
locker.unlockWrite(key)
job.join()
assertTrue(readAcquired)
}
@Test
fun testKeyWriteBlocksGlobalRead() = runTest {
locker.lockWrite("key1")
var globalReadAcquired = false
val job = launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead() locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() } assertFalse(globalReadAcquired, "Global read should be blocked by key write")
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.unlockWrite("key1")
} job.join()
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } assertTrue(globalReadAcquired)
} }
@Test @Test
fun writesBlockingGlobalWrite() = runTest { fun testKeyWriteIsExclusiveForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
locker.lockWrite(key)
val testKeys = (0 until 100).map { "test$it" } var secondWriteAcquired = false
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach { locker.unlockWrite(key)
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
val jobs = listOf(
launch {
locker.acquireRead("key2")
results["read-key2"] = true
locker.releaseRead("key2")
},
launch {
locker.lockWrite("key3")
results["write-key3"] = true
locker.unlockWrite("key3")
} }
next.forEach { )
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
jobs.joinAll()
assertEquals(2, results.size, "Operations on different keys should succeed")
locker.unlockWrite("key1")
}
// ==================== Complex Scenarios ====================
@Test
fun testMultipleReadersThenWriter() = runTest {
val key = "testKey"
val readCount = 5
val readers = mutableListOf<Job>()
repeat(readCount) {
readers.add(launch {
locker.acquireRead(key)
delay(100.milliseconds)
locker.releaseRead(key)
})
}
delay(50.milliseconds) // Let readers acquire
var writerExecuted = false
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
delay(50.milliseconds)
assertFalse(writerExecuted, "Writer should wait for all readers")
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should execute after all readers done")
}
@Test
fun testWriterThenMultipleReaders() = runTest {
val key = "testKey"
locker.lockWrite(key)
val readerFlags = mutableListOf<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
} }
} }
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
val readPermitsBeforeLock = locker.readSemaphore().freePermits locker.unlockWrite(key)
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } readers.joinAll()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(it) assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(50.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails { launch {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
} }
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
assertTrue(executed["write-key1-start"] == true)
assertTrue(executed["read-key2"] == true)
assertTrue(executed["write-key1-end"] == true)
}
@Test
fun testReleaseWithoutAcquireReturnsFalse() = runTest {
assertFalse(locker.releaseRead(), "Release without acquire should return false")
assertFalse(locker.releaseRead("key1"), "Release without acquire should return false")
}
@Test
fun testUnlockWithoutLockReturnsFalse() = runTest {
assertFalse(locker.unlockWrite(), "Unlock without lock should return false")
assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false")
}
@Test
fun testProperReleaseReturnsTrue() = runTest {
locker.acquireRead()
assertTrue(locker.releaseRead(), "Release after acquire should return true")
locker.acquireRead("key1")
assertTrue(locker.releaseRead("key1"), "Release after acquire should return true")
}
@Test
fun testProperUnlockReturnsTrue() = runTest {
locker.lockWrite()
assertTrue(locker.unlockWrite(), "Unlock after lock should return true")
locker.lockWrite("key1")
assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true")
}
// ==================== Stress Tests ====================
@Test
fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) {
val operations = 100
val keys = listOf("key1", "key2", "key3", "key4", "key5")
val jobs = mutableListOf<Job>()
repeat(operations) { i ->
val key = keys[i % keys.size]
when (i % 4) {
0 -> jobs.add(launch {
locker.acquireRead(key)
delay(10.milliseconds)
locker.releaseRead(key)
})
1 -> jobs.add(launch {
locker.lockWrite(key)
delay(10.milliseconds)
locker.unlockWrite(key)
})
2 -> jobs.add(launch {
locker.acquireRead()
delay(10.milliseconds)
locker.releaseRead()
})
3 -> jobs.add(launch {
locker.lockWrite()
delay(10.milliseconds)
locker.unlockWrite()
})
}
}
jobs.joinAll()
// If we reach here without deadlock or exceptions, test passes
}
@Test
fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) {
val key = "testKey"
var writerExecuted = false
// Start continuous readers
val readers = List(10) {
launch {
repeat(5) {
locker.acquireRead(key)
delay(50.milliseconds)
locker.releaseRead(key)
delay(10.milliseconds)
}
}
}
delay(100.milliseconds)
// Try to acquire write lock
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should eventually execute")
} }
} }

View File

@@ -1,33 +1,31 @@
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests { class SpecialMutableStateFlowTests {
@Test @Test
fun simpleTest() = runTest { fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
specialMutableStateFlow.first { it == 1 } mutableRedeliverStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value) assertEquals(1, mutableRedeliverStateFlow.value)
} }
@Test @Test
fun specialTest() = runTest { fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) { subscriberJob = mutableRedeliverStateFlow.subscribe(this) {
when (it) { when (it) {
1 -> specialMutableStateFlow.value = 2 1 -> mutableRedeliverStateFlow.value = 2
2 -> subscriberJob.cancel() 2 -> subscriberJob.cancel()
} }
} }
specialMutableStateFlow.value = 1 mutableRedeliverStateFlow.value = 1
subscriberJob.join() subscriberJob.join()
assertEquals(2, specialMutableStateFlow.value) assertEquals(2, mutableRedeliverStateFlow.value)
} }
} }

View File

@@ -2,9 +2,22 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
/**
* Convenience property to access [Dispatchers.IO] for I/O-bound operations.
* This dispatcher is optimized for offloading blocking I/O tasks to a shared pool of threads.
*/
val IO val IO
get() = Dispatchers.IO get() = Dispatchers.IO
/**
* Executes the given [block] on the IO dispatcher and returns its result.
* This is a convenience function for executing I/O-bound operations like reading files,
* network requests, or database queries.
*
* @param T The return type of the block
* @param block The suspending function to execute on the IO dispatcher
* @return The result of executing the block
*/
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn( suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
IO, IO,
block block

View File

@@ -3,6 +3,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
/**
* Launches a coroutine in the current thread using [Dispatchers.Unconfined] and blocks until it completes,
* returning its result. The coroutine will start execution in the current thread and will continue
* in the same thread until the first suspension point.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute in the current thread
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T { fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T {
val scope = CoroutineScope(Dispatchers.Unconfined) val scope = CoroutineScope(Dispatchers.Unconfined)
return scope.launchSynchronously(block) return scope.launchSynchronously(block)

View File

@@ -2,6 +2,16 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.* import kotlinx.coroutines.*
/**
* Launches a coroutine and blocks the current thread until the coroutine completes, returning its result.
* This is useful for bridging between suspending and non-suspending code in JVM environments.
* The coroutine is launched with [CoroutineStart.UNDISPATCHED] to start execution immediately.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T { fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
var result: Result<T>? = null var result: Result<T>? = null
val objectToSynchronize = Object() val objectToSynchronize = Object()
@@ -22,7 +32,31 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
return result!!.getOrThrow() return result!!.getOrThrow()
} }
/**
* Launches a coroutine in a new [CoroutineScope] with [Dispatchers.Default] and blocks the current thread
* until the coroutine completes, returning its result.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
* @throws Throwable if the coroutine throws an exception
*/
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block) fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
/**
* Alias for [launchSynchronously]. Launches a coroutine and blocks the current thread until it completes.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
*/
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block) fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
/**
* Alias for [launchSynchronously]. Launches a coroutine in a new scope and blocks the current thread until it completes.
*
* @param T The return type of the suspending block
* @param block The suspending function to execute synchronously
* @return The result of the suspending block
*/
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block) fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -12,12 +12,35 @@ private val BASE64_INVERSE_ALPHABET = IntArray(256) {
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this] internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
/**
* Type alias representing a Base64-encoded string.
*/
typealias EncodedBase64String = String typealias EncodedBase64String = String
/**
* Type alias representing a Base64-encoded byte array.
*/
typealias EncodedByteArray = ByteArray typealias EncodedByteArray = ByteArray
/**
* Encodes this string to Base64 format, returning the result as a string.
*
* @return A Base64-encoded string
*/
fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String() fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
/**
* Encodes this string to Base64 format, returning the result as a byte array.
*
* @return A Base64-encoded byte array
*/
fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64() fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
/**
* Encodes this byte array to Base64 format, returning the result as a string.
*
* @return A Base64-encoded string with padding ('=') characters
*/
fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString { fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
var i = 0 var i = 0
while (this@encodeBase64String.size > i) { while (this@encodeBase64String.size > i) {
@@ -45,11 +68,33 @@ fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
i += read i += read
} }
} }
/**
* Encodes this byte array to Base64 format, returning the result as a byte array.
*
* @return A Base64-encoded byte array
*/
fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray() fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
/**
* Decodes this Base64-encoded string back to the original byte array.
*
* @return The decoded byte array
*/
fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64() fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
/**
* Decodes this Base64-encoded string back to the original string.
*
* @return The decoded string
*/
fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString() fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString()
/**
* Decodes this Base64-encoded byte array back to the original byte array.
*
* @return The decoded byte array
*/
fun EncodedByteArray.decodeBase64(): SourceBytes { fun EncodedByteArray.decodeBase64(): SourceBytes {
val result = mutableListOf<Byte>() val result = mutableListOf<Byte>()
val data = ByteArray(4) val data = ByteArray(4)
@@ -74,4 +119,10 @@ fun EncodedByteArray.decodeBase64(): SourceBytes {
return result.toByteArray() return result.toByteArray()
} }
/**
* Decodes this Base64-encoded byte array back to the original string.
*
* @return The decoded string
*/
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString() fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()

View File

@@ -1,7 +1,16 @@
package dev.inmo.micro_utils.crypto package dev.inmo.micro_utils.crypto
/**
* Character array used for hexadecimal encoding (lowercase).
*/
val HEX_ARRAY = "0123456789abcdef".toCharArray() val HEX_ARRAY = "0123456789abcdef".toCharArray()
/**
* Converts this byte array to a hexadecimal string representation (lowercase).
* Each byte is represented as two hex characters.
*
* @return A lowercase hex string (e.g., "48656c6c6f" for "Hello")
*/
fun SourceBytes.hex(): String { fun SourceBytes.hex(): String {
val hexChars = CharArray(size * 2) val hexChars = CharArray(size * 2)
for (j in indices) { for (j in indices) {
@@ -12,4 +21,9 @@ fun SourceBytes.hex(): String {
return hexChars.concatToString() return hexChars.concatToString()
} }
/**
* Converts this string to a hexadecimal representation by first encoding it as UTF-8 bytes.
*
* @return A lowercase hex string representation of the UTF-8 encoded bytes
*/
fun SourceString.hex(): String = encodeToByteArray().hex() fun SourceString.hex(): String = encodeToByteArray().hex()

View File

@@ -2,7 +2,21 @@ package dev.inmo.micro_utils.crypto
import korlibs.crypto.md5 import korlibs.crypto.md5
/**
* Type alias representing an MD5 hash as a hex-encoded string (32 characters).
*/
typealias MD5 = String typealias MD5 = String
/**
* Computes the MD5 hash of this byte array and returns it as a lowercase hex string.
*
* @return The MD5 hash as a 32-character lowercase hex string
*/
fun SourceBytes.md5(): MD5 = md5().hexLower fun SourceBytes.md5(): MD5 = md5().hexLower
/**
* Computes the MD5 hash of this string (encoded as UTF-8 bytes) and returns it as a lowercase hex string.
*
* @return The MD5 hash as a 32-character lowercase hex string
*/
fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower

View File

@@ -26,7 +26,7 @@ kotlin {
project.parent.subprojects.forEach { project.parent.subprojects.forEach {
if ( if (
it != project it.name != project.name
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") } && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -44,7 +44,7 @@ kotlin {
project.parent.subprojects.forEach { project.parent.subprojects.forEach {
if ( if (
it != project it.name != project.name
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jsMain") } && it.kotlin.sourceSets.any { it.name.contains("jsMain") }
@@ -60,7 +60,7 @@ kotlin {
project.parent.subprojects.forEach { project.parent.subprojects.forEach {
if ( if (
it != project it.name != project.name
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("jvmMain") } && it.kotlin.sourceSets.any { it.name.contains("jvmMain") }
@@ -76,7 +76,7 @@ kotlin {
project.parent.subprojects.forEach { project.parent.subprojects.forEach {
if ( if (
it != project it.name != project.name
&& it.hasProperty("kotlin") && it.hasProperty("kotlin")
&& it.kotlin.sourceSets.any { it.name.contains("commonMain") } && it.kotlin.sourceSets.any { it.name.contains("commonMain") }
&& it.kotlin.sourceSets.any { it.name.contains("androidMain") } && it.kotlin.sourceSets.any { it.name.contains("androidMain") }

View File

@@ -30,3 +30,13 @@ allprojects {
} }
} }
} }
tasks.register("getPublishableModules") {
doLast {
rootProject.subprojects.each { project ->
if (project.plugins.hasPlugin('maven-publish')) {
println(":${project.name}")
}
}
}
}

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -1,5 +1,13 @@
package dev.inmo.micro_utils.fsm.common package dev.inmo.micro_utils.fsm.common
/**
* Represents a state in a finite state machine (FSM).
* Each state must have an associated context that identifies it uniquely within its chain.
*/
interface State { interface State {
/**
* The context object that uniquely identifies this state within a state chain.
* States with the same context are considered to belong to the same chain.
*/
val context: Any val context: Any
} }

View File

@@ -1,6 +1,26 @@
package dev.inmo.micro_utils.fsm.common.utils package dev.inmo.micro_utils.fsm.common.utils
/**
* A handler function type for dealing with errors during state handling in a finite state machine.
* The handler receives the state that caused the error and the thrown exception, and can optionally
* return a new state to continue the chain, or null to end the chain.
*
* @param T The state type that caused the error
* @param Throwable The exception that was thrown during state handling
* @return A new state to continue with, or null to end the state chain
*/
typealias StateHandlingErrorHandler<T> = suspend (T, Throwable) -> T? typealias StateHandlingErrorHandler<T> = suspend (T, Throwable) -> T?
/**
* The default error handler that returns null for all errors, effectively ending the state chain.
*/
val DefaultStateHandlingErrorHandler: StateHandlingErrorHandler<*> = { _, _ -> null } val DefaultStateHandlingErrorHandler: StateHandlingErrorHandler<*> = { _, _ -> null }
/**
* Returns a typed version of the [DefaultStateHandlingErrorHandler].
*
* @param T The state type
* @return A [StateHandlingErrorHandler] for type [T]
*/
inline fun <T> defaultStateHandlingErrorHandler(): StateHandlingErrorHandler<T> = DefaultStateHandlingErrorHandler as StateHandlingErrorHandler<T> inline fun <T> defaultStateHandlingErrorHandler(): StateHandlingErrorHandler<T> = DefaultStateHandlingErrorHandler as StateHandlingErrorHandler<T>

View File

@@ -4,7 +4,7 @@ plugins {
id "com.android.library" id "com.android.library"
} }
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
kotlin { kotlin {
sourceSets { sourceSets {

View File

@@ -1,9 +1,15 @@
interface InjectedExecOps {
@Inject //@javax.inject.Inject
ExecOperations getExecOps()
}
private String getCurrentVersionChangelog() { private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream() OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
def injected = project.objects.newInstance(InjectedExecOps)
injected.execOps.exec {
commandLine 'chmod', "+x", './changelog_parser.sh' commandLine 'chmod', "+x", './changelog_parser.sh'
} }
exec { injected.execOps.exec {
standardOutput = changelogDataOS standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md' commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
} }
@@ -11,12 +17,20 @@ private String getCurrentVersionChangelog() {
return changelogDataOS.toString().trim() return changelogDataOS.toString().trim()
} }
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle' def githubTokenVariableName = "GITHUB_RELEASE_TOKEN"
def githubTokenVariableFromEnv = System.getenv(githubTokenVariableName)
def secretFile = new File(projectDir, "secret.gradle")
if (secretFile.exists() || project.hasProperty(githubTokenVariableName) || (githubTokenVariableFromEnv != "" && githubTokenVariableFromEnv != null)) {
if (secretFile.exists()) {
apply from: './secret.gradle'
}
apply plugin: "com.github.breadmoirai.github-release" apply plugin: "com.github.breadmoirai.github-release"
def githubReleaseToken = project.hasProperty(githubTokenVariableName) ? project.property(githubTokenVariableName).toString() : githubTokenVariableFromEnv
githubRelease { githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}" token githubReleaseToken
owner = "InsanusMokrassar" owner = "InsanusMokrassar"
repo = "MicroUtils" repo = "MicroUtils"

View File

@@ -8,6 +8,9 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
## https://github.com/google/ksp/issues/2491
#ksp.useKSP2=false
# JS NPM # JS NPM
crypto_js_version=4.1.1 crypto_js_version=4.1.1
@@ -15,5 +18,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.25.3 version=0.29.3
android_code_version=293 android_code_version=313

View File

@@ -0,0 +1,7 @@
config.set({
client: {
mocha: {
timeout: 240000
}
}
});

View File

@@ -1,46 +1,51 @@
[versions] [versions]
kt = "2.1.10" kt = "2.3.21"
kt-serialization = "1.8.0" kt-serialization = "1.11.0"
kt-coroutines = "1.10.1" kt-coroutines = "1.11.0"
kslog = "1.4.1" kotlinx-browser = "0.5.0"
jb-compose = "1.7.3" kslog = "1.6.1"
jb-exposed = "0.60.0"
jb-dokka = "2.0.0"
sqlite = "3.49.1.0" jb-compose = "1.11.0"
jb-compose-material3 = "1.11.0-alpha07"
jb-compose-icons = "1.7.8"
jb-exposed = "1.3.0"
jb-dokka = "2.2.0"
sqlite = "3.53.1.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.1.1" ktor = "3.5.0"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.0.2" koin = "4.2.1"
okio = "3.10.2" okio = "3.17.0"
ksp = "2.1.10-1.0.31" ksp = "2.3.8"
kotlin-poet = "1.18.1" kotlin-poet = "2.3.0"
versions = "0.51.0" versions = "0.54.0"
nmcp = "1.5.0"
android-gradle = "8.7.+" android-gradle = "8.13.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.15.0" android-coreKtx = "1.18.0"
android-recyclerView = "1.4.0" android-recyclerView = "1.4.0"
android-appCompat = "1.7.0" android-appCompat = "1.7.1"
android-fragment = "1.8.6" android-fragment = "1.8.9"
android-espresso = "3.6.1" android-espresso = "3.7.0"
android-test = "1.2.1" android-test = "1.3.0"
android-props-minSdk = "21" android-props-minSdk = "21"
android-props-compileSdk = "35" android-props-compileSdk = "36"
android-props-buildTools = "35.0.0" android-props-buildTools = "36.0.0"
[libraries] [libraries]
@@ -55,6 +60,7 @@ kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-and
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" } kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
kt-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kt-coroutines" } kt-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "kt-coroutines" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser-wasm-js", version.ref = "kotlinx-browser" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" } ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
@@ -84,7 +90,8 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" } jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" } jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose-material3" }
jb-compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "jb-compose-icons" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" } android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
@@ -120,3 +127,4 @@ jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" } kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
commonMain {
dependencies {
implementation compose.runtime
}
}
}
}

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
androidUnitTest {
dependencies {
implementation compose.uiTest
}
}
}
}

View File

@@ -0,0 +1,15 @@
kotlin {
sourceSets {
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
implementation compose.uiTest
}
}
}
}

View File

@@ -0,0 +1,9 @@
kotlin {
sourceSets {
jsMain {
dependencies {
implementation compose.web.core
}
}
}
}

View File

@@ -0,0 +1,24 @@
project.version = "$version"
project.group = "$group"
kotlin {
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,25 @@
project.version = "$version"
project.group = "$group"
kotlin {
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api libs.kt.serialization
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -0,0 +1,34 @@
kotlin {
androidTarget {
publishLibraryVariants(
"release",
"debug",
)
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
androidInstrumentedTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"

View File

@@ -0,0 +1,30 @@
kotlin {
js (IR) {
browser {
testTask {
useMocha {
timeout = "240000"
}
}
}
nodejs {
testTask {
useMocha {
timeout = "240000"
}
}
}
}
sourceSets {
jsMain {
dependencies {
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
}
}

View File

@@ -0,0 +1,21 @@
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
jvmMain {
dependencies {
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More