An uploaded image's favicon now carries the stone mark like the PWA icons and is cut to a circle. The mark is larger on the favicon and on the regular (rounded-square) icons, and the settings preview matches. A render revision in the set's version gives re-saved icons a new URL past the immutable cache. Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { existsSync } from "node:fs";
|
|
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
PwaInstallIconRenderRevision,
|
|
PwaInstallIconSettingKey,
|
|
type PwaInstallIcon,
|
|
type PwaInstallIconMode,
|
|
} from "@/src/data/pwa-data";
|
|
import type { BotHue } from "@/src/dict/bot-dict";
|
|
import saveAppSetting from "@/src/functions/backend/db/app-settings/save-app-setting";
|
|
import renderInstallIcons from "@/src/functions/server/pwa/render-install-icons";
|
|
import AppError from "@/src/utils/app-error";
|
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
|
import grabInstallIconDir from "./grab-install-icon-dir";
|
|
import grabPwaBackground from "./grab-pwa-background";
|
|
import normalizeInstallIconUpload from "./normalize-install-icon-upload";
|
|
|
|
type Params = {
|
|
mode: PwaInstallIconMode;
|
|
stone: BotHue;
|
|
mark: boolean;
|
|
/**
|
|
* A newly picked image, decoded already.
|
|
*/
|
|
image?: Buffer | null;
|
|
};
|
|
|
|
/**
|
|
* Saves the install's icon and draws its icon set. A new image replaces
|
|
* the stored one; going back to a stone drops it. The ember stone is the
|
|
* stock icon, so it needs no set. Older sets are removed once the new one
|
|
* is in place; each has its own folder, so a cached icon is never stale.
|
|
*/
|
|
export default async function saveInstallIcon({ mode, stone, mark, image }: Params) {
|
|
const dir = grabInstallIconDir();
|
|
const source_path = path.join(dir, "source.png");
|
|
const background = await grabPwaBackground();
|
|
|
|
await mkdir(dir, { recursive: true });
|
|
|
|
if (image) {
|
|
await writeFile(source_path, await normalizeInstallIconUpload({ buffer: image, background }));
|
|
}
|
|
|
|
if (mode == "stone") {
|
|
await rm(source_path, { force: true });
|
|
}
|
|
|
|
if (mode == "image" && !existsSync(source_path)) {
|
|
throw new AppError(`Upload an image first.`);
|
|
}
|
|
|
|
const source = mode == "image" ? Buffer.from(await Bun.file(source_path).arrayBuffer()) : null;
|
|
const is_stock = mode == "stone" && stone == "ember";
|
|
|
|
const version = is_stock
|
|
? null
|
|
: createHash("sha256")
|
|
.update(JSON.stringify({ mode, stone, mark, render: PwaInstallIconRenderRevision }))
|
|
.update(source || "")
|
|
.digest("hex")
|
|
.slice(0, 12);
|
|
|
|
const icon: PwaInstallIcon = { mode, stone, mark, version };
|
|
|
|
if (version) {
|
|
const stone_svg = Buffer.from(
|
|
await Bun.file(
|
|
path.join(grabDirNames().ROOT_DIR, "public/brand/icons", `${stone}.svg`),
|
|
).arrayBuffer(),
|
|
);
|
|
const files = await renderInstallIcons({ icon, stone_svg, image: source, background });
|
|
const set_dir = path.join(dir, version);
|
|
|
|
await mkdir(set_dir, { recursive: true });
|
|
|
|
for (const [name, bytes] of Object.entries(files)) {
|
|
await writeFile(path.join(set_dir, `${name}.png`), bytes);
|
|
}
|
|
}
|
|
|
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
if (entry.isDirectory() && entry.name != version) {
|
|
await rm(path.join(dir, entry.name), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
await saveAppSetting({
|
|
key: PwaInstallIconSettingKey,
|
|
value: is_stock ? null : JSON.stringify(icon),
|
|
});
|
|
|
|
return icon;
|
|
}
|