Das Installationsfenster sieht aus wie eines
Zwei Symbole auf grauem Grund sagen nicht, was zu tun ist. Jetzt: dunkler Verlauf in den Onyx-Farben, Titel, ein Satz darunter, eine Haarlinie und ein Pfeil, der vom Programm zum Programme-Ordner zeigt. Die Fassung steht in der Fußzeile — damit man später weiß, was man installiert hat. Der Hintergrund wird gezeichnet, nicht mitgeliefert: ein Bild im Verzeichnis wäre eine Datei, die niemand mehr anfasst und die bei jeder Farbänderung veraltet. So stehen die Onyx-Farben genau einmal, im selben Werkzeugkasten wie der Rest. Der Aufwand steckt nicht im Bild, sondern in der Reihenfolge: Fenstergröße, Symbolplätze und Hintergrund leben in der .DS_Store des Abbilds. Die schreibt nur der Finder, und nur in ein beschreibbares Abbild. Also erst UDRW, dann über AppleScript einrichten, warten bis der Finder geschrieben hat, dann zusammenpressen. Schlägt die Automatisierung fehl, bleibt das Fenster schlicht statt das Skript abzubrechen. Als Bibliothek, weil beide Wege dasselbe Fenster zeigen sollen — der lokale wie der notarisierte. Und als TIFF mit beiden Auflösungen: als reines PNG wäre der Hintergrund auf einem Retina-Bildschirm doppelt so groß wie das Fenster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
125
Scripts/dmg-background.swift
Normal file
125
Scripts/dmg-background.swift
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Zeichnet den Hintergrund des Installationsfensters.
|
||||||
|
//
|
||||||
|
// Gezeichnet statt mitgeliefert: ein Bild im Verzeichnis wäre eine Datei, die
|
||||||
|
// niemand mehr anfasst und die bei jeder Farbänderung veraltet. So steht das
|
||||||
|
// Aussehen im selben Werkzeugkasten wie der Rest — und die Onyx-Farben stehen
|
||||||
|
// hier genau einmal.
|
||||||
|
//
|
||||||
|
// Aufruf: dmg-background <ziel-ohne-endung>
|
||||||
|
// Erzeugt <ziel>.png und <ziel>@2x.png.
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
let arguments = CommandLine.arguments
|
||||||
|
guard arguments.count == 2 else {
|
||||||
|
FileHandle.standardError.write(Data("Aufruf: dmg-background <ziel-ohne-endung>\n".utf8))
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
let target = arguments[1]
|
||||||
|
|
||||||
|
// Maße des Fensters in Punkten. Die Symbole setzt das Skript an dieselben
|
||||||
|
// Stellen — ändert sich hier etwas, muss es dort mitwandern.
|
||||||
|
let width: CGFloat = 640
|
||||||
|
let height: CGFloat = 400
|
||||||
|
let iconY: CGFloat = 196 // Mitte der Symbolreihe, von oben
|
||||||
|
let leftIcon: CGFloat = 170
|
||||||
|
let rightIcon: CGFloat = 470
|
||||||
|
|
||||||
|
func draw(scale: CGFloat, to path: String) {
|
||||||
|
let pixelWidth = Int(width * scale)
|
||||||
|
let pixelHeight = Int(height * scale)
|
||||||
|
|
||||||
|
guard let context = CGContext(data: nil, width: pixelWidth, height: pixelHeight,
|
||||||
|
bitsPerComponent: 8, bytesPerRow: 0,
|
||||||
|
space: CGColorSpace(name: CGColorSpace.sRGB)!,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||||
|
else { exit(1) }
|
||||||
|
|
||||||
|
context.scaleBy(x: scale, y: scale)
|
||||||
|
// Von oben nach unten rechnen wie im Fenster; Core Graphics fängt unten an.
|
||||||
|
context.translateBy(x: 0, y: height)
|
||||||
|
context.scaleBy(x: 1, y: -1)
|
||||||
|
|
||||||
|
let graphics = NSGraphicsContext(cgContext: context, flipped: true)
|
||||||
|
NSGraphicsContext.current = graphics
|
||||||
|
|
||||||
|
// Grundfläche: derselbe flache Verlauf wie im Panel. Zwei Prozent Weiß oben,
|
||||||
|
// nichts unten — mehr wirkt billig.
|
||||||
|
let surface = NSColor(srgbRed: 0.043, green: 0.043, blue: 0.051, alpha: 1)
|
||||||
|
let elevated = NSColor(srgbRed: 0.078, green: 0.078, blue: 0.090, alpha: 1)
|
||||||
|
NSGradient(colors: [elevated, surface])?
|
||||||
|
.draw(in: NSRect(x: 0, y: 0, width: width, height: height), angle: -90)
|
||||||
|
|
||||||
|
// Die Haarlinie unter dem Kopf — der halbe „edle" Effekt steckt darin.
|
||||||
|
NSColor(white: 1, alpha: 0.08).setFill()
|
||||||
|
NSRect(x: 40, y: 118, width: width - 80, height: 1).fill()
|
||||||
|
|
||||||
|
let title = "Onyx"
|
||||||
|
let titleAttributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: NSFont.systemFont(ofSize: 30, weight: .semibold),
|
||||||
|
.foregroundColor: NSColor(srgbRed: 0.949, green: 0.949, blue: 0.957, alpha: 1),
|
||||||
|
.kern: -0.5,
|
||||||
|
]
|
||||||
|
let titleSize = title.size(withAttributes: titleAttributes)
|
||||||
|
title.draw(at: NSPoint(x: (width - titleSize.width) / 2, y: 48),
|
||||||
|
withAttributes: titleAttributes)
|
||||||
|
|
||||||
|
let subtitle = "Zum Installieren nach „Programme“ ziehen"
|
||||||
|
let subtitleAttributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: NSFont.systemFont(ofSize: 12, weight: .regular),
|
||||||
|
.foregroundColor: NSColor(white: 1, alpha: 0.58),
|
||||||
|
]
|
||||||
|
let subtitleSize = subtitle.size(withAttributes: subtitleAttributes)
|
||||||
|
subtitle.draw(at: NSPoint(x: (width - subtitleSize.width) / 2, y: 88),
|
||||||
|
withAttributes: subtitleAttributes)
|
||||||
|
|
||||||
|
// Der Pfeil zwischen den beiden Symbolen: er sagt in einem Bild, was der
|
||||||
|
// Satz darüber in sieben Wörtern sagt.
|
||||||
|
let accent = NSColor(srgbRed: 0.498, green: 0.659, blue: 1.0, alpha: 0.55)
|
||||||
|
accent.setStroke()
|
||||||
|
let shaft = NSBezierPath()
|
||||||
|
let from = leftIcon + 92
|
||||||
|
let to = rightIcon - 92
|
||||||
|
shaft.move(to: NSPoint(x: from, y: iconY))
|
||||||
|
shaft.line(to: NSPoint(x: to - 10, y: iconY))
|
||||||
|
shaft.lineWidth = 3
|
||||||
|
shaft.lineCapStyle = .round
|
||||||
|
shaft.stroke()
|
||||||
|
|
||||||
|
let head = NSBezierPath()
|
||||||
|
head.move(to: NSPoint(x: to - 18, y: iconY - 9))
|
||||||
|
head.line(to: NSPoint(x: to, y: iconY))
|
||||||
|
head.line(to: NSPoint(x: to - 18, y: iconY + 9))
|
||||||
|
head.lineWidth = 3
|
||||||
|
head.lineCapStyle = .round
|
||||||
|
head.lineJoinStyle = .round
|
||||||
|
head.stroke()
|
||||||
|
|
||||||
|
// Fußzeile: die Fassung gehört sichtbar dazu, damit man später weiß, was
|
||||||
|
// man da eigentlich installiert hat.
|
||||||
|
let version = ProcessInfo.processInfo.environment["ONYX_VERSION"] ?? ""
|
||||||
|
if !version.isEmpty {
|
||||||
|
let footer = "Fassung \(version)"
|
||||||
|
let footerAttributes: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: NSFont.systemFont(ofSize: 10, weight: .regular),
|
||||||
|
.foregroundColor: NSColor(white: 1, alpha: 0.34),
|
||||||
|
]
|
||||||
|
let footerSize = footer.size(withAttributes: footerAttributes)
|
||||||
|
footer.draw(at: NSPoint(x: (width - footerSize.width) / 2, y: height - 34),
|
||||||
|
withAttributes: footerAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
NSGraphicsContext.current = nil
|
||||||
|
|
||||||
|
guard let image = context.makeImage(),
|
||||||
|
let destination = CGImageDestinationCreateWithURL(
|
||||||
|
URL(fileURLWithPath: path) as CFURL, "public.png" as CFString, 1, nil)
|
||||||
|
else { exit(1) }
|
||||||
|
CGImageDestinationAddImage(destination, image, nil)
|
||||||
|
CGImageDestinationFinalize(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
import ImageIO
|
||||||
|
|
||||||
|
draw(scale: 1, to: target + ".png")
|
||||||
|
draw(scale: 2, to: target + "@2x.png")
|
||||||
75
Scripts/lib-dmg.sh
Normal file
75
Scripts/lib-dmg.sh
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Baut ein Installationsfenster, das aussieht wie eines.
|
||||||
|
#
|
||||||
|
# Von beiden Wegen benutzt — dem lokalen wie dem notarisierten —, damit das
|
||||||
|
# Fenster in beiden Fällen dasselbe ist.
|
||||||
|
#
|
||||||
|
# Der Aufwand steckt nicht im Bild, sondern in der Reihenfolge: die
|
||||||
|
# Fenstergröße, die Symbolplätze und das Hintergrundbild leben in der
|
||||||
|
# `.DS_Store` des Abbilds. Die schreibt nur der Finder, und der schreibt sie
|
||||||
|
# nur in ein **beschreibbares** Abbild. Also erst UDRW, dann einrichten, dann
|
||||||
|
# zusammenpressen.
|
||||||
|
|
||||||
|
# Erwartet: $1 App-Bundle, $2 Ziel-DMG, $3 Fenstertitel, $4 Arbeitsverzeichnis,
|
||||||
|
# $5 Fassung (für die Fußzeile)
|
||||||
|
make_pretty_dmg() {
|
||||||
|
local app="$1" dmg="$2" volname="$3" work="$4" version="$5"
|
||||||
|
local stage="$work/stage" temp="$work/rw.dmg"
|
||||||
|
local root; root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
rm -rf "$stage" "$temp"; mkdir -p "$stage/.background"
|
||||||
|
|
||||||
|
echo "→ Hintergrund zeichnen"
|
||||||
|
xcrun swiftc -O "$root/Scripts/dmg-background.swift" -o "$work/dmg-bg" 2>/dev/null
|
||||||
|
ONYX_VERSION="$version" "$work/dmg-bg" "$work/bg"
|
||||||
|
# Ein TIFF mit beiden Auflösungen: als reines PNG wäre der Hintergrund auf
|
||||||
|
# einem Retina-Bildschirm doppelt so groß wie das Fenster.
|
||||||
|
tiffutil -cathidpicheck "$work/bg.png" "$work/bg@2x.png" \
|
||||||
|
-out "$stage/.background/background.tiff" >/dev/null
|
||||||
|
|
||||||
|
cp -R "$app" "$stage/"
|
||||||
|
ln -s /Applications "$stage/Applications"
|
||||||
|
|
||||||
|
echo "→ Abbild anlegen"
|
||||||
|
hdiutil create -volname "$volname" -srcfolder "$stage" -size 100m \
|
||||||
|
-fs HFS+ -format UDRW -ov "$temp" >/dev/null
|
||||||
|
|
||||||
|
local mount="/Volumes/$volname"
|
||||||
|
hdiutil attach "$temp" -nobrowse -quiet
|
||||||
|
# Ohne kurze Pause findet das nächste Skript das Volume noch nicht.
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "→ Fenster einrichten"
|
||||||
|
osascript <<APPLESCRIPT >/dev/null || echo " (Finder nicht erreichbar — das Fenster bleibt schlicht)"
|
||||||
|
tell application "Finder"
|
||||||
|
tell disk "$volname"
|
||||||
|
open
|
||||||
|
set current view of container window to icon view
|
||||||
|
set toolbar visible of container window to false
|
||||||
|
set statusbar visible of container window to false
|
||||||
|
-- 640 x 400 Punkte Inhalt; die Zahlen sind links, oben, rechts, unten.
|
||||||
|
set the bounds of container window to {240, 140, 880, 540}
|
||||||
|
set options to the icon view options of container window
|
||||||
|
set arrangement of options to not arranged
|
||||||
|
set icon size of options to 128
|
||||||
|
set text size of options to 12
|
||||||
|
set background picture of options to file ".background:background.tiff"
|
||||||
|
set position of item "$(basename "$app")" of container window to {170, 196}
|
||||||
|
set position of item "Applications" of container window to {470, 196}
|
||||||
|
update without registering applications
|
||||||
|
delay 1
|
||||||
|
close
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
APPLESCRIPT
|
||||||
|
|
||||||
|
# Der Finder schreibt die .DS_Store verzögert; ohne das Warten liegt im
|
||||||
|
# fertigen Abbild die alte.
|
||||||
|
sync
|
||||||
|
sleep 2
|
||||||
|
hdiutil detach "$mount" -quiet || hdiutil detach "$mount" -force -quiet
|
||||||
|
|
||||||
|
echo "→ Zusammenpressen"
|
||||||
|
rm -f "$dmg"
|
||||||
|
hdiutil convert "$temp" -format UDZO -imagekey zlib-level=9 -o "$dmg" >/dev/null
|
||||||
|
rm -f "$temp"
|
||||||
|
}
|
||||||
@@ -64,16 +64,8 @@ HELPER_ID=$(codesign -d --verbose=2 "$APP/Contents/MacOS/OnyxHelper" 2>&1 \
|
|||||||
echo "✓ Signatur, Entitlements und Helfer-Kennung in Ordnung"
|
echo "✓ Signatur, Entitlements und Helfer-Kennung in Ordnung"
|
||||||
|
|
||||||
step "DMG bauen"
|
step "DMG bauen"
|
||||||
STAGE="$BUILD/stage"
|
source "$ROOT/Scripts/lib-dmg.sh"
|
||||||
rm -rf "$STAGE"; mkdir -p "$STAGE"
|
make_pretty_dmg "$APP" "$DMG" "$APP_NAME" "$BUILD" "$VERSION"
|
||||||
cp -R "$APP" "$STAGE/"
|
|
||||||
# Der Verweis auf den Programme-Ordner: ohne ihn zieht man die App irgendwohin,
|
|
||||||
# und der privilegierte Helfer verlangt einen festen Ort.
|
|
||||||
ln -s /Applications "$STAGE/Applications"
|
|
||||||
|
|
||||||
rm -f "$DMG"
|
|
||||||
hdiutil create -volname "$APP_NAME $VERSION" -srcfolder "$STAGE" \
|
|
||||||
-ov -format UDZO "$DMG" | tail -1
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "✓ Fertig: $DMG"
|
echo "✓ Fertig: $DMG"
|
||||||
|
|||||||
@@ -124,13 +124,8 @@ echo "✓ Signaturen, Entitlements, Helfer-Kennung und Adapter in Ordnung"
|
|||||||
|
|
||||||
# ------------------------------------------------------------------- DMG
|
# ------------------------------------------------------------------- DMG
|
||||||
step "DMG bauen"
|
step "DMG bauen"
|
||||||
STAGE="$BUILD/stage"
|
source "$ROOT/Scripts/lib-dmg.sh"
|
||||||
rm -rf "$STAGE"; mkdir -p "$STAGE"
|
make_pretty_dmg "$APP" "$DMG" "$APP_NAME" "$BUILD" "$VERSION"
|
||||||
cp -R "$APP" "$STAGE/"
|
|
||||||
ln -s /Applications "$STAGE/Applications"
|
|
||||||
|
|
||||||
rm -f "$DMG"
|
|
||||||
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DMG" | tail -1
|
|
||||||
|
|
||||||
# Das DMG selbst wird mitsigniert — sonst meldet Gatekeeper beim Öffnen einen
|
# Das DMG selbst wird mitsigniert — sonst meldet Gatekeeper beim Öffnen einen
|
||||||
# unbekannten Entwickler, obwohl die App darin einwandfrei ist.
|
# unbekannten Entwickler, obwohl die App darin einwandfrei ist.
|
||||||
|
|||||||
Reference in New Issue
Block a user