Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 2m22s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
CI / E2E Tests (pull_request) Failing after 31m23s
CI / Unit & Component Tests (push) Successful in 2m24s
CI / Backend Unit Tests (push) Successful in 2m6s
CI / E2E Tests (push) Failing after 30m19s
Eliminates type duplication across 6 files by introducing a single shared types module: - Comment + CommentReply: were identically defined in CommentThread, PanelDiscussion, and DocumentBottomPanel - DocumentPanelTab: was identically defined in DocumentBottomPanel and documents/[id]/+page.svelte - Annotation: was defined in both AnnotationLayer and PdfViewer (PdfViewer's variant with fileHash? is now the canonical definition) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
5.0 KiB
Svelte
172 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
|
|
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
|
|
import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte';
|
|
import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte';
|
|
import type { DocumentPanelTab } from '$lib/types';
|
|
|
|
let { data } = $props();
|
|
|
|
const doc = $derived(data.document);
|
|
const canComment = $derived((data.canAnnotate || data.canWrite) ?? false);
|
|
const canAdmin = $derived(
|
|
(data.user?.groups as Array<{ permissions: string[] }> | undefined)?.some((g) =>
|
|
g.permissions.includes('ADMIN')
|
|
) ?? false
|
|
);
|
|
const currentUserId = $derived((data.user?.id as string | undefined) ?? null);
|
|
|
|
// ── File loading ──────────────────────────────────────────────────────────────
|
|
|
|
let fileUrl = $state('');
|
|
let isLoading = $state(false);
|
|
let fileError = $state('');
|
|
|
|
$effect(() => {
|
|
if (doc?.id && doc?.filePath) {
|
|
loadFile(doc.id);
|
|
}
|
|
});
|
|
|
|
async function loadFile(id: string) {
|
|
isLoading = true;
|
|
fileError = '';
|
|
fileUrl = '';
|
|
|
|
try {
|
|
const response = await fetch(`/api/documents/${id}/file`);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) throw new Error('Nicht eingeloggt');
|
|
throw new Error('Fehler beim Laden der Datei');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
fileUrl = URL.createObjectURL(blob);
|
|
} catch (e) {
|
|
console.error(e);
|
|
fileError = 'Vorschau konnte nicht geladen werden.';
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
// ── Annotation state (lifted from PdfViewer) ──────────────────────────────────
|
|
|
|
let annotateMode = $state(false);
|
|
let activeAnnotationId = $state<string | null>(null);
|
|
let activeAnnotationPage = $state<number | null>(null);
|
|
|
|
// Close the panel when entering annotate mode so the PDF is fully visible.
|
|
$effect(() => {
|
|
if (annotateMode) panelOpen = false;
|
|
});
|
|
|
|
// ── Bottom panel state ────────────────────────────────────────────────────────
|
|
|
|
const LS_KEY_HEIGHT = 'doc-panel-height';
|
|
const LS_KEY_TAB = 'doc-panel-tab';
|
|
|
|
let panelOpen = $state(false);
|
|
let panelHeight = $state(0); // set to full height on mount
|
|
let navHeight = $state(0);
|
|
let activeTab = $state<DocumentPanelTab>('metadata');
|
|
let localStorageRestored = $state(false);
|
|
|
|
onMount(() => {
|
|
navHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
|
|
const savedHeight = localStorage.getItem(LS_KEY_HEIGHT);
|
|
const savedTab = localStorage.getItem(LS_KEY_TAB);
|
|
|
|
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
|
|
activeTab = savedTab as DocumentPanelTab;
|
|
}
|
|
const topbar = document.querySelector('[data-topbar]');
|
|
panelHeight = window.innerHeight - navHeight - (topbar?.getBoundingClientRect().height ?? 0);
|
|
if (savedHeight) {
|
|
const h = parseInt(savedHeight, 10);
|
|
if (!isNaN(h) && h >= 80) panelHeight = h;
|
|
}
|
|
|
|
localStorageRestored = true;
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') {
|
|
if (activeAnnotationId) {
|
|
activeAnnotationId = null;
|
|
activeAnnotationPage = null;
|
|
} else if (panelOpen) {
|
|
panelOpen = false;
|
|
}
|
|
}
|
|
}
|
|
document.addEventListener('keydown', onKeyDown);
|
|
return () => document.removeEventListener('keydown', onKeyDown);
|
|
});
|
|
|
|
// Persist panel state whenever it changes (after initial restore).
|
|
$effect(() => {
|
|
if (!localStorageRestored) return;
|
|
localStorage.setItem(LS_KEY_HEIGHT, String(panelHeight));
|
|
localStorage.setItem(LS_KEY_TAB, activeTab);
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{doc.title || doc.originalFilename || 'Dokument'}</title>
|
|
</svelte:head>
|
|
|
|
<div
|
|
class="fixed right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden bg-surface"
|
|
style="top: {navHeight}px"
|
|
data-hydrated
|
|
>
|
|
<DocumentTopBar
|
|
doc={doc}
|
|
canWrite={data.canWrite ?? false}
|
|
canAnnotate={data.canAnnotate ?? false}
|
|
fileUrl={fileUrl}
|
|
bind:annotateMode={annotateMode}
|
|
/>
|
|
|
|
<div class="relative flex-1 overflow-hidden">
|
|
<DocumentViewer
|
|
doc={doc}
|
|
fileUrl={fileUrl}
|
|
isLoading={isLoading}
|
|
error={fileError}
|
|
bind:annotateMode={annotateMode}
|
|
bind:activeAnnotationId={activeAnnotationId}
|
|
bind:activeAnnotationPage={activeAnnotationPage}
|
|
onAnnotationClick={(id) => {
|
|
activeAnnotationId = id;
|
|
}}
|
|
/>
|
|
<AnnotationSidePanel
|
|
documentId={doc.id}
|
|
activeAnnotationId={activeAnnotationId}
|
|
activeAnnotationPage={activeAnnotationPage}
|
|
canComment={canComment}
|
|
currentUserId={currentUserId}
|
|
canAdmin={canAdmin}
|
|
onClose={() => {
|
|
activeAnnotationId = null;
|
|
activeAnnotationPage = null;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<DocumentBottomPanel
|
|
doc={doc}
|
|
comments={(data.comments ?? []) as never[]}
|
|
canComment={canComment}
|
|
currentUserId={currentUserId}
|
|
canAdmin={canAdmin}
|
|
bind:open={panelOpen}
|
|
bind:height={panelHeight}
|
|
bind:activeTab={activeTab}
|
|
/>
|
|
</div>
|