mirror of
https://github.com/open-webui/open-webui.git
synced 2026-04-28 19:49:28 -05:00
refac
This commit is contained in:
@@ -68,22 +68,14 @@ export const downloadFileBlob = async (
|
||||
apiKey: string,
|
||||
path: string
|
||||
): 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, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }
|
||||
}).catch(() => null);
|
||||
|
||||
if (!res || !res.ok) return null;
|
||||
|
||||
const contentType = res.headers.get('content-type') ?? '';
|
||||
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();
|
||||
return { blob, filename };
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import PDFViewer from '../common/PDFViewer.svelte';
|
||||
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -40,7 +41,7 @@
|
||||
let selectedFile: string | null = null;
|
||||
let fileContent: string | null = null;
|
||||
let fileImageUrl: string | null = null;
|
||||
let filePdfUrl: string | null = null;
|
||||
let filePdfData: ArrayBuffer | null = null;
|
||||
let fileLoading = false;
|
||||
|
||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
||||
@@ -88,10 +89,7 @@
|
||||
URL.revokeObjectURL(fileImageUrl);
|
||||
fileImageUrl = null;
|
||||
}
|
||||
if (filePdfUrl) {
|
||||
URL.revokeObjectURL(filePdfUrl);
|
||||
filePdfUrl = null;
|
||||
}
|
||||
filePdfData = null;
|
||||
};
|
||||
|
||||
const loadDir = async (path: string) => {
|
||||
@@ -132,10 +130,7 @@
|
||||
if (result) fileImageUrl = URL.createObjectURL(result.blob);
|
||||
} else if (isPdf(filePath)) {
|
||||
const result = await downloadFileBlob(terminalUrl, terminalKey, filePath);
|
||||
if (result)
|
||||
filePdfUrl = URL.createObjectURL(
|
||||
new Blob([await result.blob.arrayBuffer()], { type: 'application/pdf' })
|
||||
);
|
||||
if (result) filePdfData = await result.blob.arrayBuffer();
|
||||
} else {
|
||||
fileContent = await readFile(terminalUrl, terminalKey, filePath);
|
||||
}
|
||||
@@ -236,20 +231,29 @@
|
||||
};
|
||||
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('keyup', onKeyUp);
|
||||
window.addEventListener('blur', onBlur);
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
|
||||
if (filePdfUrl) URL.revokeObjectURL(filePdfUrl);
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -338,6 +342,26 @@
|
||||
</div>
|
||||
|
||||
{#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')}>
|
||||
<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"
|
||||
@@ -376,15 +400,15 @@
|
||||
</button>
|
||||
<!-- File preview -->
|
||||
{#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}
|
||||
<img
|
||||
src={fileImageUrl}
|
||||
alt={selectedFile?.split('/').pop()}
|
||||
class="w-full h-auto object-contain p-3"
|
||||
/>
|
||||
{:else if filePdfUrl !== null}
|
||||
<embed src={filePdfUrl} type="application/pdf" class="w-full h-full min-h-[400px]" />
|
||||
{:else if filePdfData !== null}
|
||||
<PDFViewer data={filePdfData} className="w-full h-full min-h-[400px]" />
|
||||
{:else if fileContent !== null}
|
||||
<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...')}
|
||||
</div>
|
||||
{: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}
|
||||
<div class="p-4 text-xs text-red-500 dark:text-red-400">{error}</div>
|
||||
{:else if entries.length === 0 && !creatingFolder}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import dayjs from 'dayjs';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import PDFViewer from './PDFViewer.svelte';
|
||||
|
||||
export let item;
|
||||
export let show = false;
|
||||
@@ -443,10 +444,9 @@
|
||||
playsinline
|
||||
/>
|
||||
{:else if isPDF}
|
||||
<iframe
|
||||
title={item?.name}
|
||||
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||
class="w-full h-[70vh] border-0 rounded-lg"
|
||||
<PDFViewer
|
||||
url={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||
className="w-full h-[70vh] border-0 rounded-lg"
|
||||
/>
|
||||
{:else if isExcel}
|
||||
{#if excelError}
|
||||
|
||||
117
src/lib/components/common/PDFViewer.svelte
Normal file
117
src/lib/components/common/PDFViewer.svelte
Normal file
@@ -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