mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
feat(pwa): add fullscreen installable PWA with manifest, service worker, and cross-platform app icons. (#1728)
Integrated into release/v3.7.4
This commit is contained in:
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
BIN
public/icon-512.png
Normal file
BIN
public/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
75
public/sw.js
Normal file
75
public/sw.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const CACHE_NAME = "omniroute-pwa-v1";
|
||||
const APP_SHELL = [
|
||||
"/",
|
||||
"/offline",
|
||||
"/manifest.webmanifest",
|
||||
"/icon-512.png",
|
||||
"/apple-touch-icon.png",
|
||||
];
|
||||
const EXCLUDED_PATH_PREFIXES = ["/api/", "/a2a", "/dashboard/endpoint"];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(APP_SHELL))
|
||||
.then(() => self.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))))
|
||||
.then(() => self.clients.claim()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (event.request.method !== "GET") {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestUrl = new URL(event.request.url);
|
||||
const isSameOrigin = requestUrl.origin === self.location.origin;
|
||||
const isExcludedPath = EXCLUDED_PATH_PREFIXES.some((prefix) =>
|
||||
requestUrl.pathname.startsWith(prefix),
|
||||
);
|
||||
const destination = event.request.destination;
|
||||
const isStaticAsset = ["style", "script", "image", "font"].includes(destination);
|
||||
const isNavigateRequest = event.request.mode === "navigate";
|
||||
|
||||
// Never cache API/dashboard traffic with potentially auth-sensitive content.
|
||||
if (!isSameOrigin || isExcludedPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
if (isNavigateRequest) {
|
||||
try {
|
||||
return await fetch(event.request);
|
||||
} catch {
|
||||
return (await caches.match("/offline")) || Response.error();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStaticAsset) {
|
||||
return fetch(event.request);
|
||||
}
|
||||
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(event.request);
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const responseClone = networkResponse.clone();
|
||||
void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseClone));
|
||||
}
|
||||
return networkResponse;
|
||||
})(),
|
||||
);
|
||||
});
|
||||
@@ -5,12 +5,19 @@ import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getLocale } from "next-intl/server";
|
||||
import { RTL_LOCALES } from "@/i18n/config";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import type { Viewport } from "next";
|
||||
import { PwaRegister } from "@/shared/components/PwaRegister";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#0b0f1a",
|
||||
viewportFit: "cover",
|
||||
};
|
||||
|
||||
export async function generateMetadata() {
|
||||
const settings = await getSettings();
|
||||
const instanceName = settings?.instanceName || "OmniRoute";
|
||||
@@ -20,9 +27,25 @@ export async function generateMetadata() {
|
||||
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" : "/favicon.svg",
|
||||
apple: "/apple-touch-icon.svg",
|
||||
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" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -67,6 +90,7 @@ export default async function RootLayout({ children }) {
|
||||
Skip to content
|
||||
</a>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<PwaRegister />
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
29
src/app/manifest.ts
Normal file
29
src/app/manifest.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "OmniRoute",
|
||||
short_name: "OmniRoute",
|
||||
description:
|
||||
"OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
display: "fullscreen",
|
||||
orientation: "any",
|
||||
background_color: "#0b0f1a",
|
||||
theme_color: "#0b0f1a",
|
||||
icons: [
|
||||
{
|
||||
src: "/icon-512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any maskable",
|
||||
},
|
||||
{
|
||||
src: "/apple-touch-icon.png",
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
17
src/shared/components/PwaRegister.tsx
Normal file
17
src/shared/components/PwaRegister.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function PwaRegister() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.register("/sw.js").catch(() => {
|
||||
// Ignore registration failures to avoid blocking app rendering.
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user