Release 0.5.9

This commit is contained in:
hnzio committed 2026-10-23 13:16:32 +02:00
1 parent 2851c7d936
commit c3033479b5
32 files changed
+1598 -50

No files matched your search

+12 -1
View File
@@ -8,7 +8,8 @@ import {
getTaxonomy,
listPieces,
listPieceTemplates,
setPieceArchived
setPieceArchived,
setPieceFavorite
} from '#lib/server/content.js';
import { getPieceUsageContexts } from '#lib/server/shows.js';
@@ -93,5 +94,15 @@ export const actions = {
} catch (cause) {
return fail(409, { intent: 'archive', error: actionError(cause) });
}
},
favorite: async ({ locals, request }) => {
requireUser(locals.user);
const data = await request.formData();
try {
setPieceFavorite(String(data.get('pieceId') || ''), data.get('favorite') === '1');
return { intent: 'favorite', success: 'Favorit aktualisiert.' };
} catch (cause) {
return fail(404, { intent: 'favorite', error: actionError(cause) });
}
}
} satisfies Actions;
+15 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { goto, replaceState } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/state';
@@ -13,7 +14,7 @@
let visiblePieces = $derived(
data.pieces.filter(
(piece) =>
(showUsedPieces || piece.sentHistory.length === 0) &&
(showUsedPieces || piece.sentHistory.length === 0 || piece.isFavorite) &&
(showPlannedPieces || !piece.isPlannedInActiveShow)
)
);
@@ -193,6 +194,19 @@
class="piece-card"
style={`--category: ${piece.categories[0]?.color ?? '#8b8f98'}`}
>
<form method="POST" action="?/favorite" class="piece-favorite-form" use:enhance>
<input type="hidden" name="pieceId" value={piece.id} />
<input type="hidden" name="favorite" value={piece.isFavorite ? '0' : '1'} />
<button
class="piece-favorite"
class:active={piece.isFavorite}
type="submit"
aria-label={piece.isFavorite
? `${piece.title} aus Favoriten entfernen`
: `${piece.title} als Favorit markieren`}
aria-pressed={piece.isFavorite}>★</button
>
</form>
{#if piece.mediaId}<img
class="piece-thumbnail"
src={`/media/${piece.mediaId}`}
+22 -1
View File
@@ -11,12 +11,14 @@ import {
savePiece,
savePieceAsTemplate,
setPieceArchived,
setPieceFavorite,
uploadPieceImage,
type EditablePieceStatus,
type PieceInput,
type PieceSourceInput
} from '#lib/server/content.js';
import { renderMarkdown } from '#lib/server/markdown.js';
import { listTools } from '#lib/server/tools.js';
function renderedFields(
piece: Pick<
@@ -57,6 +59,9 @@ function pieceInput(data: FormData): PieceInput {
timecodeSeconds: timecode ? Number(timecode) : null
});
}
const toolIds = data.getAll('toolId').map(String);
const toolWorkUrls = data.getAll('toolWorkUrl').map(String);
const toolNotes = data.getAll('toolNote').map(String);
return {
title: text(data, 'title'),
outputTitle: text(data, 'outputTitle'),
@@ -85,7 +90,12 @@ function pieceInput(data: FormData): PieceInput {
presentationMetadataFetchedAtUtc: text(data, 'presentationMetadataFetchedAtUtc') || null,
topicIds: data.getAll('topicIds').map(String),
categoryIds: data.getAll('categoryIds').map(String),
sources
sources,
tools: toolIds.map((toolId, index) => ({
toolId,
workUrl: toolWorkUrls[index] ?? '',
note: toolNotes[index] ?? ''
}))
};
}
@@ -126,6 +136,7 @@ export const load: PageServerLoad = ({ locals, params }) => {
return {
piece,
taxonomy: getTaxonomy(),
tools: listTools(true),
templates: listPieceTemplates().filter((template) => template.id !== 'blank'),
rendered: renderedFields(piece)
};
@@ -243,5 +254,15 @@ export const actions = {
return fail(409, { intent: 'archive', error: saveError(cause).error });
}
redirect(303, '/archiv');
},
favorite: async ({ locals, params, request }) => {
requireUser(locals.user);
const data = await request.formData();
try {
setPieceFavorite(params.id, data.get('favorite') === '1');
return { intent: 'favorite', success: true };
} catch (cause) {
return fail(404, { intent: 'favorite', error: saveError(cause).error });
}
}
} satisfies Actions;
+95 -1
View File
@@ -14,6 +14,8 @@
let durationMinutes = $state(untrack(() => Number((data.piece.durationSeconds / 60).toFixed(2))));
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,6 +46,9 @@
);
let previewContent = $derived<PresentationContent>({
mode: draft.presentationMode,
topics: data.taxonomy.topics
.filter((topic) => draft.topicIds.includes(topic.id))
.map((topic) => topic.name),
title: draft.outputTitle || draft.title,
html: rendered.presentation,
source: featuredSource
@@ -147,6 +152,27 @@
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 = '';
scheduleSave();
}
function removeTool(index: number) {
pieceTools.splice(index, 1);
scheduleSave();
}
function presentationSourceChanged() {
draft.presentationMetadataFetchedAtUtc = null;
draft.presentationProvider = '';
@@ -209,7 +235,19 @@
onrequestclose={requestClose}
>
{#snippet header()}
<p class="eyebrow">Fundusfassung</p>
<div class="piece-editor-kicker">
<p class="eyebrow">Fundusfassung</p>
<form method="POST" action="?/favorite" use:enhance>
<input type="hidden" name="favorite" value={data.piece.isFavorite ? '0' : '1'} />
<button
class="piece-favorite"
class:active={data.piece.isFavorite}
type="submit"
aria-label={data.piece.isFavorite ? 'Aus Favoriten entfernen' : 'Als Favorit markieren'}
aria-pressed={data.piece.isFavorite}>★</button
>
</form>
</div>
<label class="piece-dialog-title">
<span class="sr-only">Titel</span>
<textarea
@@ -396,6 +434,62 @@
</div>{/each}
</div>{:else}<p class="context-empty">Noch keine Quelle hinterlegt.</p>{/if}
</section>
<section class="piece-source-editor">
<div class="piece-subsection-heading">
<div>
<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>
</div>
{#if pieceTools.length}<div class="source-list">
{#each pieceTools as tool, index (tool.id)}<div
class="source-row tool-assignment-row"
>
<input type="hidden" name="toolId" value={tool.toolId} />
<div class="tool-assignment-name">
<strong>{tool.name}</strong>{#if !tool.isActive}<small>Deaktiviert</small>{/if}
</div>
<label
>Arbeitslink<input
name="toolWorkUrl"
type="url"
maxlength="2000"
placeholder={tool.startUrl || 'https://…'}
bind:value={tool.workUrl}
/></label
>
<label
>Notiz<input
name="toolNote"
maxlength="2000"
placeholder="Optional"
bind:value={tool.note}
/></label
>
<button
class="text-button danger-text"
type="button"
onclick={() => removeTool(index)}>Entfernen</button
>
</div>{/each}
</div>{:else}<p class="context-empty">Noch kein Werkzeug zugeordnet.</p>{/if}
</section>
</section>
<section class="piece-editor-panel" hidden={activeSection !== 'presentation'}>
@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { requireUser } from '#lib/server/auth.js';
import { getPiece } from '#lib/server/content.js';
import { getPiece, getTaxonomy } from '#lib/server/content.js';
import { renderMarkdown } from '#lib/server/markdown.js';
import { buildEmbedUrl, type PresentationContent } from '#lib/server/presentation.js';
@@ -18,6 +18,9 @@ export const load: PageServerLoad = ({ locals, params }) => {
images: { id: string; altText: string }[];
} = {
mode: piece.presentationMode,
topics: getTaxonomy()
.topics.filter((topic) => piece.topicIds.includes(topic.id))
.map((topic) => topic.name),
title: piece.outputTitle || piece.title,
html: renderMarkdown(piece.presentationText),
source,