diff --git a/assets/icon.icns b/assets/icon.icns index 59e1ee7..46b3497 100644 Binary files a/assets/icon.icns and b/assets/icon.icns differ diff --git a/assets/icon.ico b/assets/icon.ico index 885d1fc..45100bc 100644 Binary files a/assets/icon.ico and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png index 7d24344..351c3b4 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg index 982d4e4..fc7846f 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,20 +1,71 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + - - + + + + - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 5bff9a4..7d55db5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "eslint-plugin-i18next": "^6.1.3", "jsdom": "^28.0.0", "memfs": "^4.6.0", + "png-to-ico": "^3.0.1", "tsx": "^4.6.0", "typescript": "^5.3.0", "vite": "^7.3.1", @@ -12320,6 +12321,51 @@ "node": ">=10.4.0" } }, + "node_modules/png-to-ico": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/png-to-ico/-/png-to-ico-3.0.1.tgz", + "integrity": "sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.10.3", + "minimist": "^1.2.8", + "pngjs": "^7.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/png-to-ico/node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/png-to-ico/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/package.json b/package.json index 261739b..0e628d7 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,12 @@ "start": "concurrently --kill-others \"npm run dev:renderer\" \"npm run start:electron\"", "start:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .", "build": "npm run lint && npm run db:generate && npm run build:main && npm run build:renderer", - "package": "npm run build && electron-builder --dir", - "dist": "npm run build && electron-builder", - "dist:mac": "npm run build && electron-builder --mac", - "dist:win": "npm run build && electron-builder --win", - "dist:linux": "npm run build && electron-builder --linux", + "icons:generate": "node scripts/regenerate-icons.mjs", + "package": "npm run icons:generate && npm run build && electron-builder --dir", + "dist": "npm run icons:generate && npm run build && electron-builder", + "dist:mac": "npm run icons:generate && npm run build && electron-builder --mac", + "dist:win": "npm run icons:generate && npm run build && electron-builder --win", + "dist:linux": "npm run icons:generate && npm run build && electron-builder --linux", "build:main": "node ./node_modules/typescript/bin/tsc -p tsconfig.main.json", "build:renderer": "node ./node_modules/vite/bin/vite.js build", "start:prod": "node ./node_modules/electron/cli.js .", @@ -58,6 +59,7 @@ "eslint-plugin-i18next": "^6.1.3", "jsdom": "^28.0.0", "memfs": "^4.6.0", + "png-to-ico": "^3.0.1", "tsx": "^4.6.0", "typescript": "^5.3.0", "vite": "^7.3.1", diff --git a/scripts/regenerate-icons.mjs b/scripts/regenerate-icons.mjs new file mode 100644 index 0000000..ca91883 --- /dev/null +++ b/scripts/regenerate-icons.mjs @@ -0,0 +1,128 @@ +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; +import sharp from 'sharp'; +import pngToIco from 'png-to-ico'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, '..'); + +const assetsDir = path.join(projectRoot, 'assets'); +const sourceSvgPath = path.join(assetsDir, 'icon.svg'); +const outputPngPath = path.join(assetsDir, 'icon.png'); +const outputIcnsPath = path.join(assetsDir, 'icon.icns'); +const outputIcoPath = path.join(assetsDir, 'icon.ico'); +const legacyIconsetPath = path.join(assetsDir, 'icon.iconset'); + +const icnsBaseSizes = [16, 32, 128, 256, 512]; +const icoSizes = [16, 24, 32, 48, 64, 128, 256]; + +async function exists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +function runCommand(command, args) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: 'inherit' }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}`)); + }); + }); +} + +async function writeBasePng() { + await sharp(sourceSvgPath) + .resize(1024, 1024, { fit: 'contain' }) + .png() + .toFile(outputPngPath); +} + +async function writeIconsetPngs(iconsetDir) { + for (const size of icnsBaseSizes) { + const oneX = path.join(iconsetDir, `icon_${size}x${size}.png`); + const twoX = path.join(iconsetDir, `icon_${size}x${size}@2x.png`); + + await sharp(outputPngPath).resize(size, size).png().toFile(oneX); + await sharp(outputPngPath).resize(size * 2, size * 2).png().toFile(twoX); + } +} + +async function writeIcns(iconsetDir) { + await runCommand('iconutil', ['-c', 'icns', iconsetDir, '-o', outputIcnsPath]); +} + +async function writeIco(tmpDir) { + const icoPngPaths = []; + + for (const size of icoSizes) { + const sizedPngPath = path.join(tmpDir, `ico-${size}.png`); + await sharp(outputPngPath).resize(size, size).png().toFile(sizedPngPath); + icoPngPaths.push(sizedPngPath); + } + + const icoBuffer = await pngToIco(icoPngPaths); + await fs.writeFile(outputIcoPath, icoBuffer); +} + +async function ensurePrerequisites() { + if (!(await exists(sourceSvgPath))) { + throw new Error(`Source SVG not found: ${sourceSvgPath}`); + } + + if (process.platform !== 'darwin') { + throw new Error('ICNS generation requires macOS (iconutil). Run this script on macOS.'); + } +} + +async function cleanLegacyIntermediates() { + if (await exists(legacyIconsetPath)) { + await fs.rm(legacyIconsetPath, { recursive: true, force: true }); + } +} + +async function main() { + await ensurePrerequisites(); + await cleanLegacyIntermediates(); + + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bds-icon-gen-')); + const tmpIconsetDir = path.join(tmpDir, 'icon.iconset'); + + try { + await fs.mkdir(tmpIconsetDir, { recursive: true }); + + console.log('[icons] Generating 1024x1024 PNG from SVG...'); + await writeBasePng(); + + console.log('[icons] Generating iconset PNG variants...'); + await writeIconsetPngs(tmpIconsetDir); + + console.log('[icons] Building ICNS with iconutil...'); + await writeIcns(tmpIconsetDir); + + console.log('[icons] Building ICO multi-size bundle...'); + await writeIco(tmpDir); + + console.log('[icons] Done: assets/icon.png, assets/icon.icns, assets/icon.ico'); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + await cleanLegacyIntermediates(); + } +} + +main().catch((error) => { + console.error('[icons] Failed:', error instanceof Error ? error.message : String(error)); + process.exit(1); +});