This commit is contained in:
Timothy Jaeryang Baek committed 2026-08-13 15:33:42 -06:00
1 parent 2befa8f796
commit bfb68feea7
4 files changed
+184 -79

No files matched your search

+47 -27
View File
@@ -116,6 +116,35 @@
let loadingDirs: Set<string> = new Set(); let loadingDirs: Set<string> = new Set();
let directoryMenu: { x: number; y: number } | null = null; let directoryMenu: { x: number; y: number } | null = null;
const invalidateVisibleRows = () => {
entries = [...entries];
};
/** Normalize Windows backslashes and collapse duplicate separators. */
const normalizePath = (path: string) => path.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
const cleanEntryName = (name: string) =>
normalizePath(name)
.split('/')
.filter(Boolean)
.at(-1) ?? name.replace(/\/+$/, '');
const normalizeEntries = (items: FileEntry[]) =>
items.map((entry) => ({ ...entry, name: cleanEntryName(entry.name) }));
const asDirectoryPath = (path: string) => {
const normalized = normalizePath(path || '/');
if (normalized === '/') return '/';
return normalized.endsWith('/') ? normalized : `${normalized}/`;
};
const joinPath = (parentPath: string, name: string) => {
const parent = asDirectoryPath(parentPath);
const child = cleanEntryName(name);
if (!child) return parent;
return normalizePath(`${parent}${child}`);
};
const sortEntries = (items: FileEntry[]): FileEntry[] => { const sortEntries = (items: FileEntry[]): FileEntry[] => {
return [...items].sort((a, b) => { return [...items].sort((a, b) => {
// Directories always first // Directories always first
@@ -146,15 +175,16 @@
sortBy = mode; sortBy = mode;
sortAsc = true; sortAsc = true;
} }
entries = [...entries]; invalidateVisibleRows();
treeCache = new Map(treeCache); treeCache = new Map(treeCache);
void refreshBrowser();
}; };
const filterEntries = (items: FileEntry[]) => const filterEntries = (items: FileEntry[]) =>
showHidden ? items : items.filter((entry) => !entry.name.startsWith('.')); showHidden ? items : items.filter((entry) => !entry.name.startsWith('.'));
const entryPath = (parentPath: string, entry: FileEntry) => const entryPath = (parentPath: string, entry: FileEntry) =>
entry.type === 'directory' ? `${parentPath}${entry.name}/` : `${parentPath}${entry.name}`; entry.type === 'directory' ? asDirectoryPath(joinPath(parentPath, entry.name)) : joinPath(parentPath, entry.name);
const withRowIndexes = (rows: Omit<BrowserRow, 'rowIndex'>[]): BrowserRow[] => const withRowIndexes = (rows: Omit<BrowserRow, 'rowIndex'>[]): BrowserRow[] =>
rows.map((row, rowIndex) => ({ ...row, rowIndex })); rows.map((row, rowIndex) => ({ ...row, rowIndex }));
@@ -369,14 +399,6 @@
const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf'; const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf';
const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? ''); const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
/** Normalize Windows backslashes to forward slashes. */
const normalizePath = (p: string) => p.replace(/\\/g, '/');
const asDirectoryPath = (path: string) => {
const normalized = normalizePath(path || '/');
return normalized.endsWith('/') ? normalized : `${normalized}/`;
};
const resetTreeState = () => { const resetTreeState = () => {
expandedDirs = new Set(); expandedDirs = new Set();
treeCache = new Map(); treeCache = new Map();
@@ -450,7 +472,8 @@
homePath && cwdPath && (cwdPath === homePath || cwdPath.startsWith(homePath)) homePath && cwdPath && (cwdPath === homePath || cwdPath.startsWith(homePath))
? { path: homePath, label: 'Home' } ? { path: homePath, label: 'Home' }
: undefined; : undefined;
setFileRoot(cwd?.root ?? homeRoot); const rootPath = cwd?.root?.path ? asDirectoryPath(cwd.root.path) : null;
setFileRoot(rootPath && rootPath !== '/' ? cwd?.root : (homeRoot ?? cwd?.root));
const path = cwdPath ?? fileRoot?.path ?? '/'; const path = cwdPath ?? fileRoot?.path ?? '/';
return clampToFileRoot(path); return clampToFileRoot(path);
}; };
@@ -463,7 +486,7 @@
return parts.reduce( return parts.reduce(
(acc, part) => { (acc, part) => {
const prev = acc[acc.length - 1]; const prev = acc[acc.length - 1];
acc.push({ label: part, path: `${prev.path}${part}/` }); acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) });
return acc; return acc;
}, },
[{ label: fileRoot.label, path: fileRoot.path }] [{ label: fileRoot.label, path: fileRoot.path }]
@@ -474,11 +497,11 @@
const isDrive = /^[A-Za-z]:$/.test(parts[0] ?? ''); const isDrive = /^[A-Za-z]:$/.test(parts[0] ?? '');
const root = isDrive ? { label: parts[0], path: `${parts[0]}/` } : { label: '/', path: '/' }; const root = isDrive ? { label: parts[0], path: `${parts[0]}/` } : { label: '/', path: '/' };
return (isDrive ? parts.slice(1) : parts).reduce( return (isDrive ? parts.slice(1) : parts).reduce(
(acc, part) => { (acc, part) => {
const prev = acc[acc.length - 1]; const prev = acc[acc.length - 1];
acc.push({ label: part, path: `${prev.path}${part}/` }); acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) });
return acc; return acc;
}, },
[root] [root]
); );
}; };
@@ -543,8 +566,8 @@
entries = []; entries = [];
} else { } else {
currentWritable = result.writable !== false; currentWritable = result.writable !== false;
entries = result.entries; entries = normalizeEntries(result.entries);
treeCache = new Map(treeCache).set(directory, result.entries); treeCache = new Map(treeCache).set(directory, entries);
saveTreeState(); saveTreeState();
} }
}; };
@@ -559,9 +582,10 @@
nextLoading.delete(directory); nextLoading.delete(directory);
loadingDirs = nextLoading; loadingDirs = nextLoading;
if (result === null) return null; if (result === null) return null;
treeCache = new Map(treeCache).set(directory, result.entries); const normalizedEntries = normalizeEntries(result.entries);
treeCache = new Map(treeCache).set(directory, normalizedEntries);
saveTreeState(); saveTreeState();
return result.entries; return normalizedEntries;
}; };
const refreshExpandedDirs = async () => { const refreshExpandedDirs = async () => {
@@ -582,16 +606,12 @@
next.delete(directory); next.delete(directory);
expandedDirs = next; expandedDirs = next;
saveTreeState(); saveTreeState();
invalidateVisibleRows();
return; return;
} }
expandedDirs = new Set(expandedDirs).add(directory); expandedDirs = new Set(expandedDirs).add(directory);
saveTreeState(); saveTreeState();
if (treeCache.has(directory)) {
entries = [...entries];
treeCache = new Map(treeCache);
return;
}
const result = await fetchExpandedDir(directory); const result = await fetchExpandedDir(directory);
if (result === null) { if (result === null) {
@@ -601,7 +621,7 @@
saveTreeState(); saveTreeState();
toast.error($i18n.t('Failed to load folder')); toast.error($i18n.t('Failed to load folder'));
} else { } else {
entries = [...entries]; invalidateVisibleRows();
treeCache = new Map(treeCache); treeCache = new Map(treeCache);
} }
}; };
@@ -35,6 +35,8 @@
export let onGoForward: () => void = () => {}; export let onGoForward: () => void = () => {};
let dragOverCrumb: number | null = null; let dragOverCrumb: number | null = null;
let sortMenuOpen = false;
let actionsMenuOpen = false;
let uploadInput: HTMLInputElement; let uploadInput: HTMLInputElement;
let breadcrumbEl: HTMLDivElement; let breadcrumbEl: HTMLDivElement;
@@ -49,33 +51,35 @@
</script> </script>
<div class="m-0 flex items-center gap-1 px-1 pt-0 pb-1.5 shrink-0 border-b border-gray-50 dark:border-gray-850/30"> <div class="m-0 flex items-center gap-1 px-1 pt-0 pb-1.5 shrink-0 border-b border-gray-50 dark:border-gray-850/30">
<!-- Back --> <div class="flex shrink-0 items-center gap-0.5 px-1">
<Tooltip content={$i18n.t('Back')}> <!-- Back -->
<button <Tooltip content={$i18n.t('Back')}>
class="shrink-0 flex h-5 min-w-5 items-center justify-center rounded px-1 transition-colors duration-100 {canGoBack <button
? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300' class="shrink-0 flex h-5 min-w-6 items-center justify-center rounded px-1.5 transition-colors duration-100 {canGoBack
: 'text-gray-200 dark:text-gray-700 cursor-default'}" ? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
on:click={onGoBack} : 'text-gray-200 dark:text-gray-700 cursor-default'}"
disabled={!canGoBack} on:click={onGoBack}
aria-label={$i18n.t('Back')} disabled={!canGoBack}
> aria-label={$i18n.t('Back')}
<Icon name="chevron-left" size={11} strokeWidth={1.5} /> >
</button> <Icon name="chevron-left" size={11} strokeWidth={1.5} />
</Tooltip> </button>
</Tooltip>
<!-- Forward --> <!-- Forward -->
<Tooltip content={$i18n.t('Forward')}> <Tooltip content={$i18n.t('Forward')}>
<button <button
class="shrink-0 flex h-5 min-w-5 items-center justify-center rounded px-1 transition-colors duration-100 {canGoForward class="shrink-0 flex h-5 min-w-6 items-center justify-center rounded px-1.5 transition-colors duration-100 {canGoForward
? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300' ? 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'
: 'text-gray-200 dark:text-gray-700 cursor-default'}" : 'text-gray-200 dark:text-gray-700 cursor-default'}"
on:click={onGoForward} on:click={onGoForward}
disabled={!canGoForward} disabled={!canGoForward}
aria-label={$i18n.t('Forward')} aria-label={$i18n.t('Forward')}
> >
<Icon name="chevron-right" size={11} strokeWidth={1.5} /> <Icon name="chevron-right" size={11} strokeWidth={1.5} />
</button> </button>
</Tooltip> </Tooltip>
</div>
<div <div
bind:this={breadcrumbEl} bind:this={breadcrumbEl}
@@ -143,7 +147,7 @@
</Tooltip> </Tooltip>
{#if !selectedFile} {#if !selectedFile}
<Dropdown align="end" sideOffset={4}> <Dropdown bind:show={sortMenuOpen} align="end" sideOffset={4}>
<Tooltip content={$i18n.t('Sort')}> <Tooltip content={$i18n.t('Sort')}>
<button <button
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300" class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
@@ -158,7 +162,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
on:click={() => onSort('name')} on:click={() => {
onSort('name');
sortMenuOpen = false;
}}
> >
<span class="flex-1 text-left">{$i18n.t('Name')}</span> <span class="flex-1 text-left">{$i18n.t('Name')}</span>
{#if sortBy === 'name'} {#if sortBy === 'name'}
@@ -175,7 +182,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
on:click={() => onSort('size')} on:click={() => {
onSort('size');
sortMenuOpen = false;
}}
> >
<span class="flex-1 text-left">{$i18n.t('Size')}</span> <span class="flex-1 text-left">{$i18n.t('Size')}</span>
{#if sortBy === 'size'} {#if sortBy === 'size'}
@@ -192,7 +202,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
on:click={() => onSort('date')} on:click={() => {
onSort('date');
sortMenuOpen = false;
}}
> >
<span class="flex-1 text-left">{$i18n.t('Date Modified')}</span> <span class="flex-1 text-left">{$i18n.t('Date Modified')}</span>
{#if sortBy === 'date'} {#if sortBy === 'date'}
@@ -209,7 +222,7 @@
</DropdownMenu> </DropdownMenu>
</div> </div>
</Dropdown> </Dropdown>
<Dropdown align="end" sideOffset={4}> <Dropdown bind:show={actionsMenuOpen} align="end" sideOffset={4}>
<Tooltip content={$i18n.t('Actions')}> <Tooltip content={$i18n.t('Actions')}>
<button <button
class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300" class="shrink-0 flex h-5 w-5 items-center justify-center rounded transition-colors duration-100 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
@@ -224,7 +237,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent"
on:click={onNewFolder} on:click={() => {
onNewFolder();
actionsMenuOpen = false;
}}
disabled={!writable} disabled={!writable}
> >
<Icon name="folder" size={12} strokeWidth={1.4} /> <Icon name="folder" size={12} strokeWidth={1.4} />
@@ -233,7 +249,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent"
on:click={onNewFile} on:click={() => {
onNewFile();
actionsMenuOpen = false;
}}
disabled={!writable} disabled={!writable}
> >
<Icon name="empty-page" size={12} strokeWidth={1.4} /> <Icon name="empty-page" size={12} strokeWidth={1.4} />
@@ -242,7 +261,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition disabled:opacity-40 disabled:hover:bg-transparent"
on:click={() => uploadInput?.click()} on:click={() => {
actionsMenuOpen = false;
uploadInput?.click();
}}
disabled={!writable} disabled={!writable}
> >
<Icon name="upload" size={12} strokeWidth={1.4} /> <Icon name="upload" size={12} strokeWidth={1.4} />
@@ -251,7 +273,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
on:click={onDownloadDir} on:click={() => {
onDownloadDir();
actionsMenuOpen = false;
}}
> >
<Icon name="download" size={12} strokeWidth={1.4} /> <Icon name="download" size={12} strokeWidth={1.4} />
<span>{$i18n.t('Download')}</span> <span>{$i18n.t('Download')}</span>
@@ -259,7 +284,10 @@
<button <button
type="button" type="button"
class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition" class="select-none flex h-7 w-full items-center gap-2 rounded-lg px-2 text-xs hover:bg-gray-50/40 dark:hover:bg-white/4 transition"
on:click={onToggleHidden} on:click={() => {
onToggleHidden();
actionsMenuOpen = false;
}}
> >
<Icon name="eye" size={12} strokeWidth={1.4} /> <Icon name="eye" size={12} strokeWidth={1.4} />
<span>{showHidden ? $i18n.t('Hide Hidden Files') : $i18n.t('Show Hidden Files')}</span> <span>{showHidden ? $i18n.t('Hide Hidden Files') : $i18n.t('Show Hidden Files')}</span>
@@ -9,14 +9,12 @@
import Spinner from '../../common/Spinner.svelte'; import Spinner from '../../common/Spinner.svelte';
import PDFViewer from '../../common/PDFViewer.svelte'; import PDFViewer from '../../common/PDFViewer.svelte';
import PanzoomContainer from '../../common/PanzoomContainer.svelte'; import PanzoomContainer from '../../common/PanzoomContainer.svelte';
import Tooltip from '../../common/Tooltip.svelte';
import DocxPreview from '../../common/DocxPreview.svelte'; import DocxPreview from '../../common/DocxPreview.svelte';
import PptxPreview from '../../common/PptxPreview.svelte'; import PptxPreview from '../../common/PptxPreview.svelte';
import JsonTreeView from './JsonTreeView.svelte'; import JsonTreeView from './JsonTreeView.svelte';
import NotebookView from './NotebookView.svelte'; import NotebookView from './NotebookView.svelte';
import SqliteView from './SqliteView.svelte'; import SqliteView from './SqliteView.svelte';
import FileCodeEditor from './FileCodeEditor.svelte'; import FileCodeEditor from './FileCodeEditor.svelte';
import Icon from './Icon.svelte';
let pdfViewerRef: PDFViewer; let pdfViewerRef: PDFViewer;
let fileCodeEditorRef: FileCodeEditor; let fileCodeEditorRef: FileCodeEditor;
@@ -267,6 +265,7 @@
let panzoomRef: PanzoomContainer; let panzoomRef: PanzoomContainer;
let pptxPreviewRef: PptxPreview; let pptxPreviewRef: PptxPreview;
let imageZoomLevel = 1;
export const resetImageView = () => { export const resetImageView = () => {
panzoomRef?.reset(); panzoomRef?.reset();
pptxPreviewRef?.resetView(); pptxPreviewRef?.resetView();
@@ -275,10 +274,6 @@
export const resetPdfView = () => { export const resetPdfView = () => {
pdfViewerRef?.resetView(); pdfViewerRef?.resetView();
}; };
const resetFloatingView = () => {
if (fileImageUrl !== null) panzoomRef?.reset();
};
</script> </script>
<div <div
@@ -294,6 +289,7 @@
{:else if fileImageUrl !== null} {:else if fileImageUrl !== null}
<PanzoomContainer <PanzoomContainer
bind:this={panzoomRef} bind:this={panzoomRef}
bind:zoomLevel={imageZoomLevel}
className="w-full h-full flex items-center justify-center" className="w-full h-full flex items-center justify-center"
options={{ zoomDoubleClickSpeed: 1 }} options={{ zoomDoubleClickSpeed: 1 }}
> >
@@ -480,18 +476,52 @@
{#if !fileLoading && fileImageUrl !== null} {#if !fileLoading && fileImageUrl !== null}
<div <div
class="absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 items-center rounded-lg border border-gray-200/60 bg-white/90 px-1 py-0.5 shadow-lg backdrop-blur-sm dark:border-gray-700/60 dark:bg-gray-850/90" class="absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 items-center gap-0.5 rounded-lg border border-gray-200/60 bg-white/90 px-1 py-0.5 shadow-lg backdrop-blur-sm dark:border-gray-700/60 dark:bg-gray-850/90"
> >
<Tooltip content={$i18n.t('Reset view')}> <button
<button type="button"
type="button" class="inline-flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md p-1.5 text-gray-500 transition hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
class="inline-flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md p-1.5 text-gray-500 transition hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800" on:click={() => panzoomRef?.zoomOut()}
on:click={resetFloatingView} aria-label={$i18n.t('Zoom out')}
aria-label={$i18n.t('Reset view')} >
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
> >
<Icon name="refresh" size={13} strokeWidth={1.4} /> <path
</button> fill-rule="evenodd"
</Tooltip> d="M4 10a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H4.75A.75.75 0 0 1 4 10Z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
type="button"
class="h-7 min-w-12 shrink-0 rounded-md px-1.5 py-1 text-center text-[0.6875rem] font-normal tabular-nums text-gray-500 transition hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
on:click={() => panzoomRef?.reset()}
aria-label={$i18n.t('Reset zoom')}
>
{Math.round(imageZoomLevel * 100)}%
</button>
<button
type="button"
class="inline-flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md p-1.5 text-gray-500 transition hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
on:click={() => panzoomRef?.zoomIn()}
aria-label={$i18n.t('Zoom in')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
>
<path
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
/>
</svg>
</button>
</div> </div>
{/if} {/if}
</div> </div>
@@ -10,18 +10,45 @@
export let className = ''; export let className = '';
export let options: Partial<PanZoomOptions> = {}; export let options: Partial<PanZoomOptions> = {};
export let zoomLevel = 1;
let containerElement: HTMLElement; let containerElement: HTMLElement;
let instance: PanZoom | undefined; let instance: PanZoom | undefined;
const updateZoomLevel = () => {
zoomLevel = instance?.getTransform().scale ?? 1;
};
const center = () => ({
x: containerElement?.clientWidth ? containerElement.clientWidth / 2 : 0,
y: containerElement?.clientHeight ? containerElement.clientHeight / 2 : 0
});
export const zoomIn = () => {
if (!instance) return;
const { x, y } = center();
instance.zoomTo(x, y, 1.25);
updateZoomLevel();
};
export const zoomOut = () => {
if (!instance) return;
const { x, y } = center();
instance.zoomTo(x, y, 0.8);
updateZoomLevel();
};
export const reset = () => { export const reset = () => {
instance?.moveTo(0, 0); instance?.moveTo(0, 0);
instance?.zoomAbs(0, 0, 1); instance?.zoomAbs(0, 0, 1);
updateZoomLevel();
}; };
onMount(() => { onMount(() => {
const localInstance = panzoom(containerElement, { ...defaultOpts, ...options }); const localInstance = panzoom(containerElement, { ...defaultOpts, ...options });
instance = localInstance; instance = localInstance;
updateZoomLevel();
localInstance.on('zoom', updateZoomLevel);
return () => { return () => {
localInstance.dispose(); localInstance.dispose();
}; };