Add persistent live controls and interaction notes

This commit is contained in:
hnzio committed 2026-10-24 14:31:33 +02:00
1 parent b1474bc33d
commit fa17187af4
16 files changed
+882 -62

No files matched your search

+30 -10
View File
@@ -46,6 +46,7 @@ export type PieceInput = {
intro: string;
researchText: string;
moderationNotes: string;
interactionText?: string;
transitionText: string;
presentationText: string;
presentationMode: PresentationMode;
@@ -83,7 +84,10 @@ export type PieceListItem = {
isFavorite: boolean;
};
export type PieceEditor = Omit<PieceInput, 'status' | 'tools' | 'presentationMediaId'> & {
export type PieceEditor = Omit<
PieceInput,
'status' | 'tools' | 'presentationMediaId' | 'interactionText'
> & {
id: string;
status: PieceStatus;
revision: number;
@@ -92,9 +96,12 @@ export type PieceEditor = Omit<PieceInput, 'status' | 'tools' | 'presentationMed
media: PieceMedia[];
presentationMediaId: string | null;
isFavorite: boolean;
interactionText: string;
tools: PieceTool[];
};
type ValidatedPieceInput = PieceInput;
export type PieceMedia = {
id: string;
originalName: string;
@@ -187,7 +194,7 @@ function requireTaxonomyIds(table: 'formats' | 'topics' | 'section_categories',
if (count.count !== ids.length) throw new Error('INVALID_TAXONOMY');
}
function validatePieceInput(input: PieceInput): PieceInput {
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');
@@ -252,6 +259,10 @@ function validatePieceInput(input: PieceInput): PieceInput {
intro: cleanText(input.intro, 50_000, 'intro'),
researchText: cleanText(input.researchText, 50_000, 'research'),
moderationNotes: cleanText(input.moderationNotes, 50_000, 'moderation'),
interactionText:
input.interactionText === undefined
? undefined
: cleanText(input.interactionText, 50_000, 'interaction'),
transitionText: cleanText(input.transitionText, 50_000, 'transition'),
presentationText: cleanText(input.presentationText, 50_000, 'presentation'),
presentationMode: input.presentationMode,
@@ -402,6 +413,7 @@ type PieceRow = {
intro: string;
research_text: string;
moderation_notes: string;
interaction_text: string;
transition_text: string;
presentation_text: string;
presentation_mode: PresentationMode;
@@ -428,7 +440,7 @@ export function getPiece(id: string): PieceEditor | null {
const row = database
.prepare(
`SELECT id, title, output_title, summary, format_id, status, duration_seconds, intro, research_text,
moderation_notes, transition_text, presentation_text, presentation_mode,
moderation_notes, interaction_text, transition_text, presentation_text, 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,
@@ -452,6 +464,7 @@ export function getPiece(id: string): PieceEditor | null {
intro: row.intro,
researchText: row.research_text,
moderationNotes: row.moderation_notes,
interactionText: row.interaction_text,
transitionText: row.transition_text,
presentationText: row.presentation_text,
presentationMode: row.presentation_mode,
@@ -600,7 +613,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
const result = database
.prepare(
`UPDATE pieces SET title = ?, output_title = ?, summary = ?, format_id = ?, status = ?, duration_seconds = ?,
intro = ?, research_text = ?, moderation_notes = ?, transition_text = ?, presentation_text = ?,
intro = ?, research_text = ?, moderation_notes = ?, interaction_text = COALESCE(?, interaction_text), transition_text = ?, presentation_text = ?,
presentation_mode = ?, presentation_source_position = ?, presentation_title = ?,
presentation_description = ?, presentation_image_url = ?, presentation_site_name = ?,
presentation_provider = ?, presentation_provider_id = ?, presentation_title_manual = ?,
@@ -619,6 +632,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
input.intro,
input.researchText,
input.moderationNotes,
input.interactionText ?? null,
input.transitionText,
input.presentationText,
input.presentationMode,
@@ -650,7 +664,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
return { revision: expectedRevision + 1, updatedAtUtc };
}
function blankPieceInput(): PieceInput {
function blankPieceInput(): ValidatedPieceInput {
return {
title: 'Neues Piece',
outputTitle: '',
@@ -661,6 +675,7 @@ function blankPieceInput(): PieceInput {
intro: '',
researchText: '',
moderationNotes: '',
interactionText: '',
transitionText: '',
presentationText: '',
presentationMode: 'text',
@@ -682,7 +697,7 @@ function blankPieceInput(): PieceInput {
};
}
function templateInput(templateId: string): PieceInput {
function templateInput(templateId: string): ValidatedPieceInput {
if (templateId === 'blank') return blankPieceInput();
const row = getDatabase()
.prepare('SELECT snapshot_json FROM piece_templates WHERE id = ?')
@@ -739,8 +754,8 @@ export function createPiece(template = 'blank'): string {
database
.prepare(
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, duration_seconds, intro,
research_text, moderation_notes, transition_text, presentation_text, created_at_utc,
updated_at_utc, revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
research_text, moderation_notes, interaction_text, transition_text, presentation_text, created_at_utc,
updated_at_utc, revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
)
.run(
id,
@@ -753,6 +768,7 @@ export function createPiece(template = 'blank'): string {
input.intro,
input.researchText,
input.moderationNotes,
input.interactionText ?? '',
input.transitionText,
input.presentationText,
now,
@@ -902,13 +918,16 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
database
.prepare(
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, duration_seconds, intro,
research_text, moderation_notes, transition_text, presentation_text, presentation_mode,
research_text, moderation_notes, interaction_text, transition_text, presentation_text, 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, presentation_image_manual,
presentation_metadata_fetched_at_utc, created_at_utc, updated_at_utc, revision)
VALUES (?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
VALUES (
?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1
)`
)
.run(
id,
@@ -920,6 +939,7 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
source.intro,
source.researchText,
source.moderationNotes,
source.interactionText,
source.transitionText,
source.presentationText,
source.presentationMode,
+411 -17
View File
@@ -11,6 +11,7 @@ import {
movePieceUsage,
type MovePieceUsageInput,
type PieceUsage,
type ShowStatus,
type ShowSection
} from './shows.js';
@@ -41,6 +42,7 @@ export type LiveUsage = PieceUsage & {
introHtml: string;
researchHtml: string;
moderationHtml: string;
interactionHtml: string;
transitionHtml: string;
};
@@ -55,7 +57,10 @@ export type LiveView = {
totalDurationSeconds: number;
};
liveRevision: number;
calculatedAtUtc: string;
startedAtUtc: string | null;
endedAtUtc: string | null;
isPaused: boolean;
updatedAtUtc: string;
presentationUsageId: string | null;
presentationRevision: number;
@@ -72,6 +77,15 @@ export type LiveView = {
elapsedSeconds: number;
runningSinceUtc: string | null;
};
totalTimer: {
elapsedSeconds: number;
runningSinceUtc: string | null;
};
remainingEstimate: {
seconds: number;
activeCountdownSeconds: number;
unplannedSectionCount: number;
};
};
export type PresentationView = {
@@ -152,6 +166,55 @@ function closeActiveRun(database: Database.Database, showId: string, now: string
.run(now, showId);
}
function closeShowTimerRun(database: Database.Database, showId: string, now: string) {
database
.prepare(
`UPDATE live_show_timer_runs SET ended_at_utc = ?
WHERE show_id = ? AND ended_at_utc IS NULL`
)
.run(now, showId);
}
function openShowTimerRun(database: Database.Database, showId: string, now: string) {
const active = database
.prepare('SELECT 1 FROM live_show_timer_runs WHERE show_id = ? AND ended_at_utc IS NULL')
.get(showId);
if (!active) {
database
.prepare(
`INSERT INTO live_show_timer_runs (id, show_id, started_at_utc)
VALUES (?, ?, ?)`
)
.run(randomUUID(), showId, now);
}
}
function showTimerRunning(database: Database.Database, showId: string) {
return Boolean(
database
.prepare('SELECT 1 FROM live_show_timer_runs WHERE show_id = ? AND ended_at_utc IS NULL')
.get(showId)
);
}
function elapsedRuns(
runs: { started_at_utc: string; ended_at_utc: string | null }[],
now: string
) {
return runs.reduce(
(total, run) =>
total +
Math.max(
0,
Math.floor(
(new Date(run.ended_at_utc ?? now).getTime() - new Date(run.started_at_utc).getTime()) /
1_000
)
),
0
);
}
function closeSectionTimer(database: Database.Database, showId: string, now: string) {
const timer = database
.prepare('SELECT id FROM live_section_timers WHERE show_id = ? AND ended_at_utc IS NULL')
@@ -167,6 +230,26 @@ function closeSectionTimer(database: Database.Database, showId: string, now: str
.run(now, timer.id);
}
function resumeSectionTimerRun(database: Database.Database, showId: string, now: string) {
const timer = database
.prepare('SELECT id FROM live_section_timers WHERE show_id = ? AND ended_at_utc IS NULL')
.get(showId) as { id: string } | undefined;
if (!timer) return;
const running = database
.prepare(
'SELECT 1 FROM live_section_timer_runs WHERE timer_id = ? AND ended_at_utc IS NULL'
)
.get(timer.id);
if (!running) {
database
.prepare(
`INSERT INTO live_section_timer_runs (id, timer_id, started_at_utc)
VALUES (?, ?, ?)`
)
.run(randomUUID(), timer.id, now);
}
}
function syncSectionTimer(
database: Database.Database,
showId: string,
@@ -237,7 +320,8 @@ function executeCommand(
expectedRevision: number,
action: string,
operation: (database: Database.Database, state: LiveStateRow, now: string) => void,
nowValue?: Date
nowValue?: Date,
allowCompleted = false
) {
requireCommandId(commandId);
const database = getDatabase();
@@ -251,7 +335,7 @@ function executeCommand(
const show = database.prepare('SELECT status FROM shows WHERE id = ?').get(showId) as {
status: string;
};
if (show.status === 'completed') throw new Error('SHOW_COMPLETED');
if (show.status === 'completed' && !allowCompleted) throw new Error('SHOW_COMPLETED');
if (state.revision !== expectedRevision) throw new Error('CONFLICT');
operation(database, state, now);
const updated = database
@@ -307,6 +391,7 @@ function liveUsage(
introHtml: renderMarkdown(usage.intro),
researchHtml: renderMarkdown(usage.research),
moderationHtml: renderMarkdown(usage.moderation),
interactionHtml: renderMarkdown(usage.interaction),
transitionHtml: renderMarkdown(usage.transition)
};
}
@@ -351,18 +436,7 @@ export function getLiveView(showId: string, nowValue?: Date): LiveView {
)
.all(timer.id) as { started_at_utc: string; ended_at_utc: string | null }[];
const running = runs.find((run) => run.ended_at_utc === null) ?? null;
const elapsedSeconds = runs.reduce(
(total, run) =>
total +
Math.max(
0,
Math.floor(
(new Date(run.ended_at_utc ?? now).getTime() - new Date(run.started_at_utc).getTime()) /
1_000
)
),
0
);
const elapsedSeconds = elapsedRuns(runs, now);
sectionTimer = {
sectionId: timer.section_id,
sectionName: timer.section_name,
@@ -371,6 +445,50 @@ export function getLiveView(showId: string, nowValue?: Date): LiveView {
runningSinceUtc: running?.started_at_utc ?? null
};
}
const totalTimerRuns = database
.prepare(
`SELECT started_at_utc, ended_at_utc FROM live_show_timer_runs
WHERE show_id = ? ORDER BY started_at_utc`
)
.all(showId) as { started_at_utc: string; ended_at_utc: string | null }[];
const runningTotalTimer = totalTimerRuns.find((run) => run.ended_at_utc === null) ?? null;
const totalElapsedSeconds = elapsedRuns(totalTimerRuns, now);
const remainingAnchor = current ?? next;
let reachedRemaining = !remainingAnchor;
let remainingSeconds = 0;
let activeCountdownSeconds = 0;
let unplannedSectionCount = 0;
for (const section of sections) {
if (section.id === remainingAnchor?.sectionId) reachedRemaining = true;
if (!reachedRemaining) continue;
const remainingUsages = section.usages.filter(
(usage) => usage.status === 'open' || usage.status === 'current'
);
if (remainingUsages.length === 0) continue;
if (section.id === current?.sectionId) {
if (section.durationSeconds > 0 && sectionTimer?.sectionId === section.id) {
activeCountdownSeconds = Math.max(0, section.durationSeconds - sectionTimer.elapsedSeconds);
remainingSeconds += activeCountdownSeconds;
} else {
remainingSeconds += remainingUsages.reduce((total, usage) => {
const elapsed = usage.id === current?.id ? usage.actualDurationSeconds : 0;
const remainder = Math.max(0, usage.durationSeconds - elapsed);
if (usage.id === current?.id) activeCountdownSeconds = remainder;
return total + remainder;
}, 0);
if (section.durationSeconds <= 0) unplannedSectionCount += 1;
}
} else if (section.durationSeconds > 0) {
remainingSeconds += section.durationSeconds;
} else {
remainingSeconds += remainingUsages.reduce(
(total, usage) => total + usage.durationSeconds,
0
);
unplannedSectionCount += 1;
}
}
return {
show: {
id: show.id,
@@ -380,7 +498,10 @@ export function getLiveView(showId: string, nowValue?: Date): LiveView {
totalDurationSeconds: show.totalDurationSeconds
},
liveRevision: state.revision,
calculatedAtUtc: now,
startedAtUtc: state.started_at_utc,
endedAtUtc: state.ended_at_utc,
isPaused: Boolean(state.started_at_utc && !state.ended_at_utc && !runningTotalTimer),
updatedAtUtc: state.updated_at_utc,
presentationUsageId: state.presentation_usage_id,
presentationRevision: state.presentation_revision,
@@ -391,7 +512,16 @@ export function getLiveView(showId: string, nowValue?: Date): LiveView {
completedCount: usages.filter((usage) => usage.status === 'done' || usage.status === 'skipped')
.length,
totalCount: usages.length,
sectionTimer
sectionTimer,
totalTimer: {
elapsedSeconds: totalElapsedSeconds,
runningSinceUtc: runningTotalTimer?.started_at_utc ?? null
},
remainingEstimate: {
seconds: remainingSeconds,
activeCountdownSeconds,
unplannedSectionCount
}
};
}
@@ -409,7 +539,7 @@ export function startOrResumeLive(
(database, state, now) => {
database
.prepare(
`UPDATE shows SET status = 'live', updated_at_utc = ?
`UPDATE shows SET status = 'live', revision = revision + 1, updated_at_utc = ?
WHERE id = ? AND status != 'completed'`
)
.run(now, showId);
@@ -419,10 +549,14 @@ export function startOrResumeLive(
WHERE show_id = ?`
)
.run(now, showId);
openShowTimerRun(database, showId, now);
const activeRun = database
.prepare('SELECT 1 FROM live_usage_runs WHERE show_id = ? AND ended_at_utc IS NULL')
.get(showId);
if (state.current_usage_id && activeRun) return;
if (state.current_usage_id && activeRun) {
resumeSectionTimerRun(database, showId, now);
return;
}
const current = state.current_usage_id
? orderedUsages(database, showId).find((usage) => usage.id === state.current_usage_id)
: undefined;
@@ -434,6 +568,263 @@ export function startOrResumeLive(
);
}
export function pauseLive(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'pause',
(database, state, now) => {
if (!state.started_at_utc || state.ended_at_utc) throw new Error('NOT_LIVE');
if (!showTimerRunning(database, showId)) throw new Error('ALREADY_PAUSED');
closeShowTimerRun(database, showId, now);
closeActiveRun(database, showId, now);
database
.prepare(
`UPDATE live_section_timer_runs SET ended_at_utc = ?
WHERE timer_id IN (
SELECT id FROM live_section_timers WHERE show_id = ? AND ended_at_utc IS NULL
) AND ended_at_utc IS NULL`
)
.run(now, showId);
},
nowValue
);
}
export function resumeLive(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'resume',
(database, state, now) => {
if (!state.started_at_utc || state.ended_at_utc) throw new Error('NOT_LIVE');
if (showTimerRunning(database, showId)) throw new Error('ALREADY_RUNNING');
openShowTimerRun(database, showId, now);
if (state.current_usage_id) {
database
.prepare(
`INSERT INTO live_usage_runs (id, show_id, usage_id, started_at_utc)
VALUES (?, ?, ?, ?)`
)
.run(randomUUID(), showId, state.current_usage_id, now);
}
resumeSectionTimerRun(database, showId, now);
},
nowValue
);
}
export function resetSectionTimer(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'reset-section-timer',
(database, _state, now) => {
const timer = database
.prepare(
`SELECT section_id, planned_duration_seconds
FROM live_section_timers WHERE show_id = ? AND ended_at_utc IS NULL`
)
.get(showId) as
| { section_id: string; planned_duration_seconds: number }
| undefined;
if (!timer) throw new Error('NO_SECTION_TIMER');
const running = showTimerRunning(database, showId);
closeSectionTimer(database, showId, now);
const timerId = randomUUID();
database
.prepare(
`INSERT INTO live_section_timers
(id, show_id, section_id, planned_duration_seconds, started_at_utc)
VALUES (?, ?, ?, ?, ?)`
)
.run(timerId, showId, timer.section_id, timer.planned_duration_seconds, now);
if (running) {
database
.prepare(
`INSERT INTO live_section_timer_runs (id, timer_id, started_at_utc)
VALUES (?, ?, ?)`
)
.run(randomUUID(), timerId, now);
}
},
nowValue
);
}
export function resetTotalTimer(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'reset-total-timer',
(database, state, now) => {
if (!state.started_at_utc || state.ended_at_utc) throw new Error('NOT_LIVE');
const running = showTimerRunning(database, showId);
database.prepare('DELETE FROM live_show_timer_runs WHERE show_id = ?').run(showId);
if (running) openShowTimerRun(database, showId, now);
},
nowValue
);
}
export function setShowStatus(
showId: string,
commandId: string,
expectedRevision: number,
expectedShowRevision: number,
status: Extract<ShowStatus, 'draft' | 'ready'>,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
`status-${status}`,
(database, state, now) => {
if (state.started_at_utc) throw new Error('ALREADY_STARTED');
const result = database
.prepare(
'UPDATE shows SET status = ?, revision = revision + 1, updated_at_utc = ? WHERE id = ? AND revision = ?'
)
.run(status, now, showId, expectedShowRevision);
if (result.changes !== 1) throw new Error('CONFLICT');
},
nowValue
);
}
export function endLive(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'end',
(database, state, now) => {
if (!state.started_at_utc || state.ended_at_utc) throw new Error('NOT_LIVE');
closeShowTimerRun(database, showId, now);
closeActiveRun(database, showId, now);
database
.prepare(
`UPDATE live_section_timer_runs SET ended_at_utc = ?
WHERE timer_id IN (
SELECT id FROM live_section_timers WHERE show_id = ? AND ended_at_utc IS NULL
) AND ended_at_utc IS NULL`
)
.run(now, showId);
database
.prepare(
`UPDATE show_live_states SET ended_at_utc = ? WHERE show_id = ?`
)
.run(now, showId);
database
.prepare(`UPDATE shows SET status = 'completed', revision = revision + 1, updated_at_utc = ? WHERE id = ?`)
.run(now, showId);
},
nowValue
);
}
export function resumeEndedLive(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'resume-ended',
(database, state, now) => {
if (!state.ended_at_utc) throw new Error('NOT_ENDED');
database.prepare(`UPDATE shows SET status = 'live', revision = revision + 1, updated_at_utc = ? WHERE id = ?`).run(now, showId);
database
.prepare('UPDATE show_live_states SET ended_at_utc = NULL WHERE show_id = ?')
.run(showId);
openShowTimerRun(database, showId, now);
if (state.current_usage_id) {
database
.prepare(
`INSERT INTO live_usage_runs (id, show_id, usage_id, started_at_utc)
VALUES (?, ?, ?, ?)`
)
.run(randomUUID(), showId, state.current_usage_id, now);
resumeSectionTimerRun(database, showId, now);
}
},
nowValue,
true
);
}
export function startNewLiveRun(
showId: string,
commandId: string,
expectedRevision: number,
nowValue?: Date
) {
return executeCommand(
showId,
commandId,
expectedRevision,
'start-new-run',
(database, state, now) => {
if (!state.ended_at_utc) throw new Error('NOT_ENDED');
database.prepare('DELETE FROM live_usage_runs WHERE show_id = ?').run(showId);
database.prepare('DELETE FROM live_show_timer_runs WHERE show_id = ?').run(showId);
database.prepare('DELETE FROM live_section_timers WHERE show_id = ?').run(showId);
database
.prepare(
`UPDATE piece_usages SET status = 'open'
WHERE show_section_id IN (SELECT id FROM show_sections WHERE show_id = ?)`
)
.run(showId);
database
.prepare(
`UPDATE show_live_states
SET current_usage_id = NULL, started_at_utc = ?, ended_at_utc = NULL
WHERE show_id = ?`
)
.run(now, showId);
database.prepare(`UPDATE shows SET status = 'live', revision = revision + 1, updated_at_utc = ? WHERE id = ?`).run(now, showId);
openShowTimerRun(database, showId, now);
const target = orderedUsages(database, showId).find((usage) => usage.status === 'open');
if (target) activateUsage(database, showId, target.id, now);
},
nowValue,
true
);
}
function advanceCurrent(
showId: string,
commandId: string,
@@ -447,6 +838,7 @@ function advanceCurrent(
expectedRevision,
status,
(database, state, now) => {
if (!showTimerRunning(database, showId)) throw new Error('TIMER_PAUSED');
if (!state.current_usage_id) throw new Error('NO_CURRENT');
const ordered = orderedUsages(database, showId);
const currentIndex = ordered.findIndex((usage) => usage.id === state.current_usage_id);
@@ -498,6 +890,7 @@ export function goBack(
expectedRevision,
'back',
(database, state, now) => {
if (!showTimerRunning(database, showId)) throw new Error('TIMER_PAUSED');
const ordered = orderedUsages(database, showId);
const currentIndex = state.current_usage_id
? ordered.findIndex((usage) => usage.id === state.current_usage_id)
@@ -533,6 +926,7 @@ export function selectUsage(
'select',
(database, state, now) => {
if (!state.started_at_utc) throw new Error('NOT_STARTED');
if (!showTimerRunning(database, showId)) throw new Error('TIMER_PAUSED');
usageBelongsToShow(database, showId, usageId);
if (state.current_usage_id === usageId) return;
if (state.current_usage_id) {
+22 -10
View File
@@ -47,6 +47,7 @@ export type PieceUsage = {
intro: string;
research: string;
moderation: string;
interaction: string;
transition: string;
presentation: string;
presentationMode: PresentationMode;
@@ -157,6 +158,7 @@ export type PieceUsageUpdate = {
intro: string;
research: string;
moderation: string;
interaction?: string;
transition: string;
presentation: string;
presentationMode: PresentationMode;
@@ -216,6 +218,7 @@ type UsageRow = {
intro_snapshot: string;
research_snapshot: string;
moderation_snapshot: string;
interaction_snapshot: string;
transition_snapshot: string;
presentation_snapshot: string;
presentation_mode_snapshot: PresentationMode;
@@ -445,6 +448,7 @@ function usageFromRow(database: Database.Database, row: UsageRow): PieceUsage {
intro: row.intro_snapshot,
research: row.research_snapshot,
moderation: row.moderation_snapshot,
interaction: row.interaction_snapshot,
transition: row.transition_snapshot,
presentation: row.presentation_snapshot,
presentationMode: row.presentation_mode_snapshot,
@@ -793,7 +797,7 @@ export function getShow(id: string): Show | null {
.prepare(
`SELECT u.id, u.show_section_id, u.original_piece_id, u.position, u.status,
u.duration_seconds, u.title_snapshot, u.output_title_snapshot, u.intro_snapshot, u.research_snapshot,
u.moderation_snapshot, u.transition_snapshot, u.presentation_snapshot,
u.moderation_snapshot, u.interaction_snapshot, u.transition_snapshot, u.presentation_snapshot,
u.presentation_mode_snapshot, u.presentation_source_label_snapshot,
u.presentation_source_url_snapshot, u.presentation_source_timecode_snapshot,
u.presentation_title_meta_snapshot, u.presentation_description_snapshot,
@@ -1126,7 +1130,7 @@ export function addPieceUsage(
const piece = database
.prepare(
`SELECT p.title, p.output_title, p.duration_seconds, p.intro, p.research_text, p.moderation_notes,
p.transition_text, p.presentation_text, p.status, p.presentation_mode,
p.interaction_text, p.transition_text, p.presentation_text, p.status, p.presentation_mode,
COALESCE(ps.label, '') AS presentation_source_label,
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
@@ -1143,6 +1147,7 @@ export function addPieceUsage(
intro: string;
research_text: string;
moderation_notes: string;
interaction_text: string;
transition_text: string;
presentation_text: string;
presentation_mode: PresentationMode;
@@ -1178,13 +1183,13 @@ export function addPieceUsage(
.prepare(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot, research_snapshot,
moderation_snapshot, transition_snapshot, presentation_snapshot,
moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
)
.run(
id,
@@ -1197,6 +1202,7 @@ export function addPieceUsage(
piece.intro,
piece.research_text,
piece.moderation_notes,
piece.interaction_text,
piece.transition_text,
piece.presentation_text,
piece.presentation_mode,
@@ -1284,7 +1290,7 @@ export function duplicatePieceUsage(
const source = database
.prepare(
`SELECT original_piece_id, status, duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot,
research_snapshot, moderation_snapshot, transition_snapshot, presentation_snapshot,
research_snapshot, moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
@@ -1307,13 +1313,13 @@ export function duplicatePieceUsage(
.prepare(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot, research_snapshot,
moderation_snapshot, transition_snapshot, presentation_snapshot,
moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
id,
@@ -1326,6 +1332,7 @@ export function duplicatePieceUsage(
source.intro_snapshot,
source.research_snapshot,
source.moderation_snapshot,
source.interaction_snapshot,
source.transition_snapshot,
source.presentation_snapshot,
source.presentation_mode_snapshot,
@@ -1360,7 +1367,7 @@ export function refreshPieceUsageFromOriginal(
const source = database
.prepare(
`SELECT p.title, p.output_title, p.duration_seconds, p.intro, p.research_text,
p.moderation_notes, p.transition_text, p.presentation_text, p.presentation_mode,
p.moderation_notes, p.interaction_text, p.transition_text, p.presentation_text, p.presentation_mode,
COALESCE(ps.label, '') AS presentation_source_label,
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
@@ -1379,6 +1386,7 @@ export function refreshPieceUsageFromOriginal(
intro: string;
research_text: string;
moderation_notes: string;
interaction_text: string;
transition_text: string;
presentation_text: string;
presentation_mode: PresentationMode;
@@ -1397,7 +1405,7 @@ export function refreshPieceUsageFromOriginal(
database
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, duration_seconds = ?, intro_snapshot = ?,
research_snapshot = ?, moderation_snapshot = ?, transition_snapshot = ?,
research_snapshot = ?, moderation_snapshot = ?, interaction_snapshot = ?, transition_snapshot = ?,
presentation_snapshot = ?, presentation_mode_snapshot = ?,
presentation_source_label_snapshot = ?, presentation_source_url_snapshot = ?,
presentation_source_timecode_snapshot = ?, presentation_title_meta_snapshot = ?,
@@ -1412,6 +1420,7 @@ export function refreshPieceUsageFromOriginal(
source.intro,
source.research_text,
source.moderation_notes,
source.interaction_text,
source.transition_text,
source.presentation_text,
source.presentation_mode,
@@ -1452,6 +1461,8 @@ export function updatePieceUsage(
intro: cleanText(input.intro, 50_000, 'intro'),
research: cleanText(input.research, 50_000, 'research'),
moderation: cleanText(input.moderation, 50_000, 'moderation'),
interaction:
input.interaction === undefined ? null : cleanText(input.interaction, 50_000, 'interaction'),
transition: cleanText(input.transition, 50_000, 'transition'),
presentation: cleanText(input.presentation, 50_000, 'presentation'),
ownNotes: cleanText(input.ownNotes, 50_000, 'notes')
@@ -1473,7 +1484,7 @@ export function updatePieceUsage(
database
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, intro_snapshot = ?, research_snapshot = ?,
moderation_snapshot = ?, transition_snapshot = ?, presentation_snapshot = ?,
moderation_snapshot = ?, interaction_snapshot = COALESCE(?, interaction_snapshot), transition_snapshot = ?, presentation_snapshot = ?,
presentation_mode_snapshot = ?, presentation_source_label_snapshot = ?,
presentation_source_url_snapshot = ?, presentation_source_timecode_snapshot = ?,
presentation_title_meta_snapshot = ?, presentation_description_snapshot = ?,
@@ -1487,6 +1498,7 @@ export function updatePieceUsage(
values.intro,
values.research,
values.moderation,
values.interaction,
values.transition,
values.presentation,
presentation.mode,