198 lines
5.2 KiB
JavaScript
198 lines
5.2 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const path = require("node:path");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
|
||
|
|
const VALID_VARIANTS = new Set([
|
||
|
|
"original",
|
||
|
|
"bright",
|
||
|
|
"dark",
|
||
|
|
"colorful",
|
||
|
|
"high-contrast",
|
||
|
|
"white-navy",
|
||
|
|
"white-sky",
|
||
|
|
"white-rose",
|
||
|
|
"white-emerald",
|
||
|
|
"white-amber",
|
||
|
|
"white-violet",
|
||
|
|
"rainbow",
|
||
|
|
]);
|
||
|
|
|
||
|
|
const DEFAULT_VARIANT = "original";
|
||
|
|
|
||
|
|
let currentVariant = DEFAULT_VARIANT;
|
||
|
|
let currentIconPath = null;
|
||
|
|
let preferPublicSources = false;
|
||
|
|
let useMacIconSources = false;
|
||
|
|
|
||
|
|
function isValidAppIconVariant(variant) {
|
||
|
|
return typeof variant === "string" && VALID_VARIANTS.has(variant);
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeAppIconVariant(variant) {
|
||
|
|
return isValidAppIconVariant(variant) ? variant : DEFAULT_VARIANT;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isPackagedApp(app) {
|
||
|
|
try {
|
||
|
|
return app?.isPackaged === true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function pickExistingPath(candidates) {
|
||
|
|
return candidates.find((candidate) => fs.existsSync(candidate));
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildSourceCandidates(appPath, relativeParts) {
|
||
|
|
const publicCandidate = path.join(appPath, "public", ...relativeParts);
|
||
|
|
const distCandidate = path.join(appPath, "dist", ...relativeParts);
|
||
|
|
return preferPublicSources
|
||
|
|
? [publicCandidate, distCandidate]
|
||
|
|
: [distCandidate, publicCandidate];
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveOriginalIconPath(appPath) {
|
||
|
|
const primaryParts = useMacIconSources
|
||
|
|
? ["icons", "variants", "macos", "original.png"]
|
||
|
|
: ["icons", "variants", "original.png"];
|
||
|
|
const candidates = useMacIconSources
|
||
|
|
? buildSourceCandidates(appPath, primaryParts)
|
||
|
|
: [
|
||
|
|
...buildSourceCandidates(appPath, primaryParts),
|
||
|
|
...buildSourceCandidates(appPath, ["icon-win.png"]),
|
||
|
|
...buildSourceCandidates(appPath, ["icon.png"]),
|
||
|
|
];
|
||
|
|
const existing = pickExistingPath(candidates);
|
||
|
|
if (existing) return existing;
|
||
|
|
// Dev fallback: elf SVG.
|
||
|
|
const svgCandidate = path.join(appPath, "public", "senmesh-elf-logo.svg");
|
||
|
|
if (fs.existsSync(svgCandidate)) return svgCandidate;
|
||
|
|
return candidates[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildVariantSourceCandidates(appPath, fileName) {
|
||
|
|
const relativeParts = useMacIconSources
|
||
|
|
? ["icons", "variants", "macos", fileName]
|
||
|
|
: ["icons", "variants", fileName];
|
||
|
|
return buildSourceCandidates(appPath, relativeParts);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveVariantIconPath(variant, appPath) {
|
||
|
|
const normalized = normalizeAppIconVariant(variant);
|
||
|
|
if (normalized === "original") {
|
||
|
|
return resolveOriginalIconPath(appPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
const fileName = `${normalized}.png`;
|
||
|
|
const candidates = buildVariantSourceCandidates(appPath, fileName);
|
||
|
|
const resolved = pickExistingPath(candidates);
|
||
|
|
if (resolved) return resolved;
|
||
|
|
return resolveOriginalIconPath(appPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveStrictVariantIconPath(variant, appPath) {
|
||
|
|
const normalized = normalizeAppIconVariant(variant);
|
||
|
|
if (normalized === "original") {
|
||
|
|
return resolveOriginalIconPath(appPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
const fileName = `${normalized}.png`;
|
||
|
|
const candidates = buildVariantSourceCandidates(appPath, fileName);
|
||
|
|
return pickExistingPath(candidates) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function initializeAppIconManager(appPath, options = {}) {
|
||
|
|
preferPublicSources = options.preferPublic === true;
|
||
|
|
useMacIconSources = options.isMac === true;
|
||
|
|
currentVariant = DEFAULT_VARIANT;
|
||
|
|
currentIconPath = resolveVariantIconPath(currentVariant, appPath);
|
||
|
|
return currentIconPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getAppIconPath(appPath) {
|
||
|
|
if (!currentIconPath) {
|
||
|
|
return initializeAppIconManager(appPath);
|
||
|
|
}
|
||
|
|
return currentIconPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getAppIconVariant() {
|
||
|
|
return currentVariant;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createNativeImage(nativeImage, iconPath) {
|
||
|
|
if (!nativeImage || !iconPath || !fs.existsSync(iconPath)) return null;
|
||
|
|
try {
|
||
|
|
// Read from disk so regenerated assets at the same path refresh immediately.
|
||
|
|
return nativeImage.createFromBuffer(fs.readFileSync(iconPath));
|
||
|
|
} catch {
|
||
|
|
try {
|
||
|
|
return nativeImage.createFromPath(iconPath);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyIconToWindow(win, iconPath, nativeImage) {
|
||
|
|
if (!win || win.isDestroyed?.() || !iconPath || !win.setIcon) return;
|
||
|
|
try {
|
||
|
|
const image = createNativeImage(nativeImage, iconPath);
|
||
|
|
if (image) {
|
||
|
|
win.setIcon(image);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
win.setIcon(iconPath);
|
||
|
|
} catch {
|
||
|
|
// ignore
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyAppIconVariant(variant, context) {
|
||
|
|
const { app, BrowserWindow, nativeImage, appPath, isMac } = context;
|
||
|
|
preferPublicSources = !isPackagedApp(app);
|
||
|
|
useMacIconSources = isMac === true;
|
||
|
|
const normalized = normalizeAppIconVariant(variant);
|
||
|
|
const iconPath = resolveStrictVariantIconPath(normalized, appPath);
|
||
|
|
if (!iconPath || !fs.existsSync(iconPath)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentVariant = normalized;
|
||
|
|
currentIconPath = iconPath;
|
||
|
|
|
||
|
|
const windows = BrowserWindow?.getAllWindows?.() || [];
|
||
|
|
for (const win of windows) {
|
||
|
|
applyIconToWindow(win, iconPath, nativeImage);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isMac && app?.dock?.setIcon && nativeImage) {
|
||
|
|
try {
|
||
|
|
const dockImage = createNativeImage(nativeImage, iconPath);
|
||
|
|
if (dockImage) {
|
||
|
|
app.dock.setIcon(dockImage);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// ignore
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
DEFAULT_VARIANT,
|
||
|
|
VALID_VARIANTS,
|
||
|
|
isValidAppIconVariant,
|
||
|
|
normalizeAppIconVariant,
|
||
|
|
initializeAppIconManager,
|
||
|
|
getAppIconPath,
|
||
|
|
getAppIconVariant,
|
||
|
|
resolveVariantIconPath,
|
||
|
|
resolveStrictVariantIconPath,
|
||
|
|
applyAppIconVariant,
|
||
|
|
applyIconToWindow,
|
||
|
|
};
|