Let each install pick its app icon
Settings > General gets an App icon (super admin): one of the seven stones, or an uploaded image with the picked stone as a top-left corner mark (inside a round launcher's crop, clear of the browser's badge), previewed in both launcher crops before saving. Uploads are decoded and cropped to a stored square (no SVG); the set is drawn into DATA_DIR/install-icon/<version>/ and served publicly and immutable. The manifest, head icon links and push notifications point at it; the ember stone keeps the stock icons. The generator shares the icon renderer. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -20,7 +20,10 @@ import {
|
||||
PwaShortName,
|
||||
PwaShortcuts,
|
||||
PwaStartPath,
|
||||
PwaAppIconMarkScale,
|
||||
PwaMaskableMarkScale,
|
||||
} from "../data/pwa-data";
|
||||
import renderAppIcon from "../functions/server/pwa/render-app-icon";
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "../..");
|
||||
const LOGO_SVG = path.join(ROOT, "public/logos/logo.svg");
|
||||
@@ -31,17 +34,6 @@ const MAIN_CSS = path.join(ROOT, "src/styles/main.css");
|
||||
const ICO_SIZES = [16, 24, 32, 48] as const;
|
||||
const PNG_SIZES = [...ICO_SIZES, 180, 512] as const;
|
||||
|
||||
/**
|
||||
* The app icon is the stone on paper, with room around it for the
|
||||
* launcher's mask. The favicons keep the transparent stone.
|
||||
*/
|
||||
const AppIconMarkScale = 0.8;
|
||||
/**
|
||||
* A maskable icon is cropped to a circle of 80% of the canvas, so the
|
||||
* stone stays inside it with room to spare.
|
||||
*/
|
||||
const MaskableMarkScale = 0.52;
|
||||
|
||||
function buildIco(pngs: Record<number, Buffer>): Buffer {
|
||||
const header = Buffer.alloc(6);
|
||||
header.writeUInt16LE(0, 0);
|
||||
@@ -182,36 +174,6 @@ async function grabThemeColors(): Promise<ThemeColors> {
|
||||
};
|
||||
}
|
||||
|
||||
async function renderAppIcon({
|
||||
svg,
|
||||
size,
|
||||
scale,
|
||||
background,
|
||||
}: {
|
||||
svg: Buffer;
|
||||
size: number;
|
||||
scale: number;
|
||||
background: string;
|
||||
}) {
|
||||
const mark_size = Math.max(1, Math.round(size * scale));
|
||||
const mark = await sharp(svg)
|
||||
.resize(mark_size, mark_size, { fit: "contain" })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return sharp({
|
||||
create: {
|
||||
width: size,
|
||||
height: size,
|
||||
channels: 4,
|
||||
background,
|
||||
},
|
||||
})
|
||||
.composite([{ input: mark, gravity: "center" }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
function buildManifest({ colors }: { colors: ThemeColors }) {
|
||||
return {
|
||||
id: "/",
|
||||
@@ -637,7 +599,7 @@ async function main() {
|
||||
await renderAppIcon({
|
||||
svg,
|
||||
size: 180,
|
||||
scale: AppIconMarkScale,
|
||||
scale: PwaAppIconMarkScale,
|
||||
background: colors.paper,
|
||||
}),
|
||||
);
|
||||
@@ -655,7 +617,7 @@ async function main() {
|
||||
await renderAppIcon({
|
||||
svg,
|
||||
size,
|
||||
scale: maskable ? MaskableMarkScale : AppIconMarkScale,
|
||||
scale: maskable ? PwaMaskableMarkScale : PwaAppIconMarkScale,
|
||||
background: colors.paper,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user