Correctness fixes in GlobalMediaPlayer.kt
- snapshotFlow { hasMedia } collector for initial seek used `return@collect`
which only exits the lambda; the collector kept running and each
subsequent playVideo() call accumulated a live collector that would
re-fire a stale seekTo() on the wrong media. Replaced with `Flow.first`
which terminates the collection cleanly.
- playVideo()/playAudio() reset the public MediaPlaybackState to
volume=100/isMuted=false on a new URL, but the kdroidFilter player
retains its `volume` across openUri(); muting one track and starting a
new one left the engine silent while the UI showed unmuted. Reset
`player.volume = 1f` to match the public state.
- ensureVideoPlayer()/ensureAudioPlayer() called createVideoPlayerState()
synchronously from the Compose getter; if native init throws (missing
GStreamer on Linux, broken NativeLibraryLoader extraction) the whole
window would crash. Wrapped in runCatching and changed
activeVideoPlayerState to nullable. Consumers in DesktopVideoPlayer
and GlobalFullscreenOverlay handle the null path by rendering the
thumbnail / blank backdrop respectively; playVideo()/playAudio()
surface "Video playback unavailable" through the existing
errorReason -> PlaybackErrorMessage path.
Crash mitigation (kdroidFilter 0.10.0 UAF in MacVideoPlayerSurface)
- NowPlayingBar previously mounted a SECOND VideoPlayerSurface against
the same VideoPlayerState while the feed card was already mounting
one, doubling the draw rate against the shared frame bitmap and
widening the UAF window in MacVideoPlayerSurface's RasterFromBitmap
path. Mini-preview now renders the cached thumbnail (or the music
icon fallback). 0.10.1 contains an upstream fix
("recover video playback after composition removal") but is not yet
on Maven Central — single-surface mounting is the only mitigation
we can ship today.
VideoThumbnailCache.kt
- Truncated-download cache poisoning: when an origin ignored the
Range: header and returned HTTP 200 with the full body, we capped
the copy at MAX_THUMB_BYTES and persisted the truncated file
forever. Subsequent thumbnail attempts hit the broken cache file
and re-failed JCodec/ffmpeg every time. Tag download results with
whether the server actually returned 206; on 200, extract from the
temp file and delete it (no persistent cache hit).
- Tor bypass: replaced the bare OkHttpClient with
DesktopHttpClient.currentClient() so thumbnail fetches respect the
user's Tor preference (fail-closed when Tor is expected but
bootstrapping).
- ffmpeg version probe leaked the process on hang: now drains stdout
to DISCARD and calls destroyForcibly() on timeout.
- Frame-extract ffmpeg subprocess could deadlock on a chatty stderr
pipe: redirectError(DISCARD) so we never wait on stderr; a finally
block destroys the process if anything leaked through the timeout.
CI workflow cleanup
- Removed vlc-setup download cache + pre-fetch steps from
build.yml and smoke-test-desktop.yml. They were targeting an
ir.mahozad.vlc-setup plugin we no longer apply, so they wasted
~minutes of CI time per leg and tied the build to videolan.org
reachability for no reason.
- Trimmed create-release.yml's stale VLC-plugins justification on
the linuxdeploy-vs-appimagetool comment.
.gitignore + missing per-OS ffmpeg READMEs
- The pre-PR rules blanket-ignored desktopApp/src/jvmMain/appResources/{linux,macos,windows}/
so the LGPL FFmpeg drop-in slot READMEs created in 704f4f44e never
reached the commit. Refined the ignore rules to keep stale vlc/ workspace
trees out of git (still ignored) while explicitly tracking the
ffmpeg/README.md drop-in slot under each OS. The READMEs document the
recommended LGPL build source per OS for the bundled-FFmpeg packaging
path.
Verified on macOS arm64:
./gradlew :desktopApp:compileKotlin BUILD SUCCESSFUL
./gradlew :desktopApp:test BUILD SUCCESSFUL
./gradlew :desktopApp:spotlessApply clean
Refs PR #3175 review by @davotoula.
The test-quartz-ios job only compiled :commons production code for iOS
(compileKotlinIos*), never the commonTest source set, so a test using a
JVM-only API compiled fine on JVM/Android and shipped green while silently
breaking compileTestKotlinIosSimulatorArm64.
Mirror the quartz step: run :commons:iosSimulatorArm64Test (compiles +
runs the shared commonTest suite on the simulator) and
:commons:compileTestKotlinIosArm64 (device-only compile drift). This keeps
commonTest KMP-clean going forward.
Zulu's CDN (cdn.azul.com) has been returning HTTP 520 for several
hours, breaking JDK 21 setup across multiple PRs (#3075, #3095) on
every Linux runner. actions/setup-java's internal retry budget isn't
enough to ride through it.
Temurin (Adoptium) publishes its JDKs as GitHub Releases assets, so
the download path uses GitHub's own CDN — completely independent
infrastructure. Java 21 is the same upstream OpenJDK build either way,
so Gradle, Compose Multiplatform, and jpackage behave identically.
Applied to all setup-java steps in build.yml, smoke-test-desktop.yml,
and create-release.yml.
Phase 2 task 10 of the iOS plan — flip on iOS targets for :commons.
Gradle dep resolution is fully green for iOS; actual Kotlin/Native
compilation runs on the macOS CI job (the dev container in which this
was authored can't extract the K/N LLVM toolchain).
Dep reshuffle to match what's actually KMP-available:
- commonMain: kept project(":quartz"), Compose Multiplatform, coil-compose,
androidx-collection, kotlinx-collections-immutable, kotlinx-serialization-json,
compose components-resources, androidx-lifecycle-viewmodel,
androidx-lifecycle-runtime-compose. These all publish iosArm64 +
iosSimulatorArm64 variants per `.module` inspection.
- jvmAndroid (NEW location): project(":nestsClient") (JVM+Android-only),
coil-okhttp (JVM-only), markdown-commonmark / markdown-ui /
markdown-ui-material3 (the RenderMarkdown.kt consumer is already in
jvmAndroid), and androidx-lifecycle-viewmodel-compose (AndroidX publishes
android + jvmStubs + linuxx64Stubs variants — no iOS, so the viewModel()
Composable helper stays JVM-bound until we either swap to the
org.jetbrains.androidx.lifecycle variant or accept a platform-specific
ViewModel access pattern on iOS).
- libs.versions.toml: adds androidx-lifecycle-viewmodel catalog entry.
- New intermediate source set iosMain → both iosArm64Main and
iosSimulatorArm64Main depend on it (clean place for iOS-only actuals
when KmpLock, WeakReference, etc. get their iOS implementations).
- .github/workflows/build.yml: test-quartz-ios job now also runs
:commons:compileKotlinIosArm64 + :commons:compileKotlinIosSimulatorArm64.
The one Jackson holdout in commons/commonMain. Migrating it unblocks
the iOS purity gate for :commons (Phase 1 of the iOS plan).
- Rewrites FeedDefinitionSerializer with the kotlinx.serialization JSON
tree API (JsonObject / JsonArray / JsonPrimitive). Wire format is
byte-identical, so users' existing on-disk custom-feed definitions
keep deserializing — covered by a new parsesLegacyJacksonOutput test
that pins a hand-written Jackson-shaped JSON blob.
- Adds :commons:verifyKmpPurity (mirrors the one in :quartz) and wires
it into the CI lint job alongside :quartz:verifyKmpPurity.
- Pulls in kotlinx-serialization-json as a commonMain dep; the
serialization plugin was already applied on :commons.
Phase 1 of the iOS support plan (amethyst/plans/2026-05-24-ios-support.md).
Two independent guards so JVM-only imports can't silently appear in
quartz's iOS-bound source sets:
- :quartz:verifyKmpPurity (Linux, ~1s): scans commonMain + apple/native
source sets for com.fasterxml.jackson / okhttp3 references and fails
the build with a clear pointer to the offending file:line. Wired into
the existing lint job so it runs on every PR.
- test-quartz-ios (macos-latest): runs :quartz:iosSimulatorArm64Test on
the simulator (NIP-04, NIP-17, NIP-19, NIP-49 vectors + AES-GCM and
chatroom-key tests already in quartz/src/iosTest) and additionally
compileTestKotlinIosArm64 to catch device-variant compile drift.
Fixes#2819 — v1.08.0 .deb crashed on Ubuntu with ManagementFactory
error because the build had no jlink modules() declaration. Current
main already has the fix; this PR adds CI to prevent regressions.
- Add compose.desktop.uiTestJUnit4 dependency
- DesktopLaunchSmokeTest: renders LoginScreen, asserts title text
- build.yml: xvfb for Linux leg so UI test runs on every PR
- smoke-test-desktop.yml: builds release .deb, installs it, launches
under xvfb, verifies process stays alive 10s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The :ammolite module contained no production Kotlin/Java sources (just
a manifest, build.gradle, and proguard stubs) and no module in the
codebase imports com.vitorpamplona.ammolite.*.
Removes:
- ammolite/ directory (5 files)
- :ammolite project include in settings.gradle
- implementation project(':ammolite') from :amethyst
- androidTestImplementation project(':ammolite') from :benchmark
- :ammolite:testDebugUnitTest from CI workflow and pre-push hook
- -keep class com.vitorpamplona.ammolite.** rules from
:amethyst, :commons, and :desktopApp proguard files
- Stale references in CONTRIBUTING.md, CLAUDE.md, and the
gradle-expert skill dependency-graph doc
Small build-graph win: one fewer module to configure, compile, lint,
and spotless-check on every build, and one fewer unit-test target in
both CI and the local pre-push hook.
The Linux build path of the vlc-setup plugin pulls vlc-plugins-linux from
Maven Central (ir.mahozad:vlc-plugins-linux), which only ships 3.0.20 and
3.0.20-2 — there is no 3.0.21 artifact yet. With vlcVersion set to 3.0.21,
both the CI pre-fetch step and the in-Gradle vlcDownload task hit a 404.
Match VLC_VERSION in build.yml and document the lag so future bumps wait
for the Maven artifact to catch up.
https://claude.ai/code/session_01GrZLMi3sdp6frwREmQ9cUi
Replace the nick-fields/retry wrapper around the Gradle invocation with a
curl-based pre-fetch step. The vlc-setup plugin writes its downloads to
${gradleUserHomeDir}/vlcSetup/ with overwrite(false), so dropping the
archives there ahead of time turns vlcDownload / upxDownload into no-ops.
curl --retry-all-errors --retry-max-time 900 tolerates a sustained
get.videolan.org outage far better than the plugin's de.undercouch
Download (retries(4) + 5min readTimeout, which still hit
SocketTimeoutException on Windows).
The actions/cache step at ~/.gradle/vlcSetup still captures the result
for subsequent runs; the prefetch only does real work on cache miss.
The in-build retries(4) in desktopApp/build.gradle.kts stays as a third
line of defense.
URLs and on-disk paths are read straight from the plugin source
(ir.mahozad.vlc-setup 0.1.0); macOS skips UPX because UPX cannot compress
.dylib files.
The Windows leg of the Test/Build workflow has been intermittently failing
with SocketTimeoutException inside :desktopApp:vlcDownload, even after the
in-build retry budget (retries(4) + 5min readTimeout in
desktopApp/build.gradle.kts) and the actions/cache of ~/.gradle/vlcSetup.
When get.videolan.org is unreachable for long enough to exhaust the inner
retries, the whole job dies on a cache miss.
Wrap the Test+Build step in nick-fields/retry@v4 (max_attempts: 2,
timeout_minutes: 45) so one outer retry can ride out a sustained
videolan.org outage. This mirrors the proven pattern already used in
create-release.yml for the release artifact build.
Headline numbers from a fresh `./gradlew assemble`:
- :commons material_symbols_outlined.ttf 11M -> 409K (subset to the 210
codepoints actually referenced from MaterialSymbols.kt)
- :amethyst per-ABI APKs no longer built in CI (-PdisableAbiSplits=true);
~600M of stripped_native_libs intermediates skipped per run
Changes:
- .git-hooks/pre-push runs only :amethyst:testPlayDebugUnitTest plus jvmTest
on the KMP modules instead of `./gradlew test`, which was compiling all six
amethyst variants (play/fdroid x debug/release/benchmark)
- amethyst/build.gradle adds three opt-in fast-build flags:
-PdisableAbiSplits=true skip per-ABI APK splits
-PdisableUniversalApk=true skip the universal APK output
-Pamethyst.skipMapping=true disable R8 (release+benchmark)
Defaults are unchanged; release pipelines must not set skipMapping.
- .github/workflows/build.yml uses gradle/actions/setup-gradle@v4 with
cache-read-only on PRs / cache-write on main, drops --no-daemon, collapses
the Android job to a single Gradle invocation (lint + focused debug unit
tests + assembleBenchmark with -PdisableAbiSplits=true), and globs both
APK naming patterns for the upload step.
- tools/material-symbols-subset/{subset.sh,README.md} regenerates the font
from upstream + MaterialSymbols.kt; run after adding/removing icons.
Verified: ./gradlew :amethyst:help with all three new -P flags parses cleanly,
and ./gradlew :commons:jvmJar --rerun-tasks succeeds with the subsetted font
(commons-jvm jar shrinks from ~13M to 2.9M).
https://claude.ai/code/session_01YSmkagXXN5AwGcY4upiUh1
Replace the -PskipVlcSetup=true CI bypass with a per-OS GitHub Actions
cache of ~/.gradle/vlcSetup. Cache key is hashFiles('desktopApp/build.gradle.kts')
so a VLC version bump invalidates cleanly; restore-keys lets unrelated
edits to that file still warm-start.
Cache hit: vlcDownload / upxDownload are up-to-date and we never touch
get.videolan.org. Cache miss (first run after version bump, or a new
runner): we fall back to the network, where the existing 5-attempt
retry budget in build.gradle.kts already handles flakes.
The skipVlcSetup property + AMETHYST_SKIP_VLC env hooks stay in
desktopApp/build.gradle.kts as a local-dev / CCW escape hatch — the
CCW egress can't reach get.videolan.org at all and has no actions/cache.
get.videolan.org regularly times out from GitHub-hosted runners and
Claude Code web sandboxes, taking the desktop CI build down with it
even though the failure is unrelated to the change under review.
Wire a -PskipVlcSetup=true (env: AMETHYST_SKIP_VLC=true) opt-out in
desktopApp/build.gradle.kts that disables the vlcDownload, upxDownload,
and vlcSetup tasks. Pass it from build.yml so PR CI no longer gates on
VLC reachability, and export the env from the CCW session-start hook.
Release packaging (create-release.yml) intentionally does not set the
flag, so shipped artifacts still bundle VLC.
jpackage runs `dpkg-shlibdeps` against the bundled JDK runtime's native
libs (libfontmanager.so etc. link libicu) and pins Depends to the build
host's libicu version. CI builds on ubuntu-24.04, so the .deb requires
libicu74 — uninstallable on Ubuntu 22.04 (libicu70), Debian 12 (libicu72),
Debian 11 (libicu67), and Debian 13 (libicu76). Neither jpackage nor the
Compose Multiplatform DSL exposes a way to override the auto-generated
Depends.
Add scripts/relax-deb-libicu.sh which extracts the .deb with `dpkg-deb -R`,
rewrites `libicuNN` (or any alternation thereof) to
`libicu66 | libicu67 | libicu70 | libicu72 | libicu74 | libicu76 | libicu77`,
and repacks. The script is idempotent and a no-op for .debs without a
libicu Depends.
Wire it into both release legs (desktopApp + amy CLI) in
create-release.yml, and into the desktop test/build leg in build.yml so
testers downloading the CI artifact hit the same fix.
Android Lint catches API-level, deprecation, accessibility, and resource
issues that spotless and unit tests miss. Run lint on the same variants
that get assembled (fdroidBenchmark, playBenchmark) before tests so a
lint failure surfaces quickly without consuming the full test budget.
Lint reports are uploaded as artifacts for inspection.
The bare 'test' task is ambiguous in KMP modules (candidates include
testAndroid and testAndroidHostTest). Switch quartz/commons/nestsClient
to :jvmTest so the desktop matrix unambiguously runs the JVM test
target on each OS. cli and desktopApp remain on :test (pure JVM).
nestsClient is a KMP module with both JVM and Android targets. Add it
to the desktop matrix's explicit test list so its JVM tests run on all
three desktop OSes alongside quartz/commons/cli/desktopApp.
The merged test-and-build matrix hid Android behind an OS-keyed matrix
leg, so the workflow graph no longer showed a distinct Android node.
Restore visibility by splitting into two parallel jobs after lint:
- build-desktop (matrix: ubuntu/macos/windows): runs JVM tests for the
KMP/JVM modules (quartz, commons, cli, desktopApp) and packages the
native installer per OS.
- test-and-build-android (ubuntu): runs the full test sweep and
assembleBenchmark, uploads APKs and reports.
Trade-off: Ubuntu spins up two parallel runners instead of one, but
wall time is unchanged and Android is once again a visible node.
The release workflow builds its own signed release APKs/AABs and does
not consume the debug outputs. Testers use the Benchmark APK, which is
still built and uploaded. Dropping assembleDebug removes a redundant
APK packaging pass from every PR/main run.
Combining them into a single Gradle invocation caused
:amethyst:packagePlayBenchmark to fail inside AGP's
IncrementalSplitterRunnable worker — both variants share packaging
state in one task graph and the benchmark leg trips on it.
The old workflow always ran these as two separate `./gradlew` calls.
Match that pattern. They still run on the same runner with a warm
Gradle cache, so the compilation savings from sharing quartz/commons
with the test+packageDeb step are preserved; we only pay two Gradle
startups, which is trivial.
Previously each OS ran Gradle twice (or three times on Ubuntu): once to
compile+test, then again to build desktop packages, plus a third run on
Ubuntu for Android APKs. Merging these into a single matrix job per OS
lets Gradle build the task graph once and reuse compiled classes across
test, desktop packaging, and Android assembly.
- Combine test + build-desktop into one matrix job (test-and-build)
- Fold build-android into the same Ubuntu matrix leg
- Use --no-daemon consistently; drop the redundant --stop steps
- Raise timeout to 60 min to cover the combined work
- Split build.yml into lint, test (matrix: 3 OS), build-android, and
build-desktop (matrix: 3 OS with native packaging) jobs
- Tests now run on ubuntu, macos, and windows to validate cross-platform
compatibility
- Desktop distributions (DEB, DMG, MSI) built on their native OS and
uploaded as artifacts
- Split create-release.yml into create-release, deploy-android, and
deploy-desktop jobs so desktop distributions are included in releases
- Android APK/AAB builds and Quartz publishing remain on Linux
https://claude.ai/code/session_016dxFUErY4P6WRir4xxhCEr