import type { Metadata, Viewport } from "next"; import { AeroMusic } from "@/components/AeroMusic"; import { IceBackdrop } from "@/components/IceBackdrop"; import { MotionPreference } from "@/components/MotionPreference"; import { CookieConsent } from "@/components/CookieConsent"; import { InstallSheet } from "@/components/InstallSheet"; import { OfflineBanner } from "@/components/OfflineBanner"; import { PushReminderSync } from "@/components/PushReminderSync"; import { ServiceWorkerRegistrar } from "@/components/ServiceWorkerRegistrar"; import { Toaster } from "@/components/Toaster"; import { IS_DEV_INSTANCE } from "@/lib/dev-mode"; const devInstanceJs = IS_DEV_INSTANCE ? "true" : "false"; import "./globals.css"; export const metadata: Metadata = { title: { default: "Freezmo — turn your notes into a study feed", template: "%s · Freezmo", }, description: "Freezmo turns photos and documents of your notes into a swipeable feed of short interactive study activities, powered by your own Ollama models.", applicationName: "Freezmo", manifest: "/manifest.webmanifest", appleWebApp: { capable: true, title: "Freezmo", statusBarStyle: "default", /* Startup images so launching from the home screen has no white flash. Sized for the common iPhone logical sizes at their device pixel ratio. */ startupImage: [ { url: "/icons/splash-750x1334.png", media: "(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" }, { url: "/icons/splash-828x1792.png", media: "(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" }, { url: "/icons/splash-1170x2532.png", media: "(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3)" }, { url: "/icons/splash-1179x2556.png", media: "(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3)" }, { url: "/icons/splash-1284x2778.png", media: "(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3)" }, { url: "/icons/splash-1290x2796.png", media: "(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3)" }, { url: "/icons/splash-1536x2048.png", media: "(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" }, { url: "/icons/splash-1668x2388.png", media: "(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" }, ], }, icons: { icon: IS_DEV_INSTANCE ? // Dev wears its own favicon so the two instances are never confused. [{ url: "/icons/dev-favicon.svg", type: "image/svg+xml" }] : [ // ?v=2 busts browser favicon caches after the icon redesign. { url: "/icons/favicon-32.png?v=2", sizes: "32x32", type: "image/png" }, { url: "/icons/icon-192.png?v=2", sizes: "192x192", type: "image/png" }, { url: "/icons/icon-512.png?v=2", sizes: "512x512", type: "image/png" }, ], apple: [ { url: "/icons/apple-touch-icon.png?v=2", sizes: "180x180" }, { url: "/icons/apple-touch-icon-167.png?v=2", sizes: "167x167" }, { url: "/icons/apple-touch-icon-152.png?v=2", sizes: "152x152" }, ], }, other: { // iOS standalone niceties "apple-mobile-web-app-capable": "yes", "mobile-web-app-capable": "yes", "format-detection": "telephone=no", }, }; export const viewport: Viewport = { width: "device-width", initialScale: 1, // Pinch-zoom stays available for accessibility (double-tap zoom is disabled // by `touch-action: manipulation` on the chrome instead). The input // font-size rule in globals.css stops iOS Safari's focus auto-zoom, which // ignores the viewport meta anyway. viewportFit: "cover", themeColor: IS_DEV_INSTANCE ? [{ media: "(prefers-color-scheme: light)", color: "#b45309" }, { media: "(prefers-color-scheme: dark)", color: "#b45309" }] : [ { media: "(prefers-color-scheme: light)", color: "#f4f7fd" }, { media: "(prefers-color-scheme: dark)", color: "#0e1424" }, ], }; /** * Applies the saved theme before paint to avoid a flash, and keeps the * `theme-color` meta in step with the *app* theme (the SSR metas follow the * OS preference, which is wrong once the user has picked a theme here). * The stored theme decides alone: aero is not dark, and the old ternary added * `dark` alongside `aero` whenever the OS preferred dark — the dark palette * on the bright wallpaper is what made the first load look washed out until * a theme toggle re-applied the classes. */ const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=a?false:(s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',!!d);if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;if(localStorage.getItem('freezmo.reduce-motion.v1')==='1')document.documentElement.classList.add('reduce-motion');var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`; /** * Captures Chrome's `beforeinstallprompt` (it can fire before React hydrates) * and records an install so the prompt never comes back. Static, no user data. */ const installScript = `(function(){try{ window.addEventListener('beforeinstallprompt',function(e){e.preventDefault();window.__freezmoInstallPrompt=e;window.dispatchEvent(new Event('freezmo:installprompt'));}); window.addEventListener('appinstalled',function(){try{localStorage.setItem('freezmo.install.installed','1')}catch(e){}window.dispatchEvent(new Event('freezmo:installed'));}); }catch(e){}})();`; export default function RootLayout({ children }: { children: React.ReactNode }) { return (