diff --git a/ui/public/manifest.json b/ui/public/manifest.json index 24a9859..05d9f98 100644 --- a/ui/public/manifest.json +++ b/ui/public/manifest.json @@ -21,17 +21,65 @@ "src": "/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" }, { "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", - "purpose": "any maskable" + "purpose": "maskable" + } + ], + "screenshots": [ + { + "src": "/screenshot-mobile.png", + "sizes": "1284x2778", + "type": "image/png", + "form_factor": "narrow" + }, + { + "src": "/screenshot-wide.png", + "sizes": "2048x2732", + "type": "image/png", + "form_factor": "wide" } ], "theme_color": "#FFFFFF", "background_color": "#FFFFFF", "display": "standalone", - "orientation": "any" + "orientation": "any", + "share_target": { + "action": "/share-target", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "files", + "accept": [ + "image/*", + "application/pdf", + "application/epub+zip", + "text/html", + "text/markdown" + ] + } + ] + } + } } diff --git a/ui/public/screenshot-mobile.png b/ui/public/screenshot-mobile.png new file mode 100644 index 0000000..301519e Binary files /dev/null and b/ui/public/screenshot-mobile.png differ diff --git a/ui/public/screenshot-wide.png b/ui/public/screenshot-wide.png new file mode 100644 index 0000000..74e2dcb Binary files /dev/null and b/ui/public/screenshot-wide.png differ diff --git a/ui/public/sw.js b/ui/public/sw.js new file mode 100644 index 0000000..e652da5 --- /dev/null +++ b/ui/public/sw.js @@ -0,0 +1,93 @@ +self.addEventListener("install", () => self.skipWaiting()); +self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim())); + +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + if (event.request.method !== "POST" || url.pathname !== "/share-target") { + return; + } + + event.respondWith( + (async () => { + try { + const formData = await event.request.formData(); + + const files = formData.getAll("files"); + if (files.length > 0) { + const fileData = await Promise.all( + files.map(async (file) => ({ + name: file.name, + type: file.type, + data: await file.arrayBuffer(), + lastModified: file.lastModified, + })) + ); + + const db = await openDB("AviarySharedFiles", "files"); + const tx = db.transaction("files", "readwrite"); + const store = tx.objectStore("files"); + for (const entry of fileData) { + store.add(entry); + } + await txComplete(tx); + db.close(); + } + + const title = formData.get("title") || ""; + const text = formData.get("text") || ""; + const sharedUrl = formData.get("url") || ""; + if (title || text || sharedUrl) { + const db = await openDB("AviarySharedData", "data"); + const tx = db.transaction("data", "readwrite"); + const store = tx.objectStore("data"); + store.put({ title, text, url: sharedUrl }, "shared"); + await txComplete(tx); + db.close(); + } + + return Response.redirect("/?shared=1", 303); + } catch (err) { + console.error("[SW] share-target error:", err); + return Response.redirect("/?shared=1", 303); + } + })(), + ); +}); + +function openDB(name, storeName) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName, { autoIncrement: true }); + } + }; + request.onsuccess = () => { + const db = request.result; + if (db.objectStoreNames.contains(storeName)) { + resolve(db); + } else { + db.close(); + const delReq = indexedDB.deleteDatabase(name); + delReq.onsuccess = () => { + const retry = indexedDB.open(name, 1); + retry.onupgradeneeded = () => { + retry.result.createObjectStore(storeName, { autoIncrement: true }); + }; + retry.onsuccess = () => resolve(retry.result); + retry.onerror = () => reject(retry.error); + }; + delReq.onerror = () => reject(delReq.error); + } + }; + request.onerror = () => reject(request.error); + }); +} + +function txComplete(tx) { + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} diff --git a/ui/src/HomePage.tsx b/ui/src/HomePage.tsx index b9d08ae..fef5d18 100644 --- a/ui/src/HomePage.tsx +++ b/ui/src/HomePage.tsx @@ -30,6 +30,7 @@ import { Loader2, CircleCheck, XCircle } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import { useTranslation } from "react-i18next"; import i18n from "@/lib/i18n"; +import { getSharedFiles, getSharedData, clearSharedFiles, clearSharedData } from "@/lib/sharedFiles"; const COMPRESSIBLE_EXTS = [".pdf", ".png", ".jpg", ".jpeg"]; const POLL_INTERVAL_MS = 200; @@ -432,6 +433,36 @@ export default function HomePage() { } }, []); + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("shared") !== "1") return; + + (async () => { + const [files, data] = await Promise.all([getSharedFiles(), getSharedData()]); + + if (files.length > 0) { + setSelectedFile(null); + setSelectedFiles(files); + setUrl(""); + setUrlMime(null); + } else if (data?.url) { + setUrl(data.url); + setCommittedUrl(data.url); + sniffMime(data.url).then((mt) => setUrlMime(mt)); + } else if (data?.text) { + setUrl(data.text); + setCommittedUrl(data.text); + sniffMime(data.text).then((mt) => setUrlMime(mt)); + } + + await Promise.all([clearSharedFiles(), clearSharedData()]); + + params.delete("shared"); + const newSearch = params.toString(); + window.history.replaceState({}, "", newSearch ? `?${newSearch}` : window.location.pathname); + })(); + }, []); + useEffect(() => { (window as any).aviaryInjectFile = (base64: string, filename: string, mimeType: string) => { const binaryString = atob(base64); diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 416987c..29b6b43 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -4,14 +4,8 @@ import App from './App'; import './globals.css'; import './lib/i18n'; -// Unregister any existing service workers if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => { - registration.unregister(); - console.log('Unregistered service worker:', registration.scope); - }); - }); + navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }); } ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(