Release 0.6.1

This commit is contained in:
hnzio committed 2026-10-25 18:18:23 +01:00
1 parent b80dded55d
commit d586fbd8ce
29 files changed
+398 -66

No files matched your search

+34 -12
View File
@@ -14,6 +14,7 @@ import {
export const PIECE_STATUSES = ['inbox', 'preparing', 'ready', 'archived'] as const;
export type PieceStatus = (typeof PIECE_STATUSES)[number];
export type EditablePieceStatus = Exclude<PieceStatus, 'archived'>;
export type PreparationEffort = 'low' | 'medium' | 'high' | null;
export type TaxonomyKind = 'category' | 'format' | 'topic';
export type PieceSourceInput = {
@@ -42,6 +43,7 @@ export type PieceInput = {
summary: string;
formatId: string | null;
status: EditablePieceStatus;
preparationEffort: PreparationEffort;
durationSeconds: number;
intro: string;
researchText: string;
@@ -82,6 +84,7 @@ export type PieceListItem = {
sourceCount: number;
mediaId: string | null;
isFavorite: boolean;
preparationEffort: PreparationEffort;
};
export type PieceEditor = Omit<
@@ -198,6 +201,9 @@ function validatePieceInput(input: PieceInput): ValidatedPieceInput {
const title = cleanText(input.title, 200, 'title');
if (!title) throw new Error('INVALID_TITLE');
if (!['inbox', 'preparing', 'ready'].includes(input.status)) throw new Error('INVALID_STATUS');
if (![null, 'low', 'medium', 'high'].includes(input.preparationEffort)) {
throw new Error('INVALID_PREPARATION_EFFORT');
}
if (
!Number.isInteger(input.durationSeconds) ||
input.durationSeconds < 0 ||
@@ -226,7 +232,7 @@ function validatePieceInput(input: PieceInput): ValidatedPieceInput {
}
return { label, url, timecodeSeconds: source.timecodeSeconds };
});
if (!['text', 'link', 'video'].includes(input.presentationMode)) {
if (!['text', 'image', 'link', 'video'].includes(input.presentationMode)) {
throw new Error('INVALID_PRESENTATION');
}
const sourcePosition = input.presentationSourcePosition;
@@ -236,7 +242,7 @@ function validatePieceInput(input: PieceInput): ValidatedPieceInput {
) {
throw new Error('INVALID_PRESENTATION');
}
if (input.presentationMode !== 'text' && sourcePosition === null) {
if (['link', 'video'].includes(input.presentationMode) && sourcePosition === null) {
throw new Error('INVALID_PRESENTATION');
}
const presentationImageUrl = cleanText(input.presentationImageUrl, 2_000, 'presentation_image');
@@ -249,12 +255,16 @@ function validatePieceInput(input: PieceInput): ValidatedPieceInput {
input.presentationMediaId === undefined
? undefined
: cleanText(input.presentationMediaId, 100, 'presentation_media') || null;
if (input.presentationMode === 'image' && !presentationMediaId) {
throw new Error('INVALID_PRESENTATION');
}
return {
title,
outputTitle: cleanText(input.outputTitle, 300, 'output_title'),
summary: cleanText(input.summary, 2_000, 'summary'),
formatId: input.formatId || null,
status: input.status,
preparationEffort: input.preparationEffort,
durationSeconds: input.durationSeconds,
intro: cleanText(input.intro, 50_000, 'intro'),
researchText: cleanText(input.researchText, 50_000, 'research'),
@@ -317,6 +327,7 @@ type PieceListRow = {
source_count: number;
media_id: string | null;
is_favorite: number;
preparation_effort: PreparationEffort;
};
export function listPieces(filters: {
@@ -366,6 +377,7 @@ export function listPieces(filters: {
const rows = database
.prepare(
`SELECT p.id, p.title, p.summary, p.status, p.revision, p.duration_seconds, p.is_favorite,
p.preparation_effort,
p.format_id, COALESCE(f.name, 'Ohne Format') AS format,
(SELECT count(*) FROM piece_sources ps WHERE ps.piece_id = p.id) AS source_count,
(SELECT pm.id FROM piece_media pm WHERE pm.piece_id = p.id AND pm.removed_at_utc IS NULL ORDER BY pm.position LIMIT 1) AS media_id
@@ -398,7 +410,8 @@ export function listPieces(filters: {
topics: topicStatement.all(row.id) as PieceListItem['topics'],
sourceCount: row.source_count,
mediaId: row.media_id,
isFavorite: row.is_favorite === 1
isFavorite: row.is_favorite === 1,
preparationEffort: row.preparation_effort
}));
}
@@ -433,14 +446,16 @@ type PieceRow = {
updated_at_utc: string;
is_favorite: number;
presentation_media_id: string | null;
preparation_effort: PreparationEffort;
};
export function getPiece(id: string): PieceEditor | null {
const database = getDatabase();
const row = database
.prepare(
`SELECT id, title, output_title, summary, format_id, status, duration_seconds, intro, research_text,
moderation_notes, interaction_text, transition_text, presentation_text, presentation_mode,
`SELECT id, title, output_title, summary, format_id, status, preparation_effort, duration_seconds, intro, research_text,
moderation_notes, interaction_text, transition_text, presentation_text,
CASE WHEN presentation_image_only = 1 THEN 'image' ELSE presentation_mode END AS presentation_mode,
presentation_source_position, presentation_title, presentation_description,
presentation_image_url, presentation_site_name, presentation_provider,
presentation_provider_id, presentation_title_manual, presentation_description_manual,
@@ -460,6 +475,7 @@ export function getPiece(id: string): PieceEditor | null {
summary: row.summary,
formatId: row.format_id,
status: row.status,
preparationEffort: row.preparation_effort,
durationSeconds: row.duration_seconds,
intro: row.intro,
researchText: row.research_text,
@@ -612,9 +628,9 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
const save = database.transaction(() => {
const result = database
.prepare(
`UPDATE pieces SET title = ?, output_title = ?, summary = ?, format_id = ?, status = ?, duration_seconds = ?,
`UPDATE pieces SET title = ?, output_title = ?, summary = ?, format_id = ?, status = ?, preparation_effort = ?, duration_seconds = ?,
intro = ?, research_text = ?, moderation_notes = ?, interaction_text = COALESCE(?, interaction_text), transition_text = ?, presentation_text = ?,
presentation_mode = ?, presentation_source_position = ?, presentation_title = ?,
presentation_mode = ?, presentation_image_only = ?, presentation_source_position = ?, presentation_title = ?,
presentation_description = ?, presentation_image_url = ?, presentation_site_name = ?,
presentation_provider = ?, presentation_provider_id = ?, presentation_title_manual = ?,
presentation_description_manual = ?, presentation_image_manual = ?,
@@ -628,6 +644,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
input.summary,
input.formatId,
input.status,
input.preparationEffort,
input.durationSeconds,
input.intro,
input.researchText,
@@ -635,7 +652,8 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
input.interactionText ?? null,
input.transitionText,
input.presentationText,
input.presentationMode,
input.presentationMode === 'image' ? 'text' : input.presentationMode,
input.presentationMode === 'image' ? 1 : 0,
input.presentationSourcePosition,
input.presentationTitle,
input.presentationDescription,
@@ -671,6 +689,7 @@ function blankPieceInput(): ValidatedPieceInput {
summary: '',
formatId: null,
status: 'preparing',
preparationEffort: null,
durationSeconds: 0,
intro: '',
researchText: '',
@@ -798,6 +817,7 @@ export function savePieceAsTemplate(
delete snapshot.updatedAtUtc;
delete snapshot.media;
delete snapshot.presentationMediaId;
if (snapshot.presentationMode === 'image') snapshot.presentationMode = 'text';
snapshot.status = 'preparing';
const snapshotJson = JSON.stringify(snapshot);
const database = getDatabase();
@@ -917,15 +937,15 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
database.transaction(() => {
database
.prepare(
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, duration_seconds, intro,
research_text, moderation_notes, interaction_text, transition_text, presentation_text, presentation_mode,
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, preparation_effort, duration_seconds, intro,
research_text, moderation_notes, interaction_text, transition_text, presentation_text, presentation_mode, presentation_image_only,
presentation_source_position, presentation_title, presentation_description,
presentation_image_url, presentation_site_name, presentation_provider,
presentation_provider_id, presentation_title_manual,
presentation_description_manual, presentation_image_manual,
presentation_metadata_fetched_at_utc, created_at_utc, updated_at_utc, revision)
VALUES (
?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1
)`
)
@@ -935,6 +955,7 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
source.outputTitle,
source.summary,
source.formatId,
source.preparationEffort,
source.durationSeconds,
source.intro,
source.researchText,
@@ -942,7 +963,8 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
source.interactionText,
source.transitionText,
source.presentationText,
source.presentationMode,
source.presentationMode === 'image' ? 'text' : source.presentationMode,
source.presentationMode === 'image' ? 1 : 0,
source.presentationSourcePosition,
source.presentationTitle,
source.presentationDescription,