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 c1962a79..033e24c4 100644 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 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,