refac
This commit is contained in:
@@ -68,22 +68,14 @@ export const downloadFileBlob = async (
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
path: string
|
path: string
|
||||||
): Promise<{ blob: Blob; filename: string } | null> => {
|
): Promise<{ blob: Blob; filename: string } | null> => {
|
||||||
const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`;
|
const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: { Authorization: `Bearer ${apiKey}` }
|
headers: { Authorization: `Bearer ${apiKey}` }
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
if (!res || !res.ok) return null;
|
if (!res || !res.ok) return null;
|
||||||
|
|
||||||
const contentType = res.headers.get('content-type') ?? '';
|
|
||||||
const filename = path.split('/').pop() ?? 'file';
|
const filename = path.split('/').pop() ?? 'file';
|
||||||
|
|
||||||
if (contentType.includes('application/json')) {
|
|
||||||
const json = await res.json().catch(() => null);
|
|
||||||
const blob = new Blob([json?.content ?? ''], { type: 'text/plain' });
|
|
||||||
return { blob, filename };
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
return { blob, filename };
|
return { blob, filename };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
import PDFViewer from '../common/PDFViewer.svelte';
|
||||||
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
let selectedFile: string | null = null;
|
let selectedFile: string | null = null;
|
||||||
let fileContent: string | null = null;
|
let fileContent: string | null = null;
|
||||||
let fileImageUrl: string | null = null;
|
let fileImageUrl: string | null = null;
|
||||||
let filePdfUrl: string | null = null;
|
let filePdfData: ArrayBuffer | null = null;
|
||||||
let fileLoading = false;
|
let fileLoading = false;
|
||||||
|
|
||||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
||||||
@@ -88,10 +89,7 @@
|
|||||||
URL.revokeObjectURL(fileImageUrl);
|
URL.revokeObjectURL(fileImageUrl);
|
||||||
fileImageUrl = null;
|
fileImageUrl = null;
|
||||||
}
|
}
|
||||||
if (filePdfUrl) {
|
filePdfData = null;
|
||||||
URL.revokeObjectURL(filePdfUrl);
|
|
||||||
filePdfUrl = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadDir = async (path: string) => {
|
const loadDir = async (path: string) => {
|
||||||
@@ -132,10 +130,7 @@
|
|||||||
if (result) fileImageUrl = URL.createObjectURL(result.blob);
|
if (result) fileImageUrl = URL.createObjectURL(result.blob);
|
||||||
} else if (isPdf(filePath)) {
|
} else if (isPdf(filePath)) {
|
||||||
const result = await downloadFileBlob(terminalUrl, terminalKey, filePath);
|
const result = await downloadFileBlob(terminalUrl, terminalKey, filePath);
|
||||||
if (result)
|
if (result) filePdfData = await result.blob.arrayBuffer();
|
||||||
filePdfUrl = URL.createObjectURL(
|
|
||||||
new Blob([await result.blob.arrayBuffer()], { type: 'application/pdf' })
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
fileContent = await readFile(terminalUrl, terminalKey, filePath);
|
fileContent = await readFile(terminalUrl, terminalKey, filePath);
|
||||||
}
|
}
|
||||||
@@ -236,20 +231,29 @@
|
|||||||
};
|
};
|
||||||
const onBlur = () => (shiftKey = false);
|
const onBlur = () => (shiftKey = false);
|
||||||
|
|
||||||
|
// Auto-reload directory when the browser tab regains focus
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible' && !selectedFile && configured && !loading) {
|
||||||
|
loadDir(currentPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
window.addEventListener('blur', onBlur);
|
window.addEventListener('blur', onBlur);
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur', onBlur);
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
||||||
if (filePdfUrl) URL.revokeObjectURL(filePdfUrl);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -338,6 +342,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !selectedFile}
|
{#if !selectedFile}
|
||||||
|
<Tooltip content={$i18n.t('Refresh')}>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||||
|
on:click={() => loadDir(currentPath)}
|
||||||
|
aria-label={$i18n.t('Refresh')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3.5 {loading ? 'animate-spin' : ''}"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip content={$i18n.t('New Folder')}>
|
<Tooltip content={$i18n.t('New Folder')}>
|
||||||
<button
|
<button
|
||||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||||
@@ -376,15 +400,15 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- File preview -->
|
<!-- File preview -->
|
||||||
{#if fileLoading}
|
{#if fileLoading}
|
||||||
<div class="flex justify-center pt-8"><Spinner className="size-5" /></div>
|
<div class="flex justify-center pt-8"><Spinner className="size-4" /></div>
|
||||||
{:else if fileImageUrl !== null}
|
{:else if fileImageUrl !== null}
|
||||||
<img
|
<img
|
||||||
src={fileImageUrl}
|
src={fileImageUrl}
|
||||||
alt={selectedFile?.split('/').pop()}
|
alt={selectedFile?.split('/').pop()}
|
||||||
class="w-full h-auto object-contain p-3"
|
class="w-full h-auto object-contain p-3"
|
||||||
/>
|
/>
|
||||||
{:else if filePdfUrl !== null}
|
{:else if filePdfData !== null}
|
||||||
<embed src={filePdfUrl} type="application/pdf" class="w-full h-full min-h-[400px]" />
|
<PDFViewer data={filePdfData} className="w-full h-full min-h-[400px]" />
|
||||||
{:else if fileContent !== null}
|
{:else if fileContent !== null}
|
||||||
<pre
|
<pre
|
||||||
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed p-3">{fileContent}</pre>
|
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed p-3">{fileContent}</pre>
|
||||||
@@ -401,7 +425,7 @@
|
|||||||
{$i18n.t('Uploading...')}
|
{$i18n.t('Uploading...')}
|
||||||
</div>
|
</div>
|
||||||
{:else if loading}
|
{:else if loading}
|
||||||
<div class="flex justify-center pt-8"><Spinner className="size-5" /></div>
|
<div class="flex justify-center pt-8"><Spinner className="size-4" /></div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="p-4 text-xs text-red-500 dark:text-red-400">{error}</div>
|
<div class="p-4 text-xs text-red-500 dark:text-red-400">{error}</div>
|
||||||
{:else if entries.length === 0 && !creatingFolder}
|
{:else if entries.length === 0 && !creatingFolder}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
import Tooltip from './Tooltip.svelte';
|
import Tooltip from './Tooltip.svelte';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
|
import PDFViewer from './PDFViewer.svelte';
|
||||||
|
|
||||||
export let item;
|
export let item;
|
||||||
export let show = false;
|
export let show = false;
|
||||||
@@ -443,10 +444,9 @@
|
|||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
{:else if isPDF}
|
{:else if isPDF}
|
||||||
<iframe
|
<PDFViewer
|
||||||
title={item?.name}
|
url={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||||
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
className="w-full h-[70vh] border-0 rounded-lg"
|
||||||
class="w-full h-[70vh] border-0 rounded-lg"
|
|
||||||
/>
|
/>
|
||||||
{:else if isExcel}
|
{:else if isExcel}
|
||||||
{#if excelError}
|
{#if excelError}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
|
export let url: string | null = null;
|
||||||
|
export let data: ArrayBuffer | Uint8Array | null = null;
|
||||||
|
export let className = 'w-full h-[70vh]';
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let loading = true;
|
||||||
|
let error = '';
|
||||||
|
let pdfDoc: any = null;
|
||||||
|
|
||||||
|
const renderAllPages = async () => {
|
||||||
|
if (!pdfDoc || !container) return;
|
||||||
|
|
||||||
|
// Clear previous canvases
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||||
|
const page = await pdfDoc.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
|
|
||||||
|
// Scale to fit container width
|
||||||
|
const containerWidth = container.clientWidth || 800;
|
||||||
|
const scale = containerWidth / viewport.width;
|
||||||
|
const scaledViewport = page.getViewport({ scale });
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = scaledViewport.width;
|
||||||
|
canvas.height = scaledViewport.height;
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = 'auto';
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
|
||||||
|
if (i > 1) {
|
||||||
|
canvas.style.marginTop = '4px';
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(canvas);
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
await page.render({
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: scaledViewport
|
||||||
|
}).promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPdf = async () => {
|
||||||
|
if (!url && !data) return;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfjs = await import('pdfjs-dist');
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl;
|
||||||
|
|
||||||
|
let pdfData: ArrayBuffer | Uint8Array;
|
||||||
|
if (data) {
|
||||||
|
pdfData = data;
|
||||||
|
} else {
|
||||||
|
// Fetch with credentials so auth cookies are sent
|
||||||
|
const res = await fetch(url!, { credentials: 'include' });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
pdfData = await res.arrayBuffer();
|
||||||
|
}
|
||||||
|
pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;
|
||||||
|
await renderAllPages();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('PDF render error:', e);
|
||||||
|
error = 'Failed to load PDF.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-render on resize
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadPdf();
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (pdfDoc && !loading) {
|
||||||
|
renderAllPages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (container) {
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
if (pdfDoc) {
|
||||||
|
pdfDoc.destroy();
|
||||||
|
pdfDoc = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overflow-auto {className}" bind:this={container}>
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="flex items-center justify-center h-full text-sm text-red-500">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user