Release v2.4.2

This commit is contained in:
2026-08-03 12:45:27 +01:00
parent 2b9cabdab2
commit e95928f31c
141 changed files with 7694 additions and 5078 deletions
+33 -20
View File
@@ -90,25 +90,31 @@ function getCanvasSignature(width, height) {
return normalizedWidth + 'x' + normalizedHeight;
}
async function fetchPlaylistCanvasSignature(pool, playlistId) {
async function fetchPlaylistCanvasId(pool, playlistId) {
const [playlistRows] = await pool.query('SELECT canvas_id FROM c_playlists WHERE id = ?', [playlistId]);
const storedCanvasId = Number(playlistRows && playlistRows[0] && playlistRows[0].canvas_id);
if (Number.isInteger(storedCanvasId) && storedCanvasId > 0) {
return storedCanvasId;
}
const [rows] = await pool.query(
`SELECT DISTINCT cs.width AS canvas_width, cs.height AS canvas_height
`SELECT DISTINCT st.canvas_size_id AS canvas_id
FROM c_playlist_slides ps
JOIN c_slides sl ON sl.id = ps.slide_id
LEFT JOIN c_templates st ON st.id = sl.template_id
LEFT JOIN c_canvas_sizes cs ON cs.id = st.canvas_size_id
WHERE ps.playlist_id = ?
AND cs.width IS NOT NULL
AND cs.height IS NOT NULL`,
AND st.canvas_size_id IS NOT NULL`,
[playlistId]
);
const signatures = Array.from(new Set(rows.map(function (row) {
return getCanvasSignature(row.canvas_width, row.canvas_height);
}).filter(Boolean)));
if (!signatures.length) {
const canvasIds = Array.from(new Set(rows.map(function (row) {
return Number(row.canvas_id);
}).filter(function (value) {
return Number.isInteger(value) && value > 0;
})));
if (!canvasIds.length) {
return null;
}
return signatures.length === 1 ? signatures[0] : 'mismatch';
return canvasIds.length === 1 ? canvasIds[0] : 'mismatch';
}
async function fetchScreensByPlaylistId(connection, playlistId) {
@@ -160,19 +166,25 @@ async function fetchOrderedPlaylistSlides(connection, playlistId) {
function redirectAfterSave(req, res, defaultUrl, options) {
const safeOptions = options || {};
const action = String((req && req.body && req.body.action) || req.query.action || '').toLowerCase();
const action = String((req && req.body && (req.body.action || req.body.save_action)) || req.query.action || '').toLowerCase();
const message = safeOptions.message || '';
function withMessage(url) {
if (!message) {
return url;
}
const joiner = String(url || '').indexOf('?') === -1 ? '?' : '&';
return url + joiner + 'message=' + encodeURIComponent(message);
}
if (action === 'close') {
return res.redirect(safeOptions.closeUrl || defaultUrl);
return res.redirect(withMessage(safeOptions.closeUrl || defaultUrl));
}
if (action === 'new') {
return res.redirect(safeOptions.newUrl || defaultUrl);
return res.redirect(withMessage(safeOptions.newUrl || defaultUrl));
}
const message = safeOptions.message || '';
if (message) {
const joiner = defaultUrl.indexOf('?') === -1 ? '?' : '&';
return res.redirect(defaultUrl + joiner + 'message=' + encodeURIComponent(message));
}
return res.redirect(defaultUrl);
return res.redirect(withMessage(defaultUrl));
}
module.exports = {
@@ -185,7 +197,8 @@ module.exports = {
normalizeScheduleMode: normalizeScheduleMode,
getAuditUserId: getAuditUserId,
getCanvasSignature: getCanvasSignature,
fetchPlaylistCanvasSignature: fetchPlaylistCanvasSignature,
fetchPlaylistCanvasId: fetchPlaylistCanvasId,
fetchPlaylistCanvasSignature: fetchPlaylistCanvasId,
fetchScreensByPlaylistId: fetchScreensByPlaylistId,
fetchScreensBySlideId: fetchScreensBySlideId,
fetchScreensByTemplateId: fetchScreensByTemplateId,
+215
View File
@@ -0,0 +1,215 @@
// Shared schedule rule helpers for playlist playback and editor rendering.
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
function normalizeDateTimeValue(value) {
const raw = String(value || '').trim();
return raw ? raw : null;
}
function normalizeTimeValue(value) {
const raw = String(value || '').trim();
if (!raw) {
return null;
}
const match = raw.match(/^(\d{2}):(\d{2})(?::\d{2})?$/);
if (!match) {
return null;
}
return match[1] + ':' + match[2];
}
function normalizeDayValues(value) {
let days = [];
if (Array.isArray(value)) {
days = value;
} else if (value !== undefined && value !== null && String(value).trim()) {
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
if (Array.isArray(parsed)) {
days = parsed;
}
} catch (_error) {
days = String(value)
.split(',')
.map(function (item) {
return item.trim();
})
.filter(Boolean);
}
}
return Array.from(new Set(days.map(function (day) {
return Number(day);
}).filter(function (day) {
return Number.isInteger(day) && day >= 0 && day <= 6;
}))).sort(function (left, right) {
return left - right;
});
}
function normalizeScheduleRule(rule) {
const input = rule && typeof rule === 'object' ? rule : {};
const startDatetime = normalizeDateTimeValue(
input.start_datetime || input.startDateTime || input.schedule_start_datetime || input.scheduleStartDatetime
);
const endDatetime = normalizeDateTimeValue(
input.end_datetime || input.endDateTime || input.schedule_end_datetime || input.scheduleEndDatetime
);
const startTime = normalizeTimeValue(
input.start_time || input.startTime || input.schedule_start_time || input.scheduleStartTime
);
const endTime = normalizeTimeValue(
input.end_time || input.endTime || input.schedule_end_time || input.scheduleEndTime
);
const days = normalizeDayValues(
input.days || input.day_list || input.schedule_days || input.schedule_days_json || input.scheduleDays
);
if (startDatetime || endDatetime) {
if (!startDatetime || !endDatetime) {
return null;
}
}
if (startTime || endTime) {
if (!startTime || !endTime) {
return null;
}
}
if (!startDatetime && !endDatetime && !startTime && !endTime && !days.length) {
return null;
}
const normalized = {};
if (startDatetime) {
normalized.start_datetime = startDatetime;
}
if (endDatetime) {
normalized.end_datetime = endDatetime;
}
if (startTime) {
normalized.start_time = startTime;
}
if (endTime) {
normalized.end_time = endTime;
}
if (days.length) {
normalized.days = days;
}
return normalized;
}
function parseScheduleRules(value) {
let rawRules = [];
if (Array.isArray(value)) {
rawRules = value;
} else if (value && typeof value === 'object' && Array.isArray(value.rules)) {
rawRules = value.rules;
} else {
const rawValue = String(value || '').trim();
if (!rawValue) {
return [];
}
try {
const parsed = JSON.parse(rawValue);
rawRules = Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return null;
}
}
return rawRules
.map(function (rule) {
return normalizeScheduleRule(rule);
})
.filter(Boolean);
}
function serializeScheduleRules(value) {
const normalized = parseScheduleRules(value);
if (normalized === null) {
return null;
}
return JSON.stringify(normalized);
}
function formatDateTime(value) {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
return raw.replace('T', ' ').slice(0, 16);
}
function formatTime(value) {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
return raw.slice(0, 5);
}
function formatDays(value) {
const days = normalizeDayValues(value);
return days
.map(function (day) {
return DAY_NAMES[day] || '';
})
.filter(Boolean)
.join(', ');
}
function formatScheduleRuleSummary(rule) {
const normalized = normalizeScheduleRule(rule);
if (!normalized) {
return 'Always visible';
}
const leftParts = [];
if (normalized.start_datetime && normalized.end_datetime) {
leftParts.push('Dates ' + formatDateTime(normalized.start_datetime) + ' to ' + formatDateTime(normalized.end_datetime));
} else {
leftParts.push('Any date');
}
const rightParts = [];
const days = formatDays(normalized.days || []);
if (days) {
rightParts.push(days);
}
if (normalized.start_time && normalized.end_time) {
rightParts.push(formatTime(normalized.start_time) + '-' + formatTime(normalized.end_time));
}
if (rightParts.length) {
return leftParts[0] + ': ' + rightParts.join(' ');
}
return leftParts[0];
}
function formatScheduleRulesSummary(value) {
const rules = parseScheduleRules(value) || [];
if (!rules.length) {
return 'Always visible';
}
return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules';
}
module.exports = {
normalizeDateTimeValue: normalizeDateTimeValue,
normalizeTimeValue: normalizeTimeValue,
normalizeDayValues: normalizeDayValues,
normalizeScheduleRule: normalizeScheduleRule,
parseScheduleRules: parseScheduleRules,
serializeScheduleRules: serializeScheduleRules,
formatScheduleRuleSummary: formatScheduleRuleSummary,
formatScheduleRulesSummary: formatScheduleRulesSummary
};