Regenerate Android launcher icons from the blue striped source asset. Add a repeatable generator and README documentation for the icon workflow.
134 lines
3.7 KiB
JavaScript
134 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import Jimp from "jimp";
|
|
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const sourcePath = path.join(projectRoot, "public/favicons/web-app-manifest-512x512.png");
|
|
const checkOnly = process.argv.includes("--check");
|
|
const launcherBackground = "#0787BB";
|
|
|
|
const densityScale = {
|
|
mdpi: 1,
|
|
hdpi: 1.5,
|
|
xhdpi: 2,
|
|
xxhdpi: 3,
|
|
xxxhdpi: 4,
|
|
};
|
|
|
|
const targets = [
|
|
{ relativePath: "public/icons/icon-512x512.png", size: 512, copySource: true },
|
|
{ relativePath: "public/icons/icon-192x192.png", size: 192 },
|
|
{ relativePath: "store_icon.png", size: 512, copySource: true },
|
|
];
|
|
|
|
for (const [density, scale] of Object.entries(densityScale)) {
|
|
const legacySize = Math.round(48 * scale);
|
|
const foregroundSize = Math.round(108 * scale);
|
|
targets.push(
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher.png`,
|
|
size: legacySize,
|
|
},
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher_round.png`,
|
|
size: legacySize,
|
|
},
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher_foreground.png`,
|
|
size: foregroundSize,
|
|
},
|
|
);
|
|
}
|
|
|
|
const xmlTargets = [
|
|
{
|
|
relativePath: "android/app/src/main/res/values/ic_launcher_background.xml",
|
|
content: `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n <color name="ic_launcher_background">${launcherBackground}</color>\n</resources>\n`,
|
|
},
|
|
];
|
|
|
|
function targetPath(relativePath) {
|
|
return path.join(projectRoot, relativePath);
|
|
}
|
|
|
|
async function readIfExists(filePath) {
|
|
try {
|
|
return await fs.readFile(filePath);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function writeIfChanged(relativePath, content) {
|
|
const filePath = targetPath(relativePath);
|
|
const current = await readIfExists(filePath);
|
|
|
|
if (current?.equals(content)) {
|
|
return false;
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return true;
|
|
}
|
|
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, content);
|
|
return true;
|
|
}
|
|
|
|
async function renderPng(sourceImage, size) {
|
|
const image = sourceImage.clone().resize(size, size, Jimp.RESIZE_BICUBIC);
|
|
return image.getBufferAsync(Jimp.MIME_PNG);
|
|
}
|
|
|
|
async function main() {
|
|
const sourceBuffer = await fs.readFile(sourcePath);
|
|
const sourceImage = await Jimp.read(sourceBuffer);
|
|
|
|
if (sourceImage.bitmap.width !== 512 || sourceImage.bitmap.height !== 512) {
|
|
throw new Error(`Expected ${path.relative(projectRoot, sourcePath)} to be a 512x512 PNG.`);
|
|
}
|
|
|
|
const changed = [];
|
|
|
|
for (const target of targets) {
|
|
const buffer = target.copySource ? sourceBuffer : await renderPng(sourceImage, target.size);
|
|
if (await writeIfChanged(target.relativePath, buffer)) {
|
|
changed.push(target.relativePath);
|
|
}
|
|
}
|
|
|
|
for (const target of xmlTargets) {
|
|
if (await writeIfChanged(target.relativePath, Buffer.from(target.content))) {
|
|
changed.push(target.relativePath);
|
|
}
|
|
}
|
|
|
|
if (changed.length === 0) {
|
|
console.log(`Android icon assets are current (${targets.length + xmlTargets.length} files).`);
|
|
return;
|
|
}
|
|
|
|
if (checkOnly) {
|
|
console.error("Android icon assets are out of date:");
|
|
for (const relativePath of changed) {
|
|
console.error(`- ${relativePath}`);
|
|
}
|
|
console.error("Run `npm run mobile:android:icons`.");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
console.log(`Updated ${changed.length} Android icon asset${changed.length === 1 ? "" : "s"}.`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
});
|