diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-11-13 16:40:02 -0800 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-11-13 16:40:02 -0800 |
| commit | 3803f14160c30ac470bc957f8a0f2dbe67c4fd57 (patch) | |
| tree | b7cfc3e0570ef7766e053b89ca038109c353d359 | |
| parent | 8974b115b69bc25ebf174e8c9637373c4d206325 (diff) | |
| download | rookery-main.tar.gz rookery-main.zip | |
| -rw-r--r-- | Dockerfile | 11 | ||||
| -rw-r--r-- | android/build.gradle.kts | 4 | ||||
| -rw-r--r-- | android/src/main/AndroidManifest.xml | 2 | ||||
| -rw-r--r-- | android/src/main/res/mipmap-hdpi/ic_launcher.png | bin | 0 -> 575 bytes | |||
| -rw-r--r-- | android/src/main/res/mipmap-mdpi/ic_launcher.png | bin | 0 -> 540 bytes | |||
| -rw-r--r-- | android/src/main/res/mipmap-xhdpi/ic_launcher.png | bin | 0 -> 588 bytes | |||
| -rw-r--r-- | android/src/main/res/mipmap-xxhdpi/ic_launcher.png | bin | 0 -> 628 bytes | |||
| -rw-r--r-- | android/src/main/res/mipmap-xxxhdpi/ic_launcher.png | bin | 0 -> 616 bytes | |||
| -rw-r--r-- | desktop/build.gradle.kts | 2 | ||||
| -rw-r--r-- | desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt | 3 | ||||
| -rw-r--r-- | site/.gitignore | 7 | ||||
| -rwxr-xr-x | site/assemble.sh | 12 | ||||
| -rw-r--r-- | site/assets/favicon.png | bin | 0 -> 511 bytes | |||
| -rw-r--r-- | site/assets/penguin.png | bin | 0 -> 821 bytes | |||
| -rw-r--r-- | site/assets/rookery.icns | bin | 0 -> 46522 bytes | |||
| -rwxr-xr-x | site/macos-app.sh | 67 | ||||
| -rw-r--r-- | ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt | 13 |
17 files changed, 113 insertions, 8 deletions
@@ -3,10 +3,14 @@ FROM eclipse-temurin:21-jdk AS build WORKDIR /src # Android SDK: core and ui have Android targets, so Gradle needs one even for the desktop build. ENV ANDROID_HOME=/opt/android-sdk -RUN apt-get update && apt-get install -y --no-install-recommends unzip curl && rm -rf /var/lib/apt/lists/* \ +RUN apt-get update && apt-get install -y --no-install-recommends unzip zip curl && rm -rf /var/lib/apt/lists/* \ && mkdir -p $ANDROID_HOME/cmdline-tools && curl -fsSL -o /tmp/clt.zip https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip \ && unzip -q /tmp/clt.zip -d $ANDROID_HOME/cmdline-tools && mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest && rm /tmp/clt.zip \ && yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null +# macOS Java runtimes can be linked on Linux from a macOS JDK's jmods, so keep just those (about 40 MB each). +RUN for a in aarch64 x64; do mkdir -p /opt/macjdk/$a \ + && curl -fsSL "https://api.adoptium.net/v3/binary/latest/21/ga/mac/$a/jdk/hotspot/normal/eclipse" \ + | tar -xz -C /opt/macjdk/$a --strip-components=1 --wildcards '*/Contents/Home/jmods'; done # Warm the Gradle wrapper and dependency cache before copying sources. COPY gradlew settings.gradle.kts build.gradle.kts gradle.properties ./ COPY gradle gradle @@ -16,8 +20,9 @@ COPY desktop/build.gradle.kts desktop/ COPY android/build.gradle.kts android/ RUN ./gradlew --no-daemon -q help COPY . . -# Tests, then every download: the Linux app (with its runtime), the universal jar and the Android APK. -RUN ./gradlew --no-daemon :core:jvmTest :desktop:createDistributable :desktop:universalJar :android:assembleDebug +# Tests, then every download: the Linux app (with its runtime), the universal jar and the release APK. +RUN ./gradlew --no-daemon :core:jvmTest :desktop:createDistributable :desktop:universalJar :android:assembleRelease +RUN sh site/macos-app.sh 0.1.0 # The commit goes in last, so a new commit doesn't redo the build above's cache-able layers. ARG COMMIT=unknown RUN sh site/assemble.sh /site "$COMMIT" diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 0c7509f..08498ac 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -14,6 +14,10 @@ android { versionName = "0.1.0" } buildFeatures { compose = true } + buildTypes { + // An installable release APK; there is no Play keystore, so the debug key signs it. + release { signingConfig = signingConfigs.getByName("debug") } + } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 7266865..e7da8ab 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" /> <uses-feature android:name="android.hardware.microphone" android:required="false" /> - <application android:label="rookery" android:theme="@android:style/Theme.Black.NoTitleBar" android:allowBackup="true"> + <application android:label="rookery" android:icon="@mipmap/ic_launcher" android:theme="@android:style/Theme.Black.NoTitleBar" android:allowBackup="true"> <activity android:name=".MainActivity" android:exported="true" android:windowSoftInputMode="adjustResize" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|uiMode"> <intent-filter> diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher.png b/android/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..3106080 --- /dev/null +++ b/android/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.png b/android/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..15027b5 --- /dev/null +++ b/android/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..8379328 --- /dev/null +++ b/android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..f04517b --- /dev/null +++ b/android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..fe080bc --- /dev/null +++ b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index d72a319..2a9287b 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -23,6 +23,8 @@ compose.desktop { modules("java.instrument", "java.net.http", "jdk.security.auth", "jdk.unsupported") targetFormats(TargetFormat.AppImage, TargetFormat.Deb, TargetFormat.Dmg) macOS { + // The alert penguin (same icns the site/macos-app.sh bundle uses). + iconFile = rootProject.file("site/assets/rookery.icns") infoPlist { extraKeysRawXml = """ <key>NSBluetoothAlwaysUsageDescription</key><string>Connects to your smart cube.</string> diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt index 12a856a..0629e23 100644 --- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt +++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt @@ -27,6 +27,7 @@ import coffee.liz.rookery.ui.AppModel import coffee.liz.rookery.ui.Host import coffee.liz.rookery.ui.RookeryApp import coffee.liz.rookery.ui.handleTimerKey +import coffee.liz.rookery.ui.rememberPenguinIcon import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -257,7 +258,7 @@ fun main(args: Array<String>) { } } Window( - onCloseRequest = ::exitApplication, state = state, title = "rookery", + onCloseRequest = ::exitApplication, state = state, title = "rookery", icon = rememberPenguinIcon(), onPreviewKeyEvent = { handleTimerKey(model, it) }, ) { RookeryApp(model) diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..3343c00 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,7 @@ +# site/assemble.sh can publish straight into this folder; everything except the sources is generated. +/* +!/assets/ +!/assemble.sh +!/macos-app.sh +!/nginx.conf +!/.gitignore diff --git a/site/assemble.sh b/site/assemble.sh index aed5b27..52cd84c 100755 --- a/site/assemble.sh +++ b/site/assemble.sh @@ -13,12 +13,14 @@ case $(uname -m) in x86_64|amd64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; *) ar sha() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi; } size() { du -h "$1" | cut -f1 | tr -d ' '; } -apk=$root/android/build/outputs/apk/debug/android-debug.apk +apk=$root/android/build/outputs/apk/release/android-release.apk jar=$root/desktop/build/libs/rookery-universal.jar app=$root/desktop/build/compose/binaries/main/app/rookery [ -f "$apk" ] && cp "$apk" "$out/rookery-android.apk" [ -f "$jar" ] && cp "$jar" "$out/rookery.jar" +cp "$root"/site/assets/*.png "$out/" +for z in "$root"/desktop/build/macos/rookery-macos-*.zip; do [ -f "$z" ] && cp "$z" "$out/"; done [ -d "$app" ] && tar -C "$(dirname "$app")" -czf "$out/rookery-linux-$arch.tar.gz" rookery printf '%s\n' "$commit" > "$out/commit.txt" @@ -39,6 +41,7 @@ cat > "$out/index.html" <<HTML <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="icon" href="favicon.png"> <title>rookery</title> <style> :root { --bg: black; --fg: white; --a: #c4d0c8; --h: #7d978c; --line: #3a403d; --dim: #a3ada7; } @@ -54,16 +57,19 @@ cat > "$out/index.html" <<HTML </style> </head> <body> -<h1>rookery</h1> +<h1><img src="penguin.png" alt="" width="32" height="32" style="vertical-align:-7px; image-rendering:pixelated"> rookery</h1> <p>speedcubing timer and blindfold trainer. desktop and android.</p> <hr> <h2>downloads</h2> <table> -$(row rookery-android.apk "Android 8+, debug-signed") +$(row rookery-macos-arm64.zip "macOS 11+, Apple Silicon, not notarized") +$(row rookery-macos-x64.zip "macOS 11+, Intel, not notarized") +$(row rookery-android.apk "Android 8+, release build (debug-signed)") $(row "rookery-linux-$arch.tar.gz" "Linux $arch, runtime included") $(row rookery.jar "Java 21; Linux, macOS, Windows") </table> <ul> + <li>macOS: unzip, then run <tt>xattr -dr com.apple.quarantine Rookery.app</tt> once. The app isn't notarized.</li> <li>apk: allow installing from this source when asked.</li> <li>tar.gz: extract, then run <tt>rookery/bin/rookery</tt>.</li> <li>jar: <tt>java -jar rookery.jar</tt></li> diff --git a/site/assets/favicon.png b/site/assets/favicon.png Binary files differnew file mode 100644 index 0000000..162fffc --- /dev/null +++ b/site/assets/favicon.png diff --git a/site/assets/penguin.png b/site/assets/penguin.png Binary files differnew file mode 100644 index 0000000..6e9db07 --- /dev/null +++ b/site/assets/penguin.png diff --git a/site/assets/rookery.icns b/site/assets/rookery.icns Binary files differnew file mode 100644 index 0000000..f904afd --- /dev/null +++ b/site/assets/rookery.icns diff --git a/site/macos-app.sh b/site/macos-app.sh new file mode 100755 index 0000000..e7b9bc3 --- /dev/null +++ b/site/macos-app.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# Builds Rookery.app for macOS without a Mac: a macOS Java runtime made by jlink from the macOS JDK's +# jmods, the universal jar, an Info.plist and a small launcher. It is not signed or notarized. +# usage: site/macos-app.sh <version> [dir holding aarch64/ and x64/ macOS JDKs (jmods only is enough)] +set -eu + +root=$(cd "$(dirname "$0")/.." && pwd) +version=${1:?usage: macos-app.sh <version> [jdk dir]} +jdks=${2:-/opt/macjdk} +jar=$root/desktop/build/libs/rookery-universal.jar +out=$root/desktop/build/macos + +# Use the same modules the desktop build decided the app needs (Compose records them in the runtime's release file). +release=$(ls "$root"/desktop/build/compose/binaries/main/app/rookery/lib/runtime/release \ + "$root"/desktop/build/compose/binaries/main/app/rookery.app/Contents/runtime/Contents/Home/release 2>/dev/null | head -1) +modules=$(sed -n 's/^MODULES="\(.*\)"/\1/p' "$release" | tr ' ' ',') +[ -n "$modules" ] || { echo "no module list in $release" >&2; exit 1; } + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$out" + +for pair in aarch64:arm64 x64:x64; do + jdk=${pair%%:*}; name=${pair##*:} + app=$work/$name/Rookery.app + mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" "$app/Contents/app" + + cp "$root/site/assets/rookery.icns" "$app/Contents/Resources/rookery.icns" + jlink --module-path "$jdks/$jdk/Contents/Home/jmods" --add-modules "$modules" \ + --strip-java-debug-attributes --no-header-files --no-man-pages --compress zip-6 --output "$app/Contents/runtime" + cp "$jar" "$app/Contents/app/rookery.jar" + # the universal jar carries every platform's graphics library; this app only needs its own + case $name in arm64) other=x64 ;; *) other=arm64 ;; esac + zip -qd "$app/Contents/app/rookery.jar" 'libskiko-linux-*' 'skiko-windows-*' "libskiko-macos-$other*" > /dev/null || true + + cat > "$app/Contents/MacOS/rookery" <<'LAUNCHER' +#!/bin/sh +here=$(cd "$(dirname "$0")/.." && pwd) +exec "$here/runtime/bin/java" -Xdock:name=Rookery -Xmx256m -XX:+UseSerialGC -jar "$here/app/rookery.jar" "$@" +LAUNCHER + chmod +x "$app/Contents/MacOS/rookery" + + cat > "$app/Contents/Info.plist" <<PLIST +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleName</key><string>Rookery</string> + <key>CFBundleDisplayName</key><string>Rookery</string> + <key>CFBundleIdentifier</key><string>coffee.liz.rookery</string> + <key>CFBundleExecutable</key><string>rookery</string> + <key>CFBundlePackageType</key><string>APPL</string> + <key>CFBundleIconFile</key><string>rookery</string> + <key>CFBundleShortVersionString</key><string>$version</string> + <key>CFBundleVersion</key><string>$version</string> + <key>LSMinimumSystemVersion</key><string>11.0</string> + <key>NSHighResolutionCapable</key><true/> + <key>NSBluetoothAlwaysUsageDescription</key><string>Connects to your smart cube.</string> + <key>NSMicrophoneUsageDescription</key><string>Listens to a stackmat timer on an audio input.</string> +</dict> +</plist> +PLIST + + rm -f "$out/rookery-macos-$name.zip" + (cd "$work/$name" && zip -qry "$out/rookery-macos-$name.zip" Rookery.app) + echo "macos: rookery-macos-$name.zip $(du -h "$out/rookery-macos-$name.zip" | cut -f1)" +done diff --git a/ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt b/ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt index 5dca583..4a77f33 100644 --- a/ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt +++ b/ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics @@ -82,6 +83,18 @@ fun DrawScope.drawPenguin(sheet: ImageBitmap, cell: Pair<Int, Int>, at: Offset, ) } +/** The alert penguin as the app's window icon, redrawn at whatever size the window system asks for. */ +@Composable +fun rememberPenguinIcon(): Painter { + val sheet = penguinSheet() + return remember(sheet) { + object : Painter() { + override val intrinsicSize = Size(SPRITE.toFloat(), SPRITE.toFloat()) + override fun DrawScope.onDraw() = drawPenguin(sheet, PenguinAnim.ALERT.frames.first(), Offset.Zero, size.minDimension) + } + } +} + /** A single penguin doing one thing. */ @Composable fun Penguin(anim: PenguinAnim, size: Dp = 64.dp, reducedMotion: Boolean = false, modifier: Modifier = Modifier, frameMs: Long = 220) { |
