Add player keyboard controls and release 2.7.1
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m17s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m17s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.7.1 - 2026-08-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The player page now supports keyboard navigation with arrow keys to move between slides.
|
||||||
|
|
||||||
## 2.7.0 - 2026-08-14
|
## 2.7.0 - 2026-08-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"main": "src/common.js",
|
"main": "src/common.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"main": "src/common.js",
|
"main": "src/common.js",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -427,6 +427,42 @@ function normalizeBoolean(value) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEditableTarget(target) {
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.isContentEditable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagName = String(target.tagName || '').toUpperCase();
|
||||||
|
return ['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'].indexOf(tagName) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePlayerKeydown(event) {
|
||||||
|
if (!event || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditableTarget(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
event.preventDefault();
|
||||||
|
navigateSlides(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
event.preventDefault();
|
||||||
|
navigateSlides(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handlePlayerKeydown);
|
||||||
|
|
||||||
// Move to the previous or next active slide.
|
// Move to the previous or next active slide.
|
||||||
function navigateSlides(offset) {
|
function navigateSlides(offset) {
|
||||||
const manualSlides = getCurrentActiveSlides();
|
const manualSlides = getCurrentActiveSlides();
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ function createSandbox() {
|
|||||||
playRegionAnimations() {
|
playRegionAnimations() {
|
||||||
calls.playRegionAnimations += 1;
|
calls.playRegionAnimations += 1;
|
||||||
},
|
},
|
||||||
|
addEventListener() {},
|
||||||
pulsePlayerRegionTypes: {
|
pulsePlayerRegionTypes: {
|
||||||
list() {
|
list() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
|||||||
scheduleRefreshRetry() {},
|
scheduleRefreshRetry() {},
|
||||||
syncWebpagePreloads() {},
|
syncWebpagePreloads() {},
|
||||||
syncRtmpWarmups() {},
|
syncRtmpWarmups() {},
|
||||||
|
addEventListener() {},
|
||||||
showCurrent() {
|
showCurrent() {
|
||||||
calls.showCurrent += 1;
|
calls.showCurrent += 1;
|
||||||
},
|
},
|
||||||
@@ -209,6 +210,7 @@ test('single-slide playlists re-render the active slide instead of refreshing af
|
|||||||
scheduleRefreshRetry() {},
|
scheduleRefreshRetry() {},
|
||||||
syncWebpagePreloads() {},
|
syncWebpagePreloads() {},
|
||||||
syncRtmpWarmups() {},
|
syncRtmpWarmups() {},
|
||||||
|
addEventListener() {},
|
||||||
clearActiveSlidesCache() {},
|
clearActiveSlidesCache() {},
|
||||||
showCurrent() {},
|
showCurrent() {},
|
||||||
sendCommandState() {},
|
sendCommandState() {},
|
||||||
@@ -347,6 +349,7 @@ test('deferred playlist updates preserve the current slide index', async () => {
|
|||||||
scheduleRefreshRetry() {},
|
scheduleRefreshRetry() {},
|
||||||
syncWebpagePreloads() {},
|
syncWebpagePreloads() {},
|
||||||
syncRtmpWarmups() {},
|
syncRtmpWarmups() {},
|
||||||
|
addEventListener() {},
|
||||||
clearActiveSlidesCache() {},
|
clearActiveSlidesCache() {},
|
||||||
showCurrent() {},
|
showCurrent() {},
|
||||||
sendCommandState() {},
|
sendCommandState() {},
|
||||||
@@ -406,6 +409,87 @@ test('deferred playlist updates preserve the current slide index', async () => {
|
|||||||
assert.equal(sandbox.index, 1);
|
assert.equal(sandbox.index, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('player page arrow keys move between slides', async () => {
|
||||||
|
const handlers = Object.create(null);
|
||||||
|
const calls = [];
|
||||||
|
|
||||||
|
const sandbox = {
|
||||||
|
window: null,
|
||||||
|
console,
|
||||||
|
Array,
|
||||||
|
Object,
|
||||||
|
String,
|
||||||
|
Boolean,
|
||||||
|
Number,
|
||||||
|
Math,
|
||||||
|
Date,
|
||||||
|
JSON,
|
||||||
|
Promise,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
location: { origin: 'http://localhost', href: 'http://localhost/screen/test2' },
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
handlers[type] = handler;
|
||||||
|
},
|
||||||
|
slides: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
|
lastRenderedSlide: { id: 2 },
|
||||||
|
index: 1,
|
||||||
|
timer: null,
|
||||||
|
slideOutroTimers: [],
|
||||||
|
getCurrentActiveSlides() {
|
||||||
|
return sandbox.slides;
|
||||||
|
},
|
||||||
|
clearSlideTimer() {},
|
||||||
|
applyPendingPlaylistUpdate() {},
|
||||||
|
renderSlideAtIndex(_sourceSlides, targetIndex) {
|
||||||
|
calls.push(targetIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
|
||||||
|
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-commands.js');
|
||||||
|
const script = fs.readFileSync(scriptPath, 'utf8');
|
||||||
|
vm.runInNewContext(script, sandbox, { filename: scriptPath });
|
||||||
|
|
||||||
|
assert.equal(typeof handlers.keydown, 'function');
|
||||||
|
|
||||||
|
let prevented = false;
|
||||||
|
handlers.keydown({
|
||||||
|
key: 'ArrowLeft',
|
||||||
|
target: {},
|
||||||
|
preventDefault() {
|
||||||
|
prevented = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [0]);
|
||||||
|
assert.equal(prevented, true);
|
||||||
|
|
||||||
|
sandbox.lastRenderedSlide = { id: 2 };
|
||||||
|
sandbox.index = 1;
|
||||||
|
prevented = false;
|
||||||
|
handlers.keydown({
|
||||||
|
key: 'ArrowRight',
|
||||||
|
target: {},
|
||||||
|
preventDefault() {
|
||||||
|
prevented = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [0, 2]);
|
||||||
|
assert.equal(prevented, true);
|
||||||
|
|
||||||
|
handlers.keydown({
|
||||||
|
key: 'ArrowLeft',
|
||||||
|
target: { tagName: 'INPUT' },
|
||||||
|
preventDefault() {
|
||||||
|
throw new Error('should not be called for editable targets');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [0, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
test('removing the currently visible slide from a two-slide playlist applies the one-slide update immediately', async () => {
|
test('removing the currently visible slide from a two-slide playlist applies the one-slide update immediately', async () => {
|
||||||
const calls = {
|
const calls = {
|
||||||
showCurrent: 0,
|
showCurrent: 0,
|
||||||
@@ -464,6 +548,7 @@ test('removing the currently visible slide from a two-slide playlist applies the
|
|||||||
scheduleRefreshRetry() {},
|
scheduleRefreshRetry() {},
|
||||||
syncWebpagePreloads() {},
|
syncWebpagePreloads() {},
|
||||||
syncRtmpWarmups() {},
|
syncRtmpWarmups() {},
|
||||||
|
addEventListener() {},
|
||||||
clearActiveSlidesCache() {},
|
clearActiveSlidesCache() {},
|
||||||
showCurrent() {
|
showCurrent() {
|
||||||
calls.showCurrent += 1;
|
calls.showCurrent += 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user