From 41fa50433cfe3814263bd19e7cd5a95a4bf66958 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Thu, 27 Nov 2025 09:47:58 -0800 Subject: [PATCH] Add fullscreen quick preview via portal layer --- core/src/domain/file.rs | 3 +- packages/interface/src/Explorer.tsx | 29 +- packages/interface/src/Inspector.tsx | 10 +- packages/interface/src/TopBar/TopBar.tsx | 11 +- .../src/components/Explorer/ExplorerView.tsx | 103 ++- .../components/QuickPreview/AudioPlayer.tsx | 54 +- .../QuickPreview/ContentRenderer.tsx | 28 +- .../components/QuickPreview/QuickPreview.tsx | 20 +- .../QuickPreview/QuickPreviewFullscreen.tsx | 196 ++++ .../QuickPreview/QuickPreviewOverlay.tsx | 142 +++ .../src/components/QuickPreview/Subtitles.tsx | 283 +++--- .../components/QuickPreview/VideoPlayer.tsx | 14 +- .../src/components/QuickPreview/index.ts | 2 + .../src/components/QuickPreview/useZoomPan.ts | 1 + .../src/components/SpacesSidebar/index.tsx | 10 +- packages/interface/src/context.tsx | 4 +- .../src/inspectors/FileInspector.tsx | 7 +- packages/interface/src/styles.css | 40 + packages/ts-client/src/hooks/index.ts | 3 +- .../ts-client/src/hooks/useNormalizedCache.ts | 849 ------------------ .../ts-client/src/hooks/useNormalizedQuery.ts | 68 +- 21 files changed, 741 insertions(+), 1136 deletions(-) create mode 100644 packages/interface/src/components/QuickPreview/QuickPreviewFullscreen.tsx create mode 100644 packages/interface/src/components/QuickPreview/QuickPreviewOverlay.tsx delete mode 100644 packages/ts-client/src/hooks/useNormalizedCache.ts diff --git a/core/src/domain/file.rs b/core/src/domain/file.rs index cd29b920b..217f95419 100644 --- a/core/src/domain/file.rs +++ b/core/src/domain/file.rs @@ -182,8 +182,9 @@ impl crate::domain::resource::Identifiable for File { })?; // Find entries with matching content_identity UUID + // Note: content_identity.uuid is Option, must wrap in Some() let ci_opt = content_identity::Entity::find() - .filter(content_identity::Column::Uuid.eq(sc.content_uuid)) + .filter(content_identity::Column::Uuid.eq(Some(sc.content_uuid))) .one(db) .await?; diff --git a/packages/interface/src/Explorer.tsx b/packages/interface/src/Explorer.tsx index 1527ec1d0..f4f1340c7 100644 --- a/packages/interface/src/Explorer.tsx +++ b/packages/interface/src/Explorer.tsx @@ -19,7 +19,7 @@ import { import { KeyboardHandler } from "./components/Explorer/KeyboardHandler"; import { TagAssignmentMode } from "./components/Explorer/TagAssignmentMode"; import { SpacesSidebar } from "./components/SpacesSidebar"; -import { QuickPreviewModal } from "./components/QuickPreview"; +import { QuickPreviewFullscreen, PREVIEW_LAYER_ID } from "./components/QuickPreview"; import { createExplorerRouter } from "./router"; import { useNormalizedCache } from "./context"; import { usePlatform } from "./platform"; @@ -110,11 +110,20 @@ export function ExplorerLayout() { } }; + const isPreviewActive = !!quickPreviewFileId; + return (
+ {/* Preview layer - portal target for fullscreen preview, sits between content and sidebar/inspector */} +
+ @@ -124,15 +133,14 @@ export function ExplorerLayout() { animate={{ x: 0, width: 220 }} exit={{ x: -220, width: 0 }} transition={{ duration: 0.3, ease: [0.25, 1, 0.5, 1] }} - className="overflow-hidden" + className="relative z-50 overflow-hidden" > - - {/**/} + )} -
+
{/* Router content renders here */}
@@ -154,21 +162,22 @@ export function ExplorerLayout() { animate={{ width: 280 }} exit={{ width: 0 }} transition={{ duration: 0.3, ease: [0.25, 1, 0.5, 1] }} - className="overflow-hidden" + className="relative z-50 overflow-hidden" > -
+
)} - {/* Quick Preview Modal - TODO: Fix files reference */} + {/* Quick Preview - renders via portal into preview layer */} {quickPreviewFileId && ( - goToPreviousPreview([])} hasPrevious={false} hasNext={false} + sidebarWidth={sidebarVisible ? 220 : 0} + inspectorWidth={inspectorVisible && !isOverview && !isKnowledgeView ? 280 : 0} /> )}
diff --git a/packages/interface/src/Inspector.tsx b/packages/interface/src/Inspector.tsx index bcfa685f5..f485b0257 100644 --- a/packages/interface/src/Inspector.tsx +++ b/packages/interface/src/Inspector.tsx @@ -7,6 +7,7 @@ import { usePlatform } from "./platform"; import { useSelection } from "./components/Explorer/SelectionContext"; import { FileInspector } from "./inspectors/FileInspector"; import { LocationInspector } from "./inspectors/LocationInspector"; +import clsx from "clsx"; export type InspectorVariant = | { type: "file"; file: File } @@ -18,12 +19,14 @@ interface InspectorProps { onPopOut?: () => void; showPopOutButton?: boolean; currentLocation?: LocationInfo | null; + isPreviewActive?: boolean; } export function Inspector({ onPopOut, showPopOutButton = true, currentLocation, + isPreviewActive = false, }: InspectorProps) { const { selectedFiles } = useSelection(); @@ -41,7 +44,12 @@ export function Inspector({ // No need for interface package to call platform-specific commands return ( -
+
{/* Variant-specific content */} {!variant || variant.type === "empty" ? ( diff --git a/packages/interface/src/TopBar/TopBar.tsx b/packages/interface/src/TopBar/TopBar.tsx index 01a1ebde8..e2914fa53 100644 --- a/packages/interface/src/TopBar/TopBar.tsx +++ b/packages/interface/src/TopBar/TopBar.tsx @@ -5,9 +5,10 @@ import clsx from "clsx"; interface TopBarProps { sidebarWidth?: number; inspectorWidth?: number; + isPreviewActive?: boolean; } -export const TopBar = memo(function TopBar({ sidebarWidth = 0, inspectorWidth = 0 }: TopBarProps) { +export const TopBar = memo(function TopBar({ sidebarWidth = 0, inspectorWidth = 0, isPreviewActive = false }: TopBarProps) { const { setLeftRef, setCenterRef, setRightRef } = useTopBar(); const leftRef = useRef(null); const centerRef = useRef(null); @@ -21,7 +22,7 @@ export const TopBar = memo(function TopBar({ sidebarWidth = 0, inspectorWidth = return (
- {/* Right fade mask */} -
+ {/* Right fade mask - hide when preview active */} + {!isPreviewActive && ( +
+ )}
); diff --git a/packages/interface/src/components/Explorer/ExplorerView.tsx b/packages/interface/src/components/Explorer/ExplorerView.tsx index a18a44ec3..8071f0577 100644 --- a/packages/interface/src/components/Explorer/ExplorerView.tsx +++ b/packages/interface/src/components/Explorer/ExplorerView.tsx @@ -44,8 +44,11 @@ export function ExplorerView() { currentPath, setCurrentPath, devices, + quickPreviewFileId, } = useExplorer(); + const isPreviewActive = !!quickPreviewFileId; + // Fetch locations to get the SdPath for this locationId const locationsQuery = useNormalizedCache({ wireMethod: "query:locations.list", @@ -99,59 +102,61 @@ export function ExplorerView() { return ( <> - - setSidebarVisible(!sidebarVisible)} - active={sidebarVisible} - /> - + {!isPreviewActive && ( + setSidebarVisible(!sidebarVisible)} + active={sidebarVisible} + /> + + + + + {currentPath && ( + + )} +
+ } + right={ +
+ + setTagModeActive(!tagModeActive)} + active={tagModeActive} + tooltip="Tag Mode (T)" + /> + + + setInspectorVisible(!inspectorVisible)} + active={inspectorVisible} /> - - {currentPath && ( - - )} -
- } - right={ -
- - setTagModeActive(!tagModeActive)} - active={tagModeActive} - tooltip="Tag Mode (T)" - /> - - - - setInspectorVisible(!inspectorVisible)} - active={inspectorVisible} - /> -
- } - /> +
+ } + /> + )}
diff --git a/packages/interface/src/components/QuickPreview/AudioPlayer.tsx b/packages/interface/src/components/QuickPreview/AudioPlayer.tsx index 7a178a9f3..42fc43533 100644 --- a/packages/interface/src/components/QuickPreview/AudioPlayer.tsx +++ b/packages/interface/src/components/QuickPreview/AudioPlayer.tsx @@ -7,10 +7,8 @@ import { SkipBack, SkipForward, } from "@phosphor-icons/react"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import type { File } from "@sd/ts-client"; -import { File as FileComponent } from "../Explorer/File"; -import { formatBytes } from "../Explorer/utils"; interface SubtitleCue { index: number; @@ -240,7 +238,7 @@ export function AudioPlayer({ src, file }: AudioPlayerProps) { }; return ( -
+
{/* Hidden audio element */}