diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 737cb9bf..59f0e49d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -140,3 +140,7 @@ kotlin { languageVersion.set(JavaLanguageVersion.of(Config.androidJvmTarget.target)) } } + +configurations.all { + exclude(group = "org.jetbrains.runtime", module = "jbr-api") +} diff --git a/build.gradle.kts b/build.gradle.kts index 81e2264f..e78e9c0a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import Config.migrationCode import Config.serverCode import Config.tachideskVersion import com.codingfeline.buildkonfig.compiler.FieldSpec.Type +import com.google.devtools.ksp.gradle.KspAATask import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.ComposePlugin import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -77,7 +78,7 @@ subprojects { } plugins.withType { configure { - compileSdkVersion(34) + compileSdkVersion(36) defaultConfig { minSdk = 21 targetSdk = 31 @@ -103,6 +104,9 @@ subprojects { } } plugins.withType { + tasks.withType { + mustRunAfter("generateBuildKonfig") + } configure { defaultConfigs { buildConfigField(Type.STRING, "NAME", rootProject.name, const = true) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index a05bc862..58645a89 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -4,11 +4,10 @@ object Config { const val migrationCode = 5 // Suwayomi-Server version - const val tachideskVersion = "v1.0.0" + const val tachideskVersion = "v2.1.1959" // Match this to the Suwayomi-Server commit count - const val serverCode = 1498 - const val preview = false - const val previewCommit = "54df9d634a1e83143a6cacf6206b6504721b6ca8" + const val serverCode = 1959 + const val preview = true val desktopJvmTarget = JvmTarget.JVM_17 val androidJvmTarget = JvmTarget.JVM_17 diff --git a/buildSrc/src/main/kotlin/TachideskTasks.kt b/buildSrc/src/main/kotlin/TachideskTasks.kt index 6e6d4092..51bed6ad 100644 --- a/buildSrc/src/main/kotlin/TachideskTasks.kt +++ b/buildSrc/src/main/kotlin/TachideskTasks.kt @@ -1,5 +1,4 @@ import Config.preview -import Config.previewCommit import Config.serverCode import Config.tachideskVersion import de.undercouch.gradle.tasks.download.Download @@ -66,7 +65,7 @@ private fun isSigning(properties: Map) = properties["compose.deskt private const val tmpPath = "tmp" private val apiUrl = if (preview) { - "https://api.github.com/repos/Suwayomi/Suwayomi-Server-preview/releases/tags/$previewCommit" + "https://api.github.com/repos/Suwayomi/Suwayomi-Server-preview/releases/tags/$tachideskVersion" } else { "https://api.github.com/repos/Suwayomi/Suwayomi-Server/releases/tags/$tachideskVersion" } @@ -140,8 +139,8 @@ fun TaskContainerScope.registerTachideskTasks(project: Project) { .forEach { val tmpFile = macJarFolder / it.name it.copyTo(tmpFile) - exec { - commandLine( + Runtime.getRuntime().exec( + arrayOf( "/usr/bin/codesign", "-vvvv", "--timestamp", @@ -151,7 +150,7 @@ fun TaskContainerScope.registerTachideskTasks(project: Project) { "--sign", "Developer ID Application: ${getSigningIdentity()}", tmpFile.absolutePathString(), ) - } + ) tmpFile.copyTo(it, overwrite = true) tmpFile.deleteExisting() diff --git a/data/build.gradle.kts b/data/build.gradle.kts index b69d062f..bfb62094 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,3 +1,5 @@ +import com.google.devtools.ksp.gradle.KspAATask + plugins { id(libs.plugins.kotlin.multiplatform.get().pluginId) id(libs.plugins.kotlin.serialization.get().pluginId) @@ -141,3 +143,7 @@ apollo { } } } + +tasks.withType { + mustRunAfter("generateServiceApolloSources") +} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 2819fc79..0d607c77 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -1,6 +1,7 @@ import Config.migrationCode import Config.serverCode import Config.tachideskVersion +import com.google.devtools.ksp.gradle.KspAATask import org.gradle.jvm.tasks.Jar import org.jetbrains.compose.compose import org.jetbrains.compose.desktop.application.dsl.TargetFormat @@ -231,3 +232,11 @@ buildConfig { buildConfigField("String", "TACHIDESK_SP_VERSION", tachideskVersion.wrap()) buildConfigField("int", "SERVER_CODE", serverCode.toString()) } + +tasks.withType { + mustRunAfter( + "generateBuildConfig", + "generateResourceAccessorsForMain", + "generateComposeResClass" + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ad6d9a1..f21197c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,37 +1,37 @@ [versions] # Kotlin -kotlin = "2.0.20" +kotlin = "2.2.20" coroutines = "1.9.0" # Serialization -json = "1.7.3" +json = "1.9.0" # Compose -composeGradle = "1.6.11" +composeGradle = "1.9.0" # Compose Libraries parcelize = "0.9.0" -voyager = "1.0.0" +voyager = "1.1.0-beta03" accompanist = "0.30.1" googleAccompanist = "0.30.1" imageloader = "1.8.1" materialDialogs = "0.9.5" # Android -androidGradle = "8.6.1" +androidGradle = "8.12.0" core = "1.13.1" appCompat = "1.7.0" activityCompose = "1.9.2" work = "2.9.1" # Android Lifecycle -lifecycle = "2.8.6" +lifecycle = "2.9.4" # Swing darklaf = "3.0.2" # Ksp -ksp = "2.0.20-1.0.25" +ksp = "2.2.20-2.0.3" # Dependency Injection kotlinInject = "0.7.2" @@ -54,7 +54,7 @@ appDirs = "1.2.0" multiplatformSettings = "1.2.0" # Utility -desugarJdkLibs = "2.1.2" +desugarJdkLibs = "2.1.5" aboutLibraries = "11.2.3" dateTime = "0.6.1" immutableCollections = "0.3.8" @@ -62,17 +62,17 @@ korge = "5.4.0" gradleDownloadTask = "5.6.0" # Localization -moko = "0.24.3" +moko = "0.25.1" # BuildConfigs -buildconfig = "5.5.0" -buildkonfig = "0.15.2" +buildconfig = "5.6.8" +buildkonfig = "0.17.1" # Linter kotlinter = "4.4.1" # Version updates -versions = "0.51.0" +versions = "0.53.0" # Optimizer proguard = "7.5.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..2c352119 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 09523c0e..2e111328 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 1756333a..1cbaedbe 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -1,3 +1,4 @@ +import com.google.devtools.ksp.gradle.KspAATask import org.jetbrains.compose.compose plugins { @@ -157,3 +158,12 @@ buildkonfig { android { namespace = "ca.gosyer.jui.presentation" } + +tasks.withType { + mustRunAfter( + "generateResourceAccessorsForDesktopMain", + "generateResourceAccessorsForJvmMain", + "generateResourceAccessorsForCommonMain", + "generateComposeResClass" + ) +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/Toolbar.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/Toolbar.kt index 4ce014d7..f1a21f09 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/Toolbar.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/Toolbar.kt @@ -47,7 +47,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -398,7 +398,7 @@ fun TextActionIcon( enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 32.dp), + indication = ripple(bounded = false, radius = 32.dp), ) .size(56.dp), verticalArrangement = Arrangement.SpaceAround, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt index 7c72645f..789efc31 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt @@ -116,7 +116,7 @@ fun DownloadsScreenContent( ) { items(downloadQueue, key = { "${it.mangaId}-${it.chapterIndex}" }) { DownloadsItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), item = it, onClickCover = { onMangaClick(it.mangaId) }, onClickCancel = stopDownload, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt index 3b04c16e..14d68d75 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt @@ -145,13 +145,13 @@ fun ExtensionsScreenContent( is ExtensionUI.Header -> Text( it.header, style = MaterialTheme.typography.h6, - modifier = Modifier.animateItemPlacement() + modifier = Modifier.animateItem() .padding(16.dp, 16.dp, 16.dp, 4.dp), ) is ExtensionUI.ExtensionItem -> Column { ExtensionItem( - Modifier.animateItemPlacement(), + Modifier.animateItem(), it, onInstallClicked = installExtension, onUpdateClicked = updateExtension, diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt index f72bbcfa..bdf0bba8 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt @@ -8,7 +8,7 @@ package ca.gosyer.jui.uicore.components import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -20,7 +20,7 @@ actual fun Modifier.buttonModifier( composed { combinedClickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), onLongClick = onHintClick, onClick = onClick, ) diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropdownIconButton.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropdownIconButton.kt index f33c68e8..03963a5a 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropdownIconButton.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropdownIconButton.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.size import androidx.compose.material.DropdownMenu -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -46,7 +46,7 @@ fun DropdownIconButton( .clickable( remember { MutableInteractionSource() }, role = Role.Button, - indication = rememberRipple(bounded = false, radius = 24.dp), + indication = ripple(bounded = false, radius = 24.dp), ) { showMenu = true } 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 0a3f17e2..ab77fc49 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,12 +6,10 @@ 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.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -26,9 +24,11 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable @@ -36,10 +36,14 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMaxBy import kotlinx.coroutines.flow.distinctUntilChanged import kotlin.math.abs +import kotlin.math.absoluteValue import kotlin.math.sign @Composable @@ -234,103 +238,150 @@ 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: SnapPositionInLayout = CenterToCenter, -) = object : SnapLayoutInfoProvider { - private val layoutInfo: LazyListLayoutInfo - get() = lazyListState.layoutInfo + snapPosition: SnapPosition = SnapPosition.Center, + density: State, +): SnapLayoutInfoProvider = + object : SnapLayoutInfoProvider { - // Single page snapping is the default - override fun calculateApproachOffset(initialVelocity: Float): Float = 0f + private val layoutInfo: LazyListLayoutInfo + get() = lazyListState.layoutInfo - override fun calculateSnappingOffset(currentVelocity: Float): Float { - var lowerBoundOffset = Float.NEGATIVE_INFINITY - var upperBoundOffset = Float.POSITIVE_INFINITY - - layoutInfo.visibleItemsInfo.fastForEach { item -> - val offset = - 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) { - lowerBoundOffset = offset + private val averageItemSize: Int + get() { + val layoutInfo = layoutInfo + return if (layoutInfo.visibleItemsInfo.isEmpty()) { + 0 + } else { + val numberOfItems = layoutInfo.visibleItemsInfo.size + layoutInfo.visibleItemsInfo.sumOf { it.size } / numberOfItems + } } - // Find item that is closest to center, but after it - if (offset >= 0 && offset < upperBoundOffset) { - upperBoundOffset = offset - } + override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float { + return (decayOffset.absoluteValue - averageItemSize).coerceAtLeast(0.0f) * + decayOffset.sign } - return calculateFinalOffset( - currentVelocity, - lowerBoundOffset, - upperBoundOffset, - ) + override fun calculateSnapOffset(velocity: Float): Float { + var lowerBoundOffset = Float.NEGATIVE_INFINITY + var upperBoundOffset = Float.POSITIVE_INFINITY + + layoutInfo.visibleItemsInfo.fastForEach { item -> + val offset = + calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize = layoutInfo.singleAxisViewportSize, + beforeContentPadding = layoutInfo.beforeContentPadding, + afterContentPadding = layoutInfo.afterContentPadding, + itemSize = item.size, + itemOffset = item.offset, + itemIndex = item.index, + snapPosition = snapPosition, + itemCount = layoutInfo.totalItemsCount, + ) + + // Find item that is closest to the center + if (offset <= 0 && offset > lowerBoundOffset) { + lowerBoundOffset = offset + } + + // Find item that is closest to center, but after it + if (offset >= 0 && offset < upperBoundOffset) { + upperBoundOffset = offset + } + } + + return calculateFinalOffset( + with(density.value) { calculateFinalSnappingItem(velocity) }, + lowerBoundOffset, + upperBoundOffset, + ) + } } - @OptIn(ExperimentalFoundationApi::class) - private fun calculateDistanceToDesiredSnapPosition( - mainAxisViewPortSize: Int, - beforeContentPadding: Int, - afterContentPadding: Int, - itemSize: Int, - itemOffset: Int, - itemIndex: Int, - snapPositionInLayout: SnapPositionInLayout, - ): Float { - val containerSize = mainAxisViewPortSize - beforeContentPadding - afterContentPadding +@Composable +private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): FlingBehavior { + // return rememberSnapFlingBehavior(lazyListState) + val density = rememberUpdatedState(LocalDensity.current) + val snappingLayout = remember(lazyListState) { lazyListSnapLayoutInfoProvider(lazyListState, density = density) } + return rememberSnapFlingBehavior(snappingLayout) +} - val desiredDistance = with(snapPositionInLayout) { - position(containerSize, itemSize, beforeContentPadding, afterContentPadding, itemIndex) - }.toFloat() +internal val LazyListLayoutInfo.singleAxisViewportSize: Int + get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width - return itemOffset - desiredDistance +@kotlin.jvm.JvmInline +internal value class FinalSnappingItem +internal constructor(@Suppress("unused") private val value: Int) { + companion object { + + val ClosestItem: FinalSnappingItem = FinalSnappingItem(0) + + val NextItem: FinalSnappingItem = FinalSnappingItem(1) + + val PreviousItem: FinalSnappingItem = FinalSnappingItem(2) + } +} + +internal fun Density.calculateFinalSnappingItem(velocity: Float): FinalSnappingItem { + return if (velocity.absoluteValue < 400.dp.toPx()) { + FinalSnappingItem.ClosestItem + } else { + if (velocity > 0) FinalSnappingItem.NextItem else FinalSnappingItem.PreviousItem + } +} + +internal fun calculateFinalOffset( + snappingOffset: FinalSnappingItem, + lowerBound: Float, + upperBound: Float, +): Float { + fun Float.isValidDistance(): Boolean { + return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY } - private fun calculateFinalOffset( - velocity: Float, - lowerBound: Float, - upperBound: Float, - ): Float { - fun Float.isValidDistance(): Boolean = this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY - - val finalDistance = when (sign(velocity)) { - 0f -> { + val finalDistance = + when (snappingOffset) { + FinalSnappingItem.ClosestItem -> { if (abs(upperBound) <= abs(lowerBound)) { upperBound } else { lowerBound } } - - 1f -> upperBound - - -1f -> lowerBound - + FinalSnappingItem.NextItem -> upperBound + FinalSnappingItem.PreviousItem -> lowerBound else -> 0f } - return if (finalDistance.isValidDistance()) { - finalDistance - } else { - 0f - } + return if (finalDistance.isValidDistance()) { + finalDistance + } else { + 0f } } -@Composable -private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): FlingBehavior { - val snappingLayout = remember(lazyListState) { lazyListSnapLayoutInfoProvider(lazyListState) } - return rememberSnapFlingBehavior(snappingLayout) -} +internal fun calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize: Int, + beforeContentPadding: Int, + afterContentPadding: Int, + itemSize: Int, + itemOffset: Int, + itemIndex: Int, + snapPosition: SnapPosition, + itemCount: Int, +): Float { + val desiredDistance = + with(snapPosition) { + position( + mainAxisViewPortSize, + itemSize, + beforeContentPadding, + afterContentPadding, + itemIndex, + itemCount, + ) + } + .toFloat() -private val LazyListLayoutInfo.singleAxisViewportSize: Int - get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width + return itemOffset - desiredDistance +}