mirror of
https://github.com/Spengreb/sync.git
synced 2026-06-09 23:02:05 +00:00
Merge pull request #15 from Spengreb/schedule
Calendar for showing scheduled events
This commit is contained in:
commit
c102b90ef6
8 changed files with 354 additions and 5 deletions
|
|
@ -26,6 +26,8 @@ function parseShowRow(row) {
|
||||||
channel_name: row.channel_name,
|
channel_name: row.channel_name,
|
||||||
channel_id: row.channel_id,
|
channel_id: row.channel_id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
notes: row.notes || null,
|
||||||
|
color: row.color || null,
|
||||||
playlist,
|
playlist,
|
||||||
timezone: row.timezone,
|
timezone: row.timezone,
|
||||||
scheduled_for: row.scheduled_for,
|
scheduled_for: row.scheduled_for,
|
||||||
|
|
@ -49,6 +51,8 @@ function parseShowRow(row) {
|
||||||
function serializeShowInput(input) {
|
function serializeShowInput(input) {
|
||||||
return {
|
return {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
notes: input.notes || null,
|
||||||
|
color: input.color || null,
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,8 @@ export async function initTables() {
|
||||||
.references('id').inTable('channels')
|
.references('id').inTable('channels')
|
||||||
.onDelete('cascade');
|
.onDelete('cascade');
|
||||||
t.string('name', 100).notNullable();
|
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.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();
|
||||||
|
|
|
||||||
|
|
@ -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 = 12;
|
const DB_VERSION = 13;
|
||||||
var hasUpdates = [];
|
var hasUpdates = [];
|
||||||
|
|
||||||
module.exports.checkVersion = function () {
|
module.exports.checkVersion = function () {
|
||||||
|
|
@ -53,6 +53,8 @@ function update(version, cb) {
|
||||||
addChannelOwnerLastSeenColumn(cb);
|
addChannelOwnerLastSeenColumn(cb);
|
||||||
} else if (version < 12) {
|
} else if (version < 12) {
|
||||||
addUserInactiveColumn(cb);
|
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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const showDB = require('../../../database/shows');
|
||||||
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 { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware');
|
const { getChannelRow, getUserEffectiveRank, hashToken } = require('./middleware');
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
@ -19,6 +20,7 @@ const ACTION_MIN_RANK = {
|
||||||
run: 3,
|
run: 3,
|
||||||
cancel: 3
|
cancel: 3
|
||||||
};
|
};
|
||||||
|
const PUBLIC_SHOW_STATUSES = new Set(['scheduled', 'running', 'paused', 'completed']);
|
||||||
|
|
||||||
function sanitizePlaylist(list) {
|
function sanitizePlaylist(list) {
|
||||||
if (!Array.isArray(list)) return [];
|
if (!Array.isArray(list)) return [];
|
||||||
|
|
@ -100,9 +102,27 @@ function validateShowPayload(body, old = null) {
|
||||||
|
|
||||||
const nextRunAt = status === 'scheduled' ? scheduledFor : (old ? old.next_run_at : scheduledFor);
|
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 {
|
return {
|
||||||
value: {
|
value: {
|
||||||
name,
|
name,
|
||||||
|
notes,
|
||||||
|
color,
|
||||||
playlist,
|
playlist,
|
||||||
timezone,
|
timezone,
|
||||||
scheduled_for: scheduledFor,
|
scheduled_for: scheduledFor,
|
||||||
|
|
@ -181,6 +201,18 @@ router.get('/', async (req, res) => {
|
||||||
res.json(showsList);
|
res.json(showsList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/public', async (req, res) => {
|
||||||
|
let channelRow;
|
||||||
|
try {
|
||||||
|
channelRow = await getChannelRow(req.params.channel);
|
||||||
|
} catch (_err) {
|
||||||
|
return res.status(404).json({ error: 'Channel not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const showsList = await showDB.listShows(channelRow.id);
|
||||||
|
res.json(showsList.filter(show => PUBLIC_SHOW_STATUSES.has(show.status)));
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/:id', async (req, res) => {
|
router.get('/:id', async (req, res) => {
|
||||||
const auth = await authorizeChannel(req, res);
|
const auth = await authorizeChannel(req, res);
|
||||||
if (!auth) return;
|
if (!auth) return;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ html(lang="en")
|
||||||
ul.nav.navbar-nav
|
ul.nav.navbar-nav
|
||||||
+navdefaultlinks()
|
+navdefaultlinks()
|
||||||
li: a(href="javascript:void(0)", onclick="javascript:showUserOptions()") Options
|
li: a(href="javascript:void(0)", onclick="javascript:showUserOptions()") Options
|
||||||
|
li: a#toggleschedule(href="javascript:void(0)") Show Schedule
|
||||||
li: a#showchansettings(href="javascript:void(0)", onclick="javascript:showChannelSettings()") Channel Settings
|
li: a#showchansettings(href="javascript:void(0)", onclick="javascript:showChannelSettings()") Channel Settings
|
||||||
li.dropdown
|
li.dropdown
|
||||||
a.dropdown-toggle(href="#", data-toggle="dropdown") Layout
|
a.dropdown-toggle(href="#", data-toggle="dropdown") Layout
|
||||||
|
|
@ -28,6 +29,21 @@ html(lang="en")
|
||||||
+navloginlogout()
|
+navloginlogout()
|
||||||
section#mainpage
|
section#mainpage
|
||||||
.container
|
.container
|
||||||
|
#showschedule-row.row(style="display:none;")
|
||||||
|
.col-lg-12.col-md-12
|
||||||
|
#showschedule.panel.panel-default
|
||||||
|
.panel-heading
|
||||||
|
.pull-right
|
||||||
|
.btn-group.btn-group-xs
|
||||||
|
button#showschedule-prev.btn.btn-default(type="button") Prev Week
|
||||||
|
button#showschedule-today.btn.btn-default(type="button") This Week
|
||||||
|
button#showschedule-next.btn.btn-default(type="button") Next Week
|
||||||
|
strong Channel Schedule
|
||||||
|
span#showschedule-week-label.text-muted(style="margin-left:8px")
|
||||||
|
.clear
|
||||||
|
.panel-body
|
||||||
|
p#showschedule-empty.text-muted(style="display:none; margin-bottom:8px;") No scheduled shows this week.
|
||||||
|
#showschedule-grid
|
||||||
#motdrow.row
|
#motdrow.row
|
||||||
.col-lg-12.col-md-12
|
.col-lg-12.col-md-12
|
||||||
#motdwrap.well
|
#motdwrap.well
|
||||||
|
|
@ -88,7 +104,6 @@ html(lang="en")
|
||||||
span.glyphicon.glyphicon-link
|
span.glyphicon.glyphicon-link
|
||||||
button#voteskip.btn.btn-sm.btn-default(title="Voteskip")
|
button#voteskip.btn.btn-sm.btn-default(title="Voteskip")
|
||||||
span.glyphicon.glyphicon-step-forward
|
span.glyphicon.glyphicon-step-forward
|
||||||
|
|
||||||
#playlistrow.row
|
#playlistrow.row
|
||||||
#leftpane.col-lg-5.col-md-5
|
#leftpane.col-lg-5.col-md-5
|
||||||
#leftpane-inner.row
|
#leftpane-inner.row
|
||||||
|
|
@ -244,6 +259,25 @@ html(lang="en")
|
||||||
+permeditor()
|
+permeditor()
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-default(type="button", data-dismiss="modal") Close
|
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
|
#pmbar
|
||||||
include footer
|
include footer
|
||||||
+footer()
|
+footer()
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,12 @@ 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-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
|
.form-group
|
||||||
label.control-label.col-sm-3(for="cs-shows-timezone") Timezone
|
label.control-label.col-sm-3(for="cs-shows-timezone") Timezone
|
||||||
.col-sm-9
|
.col-sm-9
|
||||||
|
|
@ -296,6 +302,11 @@ mixin shows
|
||||||
label(for="cs-shows-start-playback")
|
label(for="cs-shows-start-playback")
|
||||||
input#cs-shows-start-playback(type="checkbox")
|
input#cs-shows-start-playback(type="checkbox")
|
||||||
| Skip current playback and start this show's first item immediately
|
| 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
|
.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
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,53 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#showschedule-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#showschedule-grid table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschedule-time-col {
|
||||||
|
width: 60px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschedule-cell {
|
||||||
|
min-height: 30px;
|
||||||
|
cursor: default;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschedule-cell.showschedule-admin {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschedule-show {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschedule-show.status-scheduled { background: #337ab7; }
|
||||||
|
.showschedule-show.status-running { background: #5cb85c; }
|
||||||
|
.showschedule-show.status-paused { background: #f0ad4e; color: #222; }
|
||||||
|
.showschedule-show.status-completed { background: #777; }
|
||||||
|
|
||||||
|
#showdetails-notes img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
197
www/js/ui.js
197
www/js/ui.js
|
|
@ -28,6 +28,24 @@ $("#togglemotd").on('click', function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateScheduleToggleLabel() {
|
||||||
|
var row = $("#showschedule-row");
|
||||||
|
if (!row.length) return;
|
||||||
|
$("#toggleschedule").text(row.is(":visible") ? "Hide Schedule" : "Show Schedule");
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#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();
|
||||||
|
|
||||||
/* chatbox */
|
/* chatbox */
|
||||||
|
|
||||||
$("#modflair").on('click', function () {
|
$("#modflair").on('click', function () {
|
||||||
|
|
@ -1416,11 +1434,17 @@ var CSTShows = (function () {
|
||||||
var draftPlaylist = [];
|
var draftPlaylist = [];
|
||||||
var timezoneOptionsLoaded = false;
|
var timezoneOptionsLoaded = false;
|
||||||
var resolvingTitles = false;
|
var resolvingTitles = false;
|
||||||
|
var weekOffset = 0;
|
||||||
|
var cachedShows = [];
|
||||||
|
|
||||||
function apiBase() {
|
function apiBase() {
|
||||||
return '/api/v1/channels/' + CHANNEL.name + '/shows';
|
return '/api/v1/channels/' + CHANNEL.name + '/shows';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publicApiBase() {
|
||||||
|
return apiBase() + '/public';
|
||||||
|
}
|
||||||
|
|
||||||
function loadTimezoneOptions() {
|
function loadTimezoneOptions() {
|
||||||
if (timezoneOptionsLoaded) return;
|
if (timezoneOptionsLoaded) return;
|
||||||
timezoneOptionsLoaded = true;
|
timezoneOptionsLoaded = true;
|
||||||
|
|
@ -1604,11 +1628,21 @@ var CSTShows = (function () {
|
||||||
function readFormPayload() {
|
function readFormPayload() {
|
||||||
var scheduledRaw = $('#cs-shows-scheduled-for').val();
|
var scheduledRaw = $('#cs-shows-scheduled-for').val();
|
||||||
var timezone = $('#cs-shows-timezone').val().trim();
|
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) {
|
if (!timezone) {
|
||||||
timezone = 'UTC';
|
timezone = 'UTC';
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: $('#cs-shows-name').val().trim(),
|
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,
|
scheduled_for: scheduledRaw ? new Date(scheduledRaw).toISOString() : null,
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
recurrence: $('#cs-shows-recurrence').val(),
|
recurrence: $('#cs-shows-recurrence').val(),
|
||||||
|
|
@ -1626,6 +1660,7 @@ var CSTShows = (function () {
|
||||||
loadTimezoneOptions();
|
loadTimezoneOptions();
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
$('#cs-shows-name').val('');
|
$('#cs-shows-name').val('');
|
||||||
|
$('#cs-shows-notes').val('');
|
||||||
$('#cs-shows-scheduled-for').val('');
|
$('#cs-shows-scheduled-for').val('');
|
||||||
var detectedTz = 'UTC';
|
var detectedTz = 'UTC';
|
||||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||||
|
|
@ -1639,6 +1674,8 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-fill-mode').val('append');
|
$('#cs-shows-fill-mode').val('append');
|
||||||
$('#cs-shows-conflict-skip').prop('checked', false);
|
$('#cs-shows-conflict-skip').prop('checked', false);
|
||||||
$('#cs-shows-start-playback').prop('checked', false);
|
$('#cs-shows-start-playback').prop('checked', false);
|
||||||
|
$('#cs-shows-color').val('#337AB7');
|
||||||
|
$('#cs-shows-color-hex').val('');
|
||||||
$('#cs-shows-mediaurl').val('');
|
$('#cs-shows-mediaurl').val('');
|
||||||
draftPlaylist = [];
|
draftPlaylist = [];
|
||||||
renderDraftPlaylist();
|
renderDraftPlaylist();
|
||||||
|
|
@ -1648,6 +1685,7 @@ var CSTShows = (function () {
|
||||||
loadTimezoneOptions();
|
loadTimezoneOptions();
|
||||||
selectedId = show.id;
|
selectedId = show.id;
|
||||||
$('#cs-shows-name').val(show.name);
|
$('#cs-shows-name').val(show.name);
|
||||||
|
$('#cs-shows-notes').val(show.notes || '');
|
||||||
$('#cs-shows-scheduled-for').val(toLocalDateInput(show.scheduled_for));
|
$('#cs-shows-scheduled-for').val(toLocalDateInput(show.scheduled_for));
|
||||||
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) {
|
||||||
|
|
@ -1658,6 +1696,8 @@ var CSTShows = (function () {
|
||||||
$('#cs-shows-fill-mode').val(show.fill_mode || 'append');
|
$('#cs-shows-fill-mode').val(show.fill_mode || 'append');
|
||||||
$('#cs-shows-conflict-skip').prop('checked', (show.conflict_mode || 'force') === 'skip');
|
$('#cs-shows-conflict-skip').prop('checked', (show.conflict_mode || 'force') === 'skip');
|
||||||
$('#cs-shows-start-playback').prop('checked', !!show.start_playback);
|
$('#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) {
|
draftPlaylist = (show.playlist || []).map(function (item) {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|
@ -1670,6 +1710,125 @@ var CSTShows = (function () {
|
||||||
resolveDraftTitles();
|
resolveDraftTitles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openShowsEditor() {
|
||||||
|
showChannelSettings();
|
||||||
|
$("#channeloptions a[href='#cs-shows']").tab('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefillScheduledDate(date) {
|
||||||
|
clearForm();
|
||||||
|
selectedId = null;
|
||||||
|
$('#cs-shows-scheduled-for').val(toLocalDateInput(date.getTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function weekStartFromOffset(offset) {
|
||||||
|
var d = new Date();
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
var day = d.getDay();
|
||||||
|
var mondayShift = (day + 6) % 7;
|
||||||
|
d.setDate(d.getDate() - mondayShift + (offset * 7));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayKey(date) {
|
||||||
|
return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCellKey(date) {
|
||||||
|
return dayKey(date) + '-' + date.getHours();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScheduleCalendar(shows) {
|
||||||
|
var grid = $('#showschedule-grid');
|
||||||
|
if (!grid.length) return;
|
||||||
|
|
||||||
|
var weekStart = weekStartFromOffset(weekOffset);
|
||||||
|
var weekEnd = new Date(weekStart.getTime());
|
||||||
|
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||||
|
$('#showschedule-week-label').text(
|
||||||
|
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('<p class="text-muted">No notes for this show.</p>');
|
||||||
|
} else {
|
||||||
|
$('#showdetails-notes').html(notes);
|
||||||
|
}
|
||||||
|
$('#showdetails').modal();
|
||||||
|
}
|
||||||
|
|
||||||
|
var byCell = {};
|
||||||
|
shows.forEach(function (show) {
|
||||||
|
var at = show.next_run_at || show.scheduled_for;
|
||||||
|
if (!at) return;
|
||||||
|
var d = new Date(at);
|
||||||
|
if (d < weekStart || d > new Date(weekEnd.getTime() + 86399999)) return;
|
||||||
|
var key = toCellKey(d);
|
||||||
|
if (!byCell[key]) byCell[key] = [];
|
||||||
|
byCell[key].push({ show: show, date: d });
|
||||||
|
});
|
||||||
|
|
||||||
|
var isAdmin = CLIENT.rank >= 2;
|
||||||
|
var table = $('<table class="table table-bordered table-condensed">');
|
||||||
|
var thead = $('<thead><tr><th class="showschedule-time-col">Time</th></tr></thead>').appendTo(table);
|
||||||
|
var hrow = thead.find('tr');
|
||||||
|
for (var i = 0; i < 7; i++) {
|
||||||
|
var day = new Date(weekStart.getTime());
|
||||||
|
day.setDate(weekStart.getDate() + i);
|
||||||
|
hrow.append($('<th>').text(day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tbody = $('<tbody>').appendTo(table);
|
||||||
|
for (var hour = 0; hour < 24; hour++) {
|
||||||
|
var tr = $('<tr>').appendTo(tbody);
|
||||||
|
tr.append($('<td class="showschedule-time-col text-muted">').text(String(hour).padStart(2, '0') + ':00'));
|
||||||
|
for (var col = 0; col < 7; col++) {
|
||||||
|
var cellDate = new Date(weekStart.getTime());
|
||||||
|
cellDate.setDate(weekStart.getDate() + col);
|
||||||
|
cellDate.setHours(hour, 0, 0, 0);
|
||||||
|
var key = toCellKey(cellDate);
|
||||||
|
var cell = $('<td class="showschedule-cell">').appendTo(tr);
|
||||||
|
if (isAdmin) {
|
||||||
|
cell.addClass('showschedule-admin').attr('title', 'Click to create show at this time');
|
||||||
|
(function (prefill) {
|
||||||
|
cell.on('click', function (ev) {
|
||||||
|
if ($(ev.target).closest('.showschedule-show').length) return;
|
||||||
|
openShowsEditor();
|
||||||
|
prefillScheduledDate(prefill);
|
||||||
|
});
|
||||||
|
})(new Date(cellDate.getTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = byCell[key] || [];
|
||||||
|
items.sort(function (a, b) { return a.date - b.date; });
|
||||||
|
items.forEach(function (item) {
|
||||||
|
var label = item.date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' ' + item.show.name;
|
||||||
|
$('<a href="javascript:void(0)" class="showschedule-show">')
|
||||||
|
.addClass('status-' + (item.show.status || 'scheduled'))
|
||||||
|
.text(label)
|
||||||
|
.css('background', item.show.color || '')
|
||||||
|
.on('click', function () {
|
||||||
|
if (isAdmin) {
|
||||||
|
openShowsEditor();
|
||||||
|
selectShow(item.show);
|
||||||
|
} else {
|
||||||
|
openShowDetailsModal(item.show, item.date);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.appendTo(cell);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.empty().append(table);
|
||||||
|
$('#showschedule-empty').toggle(shows.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
function action(id, actionName) {
|
function action(id, actionName) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: apiBase() + '/' + id + '/action',
|
url: apiBase() + '/' + id + '/action',
|
||||||
|
|
@ -1733,8 +1892,18 @@ var CSTShows = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
$.getJSON(apiBase(), render).fail(function () {
|
var endpoint = CLIENT.rank >= 2 ? apiBase() : publicApiBase();
|
||||||
$('#cs-shows-list').html('<tr><td colspan=\"6\" class=\"text-danger\">Failed to load shows</td></tr>');
|
$.getJSON(endpoint, function (shows) {
|
||||||
|
cachedShows = Array.isArray(shows) ? shows : [];
|
||||||
|
if (CLIENT.rank >= 2) {
|
||||||
|
render(cachedShows);
|
||||||
|
}
|
||||||
|
renderScheduleCalendar(cachedShows);
|
||||||
|
}).fail(function () {
|
||||||
|
if (CLIENT.rank >= 2) {
|
||||||
|
$('#cs-shows-list').html('<tr><td colspan=\"6\" class=\"text-danger\">Failed to load shows</td></tr>');
|
||||||
|
}
|
||||||
|
$('#showschedule-grid').html('<div class=\"text-danger\">Failed to load schedule</div>');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1782,6 +1951,15 @@ var CSTShows = (function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#cs-shows-clear').on('click', clearForm);
|
$('#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({
|
$('#cs-shows-playlist-list').sortable({
|
||||||
update: function () {
|
update: function () {
|
||||||
var nextDraft = [];
|
var nextDraft = [];
|
||||||
|
|
@ -1797,8 +1975,21 @@ var CSTShows = (function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).disableSelection();
|
}).disableSelection();
|
||||||
|
$('#showschedule-prev').on('click', function () {
|
||||||
|
weekOffset--;
|
||||||
|
renderScheduleCalendar(cachedShows);
|
||||||
|
});
|
||||||
|
$('#showschedule-next').on('click', function () {
|
||||||
|
weekOffset++;
|
||||||
|
renderScheduleCalendar(cachedShows);
|
||||||
|
});
|
||||||
|
$('#showschedule-today').on('click', function () {
|
||||||
|
weekOffset = 0;
|
||||||
|
renderScheduleCalendar(cachedShows);
|
||||||
|
});
|
||||||
renderDraftPlaylist();
|
renderDraftPlaylist();
|
||||||
clearForm();
|
clearForm();
|
||||||
|
load();
|
||||||
|
|
||||||
return { load: load };
|
return { load: load, selectShow: selectShow, prefillScheduledDate: prefillScheduledDate };
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue