diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 004056d..e176ab2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,15 +1,89 @@ -import { Router, Route } from "@solidjs/router"; +import { Router, Route, useLocation, useNavigate } from "@solidjs/router"; +import type { RouteSectionProps } from "@solidjs/router"; +import type { Component } from "solid-js"; import HomePage from "./pages/HomePage"; import { MyDrive } from "./pages/MyDrive"; import { useWebSocket, WebSocketProvider } from "./Websockets"; import MyCollections from "./pages/MyCollections"; import Account from "./pages/Account"; import { AppContext, ContextProvider } from "./Context"; -import { createEffect, useContext } from "solid-js"; +import { createEffect, useContext, onMount, onCleanup } from "solid-js"; import { UniversalMessageHandler } from "./library/functions"; import CollectionPage from "./pages/Collection"; +import toast from "solid-toast"; +// Mounted inside Router so router hooks are valid +let __globalPasteListenerAttached = false; +const GlobalPasteHandler = () => { + const navigate = useNavigate(); + const location = useLocation(); + const ctx = useContext(AppContext)!; + onMount(() => { + if (__globalPasteListenerAttached) return; + const pasteHandler = (e: ClipboardEvent) => { + try { + // If a page-level handler already handled it, skip + if (e.defaultPrevented) return; + // Ignore when typing in inputs/textarea/contenteditable + const ae = document.activeElement as HTMLElement | null; + if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return; + + const dt = e.clipboardData; + if (!dt) return; + const fromFiles = dt.files ? Array.from(dt.files) : []; + const fromItems = dt.items ? Array.from(dt.items) + .filter(it => it.kind === 'file') + .map(it => it.getAsFile()) + .filter((f): f is File => !!f && f.size > 0) : []; + // Merge and dedupe by name|size|type|lastModified to avoid duplicates + const seen = new Set(); + const files: File[] = []; + for (const f of [...fromFiles, ...fromItems]) { + const key = `${f.name}|${f.size}|${f.type}|${(f as any).lastModified ?? ''}`; + if (!seen.has(key)) { seen.add(key); files.push(f); } + } + if (files.length === 0) { + // Gracefully ignore pure text pastes globally + return; + } + + // Prevent default so page-level handlers don't also process and browser doesn't paste images + e.preventDefault(); + + const path = location.pathname || ""; + if (path.startsWith("/collection")) { + document.dispatchEvent(new CustomEvent("open-collection-upload", { detail: { files } })); + return; + } + + const openDriveUpload = () => { + document.dispatchEvent(new CustomEvent("open-drive-upload", { detail: { files } })); + }; + + if (path !== "/my_drive") { + // Use context to pass pending files and route + ctx.setPendingDriveUploadFiles?.(files); + try { toast.success(`Opening My Drive to upload ${files.length} file${files.length>1?'s':''}…`); } catch {} + navigate("/my_drive"); + } else { + openDriveUpload(); + } + } catch (err) { + console.error('Global paste handler error:', err); + } + }; + + document.addEventListener('paste', pasteHandler); + __globalPasteListenerAttached = true; + onCleanup(() => { + document.removeEventListener('paste', pasteHandler); + __globalPasteListenerAttached = false; + }); + }); + return null; +} + const UncontextedApp = () => { const { socket: getSocket } = useWebSocket(); const ctx = useContext(AppContext)!; @@ -22,15 +96,26 @@ const UncontextedApp = () => { }) return ( - - - - - + + + + + + + ) } +const RootLayout: Component> = (props) => { + return ( + <> + + {props.children} + + ); +} + const App = () => { return ( diff --git a/frontend/src/Context.tsx b/frontend/src/Context.tsx index 0eff953..c60279c 100644 --- a/frontend/src/Context.tsx +++ b/frontend/src/Context.tsx @@ -9,6 +9,7 @@ const ContextProvider: ParentComponent = (props) => { const [userCollections, setUserCollections] = createSignal>(new Set()); const [knownCollections, setKnownCollections] = createSignal({}); const [knownCollectionCards, setKnownCollectionCards] = createSignal({}); + const [pendingDriveUploadFiles, setPendingDriveUploadFiles] = createSignal(null); const contextValue: AppContextType = { files: files, setFiles: setFiles, @@ -18,6 +19,8 @@ const ContextProvider: ParentComponent = (props) => { setKnownCollections: setKnownCollections, knownCollectionCards: knownCollectionCards, setKnownCollectionCards: setKnownCollectionCards, + pendingDriveUploadFiles, + setPendingDriveUploadFiles, }; return ( diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index c5ca1a5..9cc97b1 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -42,7 +42,7 @@ const DesktopNavbar: Component<{ CurrentPage: Pages }> = (props) => { -
+
{navigate("/account")}}>
@@ -130,7 +130,7 @@ const MobileNavbar: Component<{ CurrentPage: Pages }> = (props) => {
GitHub -
+
)} {(status() === "error" || status() === "disconnected") && ( -
+
@@ -129,7 +129,7 @@ const FileUploadPreview: Component<{

{formatFileSize(file.size)}

-
+
{ + const ctx = useContext(AppContext)!; const [selectedFiles, setSelectedFiles] = createSignal([]); const [uploadProgressMap, setUploadProgressMap] = createSignal>({}); const [isUploading, setIsUploading] = createSignal(false); @@ -419,11 +420,15 @@ const UploadPopup: Component = () => { // If navigation stored pending files, consume them (only if not already open) queueMicrotask(() => { if (!open()) { - const pending = (window as any).__pendingDriveUploadFiles as File[] | undefined; - if (pending && pending.length) { - addDroppedFiles(pending); - (window as any).__pendingDriveUploadFiles = undefined; - setOpen(true); + try { + const pending = ctx.pendingDriveUploadFiles?.() || null; + if (pending && pending.length) { + addDroppedFiles(pending); + ctx.setPendingDriveUploadFiles?.(null); + setOpen(true); + } + } catch (err) { + console.error('Error consuming pending drive upload files from context:', err); } } });