mirror of
https://github.com/Spengreb/sync.git
synced 2026-06-10 15:22:04 +00:00
Add many UX improvements to channel schedule
This commit is contained in:
parent
4ec1e83337
commit
dd1bf9d55b
11 changed files with 571 additions and 31 deletions
|
|
@ -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 = {
|
module.exports = {
|
||||||
listByChannel,
|
listByChannel,
|
||||||
getByChannelProvider,
|
getByChannelProvider,
|
||||||
|
|
@ -170,5 +228,6 @@ module.exports = {
|
||||||
disconnectIntegration,
|
disconnectIntegration,
|
||||||
updateIntegrationSyncResult,
|
updateIntegrationSyncResult,
|
||||||
getExternalEvent,
|
getExternalEvent,
|
||||||
upsertExternalEvent
|
upsertExternalEvent,
|
||||||
|
getGoogleLinksForShows
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ function parseShowRow(row) {
|
||||||
playlist,
|
playlist,
|
||||||
timezone: row.timezone,
|
timezone: row.timezone,
|
||||||
scheduled_for: row.scheduled_for,
|
scheduled_for: row.scheduled_for,
|
||||||
|
estimated_end_at: row.estimated_end_at || null,
|
||||||
next_run_at: row.next_run_at,
|
next_run_at: row.next_run_at,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
recurrence: row.recurrence,
|
recurrence: row.recurrence,
|
||||||
|
|
@ -56,6 +57,7 @@ function serializeShowInput(input) {
|
||||||
playlist: JSON.stringify(input.playlist || []),
|
playlist: JSON.stringify(input.playlist || []),
|
||||||
timezone: input.timezone,
|
timezone: input.timezone,
|
||||||
scheduled_for: input.scheduled_for,
|
scheduled_for: input.scheduled_for,
|
||||||
|
estimated_end_at: input.estimated_end_at || null,
|
||||||
next_run_at: input.next_run_at,
|
next_run_at: input.next_run_at,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
recurrence: input.recurrence,
|
recurrence: input.recurrence,
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ export async function initTables() {
|
||||||
t.specificType('playlist', 'mediumtext character set utf8mb4 not null');
|
t.specificType('playlist', 'mediumtext character set utf8mb4 not null');
|
||||||
t.string('timezone', 64).notNullable().defaultTo('UTC');
|
t.string('timezone', 64).notNullable().defaultTo('UTC');
|
||||||
t.bigInteger('scheduled_for').notNullable();
|
t.bigInteger('scheduled_for').notNullable();
|
||||||
|
t.bigInteger('estimated_end_at').nullable();
|
||||||
t.bigInteger('next_run_at').notNullable();
|
t.bigInteger('next_run_at').notNullable();
|
||||||
t.string('status', 20).notNullable().defaultTo('draft');
|
t.string('status', 20).notNullable().defaultTo('draft');
|
||||||
t.string('recurrence', 20).notNullable().defaultTo('none');
|
t.string('recurrence', 20).notNullable().defaultTo('none');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Promise from 'bluebird';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('database/update');
|
const LOGGER = require('@calzoneman/jsli')('database/update');
|
||||||
|
|
||||||
const DB_VERSION = 15;
|
const DB_VERSION = 16;
|
||||||
var hasUpdates = [];
|
var hasUpdates = [];
|
||||||
|
|
||||||
module.exports.checkVersion = function () {
|
module.exports.checkVersion = function () {
|
||||||
|
|
@ -59,6 +59,8 @@ function update(version, cb) {
|
||||||
addCalendarIntegrationTables(cb);
|
addCalendarIntegrationTables(cb);
|
||||||
} else if (version < 15) {
|
} else if (version < 15) {
|
||||||
addCalendarIntegrationAuditColumns(cb);
|
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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,8 @@ function unpackTokens(integrationRow) {
|
||||||
|
|
||||||
async function upsertGoogleCalendarEvent(accessToken, calendarId, show) {
|
async function upsertGoogleCalendarEvent(accessToken, calendarId, show) {
|
||||||
const start = new Date(show.scheduled_for);
|
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 = {
|
const body = {
|
||||||
summary: show.name,
|
summary: show.name,
|
||||||
description: show.notes || '',
|
description: show.notes || '',
|
||||||
|
|
@ -230,7 +231,8 @@ async function upsertGoogleCalendarEvent(accessToken, calendarId, show) {
|
||||||
|
|
||||||
async function updateGoogleCalendarEvent(accessToken, calendarId, eventId, show) {
|
async function updateGoogleCalendarEvent(accessToken, calendarId, eventId, show) {
|
||||||
const start = new Date(show.scheduled_for);
|
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 = {
|
const body = {
|
||||||
summary: show.name,
|
summary: show.name,
|
||||||
description: show.notes || '',
|
description: show.notes || '',
|
||||||
|
|
|
||||||
89
src/util/markdown.js
Normal file
89
src/util/markdown.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
const XSS = require('../xss');
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInlineMarkdown(text) {
|
||||||
|
let out = escapeHtml(text);
|
||||||
|
out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
out = out.replace(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g, '<img src="$2" alt="$1">');
|
||||||
|
out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||||
|
out = out.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
||||||
|
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||||
|
out = out.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||||||
|
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, '<br>');
|
||||||
|
blocks.push('<p>' + rendered + '</p>');
|
||||||
|
paragraph = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushList() {
|
||||||
|
if (!listItems.length) return;
|
||||||
|
blocks.push('<ul>' + listItems.map(item => '<li>' + applyInlineMarkdown(item) + '</li>').join('') + '</ul>');
|
||||||
|
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('<h' + level + '>' + applyInlineMarkdown(content) + '</h' + level + '>');
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const webserver = require('../../webserver');
|
const webserver = require('../../webserver');
|
||||||
const showDB = require('../../../database/shows');
|
const showDB = require('../../../database/shows');
|
||||||
|
const calendarDB = require('../../../database/calendar-integrations');
|
||||||
const shows = require('../../../shows');
|
const shows = require('../../../shows');
|
||||||
const botDB = require('../../../database/bots');
|
const botDB = require('../../../database/bots');
|
||||||
const infoGetter = require('../../../get-info');
|
const infoGetter = require('../../../get-info');
|
||||||
const XSS = require('../../../xss');
|
const { renderNotesHtml } = require('../../../util/markdown');
|
||||||
const { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware');
|
const { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware');
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
@ -22,6 +23,25 @@ const ACTION_MIN_RANK = {
|
||||||
};
|
};
|
||||||
const PUBLIC_SHOW_STATUSES = new Set(['scheduled', 'running', 'paused', 'completed']);
|
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) {
|
function sanitizePlaylist(list) {
|
||||||
if (!Array.isArray(list)) return [];
|
if (!Array.isArray(list)) return [];
|
||||||
return list
|
return list
|
||||||
|
|
@ -69,6 +89,20 @@ function validateShowPayload(body, old = null) {
|
||||||
if (!scheduledFor) {
|
if (!scheduledFor) {
|
||||||
return { error: 'scheduled_for must be a valid date or timestamp' };
|
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');
|
const recurrence = String(body.recurrence || (old && old.recurrence) || 'none');
|
||||||
if (!RECURRENCES.has(recurrence)) {
|
if (!RECURRENCES.has(recurrence)) {
|
||||||
|
|
@ -105,7 +139,7 @@ function validateShowPayload(body, old = null) {
|
||||||
const notesRaw = body.notes !== undefined ? body.notes : (old ? old.notes : null);
|
const notesRaw = body.notes !== undefined ? body.notes : (old ? old.notes : null);
|
||||||
let notes = null;
|
let notes = null;
|
||||||
if (typeof notesRaw === 'string' && notesRaw.trim() !== '') {
|
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);
|
const colorRaw = body.color !== undefined ? body.color : (old ? old.color : null);
|
||||||
|
|
@ -126,6 +160,7 @@ function validateShowPayload(body, old = null) {
|
||||||
playlist,
|
playlist,
|
||||||
timezone,
|
timezone,
|
||||||
scheduled_for: scheduledFor,
|
scheduled_for: scheduledFor,
|
||||||
|
estimated_end_at: estimatedEndAt,
|
||||||
next_run_at: nextRunAt,
|
next_run_at: nextRunAt,
|
||||||
status,
|
status,
|
||||||
recurrence,
|
recurrence,
|
||||||
|
|
@ -198,7 +233,8 @@ router.get('/', async (req, res) => {
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
|
|
||||||
const showsList = await showDB.listShows(auth.channelRow.id);
|
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) => {
|
router.get('/public', async (req, res) => {
|
||||||
|
|
@ -210,7 +246,9 @@ router.get('/public', async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const showsList = await showDB.listShows(channelRow.id);
|
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) => {
|
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' });
|
if (isNaN(id)) return res.status(400).json({ error: 'Invalid show id' });
|
||||||
const show = await showDB.getShowById(id, auth.channelRow.id);
|
const show = await showDB.getShowById(id, auth.channelRow.id);
|
||||||
if (!show) return res.status(404).json({ error: 'Show not found' });
|
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) => {
|
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);
|
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) => {
|
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);
|
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) => {
|
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);
|
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;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -272,10 +272,18 @@ html(lang="en")
|
||||||
strong Time:
|
strong Time:
|
||||||
|
|
|
|
||||||
span#showdetails-time
|
span#showdetails-time
|
||||||
|
p
|
||||||
|
strong Estimated End:
|
||||||
|
|
|
||||||
|
span#showdetails-estimated-end
|
||||||
p
|
p
|
||||||
strong Status:
|
strong Status:
|
||||||
|
|
|
|
||||||
span#showdetails-status
|
span#showdetails-status
|
||||||
|
p#showdetails-calendar-links(style="display:none;")
|
||||||
|
strong Calendar:
|
||||||
|
|
|
||||||
|
span#showdetails-calendar-links-content
|
||||||
hr
|
hr
|
||||||
#showdetails-notes
|
#showdetails-notes
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,10 @@ mixin shows
|
||||||
label.control-label.col-sm-3(for="cs-shows-scheduled-for") Scheduled For
|
label.control-label.col-sm-3(for="cs-shows-scheduled-for") Scheduled For
|
||||||
.col-sm-9
|
.col-sm-9
|
||||||
input#cs-shows-scheduled-for.form-control(type="datetime-local")
|
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
|
.form-group
|
||||||
label.control-label.col-sm-3(for="cs-shows-color") Color
|
label.control-label.col-sm-3(for="cs-shows-color") Color
|
||||||
.col-sm-3
|
.col-sm-3
|
||||||
|
|
@ -305,8 +309,10 @@ mixin shows
|
||||||
.form-group
|
.form-group
|
||||||
label.control-label.col-sm-3(for="cs-shows-notes") Notes
|
label.control-label.col-sm-3(for="cs-shows-notes") Notes
|
||||||
.col-sm-9
|
.col-sm-9
|
||||||
textarea#cs-shows-notes.form-control(rows="6", placeholder="Optional rich notes (same HTML support as MOTD)")
|
button#cs-shows-notes-toggle.btn.btn-xs.btn-default.pull-right(type="button", style="margin-bottom:6px") Preview
|
||||||
p.text-muted.small(style="margin-top:6px") Supports MOTD-style HTML. Optional.
|
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
|
.form-group
|
||||||
label.control-label.col-sm-3(for="cs-shows-mediaurl") Show Playlist
|
label.control-label.col-sm-3(for="cs-shows-mediaurl") Show Playlist
|
||||||
.col-sm-9
|
.col-sm-9
|
||||||
|
|
@ -329,8 +335,10 @@ mixin shows
|
||||||
th Name
|
th Name
|
||||||
th Status
|
th Status
|
||||||
th Next Run
|
th Next Run
|
||||||
|
th Est. End
|
||||||
th Timezone
|
th Timezone
|
||||||
th Recurrence
|
th Recurrence
|
||||||
|
th Calendar
|
||||||
th Actions
|
th Actions
|
||||||
tbody#cs-shows-list
|
tbody#cs-shows-list
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,11 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
||||||
padding: 2px !important;
|
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 {
|
.showschedule-cell.showschedule-admin {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -258,6 +263,40 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
||||||
text-decoration: none;
|
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-scheduled { background: #337ab7; }
|
||||||
.showschedule-show.status-running { background: #5cb85c; }
|
.showschedule-show.status-running { background: #5cb85c; }
|
||||||
.showschedule-show.status-paused { background: #f0ad4e; color: #222; }
|
.showschedule-show.status-paused { background: #f0ad4e; color: #222; }
|
||||||
|
|
|
||||||
304
www/js/ui.js
304
www/js/ui.js
|
|
@ -1436,6 +1436,7 @@ var CSTShows = (function () {
|
||||||
var resolvingTitles = false;
|
var resolvingTitles = false;
|
||||||
var weekOffset = 0;
|
var weekOffset = 0;
|
||||||
var cachedShows = [];
|
var cachedShows = [];
|
||||||
|
var notesEditorMode = 'edit';
|
||||||
|
|
||||||
function apiBase() {
|
function apiBase() {
|
||||||
return '/api/v1/channels/' + CHANNEL.name + '/shows';
|
return '/api/v1/channels/' + CHANNEL.name + '/shows';
|
||||||
|
|
@ -1487,6 +1488,121 @@ var CSTShows = (function () {
|
||||||
pad(d.getMinutes());
|
pad(d.getMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.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, '<code>$1</code>');
|
||||||
|
out = out.replace(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g, '<img src="$2" alt="$1">');
|
||||||
|
out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||||
|
out = out.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
||||||
|
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||||
|
out = out.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushParagraph() {
|
||||||
|
if (!paragraph.length) return;
|
||||||
|
var rendered = inline(paragraph.join('\n')).replace(/\n/g, '<br>');
|
||||||
|
blocks.push('<p>' + rendered + '</p>');
|
||||||
|
paragraph = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushList() {
|
||||||
|
if (!listItems.length) return;
|
||||||
|
blocks.push('<ul>' + listItems.map(function (item) { return '<li>' + inline(item) + '</li>'; }).join('') + '</ul>');
|
||||||
|
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('<h' + level + '>' + inline(line.replace(/^#{1,3}\s+/, '')) + '</h' + level + '>');
|
||||||
|
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('<span class="text-muted">Nothing to preview.</span>');
|
||||||
|
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() {
|
function renderDraftPlaylist() {
|
||||||
var ul = $('#cs-shows-playlist-list').empty();
|
var ul = $('#cs-shows-playlist-list').empty();
|
||||||
if (!draftPlaylist.length) {
|
if (!draftPlaylist.length) {
|
||||||
|
|
@ -1627,6 +1743,7 @@ var CSTShows = (function () {
|
||||||
|
|
||||||
function readFormPayload() {
|
function readFormPayload() {
|
||||||
var scheduledRaw = $('#cs-shows-scheduled-for').val();
|
var scheduledRaw = $('#cs-shows-scheduled-for').val();
|
||||||
|
var estimatedEndRaw = $('#cs-shows-estimated-end-at').val();
|
||||||
var timezone = $('#cs-shows-timezone').val().trim();
|
var timezone = $('#cs-shows-timezone').val().trim();
|
||||||
var notes = $('#cs-shows-notes').val();
|
var notes = $('#cs-shows-notes').val();
|
||||||
var colorHex = ($('#cs-shows-color-hex').val() || '').trim();
|
var colorHex = ($('#cs-shows-color-hex').val() || '').trim();
|
||||||
|
|
@ -1644,6 +1761,7 @@ var CSTShows = (function () {
|
||||||
notes: notes && notes.trim() ? notes : null,
|
notes: notes && notes.trim() ? notes : null,
|
||||||
color: colorHex ? colorHex.toUpperCase() : null,
|
color: colorHex ? colorHex.toUpperCase() : null,
|
||||||
scheduled_for: scheduledRaw ? new Date(scheduledRaw).toISOString() : null,
|
scheduled_for: scheduledRaw ? new Date(scheduledRaw).toISOString() : null,
|
||||||
|
estimated_end_at: estimatedEndRaw ? new Date(estimatedEndRaw).toISOString() : null,
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
recurrence: $('#cs-shows-recurrence').val(),
|
recurrence: $('#cs-shows-recurrence').val(),
|
||||||
fill_mode: $('#cs-shows-fill-mode').val(),
|
fill_mode: $('#cs-shows-fill-mode').val(),
|
||||||
|
|
@ -1662,6 +1780,7 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-name').val('');
|
$('#cs-shows-name').val('');
|
||||||
$('#cs-shows-notes').val('');
|
$('#cs-shows-notes').val('');
|
||||||
$('#cs-shows-scheduled-for').val('');
|
$('#cs-shows-scheduled-for').val('');
|
||||||
|
$('#cs-shows-estimated-end-at').val('');
|
||||||
var detectedTz = 'UTC';
|
var detectedTz = 'UTC';
|
||||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||||
detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||||
|
|
@ -1679,6 +1798,8 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-mediaurl').val('');
|
$('#cs-shows-mediaurl').val('');
|
||||||
draftPlaylist = [];
|
draftPlaylist = [];
|
||||||
renderDraftPlaylist();
|
renderDraftPlaylist();
|
||||||
|
updateNotesPreview();
|
||||||
|
setNotesEditorMode('edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectShow(show) {
|
function selectShow(show) {
|
||||||
|
|
@ -1687,6 +1808,7 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-name').val(show.name);
|
$('#cs-shows-name').val(show.name);
|
||||||
$('#cs-shows-notes').val(show.notes || '');
|
$('#cs-shows-notes').val(show.notes || '');
|
||||||
$('#cs-shows-scheduled-for').val(toLocalDateInput(show.scheduled_for));
|
$('#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';
|
var showTz = show.timezone || 'UTC';
|
||||||
if ($('#cs-shows-timezone option[value="' + showTz + '"]').length === 0) {
|
if ($('#cs-shows-timezone option[value="' + showTz + '"]').length === 0) {
|
||||||
$('<option>').attr('value', showTz).text(showTz).appendTo('#cs-shows-timezone');
|
$('<option>').attr('value', showTz).text(showTz).appendTo('#cs-shows-timezone');
|
||||||
|
|
@ -1707,6 +1829,8 @@ var CSTShows = (function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
renderDraftPlaylist();
|
renderDraftPlaylist();
|
||||||
|
updateNotesPreview();
|
||||||
|
setNotesEditorMode('edit');
|
||||||
resolveDraftTitles();
|
resolveDraftTitles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1751,26 +1875,106 @@ var CSTShows = (function () {
|
||||||
|
|
||||||
function openShowDetailsModal(show, when) {
|
function openShowDetailsModal(show, when) {
|
||||||
$('#showdetails-title').text(show.name || 'Show Details');
|
$('#showdetails-title').text(show.name || 'Show Details');
|
||||||
$('#showdetails-time').text(when.toLocaleString());
|
var startAt = when ? new Date(when) : (show.scheduled_for ? new Date(show.scheduled_for) : new Date());
|
||||||
|
var endAt = new Date(getShowOccurrenceEndMs(show, startAt.getTime()));
|
||||||
|
$('#showdetails-time').text(startAt.toLocaleString());
|
||||||
|
$('#showdetails-estimated-end').text(endAt.toLocaleString());
|
||||||
$('#showdetails-status').text(show.status || 'scheduled');
|
$('#showdetails-status').text(show.status || 'scheduled');
|
||||||
var notes = (show.notes || '').trim();
|
var linksWrap = $('#showdetails-calendar-links');
|
||||||
if (!notes) {
|
var linksContent = $('#showdetails-calendar-links-content').empty();
|
||||||
|
var googleLinks = show && show.calendar_links && show.calendar_links.google
|
||||||
|
? show.calendar_links.google
|
||||||
|
: null;
|
||||||
|
if (googleLinks && (googleLinks.event_url || googleLinks.calendar_url)) {
|
||||||
|
if (googleLinks.event_url) {
|
||||||
|
$('<a>')
|
||||||
|
.attr('href', googleLinks.event_url)
|
||||||
|
.attr('target', '_blank')
|
||||||
|
.attr('rel', 'noopener noreferrer')
|
||||||
|
.text('View in Google Calendar')
|
||||||
|
.appendTo(linksContent);
|
||||||
|
}
|
||||||
|
if (googleLinks.calendar_url) {
|
||||||
|
if (googleLinks.event_url) {
|
||||||
|
linksContent.append(' | ');
|
||||||
|
}
|
||||||
|
$('<a>')
|
||||||
|
.attr('href', googleLinks.calendar_url)
|
||||||
|
.attr('target', '_blank')
|
||||||
|
.attr('rel', 'noopener noreferrer')
|
||||||
|
.text('Open Google Calendar')
|
||||||
|
.appendTo(linksContent);
|
||||||
|
}
|
||||||
|
linksWrap.show();
|
||||||
|
} else {
|
||||||
|
linksWrap.hide();
|
||||||
|
}
|
||||||
|
var notesHtml = (show.notes_html || '').trim();
|
||||||
|
if (!notesHtml) {
|
||||||
$('#showdetails-notes').html('<p class="text-muted">No notes for this show.</p>');
|
$('#showdetails-notes').html('<p class="text-muted">No notes for this show.</p>');
|
||||||
} else {
|
} else {
|
||||||
$('#showdetails-notes').html(notes);
|
$('#showdetails-notes').html(notesHtml);
|
||||||
}
|
}
|
||||||
$('#showdetails').modal();
|
$('#showdetails').modal();
|
||||||
}
|
}
|
||||||
|
|
||||||
var byCell = {};
|
var blocksByStart = {};
|
||||||
|
var covered = {};
|
||||||
|
var firstSegmentPlaced = {};
|
||||||
|
function ensureDayMap(dayIdx) {
|
||||||
|
if (!covered[dayIdx]) covered[dayIdx] = {};
|
||||||
|
}
|
||||||
|
function markCovered(dayIdx, startHour, span) {
|
||||||
|
ensureDayMap(dayIdx);
|
||||||
|
for (var h = startHour + 1; h < startHour + span; h++) {
|
||||||
|
covered[dayIdx][h] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function dayIndexFor(date) {
|
||||||
|
var midnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
var weekMidnight = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate());
|
||||||
|
return Math.floor((midnight.getTime() - weekMidnight.getTime()) / (24 * 60 * 60 * 1000));
|
||||||
|
}
|
||||||
shows.forEach(function (show) {
|
shows.forEach(function (show) {
|
||||||
var at = show.next_run_at || show.scheduled_for;
|
var at = show.next_run_at || show.scheduled_for;
|
||||||
if (!at) return;
|
if (!at) return;
|
||||||
var d = new Date(at);
|
var start = new Date(at);
|
||||||
if (d < weekStart || d > new Date(weekEnd.getTime() + 86399999)) return;
|
var end = show.estimated_end_at ? new Date(show.estimated_end_at) : new Date(start.getTime() + 60 * 60 * 1000);
|
||||||
var key = toCellKey(d);
|
if (end <= start) end = new Date(start.getTime() + 60 * 60 * 1000);
|
||||||
if (!byCell[key]) byCell[key] = [];
|
|
||||||
byCell[key].push({ show: show, date: d });
|
var occurrenceEnd = new Date(getShowOccurrenceEndMs(show, start.getTime()));
|
||||||
|
var visibleStart = start < weekStart ? new Date(weekStart.getTime()) : start;
|
||||||
|
var visibleEnd = occurrenceEnd > new Date(weekEnd.getTime() + 86399999) ? new Date(weekEnd.getTime() + 86399999) : occurrenceEnd;
|
||||||
|
if (visibleEnd <= visibleStart) return;
|
||||||
|
|
||||||
|
var cursor = new Date(visibleStart.getTime());
|
||||||
|
while (cursor < visibleEnd) {
|
||||||
|
var dayStart = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), 0, 0, 0, 0);
|
||||||
|
var dayEnd = new Date(dayStart.getTime() + (24 * 60 * 60 * 1000));
|
||||||
|
var segEnd = visibleEnd < dayEnd ? visibleEnd : dayEnd;
|
||||||
|
var dayIdx = dayIndexFor(cursor);
|
||||||
|
if (dayIdx >= 0 && dayIdx < 7) {
|
||||||
|
var slotStart = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), cursor.getHours(), 0, 0, 0);
|
||||||
|
var startHour = slotStart.getHours();
|
||||||
|
var span = Math.max(1, Math.ceil((segEnd.getTime() - slotStart.getTime()) / (60 * 60 * 1000)));
|
||||||
|
if (startHour + span > 24) span = 24 - startHour;
|
||||||
|
var startKey = dayIdx + '-' + startHour;
|
||||||
|
if (!blocksByStart[startKey]) blocksByStart[startKey] = [];
|
||||||
|
var isFirstVisibleSegment = !firstSegmentPlaced[show.id];
|
||||||
|
blocksByStart[startKey].push({
|
||||||
|
show: show,
|
||||||
|
date: start,
|
||||||
|
end: occurrenceEnd,
|
||||||
|
span: span,
|
||||||
|
isStart: isFirstVisibleSegment
|
||||||
|
});
|
||||||
|
if (isFirstVisibleSegment) firstSegmentPlaced[show.id] = true;
|
||||||
|
if (span > 1) {
|
||||||
|
markCovered(dayIdx, startHour, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = dayEnd;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var isAdmin = CLIENT.rank >= 2;
|
var isAdmin = CLIENT.rank >= 2;
|
||||||
|
|
@ -1791,7 +1995,9 @@ var CSTShows = (function () {
|
||||||
var cellDate = new Date(weekStart.getTime());
|
var cellDate = new Date(weekStart.getTime());
|
||||||
cellDate.setDate(weekStart.getDate() + col);
|
cellDate.setDate(weekStart.getDate() + col);
|
||||||
cellDate.setHours(hour, 0, 0, 0);
|
cellDate.setHours(hour, 0, 0, 0);
|
||||||
var key = toCellKey(cellDate);
|
if (covered[col] && covered[col][hour]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var cell = $('<td class="showschedule-cell">').appendTo(tr);
|
var cell = $('<td class="showschedule-cell">').appendTo(tr);
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
cell.addClass('showschedule-admin').attr('title', 'Click to create show at this time');
|
cell.addClass('showschedule-admin').attr('title', 'Click to create show at this time');
|
||||||
|
|
@ -1804,23 +2010,53 @@ var CSTShows = (function () {
|
||||||
})(new Date(cellDate.getTime()));
|
})(new Date(cellDate.getTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = byCell[key] || [];
|
var startKey = col + '-' + hour;
|
||||||
|
var items = blocksByStart[startKey] || [];
|
||||||
items.sort(function (a, b) { return a.date - b.date; });
|
items.sort(function (a, b) { return a.date - b.date; });
|
||||||
|
if (items.length === 1 && items[0].span > 1) {
|
||||||
|
cell.attr('rowspan', String(items[0].span));
|
||||||
|
cell
|
||||||
|
.addClass('showschedule-block-cell')
|
||||||
|
.css('background-color', getShowBlockColor(items[0].show));
|
||||||
|
}
|
||||||
items.forEach(function (item) {
|
items.forEach(function (item) {
|
||||||
var label = item.date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' ' + item.show.name;
|
var startLabel = item.date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
var endLabel = item.end
|
||||||
|
? item.end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
|
: null;
|
||||||
|
var label = endLabel
|
||||||
|
? (startLabel + '-' + endLabel + ' ' + item.show.name)
|
||||||
|
: (startLabel + ' ' + item.show.name);
|
||||||
|
var notesPreview = (item.show.notes || '').trim().replace(/\s+/g, ' ');
|
||||||
|
if (notesPreview.length > 120) {
|
||||||
|
notesPreview = notesPreview.substring(0, 117) + '...';
|
||||||
|
}
|
||||||
$('<a href="javascript:void(0)" class="showschedule-show">')
|
$('<a href="javascript:void(0)" class="showschedule-show">')
|
||||||
.addClass('status-' + (item.show.status || 'scheduled'))
|
.addClass('status-' + (item.show.status || 'scheduled'))
|
||||||
.text(label)
|
.text(label)
|
||||||
.css('background', item.show.color || '')
|
.css('background', items.length === 1 ? 'transparent' : (item.show.color || ''))
|
||||||
|
.css('opacity', item.isStart ? '1' : '0.85')
|
||||||
|
.attr('title', notesPreview || label)
|
||||||
.on('click', function () {
|
.on('click', function () {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
openShowsEditor();
|
openShowsEditor();
|
||||||
selectShow(item.show);
|
selectShow(item.show);
|
||||||
} else {
|
} else {
|
||||||
openShowDetailsModal(item.show, item.date);
|
openShowDetailsModal(item.show, item.date.getTime());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.appendTo(cell);
|
.appendTo(cell);
|
||||||
|
if (item.isStart) {
|
||||||
|
if (item.show.notes_html) {
|
||||||
|
$('<div class="showschedule-notes small">')
|
||||||
|
.html(item.show.notes_html)
|
||||||
|
.appendTo(cell);
|
||||||
|
} else if (notesPreview) {
|
||||||
|
$('<div class="showschedule-notes small">')
|
||||||
|
.text(notesPreview)
|
||||||
|
.appendTo(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1845,7 +2081,7 @@ var CSTShows = (function () {
|
||||||
function render(shows) {
|
function render(shows) {
|
||||||
var tbody = $('#cs-shows-list').empty();
|
var tbody = $('#cs-shows-list').empty();
|
||||||
if (!shows.length) {
|
if (!shows.length) {
|
||||||
tbody.append('<tr><td colspan="6" class="text-muted">No shows configured</td></tr>');
|
tbody.append('<tr><td colspan="8" class="text-muted">No shows configured</td></tr>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1856,8 +2092,37 @@ var CSTShows = (function () {
|
||||||
));
|
));
|
||||||
row.append($('<td>').text(show.status));
|
row.append($('<td>').text(show.status));
|
||||||
row.append($('<td>').text(show.next_run_at ? new Date(show.next_run_at).toLocaleString(undefined, { timeZone: show.timezone || 'UTC' }) : 'N/A'));
|
row.append($('<td>').text(show.next_run_at ? new Date(show.next_run_at).toLocaleString(undefined, { timeZone: show.timezone || 'UTC' }) : 'N/A'));
|
||||||
|
row.append($('<td>').text(show.estimated_end_at ? new Date(show.estimated_end_at).toLocaleString(undefined, { timeZone: show.timezone || 'UTC' }) : 'N/A'));
|
||||||
row.append($('<td>').text(show.timezone || 'UTC'));
|
row.append($('<td>').text(show.timezone || 'UTC'));
|
||||||
row.append($('<td>').text(show.recurrence || 'none'));
|
row.append($('<td>').text(show.recurrence || 'none'));
|
||||||
|
var calendarTd = $('<td>');
|
||||||
|
var googleLinks = show && show.calendar_links && show.calendar_links.google
|
||||||
|
? show.calendar_links.google
|
||||||
|
: null;
|
||||||
|
if (googleLinks && (googleLinks.event_url || googleLinks.calendar_url)) {
|
||||||
|
if (googleLinks.event_url) {
|
||||||
|
$('<a>')
|
||||||
|
.attr('href', googleLinks.event_url)
|
||||||
|
.attr('target', '_blank')
|
||||||
|
.attr('rel', 'noopener noreferrer')
|
||||||
|
.text('Event')
|
||||||
|
.appendTo(calendarTd);
|
||||||
|
}
|
||||||
|
if (googleLinks.calendar_url) {
|
||||||
|
if (googleLinks.event_url) {
|
||||||
|
calendarTd.append(' | ');
|
||||||
|
}
|
||||||
|
$('<a>')
|
||||||
|
.attr('href', googleLinks.calendar_url)
|
||||||
|
.attr('target', '_blank')
|
||||||
|
.attr('rel', 'noopener noreferrer')
|
||||||
|
.text('Calendar')
|
||||||
|
.appendTo(calendarTd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
calendarTd.append($('<span class="text-muted">').text('Not synced'));
|
||||||
|
}
|
||||||
|
row.append(calendarTd);
|
||||||
|
|
||||||
var actions = $('<td>');
|
var actions = $('<td>');
|
||||||
$('<button class=\"btn btn-xs btn-primary\" style=\"margin-right:4px\">Run</button>')
|
$('<button class=\"btn btn-xs btn-primary\" style=\"margin-right:4px\">Run</button>')
|
||||||
|
|
@ -1901,7 +2166,7 @@ var CSTShows = (function () {
|
||||||
renderScheduleCalendar(cachedShows);
|
renderScheduleCalendar(cachedShows);
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
if (CLIENT.rank >= 2) {
|
if (CLIENT.rank >= 2) {
|
||||||
$('#cs-shows-list').html('<tr><td colspan=\"6\" class=\"text-danger\">Failed to load shows</td></tr>');
|
$('#cs-shows-list').html('<tr><td colspan=\"8\" class=\"text-danger\">Failed to load shows</td></tr>');
|
||||||
}
|
}
|
||||||
$('#showschedule-grid').html('<div class=\"text-danger\">Failed to load schedule</div>');
|
$('#showschedule-grid').html('<div class=\"text-danger\">Failed to load schedule</div>');
|
||||||
});
|
});
|
||||||
|
|
@ -1960,6 +2225,10 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-color').val(v);
|
$('#cs-shows-color').val(v);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$('#cs-shows-notes').on('input', updateNotesPreview);
|
||||||
|
$('#cs-shows-notes-toggle').on('click', function () {
|
||||||
|
setNotesEditorMode(notesEditorMode === 'edit' ? 'preview' : 'edit');
|
||||||
|
});
|
||||||
$('#cs-shows-playlist-list').sortable({
|
$('#cs-shows-playlist-list').sortable({
|
||||||
update: function () {
|
update: function () {
|
||||||
var nextDraft = [];
|
var nextDraft = [];
|
||||||
|
|
@ -1989,6 +2258,7 @@ var CSTShows = (function () {
|
||||||
});
|
});
|
||||||
renderDraftPlaylist();
|
renderDraftPlaylist();
|
||||||
clearForm();
|
clearForm();
|
||||||
|
setNotesEditorMode('edit');
|
||||||
load();
|
load();
|
||||||
|
|
||||||
return { load: load, selectShow: selectShow, prefillScheduledDate: prefillScheduledDate };
|
return { load: load, selectShow: selectShow, prefillScheduledDate: prefillScheduledDate };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue