Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/web/src/components/ImportShowFlowModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Each item in the \`items\` array must follow this format:
"id": "unique-id-here",
"type": "item",
"itemNumber": "1",
"startTime": "00:00:00",
"startTime": "19:00:00", // Actual show time (e.g., 7:00 PM)
"preset": "Scene or preset name",
"duration": "00:30",
"privateNotes": "Internal notes not shown to audience",
Expand All @@ -68,7 +68,7 @@ Each item in the \`items\` array must follow this format:
"id": "unique-id-here",
"type": "header",
"itemNumber": "", // Leave empty for headers
"startTime": "00:00:00", // Optional for headers
"startTime": "19:00:00", // Actual show time when section starts
"headerTitle": "Section Name",
"highlightColor": "#0000FF" // Optional, but recommended for visual organization
}
Expand All @@ -79,7 +79,7 @@ Each item in the \`items\` array must follow this format:
- **id**: Generate a unique identifier for each item (e.g., "item-1", "header-1", or UUID)
- **type**: Either "item" for regular entries or "header" for section dividers
- **itemNumber**: Sequential number for items (string). Leave empty ("") for headers
- **startTime**: Time in HH:MM:SS format (e.g., "00:15:30"). Calculate based on previous items
- **startTime**: The actual absolute time when this item/header starts in HH:MM:SS format (e.g., "19:30:00" for 7:30 PM). This is NOT relative to show start - it's the real wall-clock time
- **duration**: Duration in MM:SS format (e.g., "05:30" for 5 minutes 30 seconds)
- **preset**: The scene, preset, or segment name (descriptive and clear)
- **privateNotes**: Internal notes for operators (technical details, warnings, reminders)
Expand Down Expand Up @@ -116,7 +116,7 @@ For specialized productions, add custom fields:

## Conversion Guidelines:

1. **Time Calculation**: Start at "00:00:00" and calculate each subsequent startTime by adding the previous item's duration
1. **Time Calculation**: Use actual absolute wall-clock times for all start times. If the show starts at 7:00 PM (19:00), the first item should be "19:00:00", not "00:00:00". Calculate each subsequent startTime by adding the previous item's duration to its start time, plus any intentional gaps/buffers
2. **Section Organization**: Use headers to break the show into logical sections
3. **Color Coding**: Use highlight colors to help operators quickly identify different types of content
4. **Technical Details**: Include specific technical information that operators would need:
Expand Down Expand Up @@ -148,15 +148,15 @@ Q&A session
"id": "header-1",
"type": "header",
"itemNumber": "",
"startTime": "00:00:00",
"startTime": "18:30:00",
"headerTitle": "Pre-Show",
"highlightColor": "#4A90E2"
},
{
"id": "item-1",
"type": "item",
"itemNumber": "1",
"startTime": "00:00:00",
"startTime": "18:30:00",
"preset": "Walk-in Music & House Open",
"duration": "30:00",
"privateNotes": "Loop playlist, check all wireless mics",
Expand All @@ -169,7 +169,7 @@ Q&A session
"id": "item-2",
"type": "item",
"itemNumber": "2",
"startTime": "00:30:00",
"startTime": "19:00:00",
"preset": "CEO Opening Speech",
"duration": "10:00",
"privateNotes": "CEO enters from stage left, mic check complete",
Expand Down
160 changes: 140 additions & 20 deletions apps/web/src/pages/RunOfShowEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,32 +519,152 @@ const RunOfShowEditor: React.FC = () => {
if (targetIndex >= items.length) return;
}

// Swap items
[items[currentIndex], items[targetIndex]] = [items[targetIndex], items[currentIndex]];
// Step 1: Calculate gaps BEFORE reordering
// This captures the gap each item has relative to its IMMEDIATE previous item
const itemsWithGaps = items.map((item, index) => {
let gap = 0;

// Recalculate item numbers and start times
let itemCount = 1;
let cumulativeTimeSeconds = 0;

const recalculatedItems = items.map((item) => {
if (item.type === "item") {
// Update item number
item.itemNumber = itemCount.toString();
itemCount++;
const itemStartTime = parseTimeToSeconds(item.startTime || "");

if (itemStartTime === null) {
// Without a valid start time we cannot compute a reliable gap
return { ...item, calculatedGap: 0 };
}

// Walk backwards to find the previous item's end time, skipping headers
let prevEndTime: number | null = null;
for (let i = index - 1; i >= 0; i--) {
const prev = items[i];
if (prev.type !== "item") continue;

const prevStart = parseTimeToSeconds(prev.startTime || "");
const prevDuration = parseDurationToSeconds(prev.duration || "");

if (prevStart !== null) {
prevEndTime = prevStart + (prevDuration ?? 0);
break;
}

// Update start time based on cumulative time
item.startTime = formatSecondsToTime(cumulativeTimeSeconds);
// If no start, but we have duration, keep looking further back
// However, don't add duration without a known anchor start.
if (prevStart === null) continue;
}

// Add this item's duration to cumulative time
const durationSeconds = parseDurationToSeconds(item.duration || "");
if (durationSeconds !== null) {
cumulativeTimeSeconds += durationSeconds;
if (prevEndTime !== null) {
gap = itemStartTime - prevEndTime;
if (gap < 0) gap = 0;
} else {
// No previous item with a valid start; treat as no gap to preserve
gap = 0;
}
} else if (item.type === "header") {
// Headers can have a start time matching the next item
item.startTime = formatSecondsToTime(cumulativeTimeSeconds);
}
return item;

return { ...item, calculatedGap: gap };
});

// Step 2: Swap items
[itemsWithGaps[currentIndex], itemsWithGaps[targetIndex]] = [
itemsWithGaps[targetIndex],
itemsWithGaps[currentIndex],
];

// Establish initial cumulative time from earliest anchored start (header or item)
const firstAnchoredStart = (() => {
for (const it of itemsWithGaps) {
const t = parseTimeToSeconds(it.startTime || "");
if (t !== null) return t;
}
return 0;
})();

// Step 3: Recalculate item numbers and start times using stored gaps
let itemCount = 1;
let cumulativeEndTimeSeconds = firstAnchoredStart;

const recalculatedItems = itemsWithGaps.map((item) => {
if (item.type === "header") {
const existingStartTime = parseTimeToSeconds(item.startTime || "");
let newHeaderStart: string;

if (existingStartTime !== null) {
// Advance pointer to header time if it's ahead, clamp if behind
if (existingStartTime > cumulativeEndTimeSeconds) {
cumulativeEndTimeSeconds = existingStartTime;
newHeaderStart = item.startTime!;
} else {
newHeaderStart = formatSecondsToTime(cumulativeEndTimeSeconds);
}
} else {
newHeaderStart = formatSecondsToTime(cumulativeEndTimeSeconds);
}

// Return header without calculatedGap property
return {
id: item.id,
type: item.type,
itemNumber: item.itemNumber,
startTime: newHeaderStart,
highlightColor: item.highlightColor,
headerTitle: item.headerTitle,
} as RunOfShowItem;
}

// Items - extract gap and other properties
const itemWithGap = item as RunOfShowItem & { calculatedGap?: number };
const gap = itemWithGap.calculatedGap || 0;
const newStartTime = cumulativeEndTimeSeconds + gap;
const durationSeconds = parseDurationToSeconds(item.duration || "");

itemCount++;
cumulativeEndTimeSeconds = newStartTime;
if (durationSeconds !== null) {
cumulativeEndTimeSeconds += durationSeconds;
}

// Return item without calculatedGap property
return {
id: item.id,
type: item.type,
itemNumber: itemCount.toString(),
startTime: formatSecondsToTime(newStartTime),
highlightColor: item.highlightColor,
preset: item.preset,
duration: item.duration,
privateNotes: item.privateNotes,
productionNotes: item.productionNotes,
audio: item.audio,
video: item.video,
lights: item.lights,
// Include custom column values
...Object.keys(item)
.filter(
(key) =>
![
"id",
"type",
"itemNumber",
"startTime",
"highlightColor",
"preset",
"duration",
"privateNotes",
"productionNotes",
"audio",
"video",
"lights",
"calculatedGap",
"headerTitle",
].includes(key),
)
.reduce(
(acc, key) => {
acc[key] = item[key as keyof RunOfShowItem];
return acc;
},
{} as Record<string, string | number | boolean | undefined>,
),
} as RunOfShowItem;
});

setRunOfShow({ ...runOfShow, items: recalculatedItems });
Expand Down