feat(desktop): add Dock icon preview command
This commit is contained in:
1 parent
dfe8abd965
commit
a500db5289
4 files changed
+160
-1
No files matched your search
@@ -36,6 +36,15 @@ This writes `apps/desktop/assets/icon.icns` (full ICNS) and `icon.png` (1024
|
||||
master). Packaging validation rejects incomplete ICNS types. Runtime also calls
|
||||
`app.dock.setIcon` as a Launch Services / Dock cache fallback.
|
||||
|
||||
Preview the committed icon at its real Dock size without starting EdgeEver:
|
||||
|
||||
```sh
|
||||
bun run preview:desktop:icon
|
||||
```
|
||||
|
||||
The temporary preview stays in the Dock until you quit it from the icon's
|
||||
context menu.
|
||||
|
||||
Build an unsigned installer for the current platform with:
|
||||
|
||||
```sh
|
||||
|
||||
+2
-1
@@ -22,6 +22,7 @@
|
||||
"prepare:brand:icons": "bun scripts/trace-brand-icon.mjs && bun scripts/prepare-brand-icons.mjs",
|
||||
"prepare:mobile:icons": "bun scripts/prepare-mobile-icons.mjs",
|
||||
"prepare:desktop:icons": "bun scripts/prepare-desktop-icons.mjs",
|
||||
"preview:desktop:icon": "bun scripts/preview-macos-dock-icon.mjs",
|
||||
"build:android:fast": "bash scripts/build-android-local.sh fast",
|
||||
"build:android:fast:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast",
|
||||
"build:android:apk": "bash scripts/build-android-local.sh apk",
|
||||
@@ -43,7 +44,7 @@
|
||||
"build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist",
|
||||
"benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs",
|
||||
"verify:web-performance": "bun scripts/verify-web-performance.mjs",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
|
||||
"test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs scripts/preview-macos-dock-icon.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
|
||||
"release": "bun scripts/release.mjs",
|
||||
"publish:stores": "bun scripts/store-delivery.mjs",
|
||||
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { copyFile, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { assertMacIcnsComplete } from "./desktop-icns.mjs";
|
||||
|
||||
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultIconPath = join(projectRoot, "apps/desktop/assets/icon.icns");
|
||||
|
||||
export const PREVIEW_APP_NAME = "EdgeEver Logo Preview";
|
||||
|
||||
export const createPreviewInfoPlist = (bundleIdentifier) => `<?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>CFBundleDisplayName</key>
|
||||
<string>${PREVIEW_APP_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>DockIconPreview</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${bundleIdentifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PREVIEW_APP_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<false/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
|
||||
export const previewSwiftSource = `import AppKit
|
||||
|
||||
final class DockIconPreviewDelegate: NSObject, NSApplicationDelegate {
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
if let iconPath = Bundle.main.path(forResource: "AppIcon", ofType: "icns"),
|
||||
let icon = NSImage(contentsOfFile: iconPath) {
|
||||
NSApp.applicationIconImage = icon
|
||||
}
|
||||
NSApp.activate(ignoringOtherApps: false)
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
let application = NSApplication.shared
|
||||
let delegate = DockIconPreviewDelegate()
|
||||
application.delegate = delegate
|
||||
application.run()
|
||||
`;
|
||||
|
||||
export const assertPreviewPlatform = (platform) => {
|
||||
if (platform !== "darwin") {
|
||||
throw new Error("Dock icon preview is only available on macOS.");
|
||||
}
|
||||
};
|
||||
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout || result.error}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const launchDockIconPreview = async ({ iconPath = defaultIconPath } = {}) => {
|
||||
assertPreviewPlatform(process.platform);
|
||||
const resolvedIconPath = resolve(iconPath);
|
||||
assertMacIcnsComplete(await readFile(resolvedIconPath), resolvedIconPath);
|
||||
|
||||
const previewRoot = await mkdtemp(join(tmpdir(), "edgeever-dock-icon-preview-"));
|
||||
const appPath = join(previewRoot, `${PREVIEW_APP_NAME}.app`);
|
||||
const contentsPath = join(appPath, "Contents");
|
||||
const macosPath = join(contentsPath, "MacOS");
|
||||
const resourcesPath = join(contentsPath, "Resources");
|
||||
const sourcePath = join(previewRoot, "DockIconPreview.swift");
|
||||
const executablePath = join(macosPath, "DockIconPreview");
|
||||
const bundleIdentifier = `org.edgeever.logo-preview.v${Date.now()}p${process.pid}`;
|
||||
|
||||
await mkdir(macosPath, { recursive: true });
|
||||
await mkdir(resourcesPath, { recursive: true });
|
||||
await writeFile(sourcePath, previewSwiftSource);
|
||||
await writeFile(join(contentsPath, "Info.plist"), createPreviewInfoPlist(bundleIdentifier));
|
||||
await copyFile(resolvedIconPath, join(resourcesPath, "AppIcon.icns"));
|
||||
|
||||
run("xcrun", ["swiftc", sourcePath, "-o", executablePath, "-framework", "AppKit"]);
|
||||
run("codesign", ["--force", "--deep", "--sign", "-", appPath]);
|
||||
run("open", ["-n", appPath]);
|
||||
|
||||
return { appPath, iconPath: resolvedIconPath };
|
||||
};
|
||||
|
||||
if (import.meta.main) {
|
||||
try {
|
||||
const iconPath = process.argv[2] ? resolve(process.cwd(), process.argv[2]) : defaultIconPath;
|
||||
const result = await launchDockIconPreview({ iconPath });
|
||||
console.log(`[preview-macos-dock-icon] showing ${basename(result.iconPath)} in the Dock as “${PREVIEW_APP_NAME}”`);
|
||||
console.log("[preview-macos-dock-icon] right-click the Dock icon and choose Quit when finished");
|
||||
} catch (error) {
|
||||
console.error(`[preview-macos-dock-icon] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
PREVIEW_APP_NAME,
|
||||
assertPreviewPlatform,
|
||||
createPreviewInfoPlist,
|
||||
previewSwiftSource,
|
||||
} from "./preview-macos-dock-icon.mjs";
|
||||
|
||||
describe("macOS Dock icon preview", () => {
|
||||
test("creates a regular app bundle that declares the preview icon", () => {
|
||||
const plist = createPreviewInfoPlist("org.edgeever.logo-preview.test");
|
||||
expect(plist).toContain(`<string>${PREVIEW_APP_NAME}</string>`);
|
||||
expect(plist).toContain("<string>AppIcon</string>");
|
||||
expect(plist).toContain("<string>org.edgeever.logo-preview.test</string>");
|
||||
expect(plist).toContain("<key>LSUIElement</key>\n <false/>");
|
||||
});
|
||||
|
||||
test("keeps the Cocoa preview alive and sets its Dock icon", () => {
|
||||
expect(previewSwiftSource).toContain("NSApp.setActivationPolicy(.regular)");
|
||||
expect(previewSwiftSource).toContain("NSApp.applicationIconImage = icon");
|
||||
expect(previewSwiftSource).toContain("application.run()");
|
||||
});
|
||||
|
||||
test("rejects unsupported platforms", () => {
|
||||
expect(() => assertPreviewPlatform("linux")).toThrow(/only available on macOS/i);
|
||||
expect(() => assertPreviewPlatform("darwin")).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user