Add onboarding weather and template gradients
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
(function () {
|
||||
var form = document.getElementById('onboarding-pair-form');
|
||||
var pairingCard = document.getElementById('onboarding-pair-card');
|
||||
var pairingProgress = document.getElementById('onboarding-pair-progress');
|
||||
var message = document.getElementById('onboarding-pair-message');
|
||||
var codeInputsContainer = document.getElementById('onboarding-pair-code-inputs');
|
||||
var codeField = document.getElementById('onboarding-pair-code');
|
||||
var clientIdField = document.getElementById('onboarding-pair-client-id');
|
||||
var anotherButton = document.getElementById('onboarding-pair-another');
|
||||
var anotherButtonLabel = document.getElementById('onboarding-pair-another-label');
|
||||
var manualButton = document.getElementById('onboarding-pair-manual');
|
||||
var connectButton = document.getElementById('onboarding-pair-connect');
|
||||
var clientListButton = document.getElementById('onboarding-pair-client-list');
|
||||
var scanner = document.getElementById('onboarding-scanner');
|
||||
var scannerVideo = document.getElementById('onboarding-scanner-video');
|
||||
var scannerCapture = document.getElementById('onboarding-scanner-capture');
|
||||
var scannerClose = document.getElementById('onboarding-scanner-close');
|
||||
var scannerMessage = document.getElementById('onboarding-scanner-message');
|
||||
var scannerDebugOutput = document.getElementById('onboarding-scanner-debug');
|
||||
var scannerDebug = new URLSearchParams(window.location.search).get('scanner-debug') === '1';
|
||||
if (!form || !message) {
|
||||
return;
|
||||
}
|
||||
function getClientId() {
|
||||
var storageKey = 'pulse-signage-player-client-id';
|
||||
var clientId = clientIdField ? String(clientIdField.value || '').trim() : '';
|
||||
if (!clientId) {
|
||||
try { clientId = String(window.sessionStorage.getItem(storageKey) || '').trim(); } catch (_error) {}
|
||||
}
|
||||
if (!clientId) {
|
||||
clientId = window.crypto && typeof window.crypto.randomUUID === 'function'
|
||||
? window.crypto.randomUUID()
|
||||
: 'client-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 12);
|
||||
try { window.sessionStorage.setItem(storageKey, clientId); } catch (_error) {}
|
||||
}
|
||||
if (clientIdField) { clientIdField.value = clientId; }
|
||||
return clientId;
|
||||
}
|
||||
getClientId();
|
||||
function traceScanner(value) {
|
||||
if (!scannerDebug || !scannerDebugOutput) { return; }
|
||||
scannerDebugOutput.classList.remove('d-none');
|
||||
var lines = (scannerDebugOutput.textContent ? scannerDebugOutput.textContent.split('\n') : []).filter(Boolean);
|
||||
lines.push(new Date().toISOString().slice(11, 19) + ' ' + value);
|
||||
scannerDebugOutput.textContent = lines.slice(-8).join('\n');
|
||||
}
|
||||
|
||||
var codeInputs = codeInputsContainer ? Array.prototype.slice.call(codeInputsContainer.querySelectorAll('[data-pairing-code-input]')) : [];
|
||||
function normalizeCode(value) {
|
||||
return String(value || '').replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, codeInputs.length);
|
||||
}
|
||||
function syncCodeField() {
|
||||
if (codeField) {
|
||||
codeField.value = codeInputs.map(function (input) { return input.value; }).join('').toUpperCase();
|
||||
}
|
||||
}
|
||||
function setCode(value, startIndex) {
|
||||
var normalized = normalizeCode(value);
|
||||
var index = startIndex || 0;
|
||||
normalized.split('').forEach(function (character) {
|
||||
if (index < codeInputs.length) {
|
||||
codeInputs[index].value = character;
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
syncCodeField();
|
||||
if (index < codeInputs.length) {
|
||||
codeInputs[index].focus();
|
||||
} else if (codeInputs.length) {
|
||||
codeInputs[codeInputs.length - 1].focus();
|
||||
}
|
||||
}
|
||||
var scannerStream = null;
|
||||
var scannerFrame = null;
|
||||
var scannerLastDecodeAt = 0;
|
||||
var scannerCanvas = document.createElement('canvas');
|
||||
var scannerWorkCanvas = document.createElement('canvas');
|
||||
var scannerDecodeCanvas = document.createElement('canvas');
|
||||
|
||||
function decodeQrCanvas(canvas) {
|
||||
if (typeof window.jsQR !== 'function') { return null; }
|
||||
var workCanvas = canvas;
|
||||
var maximumDimension = 1024;
|
||||
if (Math.max(canvas.width, canvas.height) > maximumDimension) {
|
||||
var scale = maximumDimension / Math.max(canvas.width, canvas.height);
|
||||
scannerWorkCanvas.width = Math.round(canvas.width * scale);
|
||||
scannerWorkCanvas.height = Math.round(canvas.height * scale);
|
||||
scannerWorkCanvas.getContext('2d').drawImage(canvas, 0, 0, scannerWorkCanvas.width, scannerWorkCanvas.height);
|
||||
workCanvas = scannerWorkCanvas;
|
||||
}
|
||||
var context = workCanvas.getContext('2d', { willReadFrequently: true });
|
||||
var imageData = context.getImageData(0, 0, workCanvas.width, workCanvas.height);
|
||||
var result = window.jsQR(imageData.data, workCanvas.width, workCanvas.height, { inversionAttempts: 'attemptBoth' });
|
||||
if (result) { return result; }
|
||||
|
||||
scannerDecodeCanvas.width = workCanvas.width;
|
||||
scannerDecodeCanvas.height = workCanvas.height;
|
||||
var decodeContext = scannerDecodeCanvas.getContext('2d', { willReadFrequently: true });
|
||||
var source = new Uint8ClampedArray(imageData.data);
|
||||
var cleaned = new Uint8ClampedArray(source.length);
|
||||
for (var cleanY = 0; cleanY < workCanvas.height; cleanY += 1) {
|
||||
for (var cleanX = 0; cleanX < workCanvas.width; cleanX += 1) {
|
||||
var cleanSamples = [];
|
||||
for (var cleanOffsetY = -1; cleanOffsetY <= 1; cleanOffsetY += 1) {
|
||||
for (var cleanOffsetX = -1; cleanOffsetX <= 1; cleanOffsetX += 1) {
|
||||
var cleanSampleX = Math.max(0, Math.min(workCanvas.width - 1, cleanX + cleanOffsetX));
|
||||
var cleanSampleY = Math.max(0, Math.min(workCanvas.height - 1, cleanY + cleanOffsetY));
|
||||
cleanSamples.push(source[(cleanSampleY * workCanvas.width + cleanSampleX) * 4]);
|
||||
}
|
||||
}
|
||||
cleanSamples.sort(function (left, right) { return left - right; });
|
||||
var cleanIndex = (cleanY * workCanvas.width + cleanX) * 4;
|
||||
cleaned[cleanIndex] = cleaned[cleanIndex + 1] = cleaned[cleanIndex + 2] = cleanSamples[4];
|
||||
cleaned[cleanIndex + 3] = 255;
|
||||
}
|
||||
}
|
||||
source = cleaned;
|
||||
var expanded = decodeContext.createImageData(workCanvas.width, workCanvas.height);
|
||||
var radius = Math.max(3, Math.round(workCanvas.width / 205));
|
||||
for (var y = 0; y < workCanvas.height; y += 1) {
|
||||
for (var x = 0; x < workCanvas.width; x += 1) {
|
||||
var samples = [];
|
||||
for (var offsetY = -radius; offsetY <= radius; offsetY += 2) {
|
||||
for (var offsetX = -radius; offsetX <= radius; offsetX += 2) {
|
||||
var sampleX = Math.max(0, Math.min(workCanvas.width - 1, x + offsetX));
|
||||
var sampleY = Math.max(0, Math.min(workCanvas.height - 1, y + offsetY));
|
||||
samples.push(source[(sampleY * workCanvas.width + sampleX) * 4]);
|
||||
}
|
||||
}
|
||||
var maximum = Math.max.apply(null, samples);
|
||||
var index = (y * workCanvas.width + x) * 4;
|
||||
expanded.data[index] = maximum;
|
||||
expanded.data[index + 1] = maximum;
|
||||
expanded.data[index + 2] = maximum;
|
||||
expanded.data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
decodeContext.putImageData(expanded, 0, 0);
|
||||
return window.jsQR(expanded.data, workCanvas.width, workCanvas.height, { inversionAttempts: 'attemptBoth' });
|
||||
}
|
||||
function closeScanner() {
|
||||
if (scannerFrame) { window.cancelAnimationFrame(scannerFrame); scannerFrame = null; }
|
||||
if (scannerStream) {
|
||||
scannerStream.getTracks().forEach(function (track) { track.stop(); });
|
||||
scannerStream = null;
|
||||
}
|
||||
if (scannerVideo) {
|
||||
scannerVideo.srcObject = null;
|
||||
scannerVideo.classList.remove('d-none');
|
||||
}
|
||||
if (scannerCapture) { scannerCapture.value = ''; }
|
||||
if (scanner) {
|
||||
scanner.classList.add('d-none');
|
||||
scanner.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
function resetPairingForm() {
|
||||
form.reset();
|
||||
codeInputsContainer.setAttribute('data-pairing-code', '');
|
||||
codeInputs.forEach(function (input) { input.value = ''; input.disabled = false; });
|
||||
if (codeField) { codeField.value = ''; }
|
||||
Array.prototype.slice.call(form.elements).forEach(function (element) {
|
||||
if (element !== anotherButton && element !== manualButton) { element.disabled = false; }
|
||||
});
|
||||
if (connectButton) { connectButton.classList.remove('d-none'); }
|
||||
if (clientListButton) { clientListButton.classList.add('d-none'); }
|
||||
if (pairingCard) { pairingCard.classList.remove('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.add('d-none'); }
|
||||
message.className = 'alert d-none';
|
||||
message.textContent = '';
|
||||
if (anotherButton) {
|
||||
anotherButton.classList.remove('btn-secondary');
|
||||
anotherButton.classList.add('btn-outline-secondary');
|
||||
anotherButton.setAttribute('aria-label', 'Scan player QR code');
|
||||
anotherButton.setAttribute('title', 'Scan player QR code');
|
||||
}
|
||||
if (anotherButtonLabel) { anotherButtonLabel.classList.add('d-none'); }
|
||||
if (manualButton) { manualButton.classList.add('d-none'); }
|
||||
}
|
||||
function codeFromScan(rawValue) {
|
||||
try {
|
||||
var scannedUrl = new URL(String(rawValue || ''), window.location.origin);
|
||||
if (scannedUrl.pathname === '/pairing') {
|
||||
return normalizeCode(scannedUrl.searchParams.get('code'));
|
||||
}
|
||||
} catch (_error) {}
|
||||
return normalizeCode(rawValue);
|
||||
}
|
||||
function applyScannedCode(rawValue) {
|
||||
var code = codeFromScan(rawValue);
|
||||
if (code.length !== codeInputs.length) {
|
||||
scannerMessage.textContent = 'That QR code is not a Pulse Signage pairing code.';
|
||||
return false;
|
||||
}
|
||||
closeScanner();
|
||||
resetPairingForm();
|
||||
setCode(code, 0);
|
||||
return true;
|
||||
}
|
||||
function scanFrame() {
|
||||
if (!scannerVideo || !scannerStream) { return; }
|
||||
if (Date.now() - scannerLastDecodeAt < 400) {
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
return;
|
||||
}
|
||||
scannerLastDecodeAt = Date.now();
|
||||
if (!window.BarcodeDetector && typeof window.jsQR === 'function') {
|
||||
var width = scannerVideo.videoWidth;
|
||||
var height = scannerVideo.videoHeight;
|
||||
if (width && height) {
|
||||
traceScanner('frame ' + width + 'x' + height);
|
||||
scannerCanvas.width = width;
|
||||
scannerCanvas.height = height;
|
||||
var context = scannerCanvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage(scannerVideo, 0, 0, width, height);
|
||||
var result = decodeQrCanvas(scannerCanvas);
|
||||
traceScanner(result ? 'decoded' : 'no QR');
|
||||
if (result && applyScannedCode(result.data)) { return; }
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
return;
|
||||
}
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
return;
|
||||
}
|
||||
scannerVideo.__pairingBarcodeDetector.detect(scannerVideo).then(function (barcodes) {
|
||||
if (barcodes.length) {
|
||||
if (applyScannedCode(barcodes[0].rawValue)) { return; }
|
||||
}
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
}).catch(function () {
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
});
|
||||
}
|
||||
function openScanner() {
|
||||
if (!scanner || !scannerVideo || (!window.BarcodeDetector && typeof window.jsQR !== 'function')) {
|
||||
message.className = 'alert alert-warning';
|
||||
message.textContent = 'Camera scanning is not supported by this browser.';
|
||||
return;
|
||||
}
|
||||
scannerMessage.textContent = 'Point your camera at the player QR code.';
|
||||
traceScanner('open BarcodeDetector=' + Boolean(window.BarcodeDetector) + ' jsQR=' + (typeof window.jsQR === 'function'));
|
||||
scannerVideo.classList.remove('d-none');
|
||||
scanner.classList.remove('d-none');
|
||||
scanner.setAttribute('aria-hidden', 'false');
|
||||
if (window.BarcodeDetector) {
|
||||
scannerVideo.__pairingBarcodeDetector = new window.BarcodeDetector({ formats: ['qr_code'] });
|
||||
}
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
traceScanner('getUserMedia unavailable');
|
||||
scannerVideo.classList.add('d-none');
|
||||
scannerMessage.textContent = 'Use the camera to capture the player QR code.';
|
||||
if (scannerCapture) { scannerCapture.click(); }
|
||||
return;
|
||||
}
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: { ideal: 'environment' },
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
focusMode: { ideal: 'continuous' }
|
||||
},
|
||||
audio: false
|
||||
}).then(function (stream) {
|
||||
scannerStream = stream;
|
||||
traceScanner('stream opened');
|
||||
traceScanner(JSON.stringify(stream.getVideoTracks()[0].getSettings ? stream.getVideoTracks()[0].getSettings() : {}));
|
||||
scannerVideo.srcObject = stream;
|
||||
return scannerVideo.play();
|
||||
}).then(function () {
|
||||
scannerFrame = window.requestAnimationFrame(scanFrame);
|
||||
}).catch(function () {
|
||||
traceScanner('stream failed');
|
||||
scannerVideo.classList.add('d-none');
|
||||
scannerMessage.textContent = 'Use the camera to capture the player QR code.';
|
||||
if (scannerCapture) { scannerCapture.click(); }
|
||||
});
|
||||
}
|
||||
if (codeInputs.length) {
|
||||
setCode(codeInputsContainer.getAttribute('data-pairing-code'), 0);
|
||||
codeInputs.forEach(function (input, index) {
|
||||
input.addEventListener('input', function () {
|
||||
var value = normalizeCode(input.value);
|
||||
input.value = value.slice(0, 1);
|
||||
if (value.length > 1) {
|
||||
setCode(value, index);
|
||||
} else {
|
||||
syncCodeField();
|
||||
if (input.value && index < codeInputs.length - 1) {
|
||||
codeInputs[index + 1].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
input.addEventListener('paste', function (event) {
|
||||
event.preventDefault();
|
||||
setCode(event.clipboardData ? event.clipboardData.getData('text') : '', index);
|
||||
});
|
||||
input.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Backspace' && !input.value && index > 0) {
|
||||
codeInputs[index - 1].focus();
|
||||
codeInputs[index - 1].value = '';
|
||||
syncCodeField();
|
||||
} else if (event.key === 'ArrowLeft' && index > 0) {
|
||||
event.preventDefault();
|
||||
codeInputs[index - 1].focus();
|
||||
} else if (event.key === 'ArrowRight' && index < codeInputs.length - 1) {
|
||||
event.preventDefault();
|
||||
codeInputs[index + 1].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
getClientId();
|
||||
syncCodeField();
|
||||
if (!codeField || codeField.value.length !== codeInputs.length) {
|
||||
message.className = 'alert alert-warning';
|
||||
message.textContent = 'Enter the complete six-character pairing code.';
|
||||
var firstEmptyInput = codeInputs.filter(function (input) { return !input.value; })[0] || codeInputs[0];
|
||||
if (firstEmptyInput) { firstEmptyInput.focus(); }
|
||||
return;
|
||||
}
|
||||
message.className = 'alert alert-info';
|
||||
message.textContent = 'Pairing player...';
|
||||
var pairingBody = new URLSearchParams(new FormData(form)).toString();
|
||||
if (pairingCard) { pairingCard.classList.add('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.remove('d-none'); }
|
||||
Array.prototype.slice.call(form.elements).forEach(function (element) {
|
||||
if (element !== anotherButton && element !== manualButton) { element.disabled = true; }
|
||||
});
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
||||
body: pairingBody
|
||||
}).then(function (response) {
|
||||
return response.text().then(function (text) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(text); } catch (_error) {}
|
||||
if (!response.ok) {
|
||||
throw new Error(payload && payload.error ? payload.error : 'Unable to pair player.');
|
||||
}
|
||||
if (payload && payload.queued) {
|
||||
message.className = 'alert alert-info';
|
||||
message.textContent = 'Pairing is still being completed. Keep this page open and wait for confirmation.';
|
||||
return;
|
||||
}
|
||||
message.className = 'alert alert-success';
|
||||
message.textContent = 'Pairing saved. The player is loading your screen.';
|
||||
if (pairingCard) { pairingCard.classList.remove('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.add('d-none'); }
|
||||
Array.prototype.slice.call(form.elements).forEach(function (element) {
|
||||
if (element !== anotherButton && element !== manualButton) { element.disabled = true; }
|
||||
});
|
||||
if (connectButton) { connectButton.classList.add('d-none'); }
|
||||
if (clientListButton) { clientListButton.classList.remove('d-none'); }
|
||||
if (anotherButton) {
|
||||
anotherButton.classList.remove('d-none');
|
||||
anotherButton.classList.remove('btn-outline-secondary');
|
||||
anotherButton.classList.add('btn-secondary');
|
||||
anotherButton.setAttribute('aria-label', 'Pair another screen');
|
||||
anotherButton.setAttribute('title', 'Pair another screen');
|
||||
}
|
||||
if (anotherButtonLabel) { anotherButtonLabel.classList.remove('d-none'); }
|
||||
if (manualButton) { manualButton.classList.remove('d-none'); }
|
||||
});
|
||||
}).catch(function (error) {
|
||||
if (pairingCard) { pairingCard.classList.remove('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.add('d-none'); }
|
||||
Array.prototype.slice.call(form.elements).forEach(function (element) {
|
||||
if (element !== anotherButton && element !== manualButton) { element.disabled = false; }
|
||||
});
|
||||
message.className = 'alert alert-danger';
|
||||
message.textContent = error && error.message ? error.message : 'Unable to pair player.';
|
||||
});
|
||||
});
|
||||
|
||||
if (anotherButton) { anotherButton.addEventListener('click', openScanner); }
|
||||
if (manualButton) {
|
||||
manualButton.addEventListener('click', function () {
|
||||
resetPairingForm();
|
||||
if (codeInputs[0]) { codeInputs[0].focus(); }
|
||||
});
|
||||
}
|
||||
if (scannerClose) { scannerClose.addEventListener('click', closeScanner); }
|
||||
if (scannerCapture) {
|
||||
scannerCapture.addEventListener('change', function () {
|
||||
var file = scannerCapture.files && scannerCapture.files[0];
|
||||
if (!file) { return; }
|
||||
scannerMessage.textContent = 'Reading QR code...';
|
||||
var image = new Image();
|
||||
image.onload = function () {
|
||||
if (window.BarcodeDetector) {
|
||||
new window.BarcodeDetector({ formats: ['qr_code'] }).detect(image).then(function (barcodes) {
|
||||
if (!barcodes.length || !applyScannedCode(barcodes[0].rawValue)) {
|
||||
scannerMessage.textContent = 'Could not find a Pulse Signage QR code in that image.';
|
||||
}
|
||||
}).catch(function () {
|
||||
scannerMessage.textContent = 'Could not read that QR image. Try again.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
var context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage(image, 0, 0);
|
||||
var result = decodeQrCanvas(canvas);
|
||||
if (!result || !applyScannedCode(result.data)) {
|
||||
scannerMessage.textContent = 'Could not find a Pulse Signage QR code in that image.';
|
||||
}
|
||||
} catch (_error) {
|
||||
scannerMessage.textContent = 'Could not read that QR image. Try again.';
|
||||
}
|
||||
};
|
||||
image.onerror = function () {
|
||||
scannerMessage.textContent = 'Could not read that QR image. Try again.';
|
||||
};
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () { image.src = reader.result; };
|
||||
reader.onerror = function () { scannerMessage.textContent = 'Could not read that QR image. Try again.'; };
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
if (scanner) {
|
||||
scanner.addEventListener('click', function (event) {
|
||||
if (event.target === scanner) { closeScanner(); }
|
||||
});
|
||||
}
|
||||
}());
|
||||
Reference in New Issue
Block a user