From 2c088a3a7ec179301f71b5f3e7d655f0576d10fd Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 1 Dec 2023 19:21:07 -0500 Subject: [PATCH] Update all dependencies --- android/build.gradle.kts | 4 +- build.gradle.kts | 13 +- buildSrc/build.gradle.kts | 2 +- buildSrc/settings.gradle.kts | 9 ++ core/build.gradle.kts | 48 +++--- data/build.gradle.kts | 42 +++-- desktop/build.gradle.kts | 2 +- domain/build.gradle.kts | 42 +++-- .../ca/gosyer/jui/domain/server/HttpClient.kt | 5 + gradle.properties | 1 - gradle/libs.versions.toml | 90 +++++------ gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 5 +- i18n/build.gradle.kts | 16 +- ios/build.gradle.kts | 147 ++---------------- presentation/build.gradle.kts | 42 +++-- .../base/image/AndroidImageLoaderBuilder.kt | 12 +- .../base/dialog/MaterialDialogProperties.kt | 10 +- .../jui/ui/base/image/ImageLoaderProvider.kt | 4 +- .../jui/ui/library/LibraryScreenViewModel.kt | 3 +- .../jui/ui/library/components/LibraryPager.kt | 2 +- .../components/LibraryScreenContent.kt | 13 +- .../jui/ui/library/settings/LibrarySheet.kt | 3 +- .../kotlin/ca/gosyer/jui/ui/main/MainMenu.kt | 8 +- .../licenses/components/LicensesContent.kt | 5 +- .../jui/ui/manga/MangaScreenViewModel.kt | 3 +- .../ca/gosyer/jui/ui/reader/ChapterLoader.kt | 6 +- .../jui/ui/reader/ReaderMenuViewModel.kt | 4 +- .../ui/reader/loader/TachideskPageLoader.kt | 25 +-- .../jui/ui/settings/SettingsServerScreen.kt | 1 + .../browse/components/SourceScreenContent.kt | 15 +- .../ui/base/components/DesktopTooltipArea.kt | 26 +++- .../base/image/DesktopImageLoaderBuilder.kt | 4 +- .../ui/base/image/IosImageLoaderBuilder.kt | 4 +- ui-core/build.gradle.kts | 43 +++-- .../jui/uicore/image/ImageLoaderImage.kt | 58 +++---- .../ca/gosyer/jui/uicore/pager/Pager.kt | 89 ++++++++--- .../ca/gosyer/jui/uicore/vm/ViewModel.kt | 4 +- .../components/DesktopBottomActionMenu.kt | 3 + 40 files changed, 385 insertions(+), 431 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index e933a2fe..c7655f2c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -82,7 +82,7 @@ dependencies { // Utility implementation(libs.dateTime) implementation(libs.immutableCollections) - implementation(libs.kds) + implementation(libs.korge.foundation) // Localization implementation(libs.moko.core) @@ -90,7 +90,7 @@ dependencies { // Testing testImplementation(kotlin("test-junit")) - testImplementation(libs.compose.ui.test.junit4) + //testImplementation(libs.compose.ui.test.junit4) testImplementation(libs.coroutines.test) } diff --git a/build.gradle.kts b/build.gradle.kts index 4d154ecc..0a3dc684 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,14 +50,16 @@ subprojects { if (name.contains("android", true)) { jvmTarget = Config.androidJvmTarget.toString() } + freeCompilerArgs += listOf("-Xexpect-actual-classes") + if (project.hasProperty("generateComposeCompilerMetrics")) { freeCompilerArgs = freeCompilerArgs + listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_metrics", + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - project.buildDir.absolutePath + "/compose_metrics" + project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath ) } } @@ -72,7 +74,7 @@ subprojects { } plugins.withType { configure { - compileSdkVersion(33) + compileSdkVersion(34) defaultConfig { minSdk = 21 targetSdk = 31 @@ -126,9 +128,8 @@ subprojects { } } - plugins.withType { + plugins.withType { configure { - version = libs.versions.ktorfit.get() logging = project.hasProperty("debugApp") } } @@ -140,7 +141,7 @@ subprojects { } plugins.withType { configure { - kotlinCompilerPlugin.set(libs.versions.composeCompiler.get()) + // kotlinCompilerPlugin.set(libs.versions.composeCompiler.get()) } } afterEvaluate { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0f5afd96..2cc63f36 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -12,5 +12,5 @@ dependencies { implementation(gradleKotlinDsl()) implementation(gradleApi()) implementation(localGroovy()) - implementation("de.undercouch:gradle-download-task:5.3.0") + implementation(libs.gradle.download.task) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 368e0488..d00f7626 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1,2 +1,11 @@ rootProject.name = "buildSrc" + + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b4ff9e3e..8c03a8b8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -28,6 +28,21 @@ kotlin { iosArm64() iosSimulatorArm64() + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("jvm") { + withAndroidTarget() + withJvm() + } + group("ios") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { all { languageSettings { @@ -53,7 +68,7 @@ kotlin { api(libs.multiplatformSettings.coroutines) api(libs.multiplatformSettings.serialization) api(libs.dateTime) - api(libs.kds) + api(libs.korge.foundation) api(compose("org.jetbrains.compose.ui:ui-text")) } } @@ -64,53 +79,28 @@ kotlin { } } - val jvmMain by creating { - dependsOn(commonMain) + val jvmMain by getting { dependencies { api(kotlin("stdlib-jdk8")) } } - val jvmTest by creating { - dependsOn(commonTest) + val jvmTest by getting { dependencies { implementation(kotlin("test")) } } val desktopMain by getting { - dependsOn(jvmMain) dependencies { api(libs.appDirs) } } val desktopTest by getting { - dependsOn(jvmTest) } val androidMain by getting { - dependsOn(jvmMain) - dependencies { - api(libs.compose.ui.text) - } } val androidUnitTest by getting { - dependsOn(jvmTest) - } - - val iosMain by creating { - dependsOn(commonMain) - } - val iosTest by creating { - dependsOn(commonTest) - } - - listOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(iosMain) - getByName(it + "Test").dependsOn(iosTest) } } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 428a7e02..bedd89ce 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -28,6 +28,21 @@ kotlin { iosArm64() iosSimulatorArm64() + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("jvm") { + withAndroidTarget() + withJvm() + } + group("ios") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { all { languageSettings { @@ -58,47 +73,30 @@ kotlin { } } - val jvmMain by creating { - dependsOn(commonMain) + val jvmMain by getting { dependencies { api(kotlin("stdlib-jdk8")) } } - val jvmTest by creating { - dependsOn(commonTest) + val jvmTest by getting { dependencies { implementation(kotlin("test")) } } val desktopMain by getting { - dependsOn(jvmMain) } val desktopTest by getting { - dependsOn(jvmTest) } val androidMain by getting { - dependsOn(jvmMain) } val androidUnitTest by getting { - dependsOn(jvmTest) } - val iosMain by creating { - dependsOn(commonMain) + val iosMain by getting { } - val iosTest by creating { - dependsOn(commonTest) - } - - listOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(iosMain) - getByName(it + "Test").dependsOn(iosTest) + val iosTest by getting { } } } diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 8452236d..e1c19dd3 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -87,7 +87,7 @@ dependencies { // Utility implementation(libs.dateTime) implementation(libs.immutableCollections) - implementation(libs.kds) + implementation(libs.korge.foundation) // Localization implementation(libs.moko.core) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index b5522e00..90ba5703 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -29,6 +29,21 @@ kotlin { iosArm64() iosSimulatorArm64() + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("jvm") { + withAndroidTarget() + withJvm() + } + group("ios") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { all { languageSettings { @@ -66,51 +81,34 @@ kotlin { } } - val jvmMain by creating { - dependsOn(commonMain) + val jvmMain by getting { dependencies { api(kotlin("stdlib-jdk8")) api(libs.ktor.okHttp) } } - val jvmTest by creating { - dependsOn(commonTest) + val jvmTest by getting { dependencies { implementation(kotlin("test")) } } val desktopMain by getting { - dependsOn(jvmMain) } val desktopTest by getting { - dependsOn(jvmTest) } val androidMain by getting { - dependsOn(jvmMain) } val androidUnitTest by getting { - dependsOn(jvmTest) } - val iosMain by creating { - dependsOn(commonMain) + val iosMain by getting { dependencies { api(libs.ktor.darwin) } } - val iosTest by creating { - dependsOn(commonTest) - } - - listOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(iosMain) - getByName(it + "Test").dependsOn(iosTest) + val iosTest by getting { } } } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt index 805b8295..98805968 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt @@ -21,6 +21,7 @@ import io.ktor.client.plugins.auth.providers.DigestAuthCredentials import io.ktor.client.plugins.auth.providers.basic import io.ktor.client.plugins.auth.providers.digest import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging @@ -49,6 +50,10 @@ fun httpClient( expectSuccess = true + defaultRequest { + url(serverPreferences.serverUrl().get().toString()) + } + engine { proxy = when (serverPreferences.proxy().get()) { Proxy.NO_PROXY -> null diff --git a/gradle.properties b/gradle.properties index 830893c7..dbdfe01e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,5 @@ android.enableJetifier=false android.useAndroidX=true kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -kotlin.native.cacheKind=none org.jetbrains.compose.experimental.uikit.enabled=true kotlin.mpp.androidSourceSetLayoutVersion=2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 152ff642..5f26f8c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,82 +1,76 @@ [versions] # Kotlin -kotlin = "1.8.22" -coroutines = "1.7.2" +kotlin = "1.9.21" +coroutines = "1.7.3" # Serialization -json = "1.5.1" +json = "1.6.2" # Compose -composeGradle = "1.4.1" -composeCompiler = "1.4.8" -composeAndroidRuntime = "1.4.3" -composeAndroidFoundation = "1.4.3" -composeAndroidUI = "1.4.3" -composeAndroidAnimation = "1.4.3" -composeAndroidMaterial = "1.4.3" +composeGradle = "1.5.11" # Compose Libraries -voyager = "1.0.0-rc06" +voyager = "1.0.0-rc10" accompanist = "0.30.1" googleAccompanist = "0.30.1" -imageloader = "1.5.3" -materialDialogs = "0.9.3" +imageloader = "1.7.1" +materialDialogs = "0.9.4" # Android -androidGradle = "8.0.2" -core = "1.9.0" -appCompat = "1.7.0-alpha02" -activityCompose = "1.7.2" -work = "2.8.1" +androidGradle = "8.1.4" +core = "1.12.0" +appCompat = "1.7.0-alpha03" +activityCompose = "1.8.1" +work = "2.9.0" # Android Lifecycle -lifecycle = "2.6.1" +lifecycle = "2.6.2" # Swing darklaf = "3.0.2" # Ksp -ksp = "1.8.22-1.0.11" +ksp = "1.9.21-1.0.15" # Dependency Injection -kotlinInject = "0.6.1" +kotlinInject = "0.6.3" # Network -ktor = "2.3.2" -ktorfit = "1.4.2" -ktorfitCompiler = "1.0.0" +ktor = "2.3.6" +ktorfit = "1.10.2" # Logging -slf4j = "2.0.7" -log4j = "2.20.0" +slf4j = "2.0.9" +log4j = "2.22.0" kmlogging = "1.3.0" # Storage -okio = "3.3.0" +okio = "3.6.0" appDirs = "1.2.1" # Preferences multiplatformSettings = "1.0.0-alpha01" # Utility -desugarJdkLibs = "2.0.3" -aboutLibraries = "10.8.0" -dateTime = "0.4.0" -immutableCollections = "0.3.5" -kds = "4.0.7" +desugarJdkLibs = "2.0.4" +aboutLibraries = "10.9.2" +dateTime = "0.5.0" +immutableCollections = "0.3.6" +korge = "5.1.0" +gradleDownloadTask = "5.4.0" # Localization moko = "0.23.0" # BuildConfigs -buildconfig = "4.1.1" -buildkonfig = "0.13.3" +buildconfig = "4.2.0" +buildkonfig = "0.15.1" # Linter -kotlinter = "3.15.0" +kotlinter = "4.1.0" # Version updates -versions = "0.47.0" +versions = "0.50.0" # Optimizer proguard = "7.3.2" @@ -92,16 +86,6 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve serialization-json-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "json" } serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "json" } -# Compose -compose-animation = { module = "androidx.compose.animation:animation", version.ref = "composeAndroidAnimation" } -compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "composeAndroidFoundation" } -compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeAndroidRuntime" } -compose-ui-core = { module = "androidx.compose.ui:ui", version.ref = "composeAndroidUI" } -compose-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "composeAndroidUI" } -compose-ui-text = { module = "androidx.compose.ui:ui-text", version.ref = "composeAndroidUI" } -compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeAndroidUI" } -compose-material-core = { module = "androidx.compose.material:material", version.ref = "composeAndroidMaterial" } -compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "composeAndroidMaterial" } # Compose UI voyager-core = { module = "cafe.adriel.voyager:voyager-core", version.ref = "voyager" } voyager-navigation = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } @@ -168,7 +152,7 @@ aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref aboutLibraries-ui = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" } dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTime" } immutableCollections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutableCollections" } -kds = { module = "com.soywiz.korlibs.kds:kds", version.ref = "kds" } +korge-foundation = { module = "com.soywiz.korge:korge-foundation", version.ref = "korge" } # Localization moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" } @@ -177,6 +161,9 @@ moko-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "m # Optimizer proguard = { module = "com.guardsquare:proguard-gradle", version.ref = "proguard" } +# Gradle +gradle-download-task = { module = "de.undercouch:gradle-download-task", version.ref = "gradleDownloadTask" } + [plugins] # Kotlin kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -195,7 +182,7 @@ compose = { id = "org.jetbrains.compose", version.ref = "composeGradle" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } # Network -ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfitCompiler" } +ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" } # Localization moko-gradle = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } @@ -215,11 +202,4 @@ aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "abo [bundles] compose-android = [ - "compose-animation", - "compose-foundation", - "compose-runtime", - "compose-ui-core", - "compose-ui-util", - "compose-material-core", - "compose-material-icons" ] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 16170 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 delta 14892 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a3638774..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..fcb6fca1 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts index 03084ac8..37de7a9b 100644 --- a/i18n/build.gradle.kts +++ b/i18n/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -31,6 +31,8 @@ kotlin { iosArm64(configure = configuration) iosSimulatorArm64(configure = configuration) + applyDefaultHierarchyTemplate() + sourceSets { val commonMain by getting { dependencies { @@ -44,6 +46,14 @@ kotlin { implementation(kotlin("test-annotations-common")) } } + + getByName("desktopMain") { + dependsOn(commonMain) + } + + getByName("androidMain") { + dependsOn(commonMain) + } } } @@ -65,7 +75,7 @@ android { } sourceSets.getByName("main") { - assets.srcDir(File(buildDir, "generated/moko/androidMain/assets")) - res.srcDir(File(buildDir, "generated/moko/androidMain/res")) + assets.srcDir(File(layout.buildDirectory.asFile.get(), "generated/moko/androidMain/assets")) + res.srcDir(File(layout.buildDirectory.asFile.get(), "generated/moko/androidMain/res")) } } diff --git a/ios/build.gradle.kts b/ios/build.gradle.kts index 5611ca1a..9f5bea42 100644 --- a/ios/build.gradle.kts +++ b/ios/build.gradle.kts @@ -1,9 +1,5 @@ import org.jetbrains.compose.compose -import org.jetbrains.compose.experimental.uikit.tasks.ExperimentalPackComposeApplicationForXCodeTask import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink -import java.io.File -import kotlin.reflect.full.declaredMemberProperties @Suppress("DSL_SCOPE_VIOLATION") plugins { @@ -17,28 +13,28 @@ plugins { kotlin { val configuration: KotlinNativeTarget.() -> Unit = { - binaries { - executable { - entryPoint = "ca.gosyer.jui.ios.main" - freeCompilerArgs = freeCompilerArgs + listOf( - "-linker-option", "-framework", "-linker-option", "Metal", - "-linker-option", "-framework", "-linker-option", "CoreText", - "-linker-option", "-framework", "-linker-option", "CoreGraphics" - ) - // TODO: the current compose binary surprises LLVM, so disable checks for now. - freeCompilerArgs = freeCompilerArgs + "-Xdisable-phases=VerifyBitcode" - } + binaries.framework { + baseName = "ios" + isStatic = true } } iosX64("uikitX64", configuration) iosArm64("uikitArm64", configuration) iosSimulatorArm64("uikitSimulatorArm64", configuration) + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("uikit") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { - val commonMain by getting - val commonTest by getting - val uikitMain by creating { - dependsOn(commonMain) + val uikitMain by getting { dependencies { implementation(projects.core) implementation(projects.i18n) @@ -100,7 +96,7 @@ kotlin { // Utility implementation(libs.dateTime) implementation(libs.immutableCollections) - implementation(libs.kds) + implementation(libs.korge.foundation) // Localization implementation(libs.moko.core) @@ -112,30 +108,7 @@ kotlin { testImplementation(libs.coroutines.test)*/ } } - val uikitTest by creating { - dependsOn(commonTest) - } - - listOf( - "uikitX64", - "uikitArm64", - "uikitSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(uikitMain) - getByName(it + "Test").dependsOn(uikitTest) - } - } -} - -compose.experimental { - uikit.application { - bundleIdPrefix = "ca.gosyer.jui.app.ios" - projectName = "Tachidesk-JUI" - // ./gradlew :app:ios:iosDeployIPhone13Debug - deployConfigurations { - simulator("IPhone13") { - device = org.jetbrains.compose.experimental.dsl.IOSDevices.IPHONE_13 - } + val uikitTest by getting { } } } @@ -151,92 +124,6 @@ dependencies { } } -kotlin { - targets.withType { - binaries.all { - // TODO: the current compose binary surprises LLVM, so disable checks for now. - freeCompilerArgs = freeCompilerArgs + "-Xdisable-phases=VerifyBitcode" - } - } -} - buildkonfig { packageName = "ca.gosyer.jui.ios.build" } - -// todo: Remove when resolved: https://github.com/icerockdev/moko-resources/issues/372 -// copy .bundle from all .klib to .kexe -tasks.withType() - .configureEach { - val linkTask: KotlinNativeLink = this - val outputDir: File = this.outputFile.get().parentFile - - @Suppress("ObjectLiteralToLambda") // lambda broke up-to-date - val action = object : Action { - override fun execute(t: Task) { - (linkTask.libraries + linkTask.sources) - .filter { library -> library.extension == "klib" } - .filter(File::exists) - .forEach { inputFile -> - val klibKonan = org.jetbrains.kotlin.konan.file.File(inputFile.path) - val klib = org.jetbrains.kotlin.library.impl.KotlinLibraryLayoutImpl( - klib = klibKonan, - component = "default" - ) - val layout = klib.extractingToTemp - - // extracting bundles - layout - .resourcesDir - .absolutePath - .let(::File) - .listFiles { file: File -> file.extension == "bundle" } - // copying bundles to app - ?.forEach { - logger.info("${it.absolutePath} copying to $outputDir") - it.copyRecursively( - target = File(outputDir, it.name), - overwrite = true - ) - } - } - } - } - doLast(action) - } - -// copy .bundle from .kexe to .app -tasks.withType() - .configureEach { - val packTask: ExperimentalPackComposeApplicationForXCodeTask = this - - val kclass = ExperimentalPackComposeApplicationForXCodeTask::class - val kotlinBinaryField = - kclass.declaredMemberProperties.single { it.name == "kotlinBinary" } - val destinationDirField = - kclass.declaredMemberProperties.single { it.name == "destinationDir" } - val executablePathField = - kclass.declaredMemberProperties.single { it.name == "executablePath" } - - @Suppress("ObjectLiteralToLambda") // lambda broke up-to-date - val action = object : Action { - override fun execute(t: Task) { - val kotlinBinary: RegularFile = - (kotlinBinaryField.get(packTask) as RegularFileProperty).get() - val destinationDir: Directory = - (destinationDirField.get(packTask) as DirectoryProperty).get() - val executablePath: String = - (executablePathField.get(packTask) as Provider).get() - - val outputDir: File = File(destinationDir.asFile, executablePath).parentFile - - val bundleSearchDir: File = kotlinBinary.asFile.parentFile - bundleSearchDir - .listFiles { file: File -> file.extension == "bundle" } - ?.forEach { file -> - file.copyRecursively(File(outputDir, file.name), true) - } - } - } - doLast(action) - } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index a285f827..cbd67d79 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -29,6 +29,21 @@ kotlin { iosArm64() iosSimulatorArm64() + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("jvm") { + withAndroidTarget() + withJvm() + } + group("ios") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { all { languageSettings { @@ -78,33 +93,28 @@ kotlin { } } - val jvmMain by creating { - dependsOn(commonMain) + val jvmMain by getting { dependencies { api(kotlin("stdlib-jdk8")) api(compose.desktop.currentOs) } } - val jvmTest by creating { - dependsOn(commonTest) + val jvmTest by getting { dependencies { implementation(kotlin("test")) } } val desktopMain by getting { - dependsOn(jvmMain) dependencies { api(libs.coroutines.swing) } } val desktopTest by getting { - dependsOn(jvmTest) } val androidMain by getting { - dependsOn(jvmMain) dependencies { api(libs.bundles.compose.android) api(libs.androidx.core) @@ -114,23 +124,11 @@ kotlin { } } val androidUnitTest by getting { - dependsOn(jvmTest) } - val iosMain by creating { - dependsOn(commonMain) + val iosMain by getting { } - val iosTest by creating { - dependsOn(commonTest) - } - - listOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(iosMain) - getByName(it + "Test").dependsOn(iosTest) + val iosTest by getting { } } } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt index 81e1cc60..2308e573 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt @@ -9,20 +9,22 @@ package ca.gosyer.jui.ui.base.image import android.os.Build import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.Bitmap +import com.seiko.imageloader.BitmapConfig import com.seiko.imageloader.cache.disk.DiskCacheBuilder import com.seiko.imageloader.cache.memory.MemoryCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryKey import com.seiko.imageloader.component.ComponentRegistryBuilder import com.seiko.imageloader.component.setupDefaultComponents -import com.seiko.imageloader.option.Options import com.seiko.imageloader.option.OptionsBuilder import com.seiko.imageloader.option.androidContext import okio.Path.Companion.toOkioPath actual fun OptionsBuilder.configure(contextWrapper: ContextWrapper) { - imageConfig = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - Options.ImageConfig.ARGB_8888 + bitmapConfig = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + BitmapConfig.ARGB_8888 } else { - Options.ImageConfig.HARDWARE + BitmapConfig.HARDWARE } androidContext(contextWrapper) } @@ -42,5 +44,5 @@ actual fun DiskCacheBuilder.configure( maxSizeBytes(1024 * 1024 * 150) // 150 MB } -actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { +actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/dialog/MaterialDialogProperties.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/dialog/MaterialDialogProperties.kt index 0156ac30..bdf9e73a 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/dialog/MaterialDialogProperties.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/dialog/MaterialDialogProperties.kt @@ -35,10 +35,10 @@ fun getMaterialDialogProperties( dismissOnClickOutside = dismissOnClickOutside, securePolicy = securePolicy, usePlatformDefaultWidth = usePlatformDefaultWidth, - position = position, - size = size, - title = title, - icon = icon, - resizable = resizable, + windowPosition = position, + windowSize = size, + windowTitle = title, + windowIcon = icon, + windowIsResizable = resizable, ) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt index 36d47fe1..96c11c8b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt @@ -13,9 +13,11 @@ import ca.gosyer.jui.domain.server.service.ServerPreferences import ca.gosyer.jui.domain.source.model.Source import ca.gosyer.jui.ui.base.ImageCache import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.Bitmap import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.cache.disk.DiskCacheBuilder import com.seiko.imageloader.cache.memory.MemoryCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryKey import com.seiko.imageloader.component.ComponentRegistryBuilder import com.seiko.imageloader.component.fetcher.MokoResourceFetcher import com.seiko.imageloader.component.keyer.Keyer @@ -138,4 +140,4 @@ expect fun DiskCacheBuilder.configure( cacheDir: String, ) -expect fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) +expect fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt index 2bf47022..4d521c03 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt @@ -27,7 +27,6 @@ import ca.gosyer.jui.ui.base.state.getStateFlow import ca.gosyer.jui.ui.util.lang.CollatorComparator import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ViewModel -import cafe.adriel.voyager.core.model.coroutineScope import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -198,7 +197,7 @@ class LibraryScreenViewModel log.warn(it) { "Failed to get manga list from category ${category.name}" } library.mangaMap.setError(category.id, it) } - .launchIn(coroutineScope) + .launchIn(scope) } } .catch { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt index 7a49ab8c..328001e1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt @@ -37,7 +37,7 @@ fun LibraryPager( ) { if (categories.isEmpty()) return - HorizontalPager(categories.size, state = pagerState) { + HorizontalPager(state = pagerState) { when (val library = getLibraryForPage(categories[it].id).value) { CategoryState.Loading -> LoadingScreen() is CategoryState.Failed -> ErrorScreen(library.e.message) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt index 6ee749b5..70a1ba93 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt @@ -12,7 +12,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -90,7 +89,9 @@ fun LibraryScreenContent( } BoxWithConstraints { - val pagerState = rememberPagerState(selectedCategoryIndex) + val pagerState = rememberPagerState(selectedCategoryIndex) { + (libraryState as? LibraryState.Loaded)?.categories?.size ?: 1 + } LaunchedEffect(pagerState.isScrollInProgress to pagerState.currentPage) { if (!pagerState.isScrollInProgress && pagerState.currentPage != selectedCategoryIndex) { onPageChanged(pagerState.currentPage) @@ -240,11 +241,7 @@ fun WideLibraryScreenContent( if (showingMenu) { Box( Modifier.fillMaxSize().pointerInput(Unit) { - forEachGesture { - detectTapGestures { - setShowingMenu(false) - } - } + detectTapGestures(onTap = { setShowingMenu(false) }) }, ) } @@ -295,7 +292,7 @@ fun ThinLibraryScreenContent( ) { val bottomSheetState = rememberModalBottomSheetState( ModalBottomSheetValue.Hidden, - confirmStateChange = { + confirmValueChange = { when (it) { ModalBottomSheetValue.Hidden -> setShowingSheet(false) ModalBottomSheetValue.Expanded, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt index 0fcd7623..f1a136c5 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt @@ -49,7 +49,7 @@ fun LibrarySheet( librarySort: @Composable () -> Unit, libraryDisplay: @Composable () -> Unit, ) { - val pagerState = rememberPagerState() + val pagerState = rememberPagerState { LibrarySheetTabs.values().size } val selectedPage = pagerState.currentPage val scope = rememberCoroutineScope() Column(Modifier.fillMaxSize()) { @@ -72,7 +72,6 @@ fun LibrarySheet( } } HorizontalPager( - pageCount = LibrarySheetTabs.values().size, state = pagerState, verticalAlignment = Alignment.Top, ) { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt index 4f81393d..e2754815 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.consumedWindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding @@ -110,7 +110,11 @@ fun WideMainMenu( } withDisplayController(controller) { val insets = WindowInsets.navigationBars.only(WindowInsetsSides.Start) - MainWindow(navigator, Modifier.padding(start = startPadding).windowInsetsPadding(insets).consumedWindowInsets(insets)) + MainWindow(navigator, + Modifier.padding(start = startPadding) + .windowInsetsPadding(insets) + .consumeWindowInsets(insets) + ) } } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/components/LicensesContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/components/LicensesContent.kt index cd246b3c..842fe5c7 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/components/LicensesContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/components/LicensesContent.kt @@ -35,6 +35,7 @@ import ca.gosyer.jui.uicore.insets.statusBars import ca.gosyer.jui.uicore.resources.stringResource import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.ui.compose.Libraries +import com.mikepenz.aboutlibraries.ui.compose.util.StableLibrary import kotlinx.collections.immutable.toImmutableList @Composable @@ -58,10 +59,10 @@ fun LicensesContent() { val state = rememberLazyListState() val uriHandler = LocalUriHandler.current Libraries( - libraries = remember(libs) { libs.libraries.toImmutableList() }, + libraries = remember(libs) { libs.libraries.map { StableLibrary(it) }.toImmutableList() }, lazyListState = state, onLibraryClick = { - it.website?.let(uriHandler::openUri) + it.library.website?.let(uriHandler::openUri) }, contentPadding = WindowInsets.bottomNav.add( WindowInsets.navigationBars.only( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt index ef44b0d2..85b13de3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt @@ -33,7 +33,6 @@ import ca.gosyer.jui.ui.base.chapter.ChapterDownloadState import ca.gosyer.jui.ui.base.model.StableHolder import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ViewModel -import cafe.adriel.voyager.core.model.coroutineScope import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -94,7 +93,7 @@ class MangaScreenViewModel private val loadingManga = MutableStateFlow(true) private val loadingChapters = MutableStateFlow(true) val isLoading = combine(loadingManga, loadingChapters) { a, b -> a || b } - .stateIn(coroutineScope, SharingStarted.Eagerly, true) + .stateIn(scope, SharingStarted.Eagerly, true) val categories = getCategories.asFlow(true) .map { it.toImmutableList() } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt index bffabf09..e3a474bb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt @@ -6,8 +6,8 @@ package ca.gosyer.jui.ui.reader -import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage import ca.gosyer.jui.domain.reader.service.ReaderPreferences +import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory import ca.gosyer.jui.ui.reader.loader.PagesState import ca.gosyer.jui.ui.reader.loader.TachideskPageLoader @@ -23,7 +23,7 @@ import org.lighthousegames.logging.logging class ChapterLoader( private val readerPreferences: ReaderPreferences, - private val getChapterPage: GetChapterPage, + private val http: Http, private val chapterCache: DiskCache, private val bitmapDecoderFactory: BitmapDecoderFactory, ) { @@ -34,7 +34,7 @@ class ChapterLoader( chapter.state = ReaderChapter.State.Loading log.debug { "Loading pages for ${chapter.chapter.name}" } - val loader = TachideskPageLoader(chapter, readerPreferences, getChapterPage, chapterCache, bitmapDecoderFactory) + val loader = TachideskPageLoader(chapter, readerPreferences, http, chapterCache, bitmapDecoderFactory) val pages = loader.getPages() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt index de422433..47136bbb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt @@ -22,6 +22,7 @@ import ca.gosyer.jui.domain.manga.model.MangaMeta import ca.gosyer.jui.domain.reader.ReaderModeWatch import ca.gosyer.jui.domain.reader.model.Direction import ca.gosyer.jui.domain.reader.service.ReaderPreferences +import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.ui.base.ChapterCache import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory import ca.gosyer.jui.ui.base.model.StableHolder @@ -82,6 +83,7 @@ class ReaderMenuViewModel private val updateMangaMeta: UpdateMangaMeta, private val updateChapterMeta: UpdateChapterMeta, private val chapterCache: ChapterCache, + private val http: Http, contextWrapper: ContextWrapper, @Assisted private val params: Params, ) : ViewModel(contextWrapper) { @@ -152,7 +154,7 @@ class ReaderMenuViewModel private val loader = ChapterLoader( readerPreferences = readerPreferences, - getChapterPage = getChapterPage, + http = http, chapterCache = chapterCache, bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper), ) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt index aa75161d..57b732e0 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt @@ -13,6 +13,7 @@ import ca.gosyer.jui.core.lang.PriorityChannel import ca.gosyer.jui.core.lang.throwIfCancellation import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage import ca.gosyer.jui.domain.reader.service.ReaderPreferences +import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory import ca.gosyer.jui.ui.base.model.StableHolder import ca.gosyer.jui.ui.reader.model.ReaderChapter @@ -23,10 +24,10 @@ import com.seiko.imageloader.asImageBitmap import com.seiko.imageloader.cache.disk.DiskCache import com.seiko.imageloader.component.decoder.DecodeResult import com.seiko.imageloader.model.DataSource -import com.seiko.imageloader.model.ImageRequest import com.seiko.imageloader.model.ImageResult import com.seiko.imageloader.option.Options import io.ktor.client.plugins.onDownload +import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsChannel import kotlinx.coroutines.CoroutineScope @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest @@ -53,7 +55,7 @@ import org.lighthousegames.logging.logging class TachideskPageLoader( val chapter: ReaderChapter, readerPreferences: ReaderPreferences, - private val getChapterPage: GetChapterPage, + private val http: Http, private val chapterCache: DiskCache, private val bitmapDecoderFactory: BitmapDecoderFactory, ) : PageLoader() { @@ -109,10 +111,14 @@ class TachideskPageLoader( private suspend fun fetchImage(page: ReaderPage) { log.debug { "Loading page ${page.index}" } - getChapterPage.asFlow(chapter.chapter, page.index) { - onDownload { bytesSentTotal, contentLength -> - page.progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) + flow { + val response = http.get("api/v1/manga/${chapter.chapter.mangaId}/chapter/${chapter.chapter.index}/page/${page.index}") { + onDownload { bytesSentTotal, contentLength -> + page.progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F) + } } + + emit(response) } .onEach { putImageInCache(it, page) @@ -134,7 +140,7 @@ class TachideskPageLoader( response: HttpResponse, page: ReaderPage, ) { - val editor = chapterCache.edit(page.cacheKey) + val editor = chapterCache.openEditor(page.cacheKey) ?: throw Exception("Couldn't open cache") try { FileSystem.SYSTEM.write(editor.data) { @@ -150,18 +156,17 @@ class TachideskPageLoader( } private suspend fun getImageFromCache(page: ReaderPage): ReaderPage.ImageDecodeState { - return chapterCache[page.cacheKey]?.use { + return chapterCache.openSnapshot(page.cacheKey)?.use { it.source().use { source -> val decoder = bitmapDecoderFactory.create( - ImageResult.Source( - ImageRequest(Any()), + ImageResult.OfSource( source, DataSource.Engine, ), Options(), ) if (decoder != null) { - runCatching { decoder.decode() as DecodeResult.Bitmap } + runCatching { decoder.decode() as DecodeResult.OfBitmap } .mapCatching { ReaderPage.ImageDecodeState.Success( it.bitmap.asImageBitmap().also { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt index b32cd4a3..d8ab3d76 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt @@ -86,6 +86,7 @@ class SettingsServerScreen : Screen { expect class SettingsServerHostViewModel : ViewModel +@Composable expect fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit class SettingsServerViewModel diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt index 735e7983..3e9595bb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt @@ -12,7 +12,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.WindowInsets @@ -228,11 +227,7 @@ private fun SourceWideScreenContent( if (showingFilters && !isLatest) { Box( Modifier.fillMaxSize().pointerInput(loading) { - forEachGesture { - detectTapGestures { - setShowingFilters(false) - } - } + detectTapGestures(onTap = { setShowingFilters(false) }) }, ) } @@ -287,7 +282,7 @@ private fun SourceThinScreenContent( ) { val bottomSheetState = rememberModalBottomSheetState( ModalBottomSheetValue.Hidden, - confirmStateChange = { + confirmValueChange = { when (it) { ModalBottomSheetValue.Hidden -> setShowingFilters(false) ModalBottomSheetValue.Expanded, @@ -364,11 +359,7 @@ private fun SourceThinScreenContent( if (showingFilters && !isLatest) { Box( Modifier.fillMaxSize().pointerInput(loading) { - forEachGesture { - detectTapGestures { - setShowingFilters(false) - } - } + detectTapGestures(onTap = { setShowingFilters(false) }) }, ) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/components/DesktopTooltipArea.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/components/DesktopTooltipArea.kt index 94c8a531..ffa0bef4 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/components/DesktopTooltipArea.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/components/DesktopTooltipArea.kt @@ -8,13 +8,35 @@ package ca.gosyer.jui.ui.base.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +@OptIn(ExperimentalFoundationApi::class) actual typealias TooltipPlacement = androidx.compose.foundation.TooltipPlacement -actual typealias CursorPointImpl = androidx.compose.foundation.TooltipPlacement.CursorPoint +@OptIn(ExperimentalFoundationApi::class) +actual class CursorPointImpl actual constructor( + offset: DpOffset, + alignment: Alignment, + windowMargin: Dp, +) : TooltipPlacement by androidx.compose.foundation.TooltipPlacement.CursorPoint( + offset = offset, + alignment = alignment, + windowMargin = windowMargin +) -actual typealias ComponentRectImpl = androidx.compose.foundation.TooltipPlacement.ComponentRect +@OptIn(ExperimentalFoundationApi::class) +actual class ComponentRectImpl actual constructor( + anchor: Alignment, + alignment: Alignment, + offset: DpOffset, +) : TooltipPlacement by androidx.compose.foundation.TooltipPlacement.ComponentRect( + anchor = anchor, + alignment = alignment, + offset = offset +) @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt index 14f2aeff..e1e37913 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt @@ -9,8 +9,10 @@ package ca.gosyer.jui.ui.base.image import ca.gosyer.jui.core.io.userDataDir import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.Bitmap import com.seiko.imageloader.cache.disk.DiskCacheBuilder import com.seiko.imageloader.cache.memory.MemoryCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryKey import com.seiko.imageloader.component.ComponentRegistryBuilder import com.seiko.imageloader.component.setupDefaultComponents import com.seiko.imageloader.option.OptionsBuilder @@ -33,5 +35,5 @@ actual fun DiskCacheBuilder.configure( maxSizeBytes(1024 * 1024 * 150) // 150 MB } -actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { +actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt index 36cdb825..938dd0ec 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt @@ -8,8 +8,10 @@ package ca.gosyer.jui.ui.base.image import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.Bitmap import com.seiko.imageloader.cache.disk.DiskCacheBuilder import com.seiko.imageloader.cache.memory.MemoryCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryKey import com.seiko.imageloader.cache.memory.maxSizePercent import com.seiko.imageloader.component.ComponentRegistryBuilder import com.seiko.imageloader.component.setupDefaultComponents @@ -47,6 +49,6 @@ private fun getCacheDir(): String { )!!.path.orEmpty() } -actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { +actual fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) { maxSizePercent(0.25) } diff --git a/ui-core/build.gradle.kts b/ui-core/build.gradle.kts index 6b7e6286..e3c68e15 100644 --- a/ui-core/build.gradle.kts +++ b/ui-core/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } kotlin { - android { + androidTarget { compilations { all { kotlinOptions.jvmTarget = Config.androidJvmTarget.toString() @@ -28,6 +28,21 @@ kotlin { iosArm64() iosSimulatorArm64() + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + applyHierarchyTemplate { + common { + group("jvm") { + withAndroidTarget() + withJvm() + } + group("ios") { + withIosX64() + withIosArm64() + withIosSimulatorArm64() + } + } + } + sourceSets { all { languageSettings { @@ -63,29 +78,24 @@ kotlin { } } - val jvmMain by creating { - dependsOn(commonMain) + val jvmMain by getting { dependencies { api(kotlin("stdlib-jdk8")) api(compose.desktop.currentOs) } } - val jvmTest by creating { - dependsOn(commonTest) + val jvmTest by getting { dependencies { implementation(kotlin("test")) } } val desktopMain by getting { - dependsOn(jvmMain) } val desktopTest by getting { - dependsOn(jvmTest) } val androidMain by getting { - dependsOn(jvmMain) dependencies { api(libs.bundles.compose.android) api(libs.androidx.core) @@ -93,23 +103,6 @@ kotlin { } } val androidUnitTest by getting { - dependsOn(jvmTest) - } - - val iosMain by creating { - dependsOn(commonMain) - } - val iosTest by creating { - dependsOn(commonTest) - } - - listOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - ).forEach { - getByName(it + "Main").dependsOn(iosMain) - getByName(it + "Test").dependsOn(iosTest) } } } diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt index edb9ee4e..2fdc1ca5 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt @@ -35,9 +35,11 @@ import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import ca.gosyer.jui.uicore.components.LoadingScreen -import com.seiko.imageloader.ImageRequestState +import com.seiko.imageloader.model.ImageAction import com.seiko.imageloader.model.ImageRequest -import com.seiko.imageloader.rememberAsyncImagePainter +import com.seiko.imageloader.rememberImageAction +import com.seiko.imageloader.rememberImageActionPainter +import com.seiko.imageloader.rememberImagePainter import org.lighthousegames.logging.logging private val log = logging() @@ -84,32 +86,31 @@ fun ImageLoaderImage( ) { key(data) { val request = remember { ImageRequest(data) } - val painter = rememberAsyncImagePainter( - request, - contentScale = contentScale, - filterQuality = filterQuality, - ) + if (animationSpec != null) { + val imageAction by rememberImageAction(request) - val progress = remember { mutableStateOf(-1F) } - val error = remember { mutableStateOf(null) } - val state by derivedStateOf { - when (val state = painter.requestState) { - is ImageRequestState.Failure -> { - progress.value = 0.0F - error.value = state.error - ImageLoaderImageState.Failure - } - is ImageRequestState.Loading -> { - progress.value = 0.0F - ImageLoaderImageState.Loading - } - ImageRequestState.Success -> { - progress.value = 1.0F - ImageLoaderImageState.Success + val progress = remember { mutableStateOf(-1F) } + val error = remember { mutableStateOf(null) } + val state by derivedStateOf { + when (val action = imageAction) { + is ImageAction.Failure -> { + progress.value = 0.0F + error.value = action.error + ImageLoaderImageState.Failure + } + is ImageAction.Loading -> { + progress.value = 0.0F + ImageLoaderImageState.Loading + } + is ImageAction.Success -> { + progress.value = 1.0F + ImageLoaderImageState.Success + } + else -> { + ImageLoaderImageState.Loading + } } } - } - if (animationSpec != null) { Crossfade(state, animationSpec = animationSpec, modifier = modifier) { Box(Modifier.fillMaxSize(), contentAlignment) { when (it) { @@ -117,7 +118,10 @@ fun ImageLoaderImage( onLoading(progress.value) } ImageLoaderImageState.Success -> Image( - painter = painter, + painter = rememberImageActionPainter( + imageAction, + filterQuality = filterQuality + ), contentDescription = contentDescription, modifier = Modifier.fillMaxSize(), alignment = alignment, @@ -136,7 +140,7 @@ fun ImageLoaderImage( } else { Box(modifier, contentAlignment) { Image( - painter = painter, + painter = rememberImagePainter(request, filterQuality = filterQuality), contentDescription = contentDescription, modifier = Modifier.fillMaxSize(), alignment = alignment, diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt index f22f526e..97182a50 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt @@ -6,9 +6,12 @@ package ca.gosyer.jui.uicore.pager +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout.Companion.CenterToCenter import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,6 +41,8 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastSumBy import kotlinx.coroutines.flow.distinctUntilChanged +import kotlin.math.abs +import kotlin.math.sign @Composable fun VerticalPager( @@ -229,9 +234,7 @@ class PagerState( // https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt private fun lazyListSnapLayoutInfoProvider( lazyListState: LazyListState, - positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize -> - layoutSize / 2f - itemSize / 2f - }, + positionInLayout: SnapPositionInLayout = CenterToCenter ) = object : SnapLayoutInfoProvider { private val layoutInfo: LazyListLayoutInfo get() = lazyListState.layoutInfo @@ -239,13 +242,21 @@ private fun lazyListSnapLayoutInfoProvider( // Single page snapping is the default override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f - override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange { + override fun Density.calculateSnappingOffset(currentVelocity: Float): Float { var lowerBoundOffset = Float.NEGATIVE_INFINITY var upperBoundOffset = Float.POSITIVE_INFINITY layoutInfo.visibleItemsInfo.fastForEach { item -> val offset = - calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout) + calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize = layoutInfo.singleAxisViewportSize, + beforeContentPadding = layoutInfo.beforeContentPadding, + afterContentPadding = layoutInfo.afterContentPadding, + itemSize = item.size, + itemOffset = item.offset, + itemIndex = item.index, + snapPositionInLayout = positionInLayout + ) // Find item that is closest to the center if (offset <= 0 && offset > lowerBoundOffset) { @@ -258,7 +269,58 @@ private fun lazyListSnapLayoutInfoProvider( } } - return lowerBoundOffset.rangeTo(upperBoundOffset) + return calculateFinalOffset( + currentVelocity, + lowerBoundOffset, + upperBoundOffset + ) + } + + @OptIn(ExperimentalFoundationApi::class) + private fun Density.calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize: Int, + beforeContentPadding: Int, + afterContentPadding: Int, + itemSize: Int, + itemOffset: Int, + itemIndex: Int, + snapPositionInLayout: SnapPositionInLayout + ): Float { + val containerSize = mainAxisViewPortSize - beforeContentPadding - afterContentPadding + + val desiredDistance = with(snapPositionInLayout) { + position(containerSize, itemSize, itemIndex) + }.toFloat() + + return itemOffset - desiredDistance + } + + + private fun calculateFinalOffset(velocity: Float, lowerBound: Float, upperBound: Float): Float { + + fun Float.isValidDistance(): Boolean { + return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY + } + + val finalDistance = when (sign(velocity)) { + 0f -> { + if (abs(upperBound) <= abs(lowerBound)) { + upperBound + } else { + lowerBound + } + } + + 1f -> upperBound + -1f -> lowerBound + else -> 0f + } + + return if (finalDistance.isValidDistance()) { + finalDistance + } else { + 0f + } } override fun Density.calculateSnapStepSize(): Float = @@ -277,20 +339,5 @@ private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): Fli return rememberSnapFlingBehavior(snappingLayout) } -private fun calculateDistanceToDesiredSnapPosition( - layoutInfo: LazyListLayoutInfo, - item: LazyListItemInfo, - positionInLayout: (layoutSize: Float, itemSize: Float) -> Float, -): Float { - val containerSize = - with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding } - - val desiredDistance = - positionInLayout(containerSize.toFloat(), item.size.toFloat()) - - val itemCurrentPosition = item.offset - return itemCurrentPosition - desiredDistance -} - private val LazyListLayoutInfo.singleAxisViewportSize: Int get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt index ff7b56ce..f8e2c402 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt @@ -10,7 +10,7 @@ import ca.gosyer.jui.core.lang.launchUI import ca.gosyer.jui.core.prefs.Preference import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.model.coroutineScope +import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch abstract class ViewModel(private val contextWrapper: ContextWrapper) : ScreenModel { protected open val scope: CoroutineScope - get() = coroutineScope + get() = screenModelScope fun Preference.asStateFlow() = PreferenceMutableStateFlow(this, scope) diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt index d464cfd7..351d2ffb 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.uicore.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource @@ -14,12 +15,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds +@OptIn(ExperimentalFoundationApi::class, ExperimentalCoroutinesApi::class) actual fun Modifier.buttonModifier( onClick: () -> Unit, onHintClick: () -> Unit,