mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-10 15:52:38 +02:00
perf: keep recent html and pdf viewers mounted to preserve state
This commit is contained in:
parent
a4cd6abb3a
commit
49a50279da
2 changed files with 74 additions and 10 deletions
|
|
@ -13,11 +13,10 @@ import { ChatInputWithMentions, type StagedAttachment } from './components/chat-
|
||||||
import { ChatMessageAttachments } from '@/components/chat-message-attachments'
|
import { ChatMessageAttachments } from '@/components/chat-message-attachments'
|
||||||
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
|
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
|
||||||
import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/bases-view';
|
import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/bases-view';
|
||||||
import { HtmlFileViewer } from '@/components/html-file-viewer';
|
|
||||||
import { ImageFileViewer } from '@/components/image-file-viewer';
|
import { ImageFileViewer } from '@/components/image-file-viewer';
|
||||||
import { VideoFileViewer } from '@/components/video-file-viewer';
|
import { VideoFileViewer } from '@/components/video-file-viewer';
|
||||||
import { PdfFileViewer } from '@/components/pdf-file-viewer';
|
|
||||||
import { AudioFileViewer } from '@/components/audio-file-viewer';
|
import { AudioFileViewer } from '@/components/audio-file-viewer';
|
||||||
|
import { PersistentViewerCache } from '@/components/persistent-viewer-cache';
|
||||||
import { useDebounce } from './hooks/use-debounce';
|
import { useDebounce } from './hooks/use-debounce';
|
||||||
import { SidebarContentPanel } from '@/components/sidebar-content';
|
import { SidebarContentPanel } from '@/components/sidebar-content';
|
||||||
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
|
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
|
||||||
|
|
@ -4722,6 +4721,15 @@ function App() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : selectedPath ? (
|
) : selectedPath ? (
|
||||||
|
<>
|
||||||
|
{/* Always-mounted persistent cache for HTML/PDF — hidden when active file is something else, so iframes preserve scroll/page/zoom across switches. */}
|
||||||
|
<div
|
||||||
|
className="flex-1 min-h-0 overflow-hidden"
|
||||||
|
style={{ display: /\.(html?|pdf)$/i.test(selectedPath) ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
<PersistentViewerCache activePath={selectedPath} />
|
||||||
|
</div>
|
||||||
|
{!/\.(html?|pdf)$/i.test(selectedPath) && (
|
||||||
selectedPath.endsWith('.md') ? (
|
selectedPath.endsWith('.md') ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-row overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-row overflow-hidden">
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
|
|
@ -4831,10 +4839,6 @@ function App() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : selectedPath?.toLowerCase().endsWith('.html') || selectedPath?.toLowerCase().endsWith('.htm') ? (
|
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
|
||||||
<HtmlFileViewer path={selectedPath} />
|
|
||||||
</div>
|
|
||||||
) : selectedPath && /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/i.test(selectedPath) ? (
|
) : selectedPath && /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/i.test(selectedPath) ? (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<ImageFileViewer path={selectedPath} />
|
<ImageFileViewer path={selectedPath} />
|
||||||
|
|
@ -4843,10 +4847,6 @@ function App() {
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<VideoFileViewer path={selectedPath} />
|
<VideoFileViewer path={selectedPath} />
|
||||||
</div>
|
</div>
|
||||||
) : selectedPath?.toLowerCase().endsWith('.pdf') ? (
|
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
|
||||||
<PdfFileViewer path={selectedPath} />
|
|
||||||
</div>
|
|
||||||
) : selectedPath && /\.(mp3|wav|m4a|ogg|flac|aac)$/i.test(selectedPath) ? (
|
) : selectedPath && /\.(mp3|wav|m4a|ogg|flac|aac)$/i.test(selectedPath) ? (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<AudioFileViewer path={selectedPath} />
|
<AudioFileViewer path={selectedPath} />
|
||||||
|
|
@ -4858,6 +4858,8 @@ function App() {
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : selectedTask ? (
|
) : selectedTask ? (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<BackgroundTaskDetail
|
<BackgroundTaskDetail
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { HtmlFileViewer } from './html-file-viewer'
|
||||||
|
import { PdfFileViewer } from './pdf-file-viewer'
|
||||||
|
|
||||||
|
const CACHE_LIMIT = 5
|
||||||
|
|
||||||
|
function isCacheable(path: string): boolean {
|
||||||
|
const lower = path.toLowerCase()
|
||||||
|
return lower.endsWith('.html') || lower.endsWith('.htm') || lower.endsWith('.pdf')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderViewer(path: string): JSX.Element | null {
|
||||||
|
const lower = path.toLowerCase()
|
||||||
|
if (lower.endsWith('.html') || lower.endsWith('.htm')) {
|
||||||
|
return <HtmlFileViewer path={path} />
|
||||||
|
}
|
||||||
|
if (lower.endsWith('.pdf')) {
|
||||||
|
return <PdfFileViewer path={path} />
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersistentViewerCacheProps {
|
||||||
|
activePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps recently-opened HTML and PDF viewers mounted in the DOM,
|
||||||
|
* toggling visibility instead of unmounting. This preserves iframe
|
||||||
|
* state (PDF page/zoom, HTML scroll/JS state) across file switches.
|
||||||
|
*/
|
||||||
|
export function PersistentViewerCache({ activePath }: PersistentViewerCacheProps) {
|
||||||
|
const [mountedPaths, setMountedPaths] = useState<string[]>(() =>
|
||||||
|
isCacheable(activePath) ? [activePath] : []
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCacheable(activePath)) return
|
||||||
|
setMountedPaths((prev) => {
|
||||||
|
if (prev.includes(activePath)) {
|
||||||
|
// Move to most-recent position
|
||||||
|
return [...prev.filter((p) => p !== activePath), activePath]
|
||||||
|
}
|
||||||
|
const next = [...prev, activePath]
|
||||||
|
return next.length > CACHE_LIMIT ? next.slice(-CACHE_LIMIT) : next
|
||||||
|
})
|
||||||
|
}, [activePath])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
{mountedPaths.map((p) => (
|
||||||
|
<div
|
||||||
|
key={p}
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{ display: p === activePath ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{renderViewer(p)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue