mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
155 lines
6.6 KiB
TypeScript
155 lines
6.6 KiB
TypeScript
// Google Fonts disabled in dev mode (Turbopack can't resolve internal font path).
|
|
// Using CSS system-font stack instead.
|
|
import "./globals.css";
|
|
import { ThemeProvider } from "@/shared/components/ThemeProvider";
|
|
import { NextIntlClientProvider } from "next-intl";
|
|
import { getMessages, getLocale, getTranslations } from "next-intl/server";
|
|
import { RTL_LOCALES } from "@/i18n/config";
|
|
import { normalizeComplianceEventTypes } from "@/i18n/request";
|
|
import { getSettings } from "@/lib/db/settings";
|
|
import type { Viewport } from "next";
|
|
import { PwaRegister } from "@/shared/components/PwaRegister";
|
|
import { LocaleAutoDetect } from "@/shared/components/LocaleAutoDetect";
|
|
import { BasePathNetworkProvider } from "@/shared/components/BasePathNetworkProvider";
|
|
|
|
// Stub: no Google Font loaded; CSS falls back to system-ui / sans-serif.
|
|
const inter = { variable: "" } as { variable: string };
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: "#0b0f1a",
|
|
viewportFit: "cover",
|
|
};
|
|
|
|
export async function generateMetadata() {
|
|
const settings = await getSettings();
|
|
const instanceName = settings?.instanceName || "OmniRoute";
|
|
const customFaviconUrl = settings?.customFaviconUrl || settings?.customFaviconBase64;
|
|
|
|
return {
|
|
title: `${instanceName} — AI Gateway for Multi-Provider LLMs`,
|
|
description:
|
|
"OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
|
|
manifest: "/manifest.webmanifest",
|
|
applicationName: instanceName,
|
|
appleWebApp: {
|
|
capable: true,
|
|
title: instanceName,
|
|
statusBarStyle: "black-translucent",
|
|
},
|
|
other: {
|
|
"mobile-web-app-capable": "yes",
|
|
},
|
|
icons: {
|
|
icon: customFaviconUrl
|
|
? "/api/settings/favicon"
|
|
: [
|
|
{ url: "/favicon.ico", sizes: "any" },
|
|
{ url: "/favicon.svg", type: "image/svg+xml" },
|
|
{ url: "/icon-512.png", type: "image/png", sizes: "512x512" },
|
|
],
|
|
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function RootLayout({ children }) {
|
|
const locale = await getLocale();
|
|
const t = await getTranslations("sidebar");
|
|
const messages = normalizeComplianceEventTypes((await getMessages()) as Record<string, unknown>);
|
|
const isRtl = RTL_LOCALES.includes(locale as (typeof RTL_LOCALES)[number]);
|
|
|
|
return (
|
|
<html lang={locale} dir={isRtl ? "rtl" : "ltr"} suppressHydrationWarning>
|
|
<head>
|
|
{/* Pre-hydration cleanup: browser extensions (Bitdefender's
|
|
bis_skin_checked, Grammarly's data-gr-ext-installed, LanguageTool's
|
|
data-lt-installed, etc.) inject attributes into the DOM after SSR
|
|
but before React hydrates, causing hydration mismatch warnings in
|
|
dev. Strip them synchronously and observe for late injections; the
|
|
observer auto-disconnects after 5s (well past typical hydration). */}
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
(function () {
|
|
var ATTRS = ["bis_skin_checked", "data-google-query-id", "data-new-gr-c-s-check-loaded", "data-gr-ext-installed", "data-lt-installed", "data-lt-tmp-id"];
|
|
function strip(el) {
|
|
for (var i = 0; i < ATTRS.length; i++) {
|
|
if (el.hasAttribute(ATTRS[i])) el.removeAttribute(ATTRS[i]);
|
|
}
|
|
}
|
|
strip(document.documentElement);
|
|
if (typeof MutationObserver === "undefined") return;
|
|
var obs = new MutationObserver(function (muts) {
|
|
for (var i = 0; i < muts.length; i++) {
|
|
var m = muts[i];
|
|
if (m.type === "attributes" && ATTRS.indexOf(m.attributeName) !== -1) {
|
|
m.target.removeAttribute(m.attributeName);
|
|
}
|
|
}
|
|
});
|
|
obs.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ATTRS });
|
|
setTimeout(function () { obs.disconnect(); }, 5000);
|
|
})();
|
|
`,
|
|
}}
|
|
/>
|
|
{/* Material Symbols icon font is self-hosted via globals.css
|
|
(@import "material-symbols/outlined.css") so icons render even when
|
|
the Google Fonts CDN is unreachable (#3695). */}
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
if (typeof window !== 'undefined') {
|
|
if (!window.crypto) {
|
|
window.crypto = {};
|
|
}
|
|
if (!window.crypto.randomUUID) {
|
|
window.crypto.randomUUID = function() {
|
|
if (window.crypto.getRandomValues) {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = window.crypto.getRandomValues(new Uint8Array(1))[0] % 16;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
};
|
|
}
|
|
}
|
|
try {
|
|
const stored = localStorage.getItem('theme');
|
|
const parsed = stored ? JSON.parse(stored) : null;
|
|
const theme = parsed?.state?.theme || 'system';
|
|
if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
} catch (e) {}
|
|
`,
|
|
}}
|
|
/>
|
|
</head>
|
|
<body className={`${inter.variable} font-sans antialiased`} suppressHydrationWarning>
|
|
<a
|
|
href="#main-content"
|
|
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-[#6366f1] focus:text-white focus:rounded-lg focus:text-sm focus:font-semibold focus:shadow-lg"
|
|
>
|
|
{t("skipToContent")}
|
|
</a>
|
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
<BasePathNetworkProvider>
|
|
<PwaRegister />
|
|
<LocaleAutoDetect />
|
|
<ThemeProvider>{children}</ThemeProvider>
|
|
</BasePathNetworkProvider>
|
|
</NextIntlClientProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|