summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Dockerfile11
-rw-r--r--android/build.gradle.kts4
-rw-r--r--android/src/main/AndroidManifest.xml2
-rw-r--r--android/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 575 bytes
-rw-r--r--android/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 540 bytes
-rw-r--r--android/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 588 bytes
-rw-r--r--android/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 628 bytes
-rw-r--r--android/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 616 bytes
-rw-r--r--desktop/build.gradle.kts2
-rw-r--r--desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt3
-rw-r--r--site/.gitignore7
-rwxr-xr-xsite/assemble.sh12
-rw-r--r--site/assets/favicon.pngbin0 -> 511 bytes
-rw-r--r--site/assets/penguin.pngbin0 -> 821 bytes
-rw-r--r--site/assets/rookery.icnsbin0 -> 46522 bytes
-rwxr-xr-xsite/macos-app.sh67
-rw-r--r--ui/src/commonMain/kotlin/coffee/liz/rookery/ui/Penguin.kt13
17 files changed, 113 insertions, 8 deletions
diff --git a/Dockerfile b/Dockerfile
index 84e02b2..de3b34d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
new file mode 100644
index 0000000..3106080
--- /dev/null
+++ b/android/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.png b/android/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..15027b5
--- /dev/null
+++ b/android/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..8379328
--- /dev/null
+++ b/android/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f04517b
--- /dev/null
+++ b/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..fe080bc
--- /dev/null
+++ b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
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
new file mode 100644
index 0000000..162fffc
--- /dev/null
+++ b/site/assets/favicon.png
Binary files differ
diff --git a/site/assets/penguin.png b/site/assets/penguin.png
new file mode 100644
index 0000000..6e9db07
--- /dev/null
+++ b/site/assets/penguin.png
Binary files differ
diff --git a/site/assets/rookery.icns b/site/assets/rookery.icns
new file mode 100644
index 0000000..f904afd
--- /dev/null
+++ b/site/assets/rookery.icns
Binary files differ
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) {