mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
The PDF preview card and full-screen reader move from the Android app to
commonsUI (jvmAndroid). The only Android piece was turning pages into
pixels, now behind `PdfPageRenderer`: Android's actual is the same
`PdfRenderer` code, the desktop's is Apache PDFBox 3.0.8.
- PDFBox is Apache-2.0; its jar bundles fonts and CMaps under OFL, BSD,
Adobe's AFM terms and CC-BY-4.0, all permissive. Runtime deps are
pdfbox-io, fontbox and commons-logging (Apache-2.0); BouncyCastle is
optional and not pulled in.
- The reader takes its action buttons as a slot. Android keeps its
immersive dialog with share and save-to-gallery; the desktop opens it in a
full-window dialog with a save button.
- The card's long press is a callback; Android passes its share sheet.
- `ViewerControlsRow` and `ViewerBackButton` move to the shared viewer
chrome.
- The card and reader's hardcoded labels ("Tap to load PDF", "Loading…",
page counts, "Unable to open PDF") move to Compose resources.
- Desktop notes now show the first page, filename and page count instead of
a bare link, and open a reader.
Tests: the renderer on PDFs written with PDFBox (page count, size in points,
rotation, pixels, a non-PDF) and an offscreen Compose render of the card
and the reader fetching a PDF from a local HTTP server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
290 lines
13 KiB
Kotlin
290 lines
13 KiB
Kotlin
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
import org.jetbrains.kotlin.gradle.plugin.mpp.DisableCacheInKotlinVersion
|
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCacheApi
|
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
|
import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable
|
|
|
|
// `:commonsUI` is the Compose half of the shared layer: every composable,
|
|
// icon, theme, Coil fetcher and Compose-resource (strings/fonts/files) that the
|
|
// GUI front ends (Android, Desktop, iOS) render. It sits on top of `:commons`
|
|
// (headless models, state, ViewModels, relay client) and is never a dependency
|
|
// of `:cli`, which keeps Compose UI + Skiko off the CLI classpath.
|
|
// Source files keep their `com.vitorpamplona.amethyst.commons.*` packages so
|
|
// the module boundary is purely a build-graph constraint — consumers did not
|
|
// have to change a single import when the split happened.
|
|
|
|
// Disables the Kotlin/Native compiler cache for an iOS test binary so the
|
|
// Compose ui-uikit klib recompiles fresh instead of linking the broken prebuilt
|
|
// cache (see the call site in the `kotlin {}` block). The version guard makes
|
|
// Kotlin re-surface this workaround once we move past 2.4.20, so it can be
|
|
// dropped when a newer Compose/Kotlin pairing fixes the cache. Wrapped in a
|
|
// helper because @OptIn only applies to declarations, not bare statements.
|
|
@OptIn(KotlinNativeCacheApi::class)
|
|
fun TestExecutable.disableUiKitPrebuiltCache() =
|
|
disableNativeCache(
|
|
DisableCacheInKotlinVersion.`2_4_20`,
|
|
"Compose ui-uikit prebuilt cache references UIViewLayoutRegion (iOS 17+); " +
|
|
"linking the iOS test binary fails under Xcode 16.4.",
|
|
)
|
|
|
|
plugins {
|
|
alias(libs.plugins.kotlinMultiplatform)
|
|
alias(libs.plugins.androidKotlinMultiplatformLibrary)
|
|
alias(libs.plugins.jetbrainsComposeCompiler)
|
|
alias(libs.plugins.composeMultiplatform)
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
freeCompilerArgs.add("-Xexpect-actual-classes")
|
|
}
|
|
|
|
jvm {
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_21)
|
|
}
|
|
}
|
|
|
|
android {
|
|
namespace = "com.vitorpamplona.amethyst.commons.ui"
|
|
compileSdk =
|
|
libs.versions.android.compileSdk
|
|
.get()
|
|
.toInt()
|
|
minSdk =
|
|
libs.versions.android.minSdk
|
|
.get()
|
|
.toInt()
|
|
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_21)
|
|
}
|
|
|
|
androidResources.enable = true
|
|
|
|
withHostTest {
|
|
isReturnDefaultValues = true
|
|
}
|
|
|
|
withDeviceTest {
|
|
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
}
|
|
}
|
|
|
|
// iOS targets — same compile-only spike as :commons.
|
|
iosArm64()
|
|
iosSimulatorArm64()
|
|
|
|
// Compose Multiplatform 1.11.x ships an `org.jetbrains.compose.ui:ui-uikit`
|
|
// prebuilt Kotlin/Native cache whose CMPLayoutRegion object hard-references
|
|
// the UIKit class `UIViewLayoutRegion` (introduced in iOS 17). Linking the
|
|
// iOS *test* executable against that cache under Xcode 16.4 fails with
|
|
// ld: Undefined symbols: _OBJC_CLASS_$_UIViewLayoutRegion
|
|
// because the cached object was built for a newer simulator SDK (18.5) than
|
|
// the test binary is being linked for (14.0). Disabling the native cache for
|
|
// the iOS test binaries makes ui-uikit recompile against the active SDK,
|
|
// where the symbol resolves. See disableUiKitPrebuiltCache() above and
|
|
// https://kotl.in/disable-native-cache
|
|
targets.withType<KotlinNativeTarget>().configureEach {
|
|
binaries.withType<TestExecutable>().configureEach {
|
|
disableUiKitPrebuiltCache()
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
commonMain {
|
|
dependencies {
|
|
// The headless half. `api` because every composable here takes
|
|
// or returns commons types (Note, User, ViewModels, states).
|
|
api(project(":commons"))
|
|
api(project(":quartz"))
|
|
|
|
// Compose Multiplatform
|
|
implementation(libs.jetbrains.compose.ui)
|
|
implementation(libs.jetbrains.compose.foundation)
|
|
implementation(libs.jetbrains.compose.runtime)
|
|
implementation(libs.jetbrains.compose.material3)
|
|
implementation(libs.jetbrains.compose.ui.tooling.preview)
|
|
|
|
// Lifecycle (KMP since 2.8.0). lifecycle-runtime-compose ships
|
|
// iOS variants; lifecycle-viewmodel-compose (the viewModel()
|
|
// Composable helper) is Android-only and lives in jvmAndroid.
|
|
implementation(libs.androidx.lifecycle.viewmodel)
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
|
|
// Image loading (Coil 3 - KMP). The okhttp network fetcher is
|
|
// JVM-only and lives in jvmAndroid; iOS will pull coil-ktor
|
|
// when that target wires its actual.
|
|
implementation(libs.coil.compose)
|
|
// NetworkHeaders on avatar requests (the local Blossom cache marker). Multiplatform,
|
|
// Apache-2.0; already on the JVM classpath through coil-network-okhttp.
|
|
implementation(libs.coil.network.core)
|
|
|
|
// LruCache (KMP-ready)
|
|
implementation(libs.androidx.collection)
|
|
|
|
// Pinch-to-zoom (net.engawapg zoomable, Compose Multiplatform: Android, desktop,
|
|
// iOS; Apache-2.0). Already in the app; the Blossom blob viewer uses it here.
|
|
implementation(libs.zoomable)
|
|
|
|
// Charts (Vico 3, Compose Multiplatform, Apache-2.0): the notification
|
|
// summary builds its chart model off the main thread. `api` because the
|
|
// model is part of that state holder's public surface.
|
|
api(libs.vico.charts.compose)
|
|
|
|
// Immutable collections
|
|
api(libs.kotlinx.collections.immutable)
|
|
|
|
// JSON trees for the LNURL-pay replies the payment stack reads.
|
|
implementation(libs.kotlinx.serialization.json)
|
|
|
|
// Compose Multiplatform Resources (strings, fonts, napplet shell files)
|
|
implementation(libs.jetbrains.compose.components.resources)
|
|
|
|
// KMP syntax highlighter (Apache-2.0) for the git code browser.
|
|
implementation(libs.highlights)
|
|
|
|
// Navigation 3 (JetBrains' multiplatform build, Apache-2.0: Android, desktop, iOS).
|
|
// The app owns its back stack (navs/NavBackStacks); NavDisplay renders it, and the
|
|
// ViewModel decorator scopes each screen's ViewModels to its entry.
|
|
implementation(libs.jetbrains.navigation3.ui)
|
|
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
|
|
}
|
|
}
|
|
|
|
commonTest {
|
|
dependencies {
|
|
implementation(libs.kotlin.test)
|
|
implementation(libs.kotlinx.coroutines.test)
|
|
}
|
|
}
|
|
|
|
jvmTest {
|
|
dependencies {
|
|
// Compose Desktop on the test classpath so composables can be
|
|
// rendered headlessly to a Skia surface (ImageComposeScene) and
|
|
// asserted on. No new third-party dependency: this is the same
|
|
// artifact jvmMain already uses, and it rasterises in software,
|
|
// so it needs no display.
|
|
implementation(compose.desktop.currentOs)
|
|
// RouteForPointerTest stubs an Account (already the app's and androidHostTest's mock lib).
|
|
implementation(libs.mockk)
|
|
}
|
|
}
|
|
|
|
// Shared JVM code for both Android and Desktop
|
|
val jvmAndroid =
|
|
create("jvmAndroid") {
|
|
dependsOn(commonMain.get())
|
|
dependencies {
|
|
// Coil's OkHttp network fetcher (JVM-only). iOS will use
|
|
// coil-ktor when the iOS Compose UI ships.
|
|
implementation(libs.coil.okhttp)
|
|
|
|
// OkHttp for the Blossom read-auth Coil fetcher.
|
|
implementation(libs.okhttp)
|
|
// executeAsync for PdfFetcher; already on every module that ships OkHttp.
|
|
implementation(libs.okhttpCoroutines)
|
|
|
|
// Markdown rendering (richtext-commonmark). The single
|
|
// consumer (RenderMarkdown.kt) lives in jvmAndroid.
|
|
// iOS support pending Phase 3 markdown decision.
|
|
implementation(libs.markdown.commonmark)
|
|
implementation(libs.markdown.ui)
|
|
implementation(libs.markdown.ui.material3)
|
|
|
|
// viewModel() Compose helper. AndroidX publishes this
|
|
// artifact for android/jvmStubs/linuxx64Stubs but not iOS,
|
|
// so it stays in jvmAndroid until we either swap to the
|
|
// org.jetbrains.androidx.lifecycle variant or accept a
|
|
// platform-specific ViewModel access pattern on iOS.
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
// ZXing core encodes the QR codes the shared QrCodeDrawer shows. Pure Java,
|
|
// Apache-2.0, and already linked by both apps.
|
|
implementation(libs.zxing)
|
|
}
|
|
}
|
|
|
|
jvmMain {
|
|
dependsOn(jvmAndroid)
|
|
dependencies {
|
|
// Desktop-specific Compose
|
|
implementation(compose.desktop.currentOs)
|
|
implementation(libs.jetbrains.compose.ui.tooling)
|
|
|
|
// Renders PDF pages for the shared PDF card and viewer; Android uses the OS's
|
|
// PdfRenderer instead. Apache-2.0 (bundled fonts and CMaps: OFL, BSD, CC-BY-4.0).
|
|
implementation(libs.pdfbox)
|
|
}
|
|
}
|
|
|
|
androidMain {
|
|
dependsOn(jvmAndroid)
|
|
dependencies {
|
|
// Android-specific Compose tooling
|
|
implementation(libs.androidx.ui.tooling.preview)
|
|
// WindowInsetsControllerCompat, for DisappearingScaffold's immersive status bar.
|
|
implementation(libs.androidx.core.ktx)
|
|
// BackHandler, behind the shared PlatformBackHandler. Apache-2.0, already in the app.
|
|
implementation(libs.androidx.activity.compose)
|
|
// Camera permission prompt for TakePicture. Apache-2.0, already in the app.
|
|
implementation(libs.accompanist.permissions)
|
|
implementation(libs.accompanist.adaptive)
|
|
// BiometricPrompt behind rememberDeviceAuthenticator. Apache-2.0, already in the app.
|
|
implementation(libs.androidx.biometric.ktx)
|
|
}
|
|
}
|
|
|
|
// iOS intermediate so iosArm64Main and iosSimulatorArm64Main share code.
|
|
val iosMain =
|
|
create("iosMain") {
|
|
dependsOn(commonMain.get())
|
|
}
|
|
getByName("iosArm64Main").dependsOn(iosMain)
|
|
getByName("iosSimulatorArm64Main").dependsOn(iosMain)
|
|
|
|
// Skiko-backed targets (desktop JVM + iOS) share pixel-format helpers
|
|
// (org.jetbrains.skia.* resolves on both through Compose). Android is
|
|
// deliberately NOT in this set — it renders through android.graphics.
|
|
val skikoMain =
|
|
create("skikoMain") {
|
|
dependsOn(commonMain.get())
|
|
dependencies {
|
|
// NavigationBackHandler for desktop (Esc) and iOS (swipe), behind
|
|
// PlatformBackHandler. Apache-2.0; Android uses AndroidX activity's BackHandler.
|
|
implementation(libs.jetbrains.navigationevent.compose)
|
|
}
|
|
}
|
|
getByName("jvmMain").dependsOn(skikoMain)
|
|
iosMain.dependsOn(skikoMain)
|
|
|
|
getByName("androidHostTest") {
|
|
dependencies {
|
|
implementation(libs.junit)
|
|
// SafeImeInsetsTest mocks the android.view.View its per-window cache is keyed on.
|
|
implementation(libs.mockk)
|
|
implementation(libs.kotlinx.coroutines.test)
|
|
}
|
|
}
|
|
|
|
getByName("androidDeviceTest") {
|
|
dependencies {
|
|
implementation(libs.androidx.junit)
|
|
implementation(libs.androidx.espresso.core)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
compose.resources {
|
|
publicResClass = true
|
|
// Kept on the pre-split package so `Res` imports in every consumer
|
|
// (amethyst, desktopApp, nappletHost) keep resolving unchanged.
|
|
packageOfResClass = "com.vitorpamplona.amethyst.commons.resources"
|
|
generateResClass = always
|
|
}
|
|
|
|
// iOS purity gate — shared task, see gradle/kmp-purity.gradle.kts.
|
|
apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
|