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:
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import iconPwaManifest from "./icon-pwa-manifest";
|
||||
|
||||
const manifest = {
|
||||
name: "cobble",
|
||||
icons: [
|
||||
{ src: "/icons/icon-192.png", sizes: "192x192" },
|
||||
{ src: "/icons/maskable-512.png", sizes: "512x512", purpose: "maskable" },
|
||||
{ src: "https://example.com/other.png" },
|
||||
],
|
||||
shortcuts: [{ name: "Help", url: "/admin/help", icons: [{ src: "/icons/icon-192.png" }] }],
|
||||
};
|
||||
|
||||
describe("iconPwaManifest", () => {
|
||||
test("leaves the stock icons alone", () => {
|
||||
expect(iconPwaManifest({ manifest, icon_base: null })).toEqual(manifest);
|
||||
});
|
||||
|
||||
test("moves every /icons/ file to the install's set", () => {
|
||||
const named = iconPwaManifest({ manifest, icon_base: "/install-icon/0123456789ab" });
|
||||
|
||||
expect(named.icons).toEqual([
|
||||
{ src: "/install-icon/0123456789ab/icon-192.png", sizes: "192x192" },
|
||||
{ src: "/install-icon/0123456789ab/maskable-512.png", sizes: "512x512", purpose: "maskable" },
|
||||
{ src: "https://example.com/other.png" },
|
||||
]);
|
||||
expect(named.shortcuts).toEqual([
|
||||
{ name: "Help", url: "/admin/help", icons: [{ src: "/install-icon/0123456789ab/icon-192.png" }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
type ManifestIcon = { src?: unknown };
|
||||
|
||||
type Params = {
|
||||
manifest: Record<string, unknown>;
|
||||
/**
|
||||
* The install's icon set (`installIconBase`), or null for the stock one.
|
||||
*/
|
||||
icon_base?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The manifest pointed at the install's own icon set: every icon under
|
||||
* `/icons/` (the app's and its shortcuts') moves to `icon_base`, which
|
||||
* holds the same file names.
|
||||
*/
|
||||
export default function iconPwaManifest({ manifest, icon_base }: Params) {
|
||||
if (!icon_base) return manifest;
|
||||
|
||||
const move = (icons: unknown) =>
|
||||
Array.isArray(icons)
|
||||
? icons.map((icon: ManifestIcon) =>
|
||||
typeof icon?.src == "string" && icon.src.startsWith("/icons/")
|
||||
? { ...icon, src: `${icon_base}/${icon.src.slice("/icons/".length)}` }
|
||||
: icon,
|
||||
)
|
||||
: icons;
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
icons: move(manifest.icons),
|
||||
shortcuts: Array.isArray(manifest.shortcuts)
|
||||
? manifest.shortcuts.map((shortcut: Record<string, unknown>) => ({
|
||||
...shortcut,
|
||||
icons: move(shortcut.icons),
|
||||
}))
|
||||
: manifest.shortcuts,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PwaInstallIconRoute, type PwaInstallIcon } from "@/src/data/pwa-data";
|
||||
|
||||
type Params = {
|
||||
icon?: PwaInstallIcon | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Where the install's own icon set is served, or null for the stock icons
|
||||
* under `/icons`.
|
||||
*/
|
||||
export default function installIconBase({ icon }: Params) {
|
||||
return icon?.version ? `${PwaInstallIconRoute}/${icon.version}` : null;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PwaDefaultInstallIcon } from "@/src/data/pwa-data";
|
||||
import parseInstallIcon from "./parse-install-icon";
|
||||
|
||||
describe("parseInstallIcon", () => {
|
||||
test("falls back to the stock icon when nothing or junk is saved", () => {
|
||||
expect(parseInstallIcon({})).toEqual(PwaDefaultInstallIcon);
|
||||
expect(parseInstallIcon({ saved: "not json" })).toEqual(PwaDefaultInstallIcon);
|
||||
expect(parseInstallIcon({ saved: "[1]" })).toEqual(PwaDefaultInstallIcon);
|
||||
});
|
||||
|
||||
test("reads a saved icon", () => {
|
||||
expect(
|
||||
parseInstallIcon({
|
||||
saved: JSON.stringify({ mode: "image", stone: "cobalt", mark: false, version: "0123456789ab" }),
|
||||
}),
|
||||
).toEqual({ mode: "image", stone: "cobalt", mark: false, version: "0123456789ab" });
|
||||
});
|
||||
|
||||
test("drops unknown values field by field", () => {
|
||||
expect(
|
||||
parseInstallIcon({
|
||||
saved: JSON.stringify({ mode: "video", stone: "plaid", mark: "yes", version: "../etc" }),
|
||||
}),
|
||||
).toEqual(PwaDefaultInstallIcon);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { BotHues, type BotHue } from "@/src/dict/bot-dict";
|
||||
import {
|
||||
PwaDefaultInstallIcon,
|
||||
PwaInstallIconModes,
|
||||
type PwaInstallIcon,
|
||||
} from "@/src/data/pwa-data";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The `install_icon` app setting, as saved.
|
||||
*/
|
||||
saved?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The install's icon from its saved JSON, field by field: anything missing
|
||||
* or unknown falls back to the stock ember icon's value.
|
||||
*/
|
||||
export default function parseInstallIcon({ saved }: Params): PwaInstallIcon {
|
||||
let raw: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
const parsed = saved ? JSON.parse(saved) : null;
|
||||
if (parsed && typeof parsed == "object") raw = parsed;
|
||||
} catch {
|
||||
raw = {};
|
||||
}
|
||||
|
||||
const mode = PwaInstallIconModes.find((value) => value == raw.mode);
|
||||
const stone = BotHues.find((hue) => hue.value == raw.stone)?.value as BotHue | undefined;
|
||||
const version =
|
||||
typeof raw.version == "string" && /^[a-f0-9]{12}$/.test(raw.version) ? raw.version : null;
|
||||
|
||||
return {
|
||||
mode: mode || PwaDefaultInstallIcon.mode,
|
||||
stone: stone || PwaDefaultInstallIcon.stone,
|
||||
mark: typeof raw.mark == "boolean" ? raw.mark : PwaDefaultInstallIcon.mark,
|
||||
version,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user