From e6f4ecdc290b4237dd0cf5479bc62383addf090b Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Mon, 15 Jun 2026 01:41:31 -0400 Subject: [PATCH] feat(SearchAssembly64): refactor download and mount functionality for improved clarity and state management --- public/service-worker.js | 5 ++ src/app/components/SearchAssembly64.tsx | 95 +++++++++++-------------- 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index 51d723d..5d22e57 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -7,6 +7,11 @@ self.addEventListener('activate', event => { }); self.addEventListener('fetch', event => { + // Only intercept same-origin requests. Cross-origin fetches (external APIs, + // CDNs) must go straight to the network — routing them through the SW causes + // the browser to see duplicate CORS headers and reject the response. + if (new URL(event.request.url).origin !== self.location.origin) return; + event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index 7bc132d..1d376ab 100644 --- a/src/app/components/SearchAssembly64.tsx +++ b/src/app/components/SearchAssembly64.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react'; +import { Search, Loader2, HardDrive, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react'; import { toast } from 'sonner'; import { humanFileSize, basename, joinPath, putFileContents } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -121,9 +121,6 @@ function entryFilename(e: ContentEntry): string { return basename(e.path) || e.path; } -function downloadUrl(item: ContentItem, entry: ContentEntry): string { - return `${LEET_BASE}/search/bin/${item.id}/${item.category}/${entry.id}`; -} function RatingStars({ value, max = 10 }: { value?: number; max?: number }) { if (!value) return null; @@ -177,7 +174,7 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA const [loadingEntries, setLoadingEntries] = useState(false); const [mountEntry, setMountEntry] = useState<{ item: ContentItem; entry: ContentEntry } | null>(null); - const [downloading, setDownloading] = useState(null); + const [isMounting, setIsMounting] = useState(false); useEffect(() => { _store.query = query; @@ -272,43 +269,39 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA } }; - // ── Download to SD ─────────────────────────────────────────────────────────── + // ── Download + Mount ──────────────────────────────────────────────────────── + // Selecting a device downloads the file to /sd/downloads/ then sets the + // device URL to the local path. The remote leet URL is never stored. - const downloadToSd = async (item: ContentItem, entry: ContentEntry) => { - setDownloading(entry.id); + const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => { + if (!mountEntry) return; + const { item, entry } = mountEntry; + setIsMounting(true); try { - const url = downloadUrl(item, entry); - const res = await fetch(url); + const res = await leetFetch(`/search/bin/${item.id}/${item.category}/${entry.id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.arrayBuffer(); const dest = joinPath(DOWNLOAD_DIR, entryFilename(entry)); await putFileContents(dest, data); - toast.success(`Saved to ${dest}`); + const newConfig = JSON.parse(JSON.stringify(config)); + if (!newConfig.devices) newConfig.devices = {}; + if (!newConfig.devices.iec) newConfig.devices.iec = {}; + if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType }; + const dev = newConfig.devices.iec[key]; + dev.url = dest; + delete dev.media_set; + if (!dev.enabled) dev.enabled = 1; + setConfig(newConfig); + setMountEntry(null); + setSelectedItem(null); + toast.success(`Downloaded and mounted "${entryFilename(entry)}" on device #${key}`); } catch (e: any) { - toast.error(`Download failed: ${e?.message ?? e}`); + toast.error(`Mount failed: ${e?.message ?? e}`); } finally { - setDownloading(null); + setIsMounting(false); } }; - // ── Mount ──────────────────────────────────────────────────────────────────── - - const mountOnDevice = (deviceType: 'drive' | 'meatloaf', key: string) => { - if (!mountEntry) return; - const { item, entry } = mountEntry; - const newConfig = JSON.parse(JSON.stringify(config)); - if (!newConfig.devices) newConfig.devices = {}; - if (!newConfig.devices.iec) newConfig.devices.iec = {}; - if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType }; - const dev = newConfig.devices.iec[key]; - dev.url = downloadUrl(item, entry); - delete dev.media_set; - if (!dev.enabled) dev.enabled = 1; - setConfig(newConfig); - setMountEntry(null); - toast.success(`Mounted "${entryFilename(entry)}" on ${deviceType} #${key}`); - }; - const mountedUrls = useMemo(() => { const s = new Set(); for (const d of Object.values(config?.devices?.iec ?? {})) { @@ -545,35 +538,22 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA {!loadingEntries && entries && entries.length > 0 && (
{entries.map(entry => { - const fname = entryFilename(entry); - const isMounted = mountedUrls.has(downloadUrl(selectedItem!, entry)); + const fname = entryFilename(entry); + const localPath = joinPath(DOWNLOAD_DIR, fname); + const isMounted = mountedUrls.has(localPath); return ( -
setMountEntry({ item: selectedItem!, entry })} + className={`px-4 py-3 rounded-lg border flex items-center gap-3 text-left w-full transition-colors ${isMounted ? 'border-blue-300 bg-blue-50' : 'border-neutral-200 hover:bg-blue-50 hover:border-blue-300'}`} > +
{fname}
{humanFileSize(entry.size)}
- - -
+ {isMounted && Mounted} + ); })}
@@ -592,7 +572,13 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
- {(() => { + {isMounting && ( +
+ +

Downloading…

+
+ )} + {!isMounting && (() => { const allDevices = Object.entries(config?.devices?.iec ?? {}); const drives = allDevices .filter(([, v]: [string, any]) => (v as any)?.type === 'drive') @@ -637,6 +623,7 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA {/* AQL help dialog */} +