feat: delta/instant upload + frontend UI/UX polish

Bundles the backend+frontend delta-upload (content-dedup) feature with a
batch of frontend fixes from this session.

Upload / dedup:
- Client-hashed delta & instant upload (deltaUpload, hashWasm vendor shim)
- Backend dedup batch endpoint (dedup_service, dedup_handler, routes)
- session store owned-hash helpers; unit tests + upload-strategy bench

Frontend UI/UX:
- Colour file-type icons in grid/list (per-type tinted tiles + glyph hue)
- Robust thumbnail fallback; PDFs now show their type icon (backend
  generates no PDF thumbnails) instead of a blank tile
- Fix PDF preview: load via a same-origin blob: iframe — the API URL is
  blocked by the global X-Frame-Options: DENY in the browser's framed
  PDF viewer, matching the existing CSP `frame-src blob:` design
- Groups: localized virtual-group description (no DB schema-note leak),
  add nav.groups to the 15 missing locales, fix primary-button contrast
- Repoint --color-text-light → --color-on-accent (was faint grey on accent)
- Nudge the admin role badge off the user-menu header divider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-20 16:33:08 +02:00
parent f8490ad96e
commit d98e3117b2
35 changed files with 901 additions and 101 deletions
+35 -30
View File
@@ -40,9 +40,14 @@
let canEdit = $state(false);
/** Image zoom factor (1 = fit). */
let zoom = $state(1);
/** PDF embed fallback engaged when the <object> stays blank ~2s. */
let pdfFallback = $state(false);
let pdfObjectEl = $state<HTMLObjectElement | null>(null);
/** Object URL for the fetched PDF blob. The PDF is rendered from a same-origin
* blob: in an <iframe> (allowed by CSP `frame-src blob:`) rather than from its
* API URL directly — that response carries the global `X-Frame-Options: DENY`
* + `frame-ancestors 'none'`, which the browser's framed PDF viewer honours,
* so an embedded API URL renders as a broken-document icon. A blob has no
* response headers, so it sidesteps the framing block. */
let pdfUrl = $state<string | null>(null);
let pdfError = $state(false);
function isImage(f: FileItem): boolean {
const m = (f.mime_type ?? '').toLowerCase();
@@ -72,7 +77,7 @@
open = false;
textContent = '';
zoom = 1;
pdfFallback = false;
pdfError = false;
onrefresh?.();
}
@@ -94,7 +99,6 @@
const f = file;
canEdit = false;
zoom = 1;
pdfFallback = false;
const k = kindOf(f);
// Office docs (WOPI-editable, non-image) open straight in the editor
@@ -119,21 +123,25 @@
.finally(() => (textLoading = false));
}
// PDF blank-render guard: if the <object> shows nothing after ~2s,
// fall back to an <embed> (some browsers refuse <object> for PDFs).
// Fetch the PDF as a same-origin blob and view it via an object URL (see
// `pdfUrl`). The cleanup revokes the URL when the file changes or the
// viewer closes; a stale response (newer file opened mid-fetch) is dropped.
if (k === 'pdf') {
const timer = setTimeout(() => {
const el = pdfObjectEl;
let blank = false;
try {
const doc = el?.contentDocument;
blank = !doc || doc.body?.innerHTML === '';
} catch {
blank = false; // cross-origin: assume it rendered
}
if (blank) pdfFallback = true;
}, 2000);
return () => clearTimeout(timer);
pdfError = false;
pdfUrl = null;
let objectUrl: string | null = null;
apiFetch(fileInlineUrl(f.id), { credentials: 'same-origin' })
.then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((blob) => {
if (file !== f || !open) return;
objectUrl = URL.createObjectURL(blob);
pdfUrl = objectUrl;
})
.catch(() => (pdfError = true));
return () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
pdfUrl = null;
};
}
});
</script>
@@ -220,18 +228,15 @@
{:else if kind === 'audio'}
<audio class="fv__audio" src={fileInlineUrl(file.id)} controls></audio>
{:else if kind === 'pdf'}
{#if pdfFallback}
<embed class="fv__pdf" src={fileInlineUrl(file.id)} type="application/pdf" />
{:else}
<object
bind:this={pdfObjectEl}
class="fv__pdf"
data={fileInlineUrl(file.id)}
type="application/pdf"
title={file.name}
>
{#if pdfError}
<div class="fv__status fv__status--center">
<Icon name="file" class="fv__big-icon" />
<p>{t('files.preview_failed', 'Could not load preview.')}</p>
</object>
</div>
{:else if pdfUrl}
<iframe class="fv__pdf" src={pdfUrl} title={file.name}></iframe>
{:else}
<p class="fv__status">{t('common.loading', 'Loading…')}</p>
{/if}
{:else if kind === 'text'}
{#if textLoading}