From c977cbd7547181b4a371e063a69bd6b5fcd2b9c8 Mon Sep 17 00:00:00 2001 From: Speng Reb Date: Sun, 31 May 2026 22:24:43 +0200 Subject: [PATCH] Basic channel schedule --- src/web/routes/api/shows.js | 13 +++ templates/channel.pug | 17 +++- www/css/cytube.css | 41 ++++++++++ www/js/ui.js | 154 +++++++++++++++++++++++++++++++++++- 4 files changed, 221 insertions(+), 4 deletions(-) diff --git a/src/web/routes/api/shows.js b/src/web/routes/api/shows.js index b4ada722..9ffa5a37 100644 --- a/src/web/routes/api/shows.js +++ b/src/web/routes/api/shows.js @@ -19,6 +19,7 @@ const ACTION_MIN_RANK = { run: 3, cancel: 3 }; +const PUBLIC_SHOW_STATUSES = new Set(['scheduled', 'running', 'paused', 'completed']); function sanitizePlaylist(list) { if (!Array.isArray(list)) return []; @@ -181,6 +182,18 @@ router.get('/', async (req, res) => { 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) => { const auth = await authorizeChannel(req, res); if (!auth) return; diff --git a/templates/channel.pug b/templates/channel.pug index 1f1aadb8..d9883b8f 100644 --- a/templates/channel.pug +++ b/templates/channel.pug @@ -15,6 +15,7 @@ html(lang="en") ul.nav.navbar-nav +navdefaultlinks() 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.dropdown a.dropdown-toggle(href="#", data-toggle="dropdown") Layout @@ -28,6 +29,21 @@ html(lang="en") +navloginlogout() section#mainpage .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 .col-lg-12.col-md-12 #motdwrap.well @@ -88,7 +104,6 @@ html(lang="en") span.glyphicon.glyphicon-link button#voteskip.btn.btn-sm.btn-default(title="Voteskip") span.glyphicon.glyphicon-step-forward - #playlistrow.row #leftpane.col-lg-5.col-md-5 #leftpane-inner.row diff --git a/www/css/cytube.css b/www/css/cytube.css index e6a7ae7a..6dda3623 100644 --- a/www/css/cytube.css +++ b/www/css/cytube.css @@ -221,6 +221,47 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry { 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-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; } + .clear { clear: both; } diff --git a/www/js/ui.js b/www/js/ui.js index 6078ef95..989af594 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -28,6 +28,20 @@ $("#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; + row.toggle(); + updateScheduleToggleLabel(); +}); +updateScheduleToggleLabel(); + /* chatbox */ $("#modflair").on('click', function () { @@ -1416,11 +1430,17 @@ var CSTShows = (function () { var draftPlaylist = []; var timezoneOptionsLoaded = false; var resolvingTitles = false; + var weekOffset = 0; + var cachedShows = []; function apiBase() { return '/api/v1/channels/' + CHANNEL.name + '/shows'; } + function publicApiBase() { + return apiBase() + '/public'; + } + function loadTimezoneOptions() { if (timezoneOptionsLoaded) return; timezoneOptionsLoaded = true; @@ -1670,6 +1690,111 @@ var CSTShows = (function () { 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() + ); + + 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 = $(''); + var 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($('').appendTo(table); + for (var hour = 0; hour < 24; hour++) { + var tr = $('').appendTo(tbody); + tr.append($(''); + var endpoint = CLIENT.rank >= 2 ? apiBase() : publicApiBase(); + $.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(''); + } + $('#showschedule-grid').html('
Failed to load schedule
'); }); } @@ -1797,8 +1932,21 @@ var CSTShows = (function () { } } }).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(); clearForm(); + load(); - return { load: load }; + return { load: load, selectShow: selectShow, prefillScheduledDate: prefillScheduledDate }; })();
Time
').text(day.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }))); + } + + var tbody = $('
').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 = $('').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; + $('') + .addClass('status-' + (item.show.status || 'scheduled')) + .text(label) + .on('click', function () { + if (isAdmin) { + openShowsEditor(); + selectShow(item.show); + } else { + alert(item.show.name + '\n' + item.date.toLocaleString() + '\nStatus: ' + item.show.status); + } + }) + .appendTo(cell); + }); + } + } + + grid.empty().append(table); + $('#showschedule-empty').toggle(shows.length === 0); + } + function action(id, actionName) { $.ajax({ url: apiBase() + '/' + id + '/action', @@ -1733,8 +1858,18 @@ var CSTShows = (function () { } function load() { - $.getJSON(apiBase(), render).fail(function () { - $('#cs-shows-list').html('
Failed to load shows
Failed to load shows