Release 0.5.10

This commit is contained in:
hnzio committed 2026-10-23 21:33:15 +02:00
1 parent c3033479b5
commit 4fe61971e4
52 files changed
+38565 -466

No files matched your search

+121 -65
View File
@@ -4,6 +4,9 @@
import { resolve } from '$app/paths';
import { onDestroy, onMount, untrack } from 'svelte';
import MarkdownEditorField from '#lib/components/MarkdownEditorField.svelte';
import AppIcon from '#lib/components/AppIcon.svelte';
import ChoicePicker from '#lib/components/ChoicePicker.svelte';
import InlineTitleEditor from '#lib/components/InlineTitleEditor.svelte';
import PieceEditorDialog from '#lib/components/PieceEditorDialog.svelte';
import PresentationDisplay from '#lib/components/PresentationDisplay.svelte';
import type { PresentationContent } from '#lib/server/presentation.js';
@@ -15,7 +18,6 @@
let revision = $state(untrack(() => data.piece.revision));
let sources = $state(untrack(() => data.piece.sources.map((source) => ({ ...source }))));
let pieceTools = $state(untrack(() => data.piece.tools.map((tool) => ({ ...tool }))));
let toolToAdd = $state('');
let rendered = $state(untrack(() => ({ ...data.rendered })));
let activeSection = $state<'script' | 'research' | 'presentation'>('script');
let saveState = $state<'idle' | 'dirty' | 'saving' | 'saved' | 'error' | 'conflict'>('idle');
@@ -44,7 +46,12 @@
? resolve('/fundus/[id]/praesentation/preview-image', { id: data.piece.id })
: ''
);
let previewContent = $derived<PresentationContent>({
let selectedMedia = $derived(
draft.media.find((media) => media.id === draft.presentationMediaId) ?? null
);
let previewContent = $derived<
PresentationContent & { images: { id: string; altText: string }[] }
>({
mode: draft.presentationMode,
topics: data.taxonomy.topics
.filter((topic) => draft.topicIds.includes(topic.id))
@@ -69,8 +76,16 @@
? `https://www.youtube-nocookie.com/embed/${draft.presentationProviderId}${featuredSource?.timecodeSeconds ? `?start=${featuredSource.timecodeSeconds}` : ''}`
: draft.presentationProvider === 'tiktok' && draft.presentationProviderId
? `https://www.tiktok.com/player/v1/${draft.presentationProviderId}`
: ''
: '',
images: selectedMedia ? [{ id: selectedMedia.id, altText: selectedMedia.altText }] : []
});
let presentationPreviewImage = $derived(
selectedMedia
? draft.presentationMode === 'text'
? '/media'
: `/media/${selectedMedia.id}`
: sourcePreviewImage
);
let pendingHref: string | null = null;
let allowNavigation = false;
@@ -152,24 +167,28 @@
scheduleSave();
}
function addTool() {
const tool = data.tools.find((item) => item.id === toolToAdd);
if (!tool || pieceTools.some((item) => item.toolId === tool.id)) return;
pieceTools.push({
id: crypto.randomUUID(),
toolId: tool.id,
name: tool.name,
startUrl: tool.startUrl,
isActive: tool.isActive,
workUrl: '',
note: ''
});
toolToAdd = '';
function removeTool(index: number) {
pieceTools.splice(index, 1);
scheduleSave();
}
function removeTool(index: number) {
pieceTools.splice(index, 1);
function selectTools(ids: string[]) {
for (const id of ids) {
if (pieceTools.some((tool) => tool.toolId === id)) continue;
const tool = data.tools.find((item) => item.id === id);
if (tool)
pieceTools.push({
id: crypto.randomUUID(),
toolId: tool.id,
name: tool.name,
startUrl: tool.startUrl,
iconName: tool.iconName,
isActive: tool.isActive,
workUrl: '',
note: ''
});
}
pieceTools = pieceTools.filter((tool) => ids.includes(tool.toolId));
scheduleSave();
}
@@ -244,22 +263,15 @@
class:active={data.piece.isFavorite}
type="submit"
aria-label={data.piece.isFavorite ? 'Aus Favoriten entfernen' : 'Als Favorit markieren'}
aria-pressed={data.piece.isFavorite}>★</button
aria-pressed={data.piece.isFavorite}><AppIcon name="star" /></button
>
</form>
</div>
<label class="piece-dialog-title">
<span class="sr-only">Titel</span>
<textarea
form="piece-editor-form"
name="title"
required
rows="1"
maxlength="200"
aria-label="Titel"
bind:value={draft.title}
oninput={scheduleSave}></textarea>
</label>
<InlineTitleEditor
form="piece-editor-form"
bind:value={draft.title}
oncommit={scheduleSave}
/>
<h1 class="sr-only" id="piece-editor-title">{draft.title}</h1>
{/snippet}
@@ -345,17 +357,16 @@
bind:value={draft.summary}></textarea></label
>
<fieldset>
<legend>Themen</legend>
<div class="choice-grid">
{#each data.taxonomy.topics as item (item.id)}<label
><input
type="checkbox"
name="topicIds"
value={item.id}
bind:group={draft.topicIds}
/>{item.name}</label
>{/each}
</div>
<legend>Themen</legend><ChoicePicker
items={data.taxonomy.topics}
selectedIds={draft.topicIds}
label="Themen"
fieldName="topicIds"
onselectionchange={(ids) => {
draft.topicIds = ids;
scheduleSave();
}}
/>
</fieldset>
<fieldset>
<legend>Vorgeschlagene Abschnittskategorien</legend>
@@ -440,22 +451,20 @@
<p class="eyebrow">Arbeitsmittel</p>
<h3>Werkzeuge</h3>
</div>
<div class="tool-picker">
<label class="sr-only" for="piece-tool-picker">Werkzeug auswählen</label>
<select id="piece-tool-picker" bind:value={toolToAdd}>
<option value="">Werkzeug auswählen</option>
{#each data.tools.filter((tool) => tool.isActive && !pieceTools.some((assigned) => assigned.toolId === tool.id)) as tool (tool.id)}
<option value={tool.id}
>{tool.name}{tool.formatIds.includes(draft.formatId ?? '')
? ' · passt zum Format'
: ''}</option
>
{/each}
</select>
<button class="secondary-button" type="button" onclick={addTool} disabled={!toolToAdd}
>Hinzufügen</button
>
</div>
<ChoicePicker
items={data.tools
.filter((tool) => tool.isActive)
.map((tool) => ({
id: tool.id,
name: tool.name,
description: tool.description,
iconName: tool.iconName,
meta: tool.formatIds.includes(draft.formatId ?? '') ? 'Passt zum Format' : ''
}))}
selectedIds={pieceTools.map((tool) => tool.toolId)}
label="Werkzeuge"
onselectionchange={selectTools}
/>
</div>
{#if pieceTools.length}<div class="source-list">
{#each pieceTools as tool, index (tool.id)}<div
@@ -463,7 +472,10 @@
>
<input type="hidden" name="toolId" value={tool.toolId} />
<div class="tool-assignment-name">
<strong>{tool.name}</strong>{#if !tool.isActive}<small>Deaktiviert</small>{/if}
<AppIcon name={tool.iconName || 'wrench'} /><span
><strong>{tool.name}</strong>{#if !tool.isActive}<small>Deaktiviert</small
>{/if}</span
>
</div>
<label
>Arbeitslink<input
@@ -590,7 +602,7 @@
<aside class="piece-presentation-preview" aria-label="Präsentationsvorschau">
<PresentationDisplay
content={previewContent}
imageSrc={sourceImageUnavailable ? '' : sourcePreviewImage}
imageSrc={sourceImageUnavailable ? '' : presentationPreviewImage}
/>
</aside>
</div>
@@ -620,17 +632,44 @@
</section>
</form>
<section class="piece-editor-panel piece-media-panel" hidden={activeSection !== 'research'}>
<section class="piece-editor-panel piece-media-panel" hidden={activeSection !== 'presentation'}>
<div class="piece-subsection-heading">
<div>
<p class="eyebrow">Private Dateien</p>
<h3>Material</h3>
<p class="eyebrow">Hochgeladene Bilder</p>
<h3>Präsentationsbild</h3>
</div>
<small>JPEG, PNG, WebP oder GIF bis 5 MB</small>
</div>
{#if data.piece.media.length}<div class="media-grid">
{#if data.piece.media.length}<label class="media-none"
><input
form="piece-editor-form"
type="radio"
name="presentationMediaId"
value=""
checked={!draft.presentationMediaId}
onchange={() => {
draft.presentationMediaId = null;
scheduleSave();
}}
/> Kein hochgeladenes Bild</label
>
<div class="media-grid">
{#each data.piece.media as media (media.id)}<figure>
<img src={`/media/${media.id}`} alt={media.altText} />
<label class="media-presentation-choice"
><input
form="piece-editor-form"
type="radio"
name="presentationMediaId"
value={media.id}
checked={draft.presentationMediaId === media.id}
onchange={() => {
draft.presentationMediaId = media.id;
scheduleSave();
}}
/><img src={`/media/${media.id}`} alt={media.altText} /><span
>Als Präsentationsbild</span
></label
>
<figcaption>
<span>{media.altText || media.originalName}</span>
<form method="POST" action="?/deleteImage">
@@ -662,6 +701,10 @@
><button class="secondary-button" type="submit" disabled={actionsBlocked}
>Bild hochladen</button
>
<label class="upload-select"
><input type="checkbox" name="selectForPresentation" value="1" checked /> Direkt als Präsentationsbild
verwenden</label
>
</form>
</section>
@@ -696,6 +739,19 @@
disabled={actionsBlocked}>Archivieren</button
>
</form>
<form
method="POST"
action="?/delete"
onsubmit={(event) =>
!confirm('Piece löschen? Bestehende Sendungsfassungen bleiben erhalten.') &&
event.preventDefault()}
>
<input type="hidden" name="revision" value={revision} /><button
class="text-button danger-text"
type="submit"
disabled={actionsBlocked}>Piece löschen</button
>
</form>
</div>
<div class="piece-dialog-primary-action">
<span