study / web /src /lib /branding.ts
xiaolongmr
优化
d77c01e
Raw History Blame Contribute Delete
5.19 kB
"use client";
import { useEffect, useRef, useState } from "react";
import { fetchBranding, type BrandingConfig } from "@/lib/api";
const BRANDING_UPDATED_EVENT = "chatgpt2api:branding-updated";
export const DEFAULT_BRANDING: Required<BrandingConfig> = {
site_title: "ChatGPT 号池管理",
site_logo_url: "",
site_header_title: "chatgpt2api",
site_external_link_label: "GitHub",
site_external_link_url: "https://github.com/basketikun/chatgpt2api",
site_external_link_icon_url: "",
site_external_link_icon_svg: "",
};
function normalizeText(value: unknown, fallback: string) {
const normalized = String(value ?? "").trim();
return normalized || fallback;
}
export function normalizeBranding(value: BrandingConfig | null | undefined): Required<BrandingConfig> {
return {
site_title: normalizeText(value?.site_title, DEFAULT_BRANDING.site_title),
site_logo_url: String(value?.site_logo_url ?? "").trim(),
site_header_title: normalizeText(value?.site_header_title, DEFAULT_BRANDING.site_header_title),
site_external_link_label: String(value?.site_external_link_label ?? DEFAULT_BRANDING.site_external_link_label).trim(),
site_external_link_url: String(value?.site_external_link_url ?? DEFAULT_BRANDING.site_external_link_url).trim(),
site_external_link_icon_url: String(value?.site_external_link_icon_url ?? "").trim(),
site_external_link_icon_svg: String(value?.site_external_link_icon_svg ?? "").trim(),
};
}
function updateFavicon(href: string) {
const targetHref = href || "/favicon.ico";
let icon = document.querySelector<HTMLLinkElement>("link[data-branding-icon='favicon']");
if (!icon) {
icon = document.createElement("link");
icon.rel = "icon";
icon.setAttribute("data-branding-icon", "favicon");
document.head.appendChild(icon);
}
icon.rel = "icon";
icon.href = targetHref;
icon.type = guessIconType(targetHref);
let appleIcon = document.querySelector<HTMLLinkElement>("link[data-branding-icon='apple-touch-icon']");
if (!appleIcon) {
appleIcon = document.createElement("link");
appleIcon.rel = "apple-touch-icon";
appleIcon.setAttribute("data-branding-icon", "apple-touch-icon");
document.head.appendChild(appleIcon);
}
appleIcon.rel = "apple-touch-icon";
appleIcon.href = targetHref;
}
function guessIconType(href: string) {
const cleanHref = href.split(/[?#]/, 1)[0]?.toLowerCase() || "";
if (cleanHref.endsWith(".svg")) {
return "image/svg+xml";
}
if (cleanHref.endsWith(".jpg") || cleanHref.endsWith(".jpeg")) {
return "image/jpeg";
}
if (cleanHref.endsWith(".webp")) {
return "image/webp";
}
if (cleanHref.endsWith(".ico")) {
return "image/x-icon";
}
return "image/png";
}
function applyBrandingToDocument(branding: Required<BrandingConfig>) {
document.title = branding.site_title;
updateFavicon(branding.site_logo_url);
}
export function publishBrandingUpdate(value: BrandingConfig | null | undefined) {
const branding = normalizeBranding(value);
if (typeof window !== "undefined") {
applyBrandingToDocument(branding);
window.dispatchEvent(new CustomEvent(BRANDING_UPDATED_EVENT, { detail: branding }));
}
return branding;
}
export function useBranding(options: { applyDocument?: boolean; documentKey?: string | null } = {}) {
const [branding, setBranding] = useState(() => normalizeBranding(null));
const [hasResolvedBranding, setHasResolvedBranding] = useState(false);
const currentBrandingRef = useRef(branding);
useEffect(() => {
let active = true;
void fetchBranding()
.then((data) => {
if (active) {
const nextBranding = normalizeBranding(data.branding);
currentBrandingRef.current = nextBranding;
setBranding(nextBranding);
setHasResolvedBranding(true);
}
})
.catch(() => undefined);
return () => {
active = false;
};
}, []);
useEffect(() => {
const handleBrandingUpdate = (event: Event) => {
const detail = (event as CustomEvent<BrandingConfig>).detail;
const nextBranding = normalizeBranding(detail);
currentBrandingRef.current = nextBranding;
setBranding(nextBranding);
setHasResolvedBranding(true);
};
window.addEventListener(BRANDING_UPDATED_EVENT, handleBrandingUpdate);
return () => window.removeEventListener(BRANDING_UPDATED_EVENT, handleBrandingUpdate);
}, []);
useEffect(() => {
if (!options.applyDocument || !hasResolvedBranding) {
return;
}
const apply = () => applyBrandingToDocument(branding);
apply();
const animationFrame = window.requestAnimationFrame(apply);
const observer = new MutationObserver(() => {
const expectedTitle = currentBrandingRef.current.site_title;
if (document.title !== expectedTitle) {
document.title = expectedTitle;
}
});
observer.observe(document.head, { childList: true, subtree: true, characterData: true });
return () => {
window.cancelAnimationFrame(animationFrame);
observer.disconnect();
};
}, [branding, hasResolvedBranding, options.applyDocument, options.documentKey]);
return branding;
}