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:
2026-10-25 10:38:25 +01:00
co-authored by Claude Opus 5.5
parent 5a603bf61f
commit e462806438
36 changed files with 1190 additions and 68 deletions
+16 -10
View File
@@ -14,6 +14,7 @@ import type {
} from "@/src/types";
import { SiteData } from "@/src/data/site-data";
import installDisplayName from "@/src/utils/install-display-name";
import installIconBase from "@/src/utils/install-icon-base";
import Layout from "../layouts/main";
import AuthLayout from "../layouts/auth";
import BotsLayout from "../layouts/bots";
@@ -97,9 +98,12 @@ export function Head({ serverRes, ctx }: BunextPageHeadFCProps) {
/**
* iOS names a home-screen app from these tags, not the manifest.
*/
const app_name = installDisplayName({
install_name: (serverRes?.props as PagePropsType | undefined)?.install_name,
});
const root_props = serverRes?.props as PagePropsType | undefined;
const app_name = installDisplayName({ install_name: root_props?.install_name });
/**
* The install's own icon set, when it has one (Settings > General).
*/
const icon_base = installIconBase({ icon: root_props?.install_icon });
return (
<>
@@ -126,21 +130,23 @@ export function Head({ serverRes, ctx }: BunextPageHeadFCProps) {
href="https://fonts.googleapis.com/css2?family=Archivo:[email protected]&display=swap"
rel="stylesheet"
/>
<link
rel="icon"
href="/favicon.ico"
sizes="16x16 24x24 32x32 48x48"
/>
{icon_base ? null : (
<link
rel="icon"
href="/favicon.ico"
sizes="16x16 24x24 32x32 48x48"
/>
)}
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon.png"
href={icon_base ? `${icon_base}/favicon.png` : "/favicon.png"}
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
href={icon_base ? `${icon_base}/apple-touch-icon.png` : "/apple-touch-icon.png"}
/>
</>
);