Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8020a12408 | ||
|
|
b7f1d800ef | ||
|
|
b0649d838e |
+4
-6
@@ -1,6 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
uploads
|
||||
.git
|
||||
.gitignore
|
||||
*.tmp
|
||||
*
|
||||
!package.json
|
||||
!src/
|
||||
!src/**
|
||||
|
||||
+7
-7
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"version": "1.1.3",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media uploads",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.lzstealth.com/LZStealth/pulse-signage.git"
|
||||
},
|
||||
"main": "src/common.js",
|
||||
"scripts": {
|
||||
"start": "node -r dotenv/config src/web.js",
|
||||
"start:web": "node -r dotenv/config src/web.js",
|
||||
"start:player": "node -r dotenv/config src/player.js",
|
||||
"dev:web": "nodemon -r dotenv/config src/web.js",
|
||||
"dev:player": "nodemon -r dotenv/config src/player.js",
|
||||
"docker:build": "docker build -t pulse-signage:test .",
|
||||
"docker:up": "docker-compose up -d",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:logs": "docker-compose logs -f --tail=100"
|
||||
"dev:player": "nodemon -r dotenv/config src/player.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.2",
|
||||
|
||||
+141
-16
@@ -126,7 +126,9 @@ a {
|
||||
top: 0;
|
||||
align-self: start;
|
||||
min-height: 100vh;
|
||||
padding: 22px 18px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 12px 12px;
|
||||
color: var(--sidebar-text);
|
||||
background: var(--sidebar-background);
|
||||
border-right: 1px solid var(--sidebar-border);
|
||||
@@ -220,7 +222,6 @@ a {
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding: 16px 14px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -228,8 +229,16 @@ a {
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.sidebar-footer--bottom {
|
||||
margin-top: auto;
|
||||
padding: 0;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sidebar-footer strong,
|
||||
.sidebar-footer span {
|
||||
.sidebar-footer > div > span,
|
||||
.sidebar-footer > .sidebar-version {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -244,7 +253,7 @@ a {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sidebar-footer span {
|
||||
.sidebar-footer > div > span {
|
||||
color: var(--sidebar-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
@@ -301,6 +310,14 @@ a {
|
||||
color: var(--sidebar-text-strong);
|
||||
}
|
||||
|
||||
.sidebar-version {
|
||||
color: var(--sidebar-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -359,7 +376,7 @@ a {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
background: var(--sidebar-background);
|
||||
}
|
||||
|
||||
.login-page-body .theme-toggle__icon--moon::after {
|
||||
@@ -408,14 +425,16 @@ html[data-theme='dark'] .theme-toggle__icon--sun {
|
||||
}
|
||||
|
||||
.theme-toggle--sidebar {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
z-index: 2;
|
||||
color: var(--sidebar-text-strong);
|
||||
}
|
||||
|
||||
.theme-toggle--sidebar .theme-toggle__icon--moon,
|
||||
.theme-toggle--sidebar .theme-toggle__icon--sun {
|
||||
color: var(--sidebar-text-strong);
|
||||
}
|
||||
|
||||
.theme-toggle--sidebar .theme-toggle__label {
|
||||
@@ -1075,6 +1094,13 @@ label {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
label > input,
|
||||
label > textarea,
|
||||
label > select,
|
||||
label > button {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
@@ -1082,7 +1108,6 @@ button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 12px;
|
||||
margin-top: 6px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 12px;
|
||||
font: inherit;
|
||||
@@ -1600,7 +1625,8 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
table.table-cards td[data-label="Actions"] {
|
||||
width: 100%;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.table-cards td .actions {
|
||||
@@ -1609,11 +1635,11 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
table.table-cards td[data-label="Actions"] .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
table.table-cards td[data-label="Actions"] .actions > * {
|
||||
@@ -1643,7 +1669,7 @@ tbody tr:last-child td {
|
||||
table.table-cards td[data-label="Actions"] .list-action {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
min-width: 96px;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1857,6 +1883,101 @@ table td .actions form {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.playlist-order-cell {
|
||||
width: 88px;
|
||||
min-width: 88px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.playlist-order-cell-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.playlist-order-number {
|
||||
min-width: 1.5em;
|
||||
font-weight: 800;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.playlist-drag-handle {
|
||||
flex: 0 0 auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
color: var(--muted);
|
||||
background: currentColor;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M12 4L8 8H16L12 4Z'/%3E%3Cpath d='M4 11H20V13H4V11Z'/%3E%3Cpath d='M4 15H20V17H4V15Z'/%3E%3Cpath d='M4 7H20V9H4V7Z'/%3E%3Cpath d='M12 20L8 16H16L12 20Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M12 4L8 8H16L12 4Z'/%3E%3Cpath d='M4 11H20V13H4V11Z'/%3E%3Cpath d='M4 15H20V17H4V15Z'/%3E%3Cpath d='M4 7H20V9H4V7Z'/%3E%3Cpath d='M12 20L8 16H16L12 20Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
cursor: grab;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
|
||||
.playlist-drag-handle {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.playlist-drag-handle:hover,
|
||||
.playlist-drag-handle:focus-visible {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .playlist-drag-handle {
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .playlist-drag-handle:hover,
|
||||
html[data-theme='dark'] .playlist-drag-handle:focus-visible {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.playlist-drag-handle:active,
|
||||
tr.is-dragging .playlist-drag-handle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
tr.sortable-chosen {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
tr.sortable-ghost {
|
||||
opacity: 0.32;
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
tr.sortable-drag {
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
#playlist-items-body tr.sortable-drag {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#playlist-items-body tr.sortable-drag > td {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#playlist-items-body tr.sortable-drag .playlist-order-cell-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#playlist-items-body tr.sortable-drag .playlist-drag-handle {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 721px) {
|
||||
}
|
||||
|
||||
.actions form {
|
||||
display: inline;
|
||||
}
|
||||
@@ -1892,6 +2013,11 @@ table td .actions form {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .chip {
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.slide-editor-top {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
@@ -1974,7 +2100,6 @@ table td .actions form {
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.slide-preview-region h1,
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
return [
|
||||
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '">',
|
||||
'<td data-label="Client/Connection ID"><div class="connection-count">' + clientId + '</div><div class="connection-id">' + connectionId + '</div></td>',
|
||||
'<td data-label="Client/Connection ID"><div class="connection-count">' + clientId + '</div><br><div class="connection-id">' + connectionId + '</div></td>',
|
||||
'<td data-label="IP">' + clientIp + '</td>',
|
||||
'<td data-label="Viewport">' + viewport + '</td>',
|
||||
'<td data-label="Connected">' + connectedAt + '</td>',
|
||||
@@ -249,7 +249,7 @@
|
||||
var connectionId = client.id ? escapeHtml(client.id) : '<span class="empty">Unknown</span>';
|
||||
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : 'No slide currently showing';
|
||||
|
||||
row.cells[0].innerHTML = '<div class="connection-count">' + clientId + '</div><div class="connection-id">' + connectionId + '</div>';
|
||||
row.cells[0].innerHTML = '<div class="connection-count">' + clientId + '</div><br><div class="connection-id">' + connectionId + '</div>';
|
||||
row.cells[1].innerHTML = clientIp;
|
||||
row.cells[2].innerHTML = viewport;
|
||||
row.cells[3].innerHTML = connectedAt;
|
||||
|
||||
@@ -230,7 +230,6 @@
|
||||
var addSlideForm = document.getElementById('playlist-add-slide-form');
|
||||
var addSlideButton = document.getElementById('playlist-add-slide-button');
|
||||
var addSlideSelect = document.getElementById('playlist-add-slide-select');
|
||||
var addDurationInput = document.getElementById('playlist-add-duration');
|
||||
var addSlideEmpty = document.getElementById('playlist-add-slide-empty');
|
||||
|
||||
if (!tbody || !form) {
|
||||
@@ -351,9 +350,6 @@
|
||||
if (addSlideSelect) {
|
||||
addSlideSelect.disabled = availableCount === 0;
|
||||
}
|
||||
if (addDurationInput) {
|
||||
addDurationInput.disabled = availableCount === 0;
|
||||
}
|
||||
if (addSlideEmpty) {
|
||||
addSlideEmpty.style.display = availableCount === 0 ? '' : 'none';
|
||||
}
|
||||
@@ -366,23 +362,49 @@
|
||||
function updateRowOrder() {
|
||||
var rows = getRows();
|
||||
rows.forEach(function (row, index) {
|
||||
var orderCell = row.querySelector('.playlist-order-cell');
|
||||
var moveUp = row.querySelector('[data-playlist-move="up"]');
|
||||
var moveDown = row.querySelector('[data-playlist-move="down"]');
|
||||
if (orderCell) {
|
||||
orderCell.textContent = String(index + 1);
|
||||
}
|
||||
if (moveUp) {
|
||||
moveUp.disabled = index === 0;
|
||||
}
|
||||
if (moveDown) {
|
||||
moveDown.disabled = index === rows.length - 1;
|
||||
var orderNumber = row.querySelector('.playlist-order-number');
|
||||
if (orderNumber) {
|
||||
orderNumber.textContent = String(index + 1);
|
||||
}
|
||||
});
|
||||
syncAddSlideOptions();
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
function lockDraggedRowWidths(row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowRect = row.getBoundingClientRect();
|
||||
row.style.width = rowRect.width + 'px';
|
||||
row.style.height = rowRect.height + 'px';
|
||||
row.style.boxSizing = 'border-box';
|
||||
|
||||
Array.prototype.forEach.call(row.children, function (cell) {
|
||||
var cellRect = cell.getBoundingClientRect();
|
||||
cell.style.width = cellRect.width + 'px';
|
||||
cell.style.height = cellRect.height + 'px';
|
||||
cell.style.boxSizing = 'border-box';
|
||||
});
|
||||
}
|
||||
|
||||
function unlockDraggedRowWidths(row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.style.width = '';
|
||||
row.style.height = '';
|
||||
row.style.boxSizing = '';
|
||||
|
||||
Array.prototype.forEach.call(row.children, function (cell) {
|
||||
cell.style.width = '';
|
||||
cell.style.height = '';
|
||||
cell.style.boxSizing = '';
|
||||
});
|
||||
}
|
||||
|
||||
function setRowSchedule(row, values) {
|
||||
var modeInput = row.querySelector('[name="schedule_mode[]"]');
|
||||
var startDatetime = row.querySelector('[name="schedule_start_datetime[]"]');
|
||||
@@ -423,9 +445,13 @@
|
||||
row.setAttribute('data-slide-id', String(values.slide_id));
|
||||
row.setAttribute('data-canvas-signature', String(values.canvas_signature || ''));
|
||||
row.innerHTML = '' +
|
||||
'<td class="playlist-order-cell"></td>' +
|
||||
'<td class="playlist-order-cell" data-label="Order">' +
|
||||
'<div class="playlist-order-cell-inner">' +
|
||||
'<button type="button" class="playlist-drag-handle" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"></button>' +
|
||||
'<span class="playlist-order-number"></span>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td>' + values.title + '<input type="hidden" name="slide_id[]" value="' + values.slide_id + '" /></td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required /></td>' +
|
||||
'<td>' +
|
||||
'<div class="playlist-schedule-summary">' + values.summary + '</div>' +
|
||||
'<input type="hidden" name="schedule_mode[]" value="' + values.schedule_mode + '" />' +
|
||||
@@ -435,9 +461,8 @@
|
||||
'<input type="hidden" name="schedule_end_time[]" value="' + values.schedule_end_time + '" />' +
|
||||
'<input type="hidden" name="schedule_days_json[]" value="' + values.schedule_days_json + '" />' +
|
||||
'</td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required /></td>' +
|
||||
'<td><div class="actions playlist-item-actions">' +
|
||||
'<button type="button" class="secondary" data-playlist-move="up">Up</button>' +
|
||||
'<button type="button" class="secondary" data-playlist-move="down">Down</button>' +
|
||||
'<button type="button" class="schedule-config-button" disabled>Schedule</button>' +
|
||||
'<button type="button" class="danger" data-playlist-remove-row>Remove</button>' +
|
||||
'</div></td>';
|
||||
@@ -445,7 +470,6 @@
|
||||
}
|
||||
|
||||
tbody.addEventListener('click', function (event) {
|
||||
var moveButton = event.target.closest('[data-playlist-move]');
|
||||
var removeButton = event.target.closest('[data-playlist-remove-row]');
|
||||
var scheduleButton = event.target.closest('[data-schedule-config]');
|
||||
var row = event.target.closest('tr[data-playlist-slide-row]');
|
||||
@@ -454,18 +478,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveButton) {
|
||||
event.preventDefault();
|
||||
var direction = moveButton.getAttribute('data-playlist-move');
|
||||
if (direction === 'up' && row.previousElementSibling && row.previousElementSibling.matches('[data-playlist-slide-row]')) {
|
||||
tbody.insertBefore(row, row.previousElementSibling);
|
||||
} else if (direction === 'down' && row.nextElementSibling && row.nextElementSibling.matches('[data-playlist-slide-row]')) {
|
||||
tbody.insertBefore(row.nextElementSibling, row);
|
||||
}
|
||||
updateRowOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeButton) {
|
||||
event.preventDefault();
|
||||
row.remove();
|
||||
@@ -481,7 +493,33 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (addSlideButton && addSlideSelect && addDurationInput) {
|
||||
if (window.Sortable) {
|
||||
Sortable.create(tbody, {
|
||||
animation: 180,
|
||||
handle: '[data-playlist-drag-handle]',
|
||||
draggable: 'tr[data-playlist-slide-row]',
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
forceFallback: true,
|
||||
fallbackOnBody: true,
|
||||
fallbackTolerance: 3,
|
||||
swapThreshold: 0.65,
|
||||
invertedSwapThreshold: 0.65,
|
||||
onChoose: function (event) {
|
||||
lockDraggedRowWidths(event && event.item);
|
||||
},
|
||||
onUnchoose: function (event) {
|
||||
unlockDraggedRowWidths(event && event.item);
|
||||
},
|
||||
onEnd: function () {
|
||||
unlockDraggedRowWidths(tbody.querySelector('.sortable-drag'));
|
||||
updateRowOrder();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (addSlideButton && addSlideSelect) {
|
||||
addSlideButton.addEventListener('click', function () {
|
||||
var selectedOption = addSlideSelect.options[addSlideSelect.selectedIndex];
|
||||
if (!selectedOption || selectedOption.disabled) {
|
||||
@@ -496,7 +534,7 @@
|
||||
slide_id: String(selectedOption.value),
|
||||
canvas_signature: String(selectedOption.getAttribute('data-canvas-signature') || ''),
|
||||
title: selectedOption.textContent || 'Slide',
|
||||
duration_seconds: Math.max(1, Number(addDurationInput.value || 10)),
|
||||
duration_seconds: 10,
|
||||
schedule_mode: 'always',
|
||||
schedule_start_datetime: '',
|
||||
schedule_end_datetime: '',
|
||||
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -110,6 +110,6 @@ module.exports = function renderPlaylistEditPage(playlist, data, message, curren
|
||||
selectableSlides: selectableSlides,
|
||||
playlistCanvasSignature: playlistCanvasSignature,
|
||||
playlistCanvasMismatch: playlistCanvasMismatch,
|
||||
scripts: ['js/playlist-schedule.js']
|
||||
scripts: ['js/sortable.min.js', 'js/playlist-schedule.js']
|
||||
});
|
||||
};
|
||||
|
||||
+7
-2
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Handlebars = require('handlebars');
|
||||
const { version: appVersion } = require('../../package.json');
|
||||
|
||||
const VIEWS_ROOT = path.join(__dirname, 'views');
|
||||
const cache = new Map();
|
||||
@@ -9,6 +10,10 @@ Handlebars.registerHelper('eq', function (left, right) {
|
||||
return left === right;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('isExternalUrl', function (value) {
|
||||
return /^https?:\/\//i.test(String(value || ''));
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('playerUrl', function (slug) {
|
||||
const base = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').replace(/\/$/, '');
|
||||
return `${base}/screen/${encodeURIComponent(slug)}`;
|
||||
@@ -47,7 +52,7 @@ function loadTemplate(relativePath) {
|
||||
}
|
||||
|
||||
function renderView(viewName, context) {
|
||||
const viewContext = Object.assign({ stylesheets: [], scripts: [] }, context || {});
|
||||
const viewContext = Object.assign({ stylesheets: [], scripts: [], appVersion: appVersion }, context || {});
|
||||
const page = loadTemplate(`${viewName}.hbs`);
|
||||
const layout = loadTemplate(path.join('layout.hbs'));
|
||||
const body = page(viewContext);
|
||||
@@ -55,7 +60,7 @@ function renderView(viewName, context) {
|
||||
}
|
||||
|
||||
function renderFragment(viewName, context) {
|
||||
const viewContext = Object.assign({ stylesheets: [], scripts: [] }, context || {});
|
||||
const viewContext = Object.assign({ stylesheets: [], scripts: [], appVersion: appVersion }, context || {});
|
||||
const page = loadTemplate(`${viewName}.hbs`);
|
||||
const layout = loadTemplate(path.join('frame-layout.hbs'));
|
||||
const body = page(viewContext);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{title}} - Signage</title>
|
||||
<title>{{title}} - Pulse</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>{{title}} - Signage</title>
|
||||
<title>{{title}} - Pulse</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
@@ -75,9 +75,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="theme-toggle theme-toggle--sidebar" data-theme-toggle aria-pressed="false" aria-label="Switch to dark mode">
|
||||
<span class="theme-toggle__icon theme-toggle__icon--moon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="sidebar-footer sidebar-footer--bottom" aria-label="Sidebar controls">
|
||||
<span class="sidebar-version">v{{appVersion}}</span>
|
||||
<button type="button" class="theme-toggle theme-toggle--sidebar" data-theme-toggle aria-pressed="false" aria-label="Switch to dark mode">
|
||||
<span class="theme-toggle__icon theme-toggle__icon--moon" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<button type="button" class="sidebar-backdrop" data-sidebar-backdrop aria-label="Close navigation"></button>
|
||||
@@ -102,7 +105,11 @@
|
||||
{{/if}}
|
||||
{{#if scripts.length}}
|
||||
{{#each scripts}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{#if (isExternalUrl this)}}
|
||||
<script src="{{this}}"></script>
|
||||
{{else}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</body>
|
||||
|
||||
@@ -29,19 +29,21 @@
|
||||
<div class="card">
|
||||
<h3>Playlist items</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Duration</th><th>Schedule</th><th>Actions</th></tr></thead>
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Duration</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body">
|
||||
{{#if playlistSlides.length}}
|
||||
{{#each playlistSlides}}
|
||||
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}">
|
||||
<td class="playlist-order-cell" data-label="Order">{{displayOrder}}</td>
|
||||
<td class="playlist-order-cell" data-label="Order">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"></button>
|
||||
<span class="playlist-order-number">{{displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
{{title}}
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Schedule">
|
||||
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
|
||||
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" />
|
||||
@@ -51,10 +53,11 @@
|
||||
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" />
|
||||
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="secondary" data-playlist-move="up" {{#if isFirst}}disabled{{/if}}>Up</button>
|
||||
<button type="button" class="secondary" data-playlist-move="down" {{#if isLast}}disabled{{/if}}>Down</button>
|
||||
<button type="button" class="schedule-config-button" data-schedule-config="/admin/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
|
||||
<button type="button" class="danger" data-playlist-remove-row>Remove</button>
|
||||
</div>
|
||||
@@ -82,9 +85,6 @@
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label style="width: 160px;">Visible for (seconds)
|
||||
<input id="playlist-add-duration" type="number" min="1" value="10" required />
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="playlist-add-slide-button">Add slide</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user