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.
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-player",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.1",
|
||||
"private": false,
|
||||
"description": "Pulse Signage player application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-web",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.1",
|
||||
"private": false,
|
||||
"description": "Pulse Signage web and bridge application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.1",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"repository": {
|
||||
|
||||
@@ -427,6 +427,42 @@ function normalizeBoolean(value) {
|
||||
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.
|
||||
function navigateSlides(offset) {
|
||||
const manualSlides = getCurrentActiveSlides();
|
||||
|
||||
@@ -92,6 +92,7 @@ function createSandbox() {
|
||||
playRegionAnimations() {
|
||||
calls.playRegionAnimations += 1;
|
||||
},
|
||||
addEventListener() {},
|
||||
pulsePlayerRegionTypes: {
|
||||
list() {
|
||||
return [
|
||||
|
||||
@@ -68,6 +68,7 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
||||
scheduleRefreshRetry() {},
|
||||
syncWebpagePreloads() {},
|
||||
syncRtmpWarmups() {},
|
||||
addEventListener() {},
|
||||
showCurrent() {
|
||||
calls.showCurrent += 1;
|
||||
},
|
||||
@@ -209,6 +210,7 @@ test('single-slide playlists re-render the active slide instead of refreshing af
|
||||
scheduleRefreshRetry() {},
|
||||
syncWebpagePreloads() {},
|
||||
syncRtmpWarmups() {},
|
||||
addEventListener() {},
|
||||
clearActiveSlidesCache() {},
|
||||
showCurrent() {},
|
||||
sendCommandState() {},
|
||||
@@ -347,6 +349,7 @@ test('deferred playlist updates preserve the current slide index', async () => {
|
||||
scheduleRefreshRetry() {},
|
||||
syncWebpagePreloads() {},
|
||||
syncRtmpWarmups() {},
|
||||
addEventListener() {},
|
||||
clearActiveSlidesCache() {},
|
||||
showCurrent() {},
|
||||
sendCommandState() {},
|
||||
@@ -406,6 +409,87 @@ test('deferred playlist updates preserve the current slide index', async () => {
|
||||
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 () => {
|
||||
const calls = {
|
||||
showCurrent: 0,
|
||||
@@ -464,6 +548,7 @@ test('removing the currently visible slide from a two-slide playlist applies the
|
||||
scheduleRefreshRetry() {},
|
||||
syncWebpagePreloads() {},
|
||||
syncRtmpWarmups() {},
|
||||
addEventListener() {},
|
||||
clearActiveSlidesCache() {},
|
||||
showCurrent() {
|
||||
calls.showCurrent += 1;
|
||||
|
||||
Reference in New Issue
Block a user