diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b924ba0..2d17c45 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,13 +15,13 @@ jobs: needs: tests runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set Up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: Grant execute permission for gradlew @@ -38,7 +38,7 @@ jobs: shell: bash - name: Publish - run: ./gradlew publishAllPublicationsToMavenCentralRepository -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} -Psigning.password=${{ secrets.SIGNING_PASSWORD }} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg) + run: ./gradlew publishToMavenCentral -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} -Psigning.password=${{ secrets.SIGNING_PASSWORD }} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg) env: - MAVEN_USERNAME: ${{ secrets.MAVEN_TOKEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_TOKEN_PASSWORD }} \ No newline at end of file + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_TOKEN_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_TOKEN_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a98c0e4..65af971 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,13 +14,13 @@ jobs: os: [macos-13, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: "Set Up JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: "Grant execute permission for gradlew" @@ -44,7 +44,7 @@ jobs: run: ./gradlew cleanMacosX64Test macosX64Test --tests "com.ucasoft.kcron.*" - name: "Tests Report" - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@v2 if: success() || failure() with: name: jUnit Tests diff --git a/README.md b/README.md index d4a59e9..e454d1f 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # KCron Cron realization for Kotlin Multiplatform -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Scogun_kcron-common&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Scogun_kcron-common) ![GitHub](https://img.shields.io/github/license/Scogun/kcron-common?color=blue) ![Publish workflow](https://github.com/Scogun/kcron-common/actions/workflows/publish.yml/badge.svg) [![Maven Central with version prefix filter](https://img.shields.io/maven-central/v/com.ucasoft.kcron/kcron-common/0.23.0?color=blue)](https://search.maven.org/artifact/com.ucasoft.kcron/kcron-common/0.23.0/jar) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Scogun_kcron-common&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Scogun_kcron-common) ![GitHub](https://img.shields.io/github/license/Scogun/kcron-common?color=blue) ![Publish workflow](https://github.com/Scogun/kcron-common/actions/workflows/publish.yml/badge.svg) [![Maven Central with version prefix filter](https://img.shields.io/maven-central/v/com.ucasoft.kcron/kcron-common/0.27.4?color=blue)](https://search.maven.org/artifact/com.ucasoft.kcron/kcron-common/0.27.4/jar) ### Features * Kotlin Multiplatform library @@ -43,6 +43,10 @@ builder * Wasm * iOS * Support different DateTime libraries (via DateTime Provider Abstractions) +* Provide UI builder for JVM, macOS, JavaScript and iOS on Compose UI multiplatform + * Multilanguage support (English and Russian now) +

KCron Compose UI

+ ### Usage #### KCron-Common library as default implementation uses [Kotlinx-DateTime](https://github.com/Kotlin/kotlinx-datetime) library ***Add with Gradle*** @@ -51,7 +55,7 @@ kotlin { sourceSets { commonMain { dependencies { - implementation 'com.ucasoft.kcron:kcron-common:0.23.0' + implementation 'com.ucasoft.kcron:kcron-common:0.27.4' } } } @@ -116,6 +120,6 @@ builder.years(2021..2025) println(builder.expression) // 0/10 5-25 5,12 ? * SUN#5 2021-2025 ``` ### Current status -This library is on beta version `0.23.0`. +This library is on beta version `0.27.4`. It is continuing to develop. Check the news! diff --git a/build.gradle.kts b/build.gradle.kts index 742c600..d473ff1 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.multiplatform) apply false - id("publish") apply false + alias(libs.plugins.maven.publish) apply false alias(libs.plugins.kover) apply false alias(libs.plugins.benchmark) apply false } @@ -9,11 +9,11 @@ allprojects { group = "com.ucasoft.kcron" - version = "0.23.0" - repositories { + google() mavenCentral() } + version = "0.27.4" tasks.withType { reports { diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..a8779f7 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,9 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "kcron" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/PublishingExtension.kt b/buildSrc/src/main/kotlin/PublishingExtension.kt index 812c782..38329b7 100644 --- a/buildSrc/src/main/kotlin/PublishingExtension.kt +++ b/buildSrc/src/main/kotlin/PublishingExtension.kt @@ -1,7 +1,30 @@ -import org.gradle.api.model.ObjectFactory -import org.gradle.kotlin.dsl.property +import org.gradle.api.publish.maven.MavenPom -open class PublishingExtension(factory: ObjectFactory) { - val name = factory.property() - val description = factory.property() +fun configurePom(name: String, description: String, pom: MavenPom) { + pom.name.set(name) + pom.description.set(description) + pom.url.set("https://github.com/Scogun/kcron-common") + pom.licenses { + license { + this.name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + pom.developers { + developer { + id.set("Scogun") + this.name.set("Sergey Antonov") + email.set("SAntonov@ucasoft.com") + } + developer { + id.set("Myshkouski") + this.name.set("Alexei Myshkouski") + email.set("alexeimyshkouski@gmail.com") + } + } + pom.scm { + connection.set("scm:git:git://github.com/Scogun/kcron-common.git") + developerConnection.set("scm:git:ssh://github.com:Scogun/kcron-common.git") + url.set("https://github.com/Scogun/kcron-common") + } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/publish.gradle.kts b/buildSrc/src/main/kotlin/publish.gradle.kts deleted file mode 100644 index ebdb19e..0000000 --- a/buildSrc/src/main/kotlin/publish.gradle.kts +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - `maven-publish` - signing -} - -val libraryData = extensions.create("libraryData", PublishingExtension::class) - -val stubJavadoc by tasks.creating(Jar::class) { - archiveClassifier.set("javadoc") -} - -publishing { - publications.configureEach { - if (this is MavenPublication) { - if (name != "kotlinMultiplatform") { - artifact(stubJavadoc) - } - pom { - name.set(libraryData.name) - description.set(libraryData.description) - url.set("https://github.com/Scogun/kcron-common") - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - developers { - developer { - id.set("Scogun") - name.set("Sergey Antonov") - email.set("SAntonov@ucasoft.com") - } - developer { - id.set("Myshkouski") - name.set("Alexei Myshkouski") - email.set("alexeimyshkouski@gmail.com") - } - } - scm { - connection.set("scm:git:git://github.com/Scogun/kcron-common.git") - developerConnection.set("scm:git:ssh://github.com:Scogun/kcron-common.git") - url.set("https://github.com/Scogun/kcron-common") - } - } - } - } - repositories { - maven { - name = "MavenCentral" - url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") - credentials { - username = System.getenv("MAVEN_USERNAME") - password = System.getenv("MAVEN_PASSWORD") - } - } - } -} - -signing { - sign(publishing.publications) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} \ No newline at end of file diff --git a/docs/images/CronUiBuilderEn.png b/docs/images/CronUiBuilderEn.png new file mode 100644 index 0000000..aa7d09d Binary files /dev/null and b/docs/images/CronUiBuilderEn.png differ diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..47c1aa5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,3 @@ kotlin.code.style=official +org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b15a5ea..dde2b28 100755 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,11 @@ [versions] -kotlin = "2.1.0" -kotlinx-datetime = "0.6.1" +kotlin = "2.2.0" +kotlinx-datetime = "0.7.1" kotest = "5.9.1" -kover = "0.9.0" -benchmark = "0.4.13" +kover = "0.9.1" +benchmark = "0.4.14" +compose = "1.8.2" +maven-publish = "0.34.0" [libraries] kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } @@ -14,4 +16,7 @@ kotest-assetions = { group = "io.kotest", name = "kotest-assertions-core", versi [plugins] multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } -benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "benchmark" } \ No newline at end of file +benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "benchmark" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..8bdaf60 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..2a84e18 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..ef07e01 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019..5eed7ee 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/kcron-abstractions/build.gradle.kts b/kcron-abstractions/build.gradle.kts index bec208b..17d82c4 100644 --- a/kcron-abstractions/build.gradle.kts +++ b/kcron-abstractions/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.multiplatform) - id("publish") + alias(libs.plugins.maven.publish) } kotlin { @@ -28,7 +28,11 @@ kotlin { } } -libraryData { - name.set("KCron Abstractions") - description.set("Abstractions for Kotlin Multiplatform Cron realization") +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + pom { + configurePom("KCron Abstractions", "Abstractions for Kotlin Multiplatform Cron realization", this) + } } \ No newline at end of file diff --git a/kcron-common/build.gradle.kts b/kcron-common/build.gradle.kts index bc1dffe..2de30c6 100644 --- a/kcron-common/build.gradle.kts +++ b/kcron-common/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.multiplatform) - id("publish") + alias(libs.plugins.maven.publish) alias(libs.plugins.kover) } @@ -40,7 +40,11 @@ kotlin { } } -libraryData { - name.set("KCron Common") - description.set("Cron realization for Kotlin Multiplatform with Kotlinx DateTime Provider") +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + pom { + configurePom("KCron Common", "Cron realization for Kotlin Multiplatform with Kotlinx DateTime Provider", this) + } } \ No newline at end of file diff --git a/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt b/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt index a901cfa..f5fc94e 100644 --- a/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt +++ b/kcron-common/src/commonTest/kotlin/com/ucasoft/kcron/BuildAndParseTests.kt @@ -5,11 +5,12 @@ import com.ucasoft.kcron.core.settings.Version import io.kotest.assertions.throwables.shouldThrowWithMessage import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.time.Clock +import kotlin.time.ExperimentalTime class BuildAndParseTests { @@ -17,6 +18,7 @@ class BuildAndParseTests { private val modernCronExpression = "30 * * ? * * 2050" + @OptIn(ExperimentalTime::class) @BeforeTest fun setupOnce() { currentYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year diff --git a/kcron-core/build.gradle.kts b/kcron-core/build.gradle.kts index 97279b2..cd76e9d 100644 --- a/kcron-core/build.gradle.kts +++ b/kcron-core/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.multiplatform) - id("publish") + alias(libs.plugins.maven.publish) alias(libs.plugins.kover) } @@ -40,7 +40,11 @@ kotlin { } } -libraryData { - name.set("KCron Core") - description.set("Cron realization for Kotlin Multiplatform") +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + pom { + configurePom("KCron Core", "Cron realization for Kotlin Multiplatform", this) + } } \ No newline at end of file diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt index 0e573fe..20c99a2 100644 --- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt +++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/exceptions/WrongPartCombination.kt @@ -5,7 +5,7 @@ import com.ucasoft.kcron.core.common.PartValue import com.ucasoft.kcron.core.parsers.CombinationRule class WrongPartCombination( - private val part: MutableMap.MutableEntry, + private val part: Map.Entry, private val dependency: CombinationRule, secondPart: PartValue ) : Throwable("Wrong part combination: part ${part.key.partName} with ${part.value.type} type requires that part ${dependency.part.partName} has ${dependency.type} type but it was ${secondPart.type}!") { diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt index 81f5960..19d21f3 100644 --- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt +++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/ParseResult.kt @@ -3,7 +3,4 @@ package com.ucasoft.kcron.core.parsers import com.ucasoft.kcron.core.common.CronPart import com.ucasoft.kcron.core.common.PartValue -class ParseResult { - - val parts = mutableMapOf() -} \ No newline at end of file +data class ParseResult(val parts: Map) \ No newline at end of file diff --git a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt index 9772a77..6b12048 100644 --- a/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt +++ b/kcron-core/src/commonMain/kotlin/com/ucasoft/kcron/core/parsers/Parser.kt @@ -70,14 +70,16 @@ class Parser { throw WrongPartsExpression(partExceptions) } - val result = ParseResult() - for ((index, parser) in partParsers.withIndex()) { - result.parts[parser.part] = PartValue(parser.group as CronGroups, expressionParts[index]) - } - return result + return ParseResult( + partParsers.withIndex().associate { (index, part) -> + part.part to PartValue( + part.group as CronGroups, + expressionParts[index] + ) + }) } - private fun ensureCombinationRules(parts: MutableMap) { + private fun ensureCombinationRules(parts: Map) { val combinationExceptions = mutableListOf() for (part in parts) { val rule = combinationRules.firstOrNull { r -> r.part == part.key && r.type == part.value.type } diff --git a/kcron-kotlinx-datetime/build.gradle.kts b/kcron-kotlinx-datetime/build.gradle.kts index 8debe37..5977b32 100644 --- a/kcron-kotlinx-datetime/build.gradle.kts +++ b/kcron-kotlinx-datetime/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.multiplatform) - id("publish") + alias(libs.plugins.maven.publish) } kotlin { @@ -33,7 +33,11 @@ kotlin { } } -libraryData { - name.set("KCron Kotlinx DateTime") - description.set("Kotlinx DateTime Provider for Kotlin Multiplatform Cron realization") +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + pom { + configurePom("KCron Kotlinx DateTime", "Kotlinx DateTime Provider for Kotlin Multiplatform Cron realization", this) + } } \ No newline at end of file diff --git a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt index 7020f16..c596be7 100644 --- a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt +++ b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTime.kt @@ -2,12 +2,15 @@ package com.ucasoft.kcron.kotlinx.datetime import com.ucasoft.kcron.abstractions.CronDateTime import kotlinx.datetime.* +import kotlin.time.Clock +import kotlin.time.ExperimentalTime class CronLocalDateTime: CronDateTime { private val dateTime: LocalDateTime + @OptIn(ExperimentalTime::class) constructor() { dateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } @@ -20,10 +23,10 @@ class CronLocalDateTime: CronDateTime { get() = dateTime.year override val month: Int - get() = dateTime.monthNumber + get() = dateTime.month.number override val dayOfMonth: Int - get() = dateTime.dayOfMonth + get() = dateTime.day override val isoDayOfWeek: Int get() = dateTime.dayOfWeek.isoDayNumber diff --git a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt index 6ba7c54..db3a1ec 100644 --- a/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt +++ b/kcron-kotlinx-datetime/src/commonMain/kotlin/com/ucasoft/kcron/kotlinx/datetime/CronLocalDateTimeExtensions.kt @@ -1,9 +1,11 @@ package com.ucasoft.kcron.kotlinx.datetime import kotlinx.datetime.* +import kotlinx.datetime.number +import kotlin.time.ExperimentalTime fun LocalDateTime.toCronLocalDateTime() = - CronLocalDateTime(this.year, this.monthNumber, this.dayOfMonth, this.hour, this.minute, this.second) + CronLocalDateTime(this.year, this.month.number, this.day, this.hour, this.minute, this.second) fun LocalDateTime.plusHours(hours: Int, timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime { return plus(this, hours, DateTimeUnit.HOUR, timeZone) @@ -17,6 +19,7 @@ fun LocalDateTime.minusDays(days: Int, timeZone: TimeZone = TimeZone.currentSyst return plus(this, if (days > 0) { days * -1 } else { days }, DateTimeUnit.DAY, timeZone) } +@OptIn(ExperimentalTime::class) private fun plus(self: LocalDateTime, value: Int, unit: DateTimeUnit, timeZone: TimeZone) : LocalDateTime { return self.toInstant(timeZone).plus(value, unit, timeZone).toLocalDateTime(timeZone) } \ No newline at end of file diff --git a/kcron-ui-builder/build.gradle.kts b/kcron-ui-builder/build.gradle.kts new file mode 100644 index 0000000..1fe2f02 --- /dev/null +++ b/kcron-ui-builder/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + alias(libs.plugins.multiplatform) + alias(libs.plugins.maven.publish) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kover) +} + +kotlin { + jvmToolchain(11) + jvm() + macosX64() + macosArm64() + js(IR) { + browser() + nodejs() + } + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + nodejs() + } + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(project(":kcron-common")) + } + } + // TODO UI Tests on Compose UI are unstable, enable when stable + /*commonTest { + dependencies { + implementation(kotlin("test")) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.uiTest) + } + }*/ + } +} + +mavenPublishing { + publishToMavenCentral() + signAllPublications() + + pom { + configurePom("KCron UI Builder", "UI Builder for KCron using JetBrains Compose Multiplatform", this) + } +} \ No newline at end of file diff --git a/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml b/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml new file mode 100644 index 0000000..90fb818 --- /dev/null +++ b/kcron-ui-builder/src/commonMain/composeResources/values-ru/strings.xml @@ -0,0 +1,60 @@ + + Минута + Час + День месяца + Месяц + День недели + + Вручную + + + Каждый + Каждые %1$d + + + + Каждая + + + Начало часа + + Полночь + 6 + 9 + Полдень + 18 + 21 + + 1-е + 15-е + Последний + Первый будний день + Последний будний день + + + Январь + Февраль + Март + Апрель + Май + Июнь + Июль + Август + Сентябрь + Октябрь + Ноябрь + Декабрь + + + + Понедельник + Вторник + Среда + Четверг + Пятница + Суббота + Воскресенье + + + Отмена + \ No newline at end of file diff --git a/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml b/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..7414a24 --- /dev/null +++ b/kcron-ui-builder/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,61 @@ + + Minute + Hour + Day of Month + Month + Day of Week + + Custom + + + Every + Every %1$d + + + + Every + + + Hour Start + + Midnight + 6 AM + 9 AM + Noon + 6 PM + 9 PM + + 1st + 15th + Last + First Weekday + Last Weekday + + + January + February + March + April + May + June + July + August + September + October + November + December + + + + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday + + + Cancel + OK + \ No newline at end of file diff --git a/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt new file mode 100644 index 0000000..c4ea244 --- /dev/null +++ b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronField.kt @@ -0,0 +1,93 @@ +package com.ucasoft.kcron.ui.builder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun CronField( + label: String, + value: String, + options: List> = emptyList(), + customPlaceholder: String = "Enter custom value", + isError: Boolean = false, + onValueChanged: (String) -> Unit = {} +) { + var open by remember { mutableStateOf(false) } + var prebuildPattern by remember { mutableStateOf(options.firstOrNull { it.second == value } ?: options.last()) } + var selectedPattern by remember { mutableStateOf(value) } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + label, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) + ) + ExposedDropdownMenuBox( + expanded = open, + onExpandedChange = { open = it }, + ) { + OutlinedTextField( + prebuildPattern.first, + onValueChange = {}, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(open) + }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable) + ) + if (options.isNotEmpty()) { + ExposedDropdownMenu( + expanded = open, + onDismissRequest = { open = false } + ) { + options.map { + DropdownMenuItem( + text = { Text(it.first) }, + onClick = { + prebuildPattern = it + if (it.second.isNotEmpty()) { + selectedPattern = it.second + onValueChanged(it.second) + } + open = false + } + ) + } + } + } + } + if (prebuildPattern.second == "") { + OutlinedTextField( + selectedPattern, + onValueChange = { + selectedPattern = it + onValueChanged(it) + }, + placeholder = { Text(customPlaceholder) }, + isError = isError + ) + } + } +} \ No newline at end of file diff --git a/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt new file mode 100644 index 0000000..468921e --- /dev/null +++ b/kcron-ui-builder/src/commonMain/kotlin/com/ucasoft/kcron/ui/builder/CronUiBuilder.kt @@ -0,0 +1,203 @@ +package com.ucasoft.kcron.ui.builder + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.ucasoft.kcron.Cron +import com.ucasoft.kcron.core.builders.Builder +import com.ucasoft.kcron.core.common.CronPart +import com.ucasoft.kcron.core.common.PartValue +import com.ucasoft.kcron.core.common.WeekDays +import com.ucasoft.kcron.core.exceptions.WrongPartCombinations +import com.ucasoft.kcron.core.exceptions.WrongPartExpression +import com.ucasoft.kcron.core.parsers.Parser +import com.ucasoft.kcron.kcron_ui_builder.generated.resources.* +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringArrayResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun CronUiBuilder( + expression: String = "* * * * *", + modifier: Modifier = Modifier, + allowCustom: Boolean = true, + firstDayOfWeek: WeekDays = WeekDays.Monday, + onBuild: (Builder<*, *, *>) -> Unit = {}, +) { + val parser = Parser() + var parts by remember { mutableStateOf(parser.parse(expression).parts) } + + Card( + modifier = modifier.padding(12.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val daysOfWeek = stringArrayResource(Res.array.days_of_week).let { + it.drop(firstDayOfWeek.ordinal) + it.take(firstDayOfWeek.ordinal) + } + val fields = listOf( + Res.string.minute to listOf( + pluralStringResource(Res.plurals.every_feminine, 1) to "*", + stringResource(Res.string.hour_start) to "0", + "15" to "15", + "30" to "30", + "45" to "45", + pluralStringResource(Res.plurals.every, 5, 5) to "0/5", + pluralStringResource(Res.plurals.every, 15, 15) to "0/15", + pluralStringResource(Res.plurals.every, 30, 30) to "0/30" + ), + Res.string.hour to listOf( + pluralStringResource(Res.plurals.every, 1) to "*", + stringResource(Res.string.midnight) to "0", + stringResource(Res.string._6_am) to "6", + stringResource(Res.string._9_am) to "9", + stringResource(Res.string.noon) to "12", + stringResource(Res.string._6_pm) to "18", + stringResource(Res.string._9_pm) to "21", + pluralStringResource(Res.plurals.every, 2, 2) to "0/2", + pluralStringResource(Res.plurals.every, 6, 6) to "0/6", + pluralStringResource(Res.plurals.every, 12, 12) to "0/12" + ), + Res.string.day_of_month to listOf( + pluralStringResource(Res.plurals.every, 1) to "*", + stringResource(Res.string.first) to "1", + stringResource(Res.string.fifteenth) to "15", + stringResource(Res.string.last) to "L", + stringResource(Res.string.first_weekday) to "1W", + stringResource(Res.string.last_weekday) to "LW", + pluralStringResource(Res.plurals.every, 2, 2) to "1/2", + pluralStringResource(Res.plurals.every, 7, 7) to "1/7", + pluralStringResource(Res.plurals.every, 14, 14) to "1/14" + ), + Res.string.month to listOf( + pluralStringResource(Res.plurals.every, 1) to "*" + ) + stringArrayResource(Res.array.months).withIndex().map { it.value to (it.index + 1).toString() }, + Res.string.day_of_week to listOf( + pluralStringResource(Res.plurals.every, 1) to "*" + ) + daysOfWeek.withIndex().map { (index, name) -> name to (index + 1).toString() } + ).map { (label, options) -> + label to if (allowCustom) + options + (stringResource(Res.string.custom) to "") + else + options + } + + fields.map { (labelRes, options) -> + var isError by remember { mutableStateOf(false) } + CronField( + stringResource(labelRes), + getPartsValue(parts, labelRes), + options, + isError = isError + ) { + try { + val copy = copyParts(parts, labelRes, it) + parser.parse(copy.entries.joinToString(" ") { it.value.value }) + parts = copy + isError = false + } catch (wrong: WrongPartExpression) { + println(wrong) + isError = true + } catch (wrong: WrongPartCombinations) { + throw wrong + } + } + } + Spacer( + Modifier.weight(1f) + ) + OutlinedTextField( + parts.entries.drop(1).take(5).joinToString(" ") { it.value.value }, + onValueChange = {}, + readOnly = true, + modifier = Modifier.background(MaterialTheme.colorScheme.primary), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onPrimary, + unfocusedTextColor = MaterialTheme.colorScheme.onPrimary + ), + textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center) + ) + Spacer( + Modifier.weight(1f) + ) + Row { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + Button( + onClick = {}, + ) { + Text(stringResource(Res.string.cancel)) + } + } + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + Button( + onClick = { + onBuild(Cron.parseAndBuild(parts.entries.joinToString(" ") { it.value.value }) { + it.firstDayOfWeek = firstDayOfWeek + }) + }, + ) { + Text(stringResource(Res.string.ok)) + } + } + } + } + } +} + +private fun getPartsValue(parts: Map, labelRes: StringResource) = when (labelRes) { + Res.string.minute -> parts[CronPart.Minutes]!!.value + Res.string.hour -> parts[CronPart.Hours]!!.value + Res.string.day_of_month -> parts[CronPart.Days]!!.value + Res.string.month -> parts[CronPart.Months]!!.value + Res.string.day_of_week -> parts[CronPart.DaysOfWeek]!!.value + else -> "" +} + +private fun copyParts(parts: Map, labelRes: StringResource, newValue: String) = when (labelRes) { + Res.string.minute -> copyParts(parts, minutes = newValue) + Res.string.hour -> copyParts(parts, hours = newValue) + Res.string.day_of_month -> copyParts(parts, dayOfMonth = newValue) + Res.string.month -> copyParts(parts, month = newValue) + Res.string.day_of_week -> copyParts(parts, dayOfWeek = newValue) + else -> parts +} + +private fun copyParts( + parts: Map, + minutes: String? = null, + hours: String? = null, + dayOfMonth: String? = null, + month: String? = null, + dayOfWeek: String? = null +) : Map { + return parts.entries.associate { + it.key to when (it.key) { + CronPart.Minutes -> PartValue(it.value.type, minutes ?: it.value.value) + CronPart.Hours -> PartValue(it.value.type, hours ?: it.value.value) + CronPart.Days -> PartValue(it.value.type, dayOfMonth ?: it.value.value) + CronPart.Months -> PartValue(it.value.type, month ?: it.value.value) + CronPart.DaysOfWeek -> PartValue(it.value.type, dayOfWeek ?: it.value.value) + else -> it.value + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a83cd1e..f7824fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "kcron" @@ -9,5 +9,6 @@ include( "kcron-common", "kcron-common-benchmark", "kcron-core", - "kcron-kotlinx-datetime" + "kcron-kotlinx-datetime", + "kcron-ui-builder" ) \ No newline at end of file