diff --git a/.changeset/fn-7031-dock-files-binary-preview.md b/.changeset/fn-7031-dock-files-binary-preview.md new file mode 100644 index 0000000000..83fa0f7d07 --- /dev/null +++ b/.changeset/fn-7031-dock-files-binary-preview.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Preview image, video, audio, and PDF files natively in the right-dock Files viewer. +category: fix +dev: Reuses the shared file-preview classification and download route in DockFilesView. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 956bba7798..daaa8a63fc 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -52,7 +52,7 @@ The **Right Dock Panel** experiment is enabled by default. To disable it, open * When enabled on desktop or tablet project screens, the right dock is a persistent far-right tools sidebar in the project content row. Use the in-dock collapse control to switch between the full tool panel and the compact far-right rail; the selected tool, expanded/collapsed state, width, and expanded modal size persist across reloads. -The dock toolbar has built-in inline tool panels for **Activity**, **Activity Log**, **Git Manager**, **Files**, and project tool launchers such as **Import from GitHub** / **Import Tasks** workflow entry points and **Automation** actions when available. **Activity**, **Activity Log**, **Git Manager**, and **Files** render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** opens by default and is the fallback when browser storage points at a removed dock key. Inline dock views have an expand button that opens the same view in a resizable modal for more room. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar. +The dock toolbar has built-in inline tool panels for **Activity**, **Activity Log**, **Git Manager**, **Files**, and project tool launchers such as **Import from GitHub** / **Import Tasks** workflow entry points and **Automation** actions when available. **Activity**, **Activity Log**, **Git Manager**, and **Files** render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** opens by default and is the fallback when browser storage points at a removed dock key. Inline dock views have an expand button that opens the same view in a resizable modal for more room. The right-dock **Files** viewer and its expanded pop-out match the Files modal for browser-previewable file types: image, video/movie, audio, and PDF selections render as native browser previews, while editable text files keep the editor and save flow. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar. Use the desktop/tablet right dock this way: diff --git a/packages/dashboard/app/components/DockFilesView.css b/packages/dashboard/app/components/DockFilesView.css index 26b577d9cb..7fae3b65b8 100644 --- a/packages/dashboard/app/components/DockFilesView.css +++ b/packages/dashboard/app/components/DockFilesView.css @@ -109,6 +109,39 @@ Hidden until a file is selected; when selected it overlays the tree as the singl min-height: 0; } +/* +FNXC:RightDockFiles 2026-06-25-00:00: +The dock Files viewer shares FileBrowserModal's native preview classes but needs dock-scoped flex sizing so image/video/audio/PDF previews fit both the compact single-panel stack and the two-pane pop-out without overflowing or showing the CodeMirror shell. +*/ +.dock-files-preview { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + width: 100%; + overflow: auto; +} + +.dock-files-preview__media { + min-width: 0; +} + +.dock-files-preview .file-browser-preview-media--image, +.dock-files-preview .file-browser-preview-media--video { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.dock-files-preview .file-browser-preview-media--audio { + width: min(100%, calc(var(--space-xl) * 18)); +} + +.dock-files-preview .file-browser-preview-media--pdf { + width: 100%; + min-height: 0; + flex: 1 1 auto; +} + .dock-files-viewer__status { padding: var(--space-md); font-size: var(--font-size-xs); @@ -203,3 +236,15 @@ DETERMINISTIC two-pane layout for the RightDockExpandModal pop-out. Driven by th .dock-files-view--two-pane .dock-files-view__viewer .dock-files-viewer__back { display: none; } + +@media (max-width: 768px) { + .dock-files-preview { + padding: var(--space-md); + } + + .dock-files-preview .file-browser-preview-media--image, + .dock-files-preview .file-browser-preview-media--video, + .dock-files-preview .file-browser-preview-media--pdf { + width: 100%; + } +} diff --git a/packages/dashboard/app/components/DockFilesView.tsx b/packages/dashboard/app/components/DockFilesView.tsx index cd57e368c1..ff16a48b76 100644 --- a/packages/dashboard/app/components/DockFilesView.tsx +++ b/packages/dashboard/app/components/DockFilesView.tsx @@ -1,10 +1,12 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArrowLeft, Maximize2, Save } from "lucide-react"; import type { PluginDashboardViewContext } from "../plugins/types"; +import { downloadFileUrl } from "../api"; import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser"; import { useWorkspaceFileEditor } from "../hooks/useWorkspaceFileEditor"; import { getScopedItem, removeScopedItem, scopedKey, setScopedItem } from "../utils/projectStorage"; +import { getFilePreviewKind, IMAGE_PREVIEW_EXTENSIONS, VIDEO_PREVIEW_EXTENSIONS, AUDIO_PREVIEW_EXTENSIONS, PDF_PREVIEW_EXTENSIONS } from "../utils/file-preview-kind"; import { FileBrowser } from "./FileBrowser"; import { FileEditor } from "./FileEditor"; import "./DockFilesView.css"; @@ -28,6 +30,30 @@ Share the current-file path through scoped localStorage (`kb-dashboard-dock-file */ export const DOCK_FILES_CURRENT_KEY = "kb-dashboard-dock-files-current"; +const BINARY_EXTENSIONS = new Set([ + ...IMAGE_PREVIEW_EXTENSIONS, + ...VIDEO_PREVIEW_EXTENSIONS, + ...AUDIO_PREVIEW_EXTENSIONS, + ...PDF_PREVIEW_EXTENSIONS, + ".exe", ".dll", ".so", ".dylib", + ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", + ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", + ".avi", ".mkv", ".flv", + ".woff", ".woff2", ".ttf", ".otf", ".eot", + ".wasm", ".bin", +]); + +function isBinaryFile(filename?: string | null): boolean { + if (!filename) { + return false; + } + const dotIndex = filename.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + return BINARY_EXTENSIONS.has(filename.slice(dotIndex).toLowerCase()); +} + /* FNXC:RightDockFiles 2026-06-22-00:00: The right-dock Files tool opens a clicked file INLINE inside the dock as a read-only viewer instead of immediately launching the resizable/movable FileBrowserModal. @@ -78,9 +104,22 @@ export function DockFilesView({ projectId, openFile, layout = "auto" }: DockFile return () => window.removeEventListener("storage", onStorage); }, [projectId]); + const selectedPreviewKind = useMemo(() => getFilePreviewKind(selectedFile), [selectedFile]); + const isPreviewOnlyFile = selectedPreviewKind !== null; + const isReadOnlyBinaryFile = Boolean(selectedFile && !isPreviewOnlyFile && isBinaryFile(selectedFile)); + const previewUrl = useMemo(() => { + if (!selectedFile || !selectedPreviewKind) { + return null; + } + return downloadFileUrl("project", selectedFile, projectId); + }, [projectId, selectedFile, selectedPreviewKind]); + /* FNXC:RightDockFiles 2026-06-22-16:28: The right-sidebar file viewer must be the same editor surface as the modal/mobile file browser: real workspace editor state, visible toolbar options, Preview/Edit for markdown, Line #, and Wrap. Use the shared editor hook instead of the old read-only content fetch so edits can be saved and the toolbar is not a reduced sidebar-only variant. + + FNXC:RightDockFiles 2026-06-25-00:00: + Known image/video/audio/PDF selections render through browser-native previews loaded from the workspace-safe download route. Keep those preview-only files out of CodeMirror so binary bytes are never fetched as editor text; editable text keeps the existing FileEditor + Save path, while known non-preview binary extensions stay read-only without loading bytes as editor content. */ const { content, @@ -90,7 +129,7 @@ export function DockFilesView({ projectId, openFile, layout = "auto" }: DockFile error: contentError, save, hasChanges, - } = useWorkspaceFileEditor("project", selectedFile, Boolean(selectedFile), projectId); + } = useWorkspaceFileEditor("project", selectedFile, Boolean(selectedFile) && !isPreviewOnlyFile && !isReadOnlyBinaryFile, projectId); const handleBack = useCallback(() => selectFile(null), [selectFile]); const handlePopOut = useCallback(() => { @@ -99,6 +138,9 @@ export function DockFilesView({ projectId, openFile, layout = "auto" }: DockFile const handleToggleLineNumbers = useCallback(() => setShowLineNumbers((current) => !current), []); const fileName = selectedFile ? selectedFile.split("/").pop() || selectedFile : ""; + const selectedPreviewTitle = selectedFile + ? t("fileBrowser.previewTitle", "Preview for {{file}}", { file: selectedFile }) + : ""; // FNXC:Files 2026-06-22-00:00: // `data-selected` on the root lets the container query distinguish "no file selected" (narrow: viewer pane hidden so only the tree shows) from "file selected" (narrow: viewer pane covers the stack). When wide both panes are always visible regardless of this flag. @@ -155,7 +197,7 @@ export function DockFilesView({ projectId, openFile, layout = "auto" }: DockFile > - {selectedFile ? ( + {selectedFile && !isPreviewOnlyFile && !isReadOnlyBinaryFile ? (