"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 = { 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 { 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("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("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) { 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).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; }