diff --git a/src/database/shows.js b/src/database/shows.js index 7f0baaed..a588054d 100644 --- a/src/database/shows.js +++ b/src/database/shows.js @@ -26,6 +26,8 @@ function parseShowRow(row) { channel_name: row.channel_name, channel_id: row.channel_id, name: row.name, + notes: row.notes || null, + color: row.color || null, playlist, timezone: row.timezone, scheduled_for: row.scheduled_for, @@ -49,6 +51,8 @@ function parseShowRow(row) { function serializeShowInput(input) { return { name: input.name, + notes: input.notes || null, + color: input.color || null, playlist: JSON.stringify(input.playlist || []), timezone: input.timezone, scheduled_for: input.scheduled_for, diff --git a/src/database/tables.js b/src/database/tables.js index f8515be3..0737c857 100644 --- a/src/database/tables.js +++ b/src/database/tables.js @@ -184,6 +184,8 @@ export async function initTables() { .references('id').inTable('channels') .onDelete('cascade'); t.string('name', 100).notNullable(); + t.specificType('notes', 'mediumtext character set utf8mb4'); + t.string('color', 7).nullable(); t.specificType('playlist', 'mediumtext character set utf8mb4 not null'); t.string('timezone', 64).notNullable().defaultTo('UTC'); t.bigInteger('scheduled_for').notNullable(); diff --git a/src/database/update.js b/src/database/update.js index 3b600966..fa73e272 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 = 12; +const DB_VERSION = 13; var hasUpdates = []; module.exports.checkVersion = function () { @@ -53,6 +53,8 @@ function update(version, cb) { addChannelOwnerLastSeenColumn(cb); } else if (version < 12) { addUserInactiveColumn(cb); + } else if (version < 13) { + addShowsNotesAndColorColumns(cb); } } @@ -141,3 +143,29 @@ function addUserInactiveColumn(cb) { } }); } + +function addShowsNotesAndColorColumns(cb) { + db.query( + "ALTER TABLE channel_shows ADD COLUMN notes MEDIUMTEXT CHARACTER SET utf8mb4 NULL", + error => { + if (error) { + LOGGER.error(`Failed to add shows notes column: ${error}`); + cb(error); + return; + } + + db.query( + "ALTER TABLE channel_shows ADD COLUMN color VARCHAR(7) NULL", + error => { + if (error) { + LOGGER.error(`Failed to add shows color column: ${error}`); + cb(error); + return; + } + + cb(); + } + ); + } + ); +} diff --git a/src/web/routes/api/shows.js b/src/web/routes/api/shows.js index 9ffa5a37..313120d3 100644 --- a/src/web/routes/api/shows.js +++ b/src/web/routes/api/shows.js @@ -4,6 +4,7 @@ const showDB = require('../../../database/shows'); const shows = require('../../../shows'); const botDB = require('../../../database/bots'); const infoGetter = require('../../../get-info'); +const XSS = require('../../../xss'); const { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware'); const router = express.Router({ mergeParams: true }); @@ -101,9 +102,27 @@ function validateShowPayload(body, old = null) { const nextRunAt = status === 'scheduled' ? scheduledFor : (old ? old.next_run_at : scheduledFor); + 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)); + } + + const colorRaw = body.color !== undefined ? body.color : (old ? old.color : null); + let color = null; + if (colorRaw !== null && colorRaw !== undefined && String(colorRaw).trim() !== '') { + const normalized = String(colorRaw).trim(); + if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) { + return { error: 'color must be a hex string like #1A2B3C' }; + } + color = normalized.toUpperCase(); + } + return { value: { name, + notes, + color, playlist, timezone, scheduled_for: scheduledFor, diff --git a/templates/channel.pug b/templates/channel.pug index d9883b8f..27c73296 100644 --- a/templates/channel.pug +++ b/templates/channel.pug @@ -259,6 +259,25 @@ html(lang="en") +permeditor() .modal-footer button.btn.btn-default(type="button", data-dismiss="modal") Close + #showdetails.modal.fade(tabindex="-1", role="dialog", aria-hidden="true") + .modal-dialog + .modal-content + .modal-header + button.close(data-dismiss="modal", aria-hidden="true") × + h4#showdetails-title Show Details + .modal-body + p + strong Time: + | + span#showdetails-time + p + strong Status: + | + span#showdetails-status + hr + #showdetails-notes + .modal-footer + button.btn.btn-default(type="button", data-dismiss="modal") Close #pmbar include footer +footer() diff --git a/templates/channeloptions.pug b/templates/channeloptions.pug index f6a751ac..4d49e383 100644 --- a/templates/channeloptions.pug +++ b/templates/channeloptions.pug @@ -267,6 +267,12 @@ 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-color") Color + .col-sm-3 + input#cs-shows-color.form-control(type="color", value="#337AB7") + .col-sm-6 + input#cs-shows-color-hex.form-control(type="text", placeholder="#337AB7", maxlength="7") .form-group label.control-label.col-sm-3(for="cs-shows-timezone") Timezone .col-sm-9 @@ -296,6 +302,11 @@ mixin shows label(for="cs-shows-start-playback") input#cs-shows-start-playback(type="checkbox") | Skip current playback and start this show's first item immediately + .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. .form-group label.control-label.col-sm-3(for="cs-shows-mediaurl") Show Playlist .col-sm-9 diff --git a/www/css/cytube.css b/www/css/cytube.css index 6dda3623..01836152 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -254,6 +254,7 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry { padding: 2px 4px; border-radius: 3px; color: #fff; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); text-decoration: none; } @@ -262,6 +263,11 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry { .showschedule-show.status-paused { background: #f0ad4e; color: #222; } .showschedule-show.status-completed { background: #777; } +#showdetails-notes img { + max-width: 100%; + height: auto; +} + .clear { clear: both; } diff --git a/www/js/ui.js b/www/js/ui.js index 989af594..adb12069 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -37,7 +37,11 @@ function updateScheduleToggleLabel() { $("#toggleschedule").on('click', function () { var row = $("#showschedule-row"); if (!row.length) return; + var willShow = !row.is(":visible"); row.toggle(); + if (willShow && window.CSTShows && typeof window.CSTShows.load === "function") { + window.CSTShows.load(); + } updateScheduleToggleLabel(); }); updateScheduleToggleLabel(); @@ -1624,11 +1628,21 @@ var CSTShows = (function () { function readFormPayload() { var scheduledRaw = $('#cs-shows-scheduled-for').val(); var timezone = $('#cs-shows-timezone').val().trim(); + var notes = $('#cs-shows-notes').val(); + var colorHex = ($('#cs-shows-color-hex').val() || '').trim(); + if (!colorHex) { + colorHex = ($('#cs-shows-color').val() || '').trim(); + } + if (!/^#[0-9a-fA-F]{6}$/.test(colorHex || '')) { + colorHex = ''; + } if (!timezone) { timezone = 'UTC'; } return { name: $('#cs-shows-name').val().trim(), + notes: notes && notes.trim() ? notes : null, + color: colorHex ? colorHex.toUpperCase() : null, scheduled_for: scheduledRaw ? new Date(scheduledRaw).toISOString() : null, timezone: timezone, recurrence: $('#cs-shows-recurrence').val(), @@ -1646,6 +1660,7 @@ var CSTShows = (function () { loadTimezoneOptions(); selectedId = null; $('#cs-shows-name').val(''); + $('#cs-shows-notes').val(''); $('#cs-shows-scheduled-for').val(''); var detectedTz = 'UTC'; if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { @@ -1659,6 +1674,8 @@ var CSTShows = (function () { $('#cs-shows-fill-mode').val('append'); $('#cs-shows-conflict-skip').prop('checked', false); $('#cs-shows-start-playback').prop('checked', false); + $('#cs-shows-color').val('#337AB7'); + $('#cs-shows-color-hex').val(''); $('#cs-shows-mediaurl').val(''); draftPlaylist = []; renderDraftPlaylist(); @@ -1668,6 +1685,7 @@ var CSTShows = (function () { loadTimezoneOptions(); selectedId = show.id; $('#cs-shows-name').val(show.name); + $('#cs-shows-notes').val(show.notes || ''); $('#cs-shows-scheduled-for').val(toLocalDateInput(show.scheduled_for)); var showTz = show.timezone || 'UTC'; if ($('#cs-shows-timezone option[value="' + showTz + '"]').length === 0) { @@ -1678,6 +1696,8 @@ var CSTShows = (function () { $('#cs-shows-fill-mode').val(show.fill_mode || 'append'); $('#cs-shows-conflict-skip').prop('checked', (show.conflict_mode || 'force') === 'skip'); $('#cs-shows-start-playback').prop('checked', !!show.start_playback); + $('#cs-shows-color').val(show.color || '#337AB7'); + $('#cs-shows-color-hex').val(show.color || ''); draftPlaylist = (show.playlist || []).map(function (item) { return { id: item.id, @@ -1729,6 +1749,19 @@ var CSTShows = (function () { weekStart.toLocaleDateString() + ' - ' + weekEnd.toLocaleDateString() ); + function openShowDetailsModal(show, when) { + $('#showdetails-title').text(show.name || 'Show Details'); + $('#showdetails-time').text(when.toLocaleString()); + $('#showdetails-status').text(show.status || 'scheduled'); + var notes = (show.notes || '').trim(); + if (!notes) { + $('#showdetails-notes').html('
No notes for this show.
'); + } else { + $('#showdetails-notes').html(notes); + } + $('#showdetails').modal(); + } + var byCell = {}; shows.forEach(function (show) { var at = show.next_run_at || show.scheduled_for; @@ -1778,12 +1811,13 @@ var CSTShows = (function () { $('') .addClass('status-' + (item.show.status || 'scheduled')) .text(label) + .css('background', item.show.color || '') .on('click', function () { if (isAdmin) { openShowsEditor(); selectShow(item.show); } else { - alert(item.show.name + '\n' + item.date.toLocaleString() + '\nStatus: ' + item.show.status); + openShowDetailsModal(item.show, item.date); } }) .appendTo(cell); @@ -1917,6 +1951,15 @@ var CSTShows = (function () { } }); $('#cs-shows-clear').on('click', clearForm); + $('#cs-shows-color').on('change', function () { + $('#cs-shows-color-hex').val(($(this).val() || '').toUpperCase()); + }); + $('#cs-shows-color-hex').on('input', function () { + var v = ($(this).val() || '').trim(); + if (/^#[0-9a-fA-F]{6}$/.test(v)) { + $('#cs-shows-color').val(v); + } + }); $('#cs-shows-playlist-list').sortable({ update: function () { var nextDraft = [];