diff --git a/src/database/calendar-integrations.js b/src/database/calendar-integrations.js
index 07027647..0c3f1bdd 100644
--- a/src/database/calendar-integrations.js
+++ b/src/database/calendar-integrations.js
@@ -163,6 +163,64 @@ async function upsertExternalEvent({ channelId, showId, integrationId, provider,
});
}
+function buildGoogleCalendarUrl(calendarId) {
+ return `https://calendar.google.com/calendar/u/0/r?cid=${encodeURIComponent(calendarId)}`;
+}
+
+function buildGoogleEventUrl(eventId, calendarId) {
+ const raw = `${eventId} ${calendarId}`;
+ const eid = Buffer.from(raw, 'utf8')
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/g, '');
+ return `https://calendar.google.com/calendar/u/0/r/eventedit/${eid}`;
+}
+
+async function getGoogleLinksForShows(channelId, showIds) {
+ if (!Array.isArray(showIds) || showIds.length === 0) {
+ return {};
+ }
+
+ const integration = await getByChannelProvider(channelId, 'google');
+ if (!integration || integration.status !== 'connected' || !integration.last_sync_at) {
+ return {};
+ }
+
+ const calendarId = integration.config && integration.config.calendar_id
+ ? String(integration.config.calendar_id).trim()
+ : '';
+ if (!calendarId) {
+ return {};
+ }
+
+ const rows = await knex()('channel_show_external_events')
+ .where({
+ channel_id: channelId,
+ integration_id: integration.id,
+ provider: 'google'
+ })
+ .whereIn('show_id', showIds)
+ .select('show_id', 'external_event_id');
+
+ const calendarUrl = buildGoogleCalendarUrl(calendarId);
+ const links = {};
+ showIds.forEach(showId => {
+ links[showId] = { calendar_url: calendarUrl };
+ });
+
+ rows.forEach(row => {
+ if (!links[row.show_id]) {
+ links[row.show_id] = { calendar_url: calendarUrl };
+ }
+ if (row.external_event_id) {
+ links[row.show_id].event_url = buildGoogleEventUrl(row.external_event_id, calendarId);
+ }
+ });
+
+ return links;
+}
+
module.exports = {
listByChannel,
getByChannelProvider,
@@ -170,5 +228,6 @@ module.exports = {
disconnectIntegration,
updateIntegrationSyncResult,
getExternalEvent,
- upsertExternalEvent
+ upsertExternalEvent,
+ getGoogleLinksForShows
};
diff --git a/src/database/shows.js b/src/database/shows.js
index a588054d..37c56647 100644
--- a/src/database/shows.js
+++ b/src/database/shows.js
@@ -31,6 +31,7 @@ function parseShowRow(row) {
playlist,
timezone: row.timezone,
scheduled_for: row.scheduled_for,
+ estimated_end_at: row.estimated_end_at || null,
next_run_at: row.next_run_at,
status: row.status,
recurrence: row.recurrence,
@@ -56,6 +57,7 @@ function serializeShowInput(input) {
playlist: JSON.stringify(input.playlist || []),
timezone: input.timezone,
scheduled_for: input.scheduled_for,
+ estimated_end_at: input.estimated_end_at || null,
next_run_at: input.next_run_at,
status: input.status,
recurrence: input.recurrence,
diff --git a/src/database/tables.js b/src/database/tables.js
index e4794d24..62318384 100644
--- a/src/database/tables.js
+++ b/src/database/tables.js
@@ -189,6 +189,7 @@ export async function initTables() {
t.specificType('playlist', 'mediumtext character set utf8mb4 not null');
t.string('timezone', 64).notNullable().defaultTo('UTC');
t.bigInteger('scheduled_for').notNullable();
+ t.bigInteger('estimated_end_at').nullable();
t.bigInteger('next_run_at').notNullable();
t.string('status', 20).notNullable().defaultTo('draft');
t.string('recurrence', 20).notNullable().defaultTo('none');
diff --git a/src/database/update.js b/src/database/update.js
index 8e7bb848..7fb8dff7 100644
--- a/src/database/update.js
+++ b/src/database/update.js
@@ -3,7 +3,7 @@ import Promise from 'bluebird';
const LOGGER = require('@calzoneman/jsli')('database/update');
-const DB_VERSION = 15;
+const DB_VERSION = 16;
var hasUpdates = [];
module.exports.checkVersion = function () {
@@ -59,6 +59,8 @@ function update(version, cb) {
addCalendarIntegrationTables(cb);
} else if (version < 15) {
addCalendarIntegrationAuditColumns(cb);
+ } else if (version < 16) {
+ addShowsEstimatedEndColumn(cb);
}
}
@@ -258,3 +260,17 @@ function addCalendarIntegrationAuditColumns(cb) {
}
);
}
+
+function addShowsEstimatedEndColumn(cb) {
+ db.query(
+ "ALTER TABLE channel_shows ADD COLUMN estimated_end_at BIGINT NULL",
+ error => {
+ if (error) {
+ LOGGER.error(`Failed to add shows estimated_end_at column: ${error}`);
+ cb(error);
+ return;
+ }
+ cb();
+ }
+ );
+}
diff --git a/src/integrations/google-calendar.js b/src/integrations/google-calendar.js
index 7d22d9f9..cd7e4b44 100644
--- a/src/integrations/google-calendar.js
+++ b/src/integrations/google-calendar.js
@@ -205,7 +205,8 @@ function unpackTokens(integrationRow) {
async function upsertGoogleCalendarEvent(accessToken, calendarId, show) {
const start = new Date(show.scheduled_for);
- const end = new Date(start.getTime() + 60 * 60 * 1000);
+ const endMs = Number(show.estimated_end_at || 0);
+ const end = new Date(endMs > start.getTime() ? endMs : (start.getTime() + 60 * 60 * 1000));
const body = {
summary: show.name,
description: show.notes || '',
@@ -230,7 +231,8 @@ async function upsertGoogleCalendarEvent(accessToken, calendarId, show) {
async function updateGoogleCalendarEvent(accessToken, calendarId, eventId, show) {
const start = new Date(show.scheduled_for);
- const end = new Date(start.getTime() + 60 * 60 * 1000);
+ const endMs = Number(show.estimated_end_at || 0);
+ const end = new Date(endMs > start.getTime() ? endMs : (start.getTime() + 60 * 60 * 1000));
const body = {
summary: show.name,
description: show.notes || '',
diff --git a/src/util/markdown.js b/src/util/markdown.js
new file mode 100644
index 00000000..b44ac492
--- /dev/null
+++ b/src/util/markdown.js
@@ -0,0 +1,89 @@
+const XSS = require('../xss');
+
+function escapeHtml(text) {
+ return String(text || '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+function applyInlineMarkdown(text) {
+ let out = escapeHtml(text);
+ out = out.replace(/`([^`]+)`/g, '$1');
+ out = out.replace(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g, '
');
+ out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1');
+ out = out.replace(/\*\*([^*]+)\*\*/g, '$1');
+ out = out.replace(/__([^_]+)__/g, '$1');
+ out = out.replace(/\*([^*]+)\*/g, '$1');
+ out = out.replace(/_([^_]+)_/g, '$1');
+ return out;
+}
+
+function markdownToHtml(markdown) {
+ const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
+ const blocks = [];
+ let paragraph = [];
+ let listItems = [];
+
+ function flushParagraph() {
+ if (!paragraph.length) return;
+ const rendered = applyInlineMarkdown(paragraph.join('\n')).replace(/\n/g, '
');
+ blocks.push('
' + rendered + '
');
+ paragraph = [];
+ }
+
+ function flushList() {
+ if (!listItems.length) return;
+ blocks.push('' + listItems.map(item => '- ' + applyInlineMarkdown(item) + '
').join('') + '
');
+ listItems = [];
+ }
+
+ lines.forEach(raw => {
+ const line = raw.trim();
+ if (!line) {
+ flushParagraph();
+ flushList();
+ return;
+ }
+
+ if (/^[-*]\s+/.test(line)) {
+ flushParagraph();
+ listItems.push(line.replace(/^[-*]\s+/, ''));
+ return;
+ }
+
+ if (/^#{1,3}\s+/.test(line)) {
+ flushParagraph();
+ flushList();
+ const level = line.match(/^#{1,3}/)[0].length;
+ const content = line.replace(/^#{1,3}\s+/, '');
+ blocks.push('' + applyInlineMarkdown(content) + '');
+ return;
+ }
+
+ paragraph.push(raw);
+ });
+
+ flushParagraph();
+ flushList();
+
+ return blocks.join('');
+}
+
+function looksLikeHtml(input) {
+ return /<\/?[a-z][\s\S]*>/i.test(input || '');
+}
+
+function renderNotesHtml(input) {
+ const notes = String(input || '').trim();
+ if (!notes) return null;
+
+ const html = looksLikeHtml(notes) ? notes : markdownToHtml(notes);
+ return XSS.sanitizeHTML(html);
+}
+
+module.exports = {
+ renderNotesHtml
+};
diff --git a/src/web/routes/api/shows.js b/src/web/routes/api/shows.js
index 313120d3..f60290a7 100644
--- a/src/web/routes/api/shows.js
+++ b/src/web/routes/api/shows.js
@@ -1,10 +1,11 @@
const express = require('express');
const webserver = require('../../webserver');
const showDB = require('../../../database/shows');
+const calendarDB = require('../../../database/calendar-integrations');
const shows = require('../../../shows');
const botDB = require('../../../database/bots');
const infoGetter = require('../../../get-info');
-const XSS = require('../../../xss');
+const { renderNotesHtml } = require('../../../util/markdown');
const { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware');
const router = express.Router({ mergeParams: true });
@@ -22,6 +23,25 @@ const ACTION_MIN_RANK = {
};
const PUBLIC_SHOW_STATUSES = new Set(['scheduled', 'running', 'paused', 'completed']);
+async function attachGoogleCalendarLinks(channelId, showsList) {
+ const ids = (showsList || []).map(show => show.id);
+ const linksByShowId = await calendarDB.getGoogleLinksForShows(channelId, ids);
+ return (showsList || []).map(show => {
+ const withNotesHtml = Object.assign({}, show, {
+ notes_html: renderNotesHtml(show.notes)
+ });
+ const googleLinks = linksByShowId[show.id];
+ if (!googleLinks) {
+ return withNotesHtml;
+ }
+ return Object.assign({}, withNotesHtml, {
+ calendar_links: {
+ google: googleLinks
+ }
+ });
+ });
+}
+
function sanitizePlaylist(list) {
if (!Array.isArray(list)) return [];
return list
@@ -69,6 +89,20 @@ function validateShowPayload(body, old = null) {
if (!scheduledFor) {
return { error: 'scheduled_for must be a valid date or timestamp' };
}
+ const estimatedEndInput = body.estimated_end_at !== undefined
+ ? body.estimated_end_at
+ : (old ? old.estimated_end_at : null);
+ const estimatedEndAt = estimatedEndInput === null || estimatedEndInput === ''
+ ? null
+ : (typeof estimatedEndInput === 'number' ? estimatedEndInput : parseSchedule(estimatedEndInput));
+ if (estimatedEndAt !== null) {
+ if (!estimatedEndAt) {
+ return { error: 'estimated_end_at must be a valid date or timestamp' };
+ }
+ if (estimatedEndAt < scheduledFor) {
+ return { error: 'estimated_end_at must be later than scheduled_for' };
+ }
+ }
const recurrence = String(body.recurrence || (old && old.recurrence) || 'none');
if (!RECURRENCES.has(recurrence)) {
@@ -105,7 +139,7 @@ function validateShowPayload(body, old = null) {
const notesRaw = body.notes !== undefined ? body.notes : (old ? old.notes : null);
let notes = null;
if (typeof notesRaw === 'string' && notesRaw.trim() !== '') {
- notes = XSS.sanitizeHTML(notesRaw.substring(0, 20000));
+ notes = notesRaw.substring(0, 20000).trim();
}
const colorRaw = body.color !== undefined ? body.color : (old ? old.color : null);
@@ -126,6 +160,7 @@ function validateShowPayload(body, old = null) {
playlist,
timezone,
scheduled_for: scheduledFor,
+ estimated_end_at: estimatedEndAt,
next_run_at: nextRunAt,
status,
recurrence,
@@ -198,7 +233,8 @@ router.get('/', async (req, res) => {
if (!auth) return;
const showsList = await showDB.listShows(auth.channelRow.id);
- res.json(showsList);
+ const withLinks = await attachGoogleCalendarLinks(auth.channelRow.id, showsList);
+ res.json(withLinks);
});
router.get('/public', async (req, res) => {
@@ -210,7 +246,9 @@ router.get('/public', async (req, res) => {
}
const showsList = await showDB.listShows(channelRow.id);
- res.json(showsList.filter(show => PUBLIC_SHOW_STATUSES.has(show.status)));
+ const filtered = showsList.filter(show => PUBLIC_SHOW_STATUSES.has(show.status));
+ const withLinks = await attachGoogleCalendarLinks(channelRow.id, filtered);
+ res.json(withLinks);
});
router.get('/:id', async (req, res) => {
@@ -221,7 +259,9 @@ router.get('/:id', async (req, res) => {
if (isNaN(id)) return res.status(400).json({ error: 'Invalid show id' });
const show = await showDB.getShowById(id, auth.channelRow.id);
if (!show) return res.status(404).json({ error: 'Show not found' });
- res.json(show);
+ res.json(Object.assign({}, show, {
+ notes_html: renderNotesHtml(show.notes)
+ }));
});
router.post('/resolve-media', async (req, res) => {
@@ -278,7 +318,9 @@ router.post('/', async (req, res) => {
});
const row = await showDB.getShowById(id, auth.channelRow.id);
- res.status(201).json(row);
+ res.status(201).json(Object.assign({}, row, {
+ notes_html: renderNotesHtml(row.notes)
+ }));
});
router.put('/:id', async (req, res) => {
@@ -304,7 +346,9 @@ router.put('/:id', async (req, res) => {
});
const row = await showDB.getShowById(id, auth.channelRow.id);
- res.json(row);
+ res.json(Object.assign({}, row, {
+ notes_html: renderNotesHtml(row.notes)
+ }));
});
router.delete('/:id', async (req, res) => {
@@ -394,7 +438,9 @@ router.post('/:id/action', async (req, res) => {
}
const row = await showDB.getShowById(id, auth.channelRow.id);
- res.json(row);
+ res.json(Object.assign({}, row, {
+ notes_html: renderNotesHtml(row.notes)
+ }));
});
module.exports = router;
diff --git a/templates/channel.pug b/templates/channel.pug
index 3ccc564f..c56030d0 100644
--- a/templates/channel.pug
+++ b/templates/channel.pug
@@ -272,10 +272,18 @@ html(lang="en")
strong Time:
|
span#showdetails-time
+ p
+ strong Estimated End:
+ |
+ span#showdetails-estimated-end
p
strong Status:
|
span#showdetails-status
+ p#showdetails-calendar-links(style="display:none;")
+ strong Calendar:
+ |
+ span#showdetails-calendar-links-content
hr
#showdetails-notes
.modal-footer
diff --git a/templates/channeloptions.pug b/templates/channeloptions.pug
index e994bb22..9859adb4 100644
--- a/templates/channeloptions.pug
+++ b/templates/channeloptions.pug
@@ -267,6 +267,10 @@ mixin shows
label.control-label.col-sm-3(for="cs-shows-scheduled-for") Scheduled For
.col-sm-9
input#cs-shows-scheduled-for.form-control(type="datetime-local")
+ .form-group
+ label.control-label.col-sm-3(for="cs-shows-estimated-end-at") Estimated End
+ .col-sm-9
+ input#cs-shows-estimated-end-at.form-control(type="datetime-local")
.form-group
label.control-label.col-sm-3(for="cs-shows-color") Color
.col-sm-3
@@ -305,8 +309,10 @@ mixin shows
.form-group
label.control-label.col-sm-3(for="cs-shows-notes") Notes
.col-sm-9
- textarea#cs-shows-notes.form-control(rows="6", placeholder="Optional rich notes (same HTML support as MOTD)")
- p.text-muted.small(style="margin-top:6px") Supports MOTD-style HTML. Optional.
+ button#cs-shows-notes-toggle.btn.btn-xs.btn-default.pull-right(type="button", style="margin-bottom:6px") Preview
+ textarea#cs-shows-notes.form-control(rows="8", placeholder="Optional Markdown notes")
+ #cs-shows-notes-rendered.form-control(style="display:none; height:auto; min-height:170px; overflow:auto;")
+ p#cs-shows-notes-help.text-muted.small(style="margin-top:6px") Supports Markdown: headings, lists, bold, italics, inline code, images, and links.
.form-group
label.control-label.col-sm-3(for="cs-shows-mediaurl") Show Playlist
.col-sm-9
@@ -329,8 +335,10 @@ mixin shows
th Name
th Status
th Next Run
+ th Est. End
th Timezone
th Recurrence
+ th Calendar
th Actions
tbody#cs-shows-list
diff --git a/www/css/cytube.css b/www/css/cytube.css
index 01836152..6fbc0b33 100644
--- a/www/css/cytube.css
+++ b/www/css/cytube.css
@@ -242,6 +242,11 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
padding: 2px !important;
}
+.showschedule-block-cell {
+ border-top: 2px solid rgba(0, 0, 0, 0.35) !important;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.35) !important;
+}
+
.showschedule-cell.showschedule-admin {
cursor: pointer;
}
@@ -258,6 +263,40 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
text-decoration: none;
}
+.showschedule-block-cell .showschedule-show {
+ font-weight: 700;
+ margin-bottom: 4px;
+}
+
+.showschedule-notes {
+ margin-top: 2px;
+ max-height: 72px;
+ overflow: hidden;
+ line-height: 1.2;
+ color: rgba(255, 255, 255, 0.95);
+}
+
+.showschedule-notes p,
+.showschedule-notes ul,
+.showschedule-notes ol {
+ margin: 0 0 4px 0;
+}
+
+.showschedule-notes img {
+ display: block;
+ max-width: 100%;
+ max-height: 42px;
+ width: auto;
+ height: auto;
+ border-radius: 2px;
+ margin: 2px 0;
+}
+
+.showschedule-notes a {
+ color: #fff;
+ text-decoration: underline;
+}
+
.showschedule-show.status-scheduled { background: #337ab7; }
.showschedule-show.status-running { background: #5cb85c; }
.showschedule-show.status-paused { background: #f0ad4e; color: #222; }
diff --git a/www/js/ui.js b/www/js/ui.js
index c2e7b081..ac654747 100644
--- a/www/js/ui.js
+++ b/www/js/ui.js
@@ -1436,6 +1436,7 @@ var CSTShows = (function () {
var resolvingTitles = false;
var weekOffset = 0;
var cachedShows = [];
+ var notesEditorMode = 'edit';
function apiBase() {
return '/api/v1/channels/' + CHANNEL.name + '/shows';
@@ -1487,6 +1488,121 @@ var CSTShows = (function () {
pad(d.getMinutes());
}
+ function escapeHtml(text) {
+ return String(text || '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function renderMarkdownNotesPreview(markdown) {
+ var lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
+ var blocks = [];
+ var paragraph = [];
+ var listItems = [];
+
+ function inline(text) {
+ var out = escapeHtml(text || '');
+ out = out.replace(/`([^`]+)`/g, '$1');
+ out = out.replace(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g, '
');
+ out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1');
+ out = out.replace(/\*\*([^*]+)\*\*/g, '$1');
+ out = out.replace(/__([^_]+)__/g, '$1');
+ out = out.replace(/\*([^*]+)\*/g, '$1');
+ out = out.replace(/_([^_]+)_/g, '$1');
+ return out;
+ }
+
+ function flushParagraph() {
+ if (!paragraph.length) return;
+ var rendered = inline(paragraph.join('\n')).replace(/\n/g, '
');
+ blocks.push('' + rendered + '
');
+ paragraph = [];
+ }
+
+ function flushList() {
+ if (!listItems.length) return;
+ blocks.push('' + listItems.map(function (item) { return '- ' + inline(item) + '
'; }).join('') + '
');
+ listItems = [];
+ }
+
+ lines.forEach(function (raw) {
+ var line = raw.trim();
+ if (!line) {
+ flushParagraph();
+ flushList();
+ return;
+ }
+ if (/^[-*]\s+/.test(line)) {
+ flushParagraph();
+ listItems.push(line.replace(/^[-*]\s+/, ''));
+ return;
+ }
+ if (/^#{1,3}\s+/.test(line)) {
+ flushParagraph();
+ flushList();
+ var level = line.match(/^#{1,3}/)[0].length;
+ blocks.push('' + inline(line.replace(/^#{1,3}\s+/, '')) + '');
+ return;
+ }
+ paragraph.push(raw);
+ });
+ flushParagraph();
+ flushList();
+ return blocks.join('');
+ }
+
+ function updateNotesPreview() {
+ var notes = $('#cs-shows-notes').val() || '';
+ if (!/\S/.test(notes)) {
+ $('#cs-shows-notes-rendered').html('Nothing to preview.');
+ return;
+ }
+ $('#cs-shows-notes-rendered').html(renderMarkdownNotesPreview(notes));
+ }
+
+ function setNotesEditorMode(mode) {
+ notesEditorMode = mode === 'preview' ? 'preview' : 'edit';
+ if (notesEditorMode === 'preview') {
+ $('#cs-shows-notes').hide();
+ $('#cs-shows-notes-rendered').show();
+ $('#cs-shows-notes-help').text('Preview mode');
+ $('#cs-shows-notes-toggle').text('Edit');
+ updateNotesPreview();
+ return;
+ }
+ $('#cs-shows-notes-rendered').hide();
+ $('#cs-shows-notes').show();
+ $('#cs-shows-notes-help').text('Supports Markdown: headings, lists, bold, italics, inline code, images, and links.');
+ $('#cs-shows-notes-toggle').text('Preview');
+ }
+
+ function getShowOccurrenceEndMs(show, startMs) {
+ if (!show || !show.estimated_end_at || !show.scheduled_for) {
+ return startMs + 60 * 60 * 1000;
+ }
+ var baseStart = Number(show.scheduled_for);
+ var baseEnd = Number(show.estimated_end_at);
+ var duration = baseEnd - baseStart;
+ if (!isFinite(duration) || duration <= 0) {
+ return startMs + 60 * 60 * 1000;
+ }
+ return startMs + duration;
+ }
+
+ function getShowBlockColor(show) {
+ if (show && show.color) {
+ return show.color;
+ }
+ var status = (show && show.status) || 'scheduled';
+ if (status === 'running') return '#5cb85c';
+ if (status === 'paused') return '#f0ad4e';
+ if (status === 'completed') return '#777777';
+ return '#337ab7';
+ }
+
function renderDraftPlaylist() {
var ul = $('#cs-shows-playlist-list').empty();
if (!draftPlaylist.length) {
@@ -1627,6 +1743,7 @@ var CSTShows = (function () {
function readFormPayload() {
var scheduledRaw = $('#cs-shows-scheduled-for').val();
+ var estimatedEndRaw = $('#cs-shows-estimated-end-at').val();
var timezone = $('#cs-shows-timezone').val().trim();
var notes = $('#cs-shows-notes').val();
var colorHex = ($('#cs-shows-color-hex').val() || '').trim();
@@ -1644,6 +1761,7 @@ var CSTShows = (function () {
notes: notes && notes.trim() ? notes : null,
color: colorHex ? colorHex.toUpperCase() : null,
scheduled_for: scheduledRaw ? new Date(scheduledRaw).toISOString() : null,
+ estimated_end_at: estimatedEndRaw ? new Date(estimatedEndRaw).toISOString() : null,
timezone: timezone,
recurrence: $('#cs-shows-recurrence').val(),
fill_mode: $('#cs-shows-fill-mode').val(),
@@ -1662,6 +1780,7 @@ var CSTShows = (function () {
$('#cs-shows-name').val('');
$('#cs-shows-notes').val('');
$('#cs-shows-scheduled-for').val('');
+ $('#cs-shows-estimated-end-at').val('');
var detectedTz = 'UTC';
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
@@ -1679,6 +1798,8 @@ var CSTShows = (function () {
$('#cs-shows-mediaurl').val('');
draftPlaylist = [];
renderDraftPlaylist();
+ updateNotesPreview();
+ setNotesEditorMode('edit');
}
function selectShow(show) {
@@ -1687,6 +1808,7 @@ var CSTShows = (function () {
$('#cs-shows-name').val(show.name);
$('#cs-shows-notes').val(show.notes || '');
$('#cs-shows-scheduled-for').val(toLocalDateInput(show.scheduled_for));
+ $('#cs-shows-estimated-end-at').val(toLocalDateInput(show.estimated_end_at));
var showTz = show.timezone || 'UTC';
if ($('#cs-shows-timezone option[value="' + showTz + '"]').length === 0) {
$('