WKVS v 1.0.0
This commit is contained in:
@@ -0,0 +1,814 @@
|
||||
<?php
|
||||
|
||||
if (!isset($segments) || !defined('APP_ROUTER')) {
|
||||
http_response_code(403);
|
||||
include $baseDir . "/error-pages/403.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
$type = $segments[1] ?? null;
|
||||
$requested_geraet = htmlspecialchars($segments[2] ?? '');
|
||||
|
||||
$allowed_types = ["display"];
|
||||
|
||||
if (!in_array($type, $allowed_types)) {
|
||||
http_response_code(400);
|
||||
include $baseDir . "/error-pages/400.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
|
||||
require_once $baseDir . '/../scripts/session_functions.php';
|
||||
|
||||
ini_wkvs_session(true);
|
||||
|
||||
$csrf_token = $_SESSION['csrf_token'] ?? '';
|
||||
|
||||
require $baseDir . '/../scripts/websocket/ws-create-token.php';
|
||||
|
||||
$WSaccesstype = "display";
|
||||
$WSaccessPermission = "R";
|
||||
|
||||
require_once $baseDir . '/../scripts/db/db-verbindung-script-guest.php';
|
||||
require_once $baseDir . '/../scripts/db/db-functions.php';
|
||||
require_once $baseDir . '/../scripts/db/db-tables.php';
|
||||
|
||||
$stmt = $guest->prepare("SELECT `name`, `id` FROM $db_tabelle_disziplinen ORDER BY start_index ASC");
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$disciplines = $result->fetch_all(MYSQLI_ASSOC);
|
||||
$lowerDisciplines = array_change_key_case(
|
||||
array_column($disciplines, 'id', 'name'),
|
||||
CASE_LOWER
|
||||
);
|
||||
|
||||
|
||||
if (!array_key_exists($requested_geraet, $lowerDisciplines)) {
|
||||
http_response_code(400);
|
||||
$message = 'Das gesuchte Gerät kann nicht gefunden werden';
|
||||
include $baseDir . "/error-pages/400.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($lowerDisciplines[$requested_geraet])) {
|
||||
http_response_code(500);
|
||||
include $baseDir . "/error-pages/500.html";
|
||||
exit;
|
||||
}
|
||||
|
||||
$geraetId = $lowerDisciplines[$requested_geraet];
|
||||
|
||||
$WSaccess = (string) $geraetId;
|
||||
|
||||
|
||||
$stmt->close();
|
||||
|
||||
// Define a small helper function to keep the code DRY (Don't Repeat Yourself)
|
||||
function sanitize_hex($color) {
|
||||
return preg_replace('/[^0-9a-fA-F#]/', '', $color);
|
||||
}
|
||||
|
||||
$keysNeeded = [
|
||||
'wkName', 'displayColourLogo', 'displayTextColourLogo', 'displayColorScoringBg',
|
||||
'displayColorScoringBgSoft', 'displayColorScoringPanel', 'displayColorScoringPanelSoft', 'displayColorScoringBorderColor',
|
||||
'displayColorScoringShadowColor', 'displayColorScoringPanelText', 'displayColorScoringPanelTextSoft',
|
||||
'displayColorScoringPanelTextNoteL', 'displayColorScoringPanelTextNoteR', 'displayIdNoteL',
|
||||
'displayIdNoteR', 'displayCTextLogo', 'displayCTextWKName'
|
||||
];
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($keysNeeded), '?'));
|
||||
|
||||
$query = "SELECT `name`, `value` FROM $db_tabelle_var WHERE `name` IN ($placeholders)";
|
||||
|
||||
$stmt = $guest->prepare($query);
|
||||
$params = str_repeat("s", count($keysNeeded));
|
||||
$stmt->bind_param($params, ...$keysNeeded);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$config = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$config[$row['name']] = $row['value'];
|
||||
}
|
||||
|
||||
$wkName = $config['wkName'] ?? null;
|
||||
$displayCTextDisplayLogo = $config['displayCTextLogo'] ?? null;
|
||||
$displayCTextDisplayWKName = $config['displayCTextWKName'] ?? null;
|
||||
$cleanColor = $config['displayColourLogo'] ?? null;
|
||||
$cleanColorText = $config['displayTextColourLogo'] ?? null;
|
||||
$displayColorScoringBg = $config['displayColorScoringBg'] ?? null;
|
||||
$displayColorScoringBgSoft = $config['displayColorScoringBgSoft'] ?? null;
|
||||
$displayColorScoringPanel = $config['displayColorScoringPanel'] ?? null;
|
||||
$displayColorScoringPanelSoft = $config['displayColorScoringPanelSoft'] ?? null;
|
||||
$displayColorScoringShadowColor = $config['displayColorScoringShadowColor'] ?? null;
|
||||
$displayColorScoringBorderColor = $config['displayColorScoringBorderColor'] ?? null;
|
||||
$displayColorScoringPanelText = $config['displayColorScoringPanelText'] ?? null;
|
||||
$displayColorScoringPanelTextSoft = $config['displayColorScoringPanelTextSoft'] ?? null;
|
||||
$displayColorScoringPanelTextNoteL = $config['displayColorScoringPanelTextNoteL'] ?? null;
|
||||
$displayColorScoringPanelTextNoteR = $config['displayColorScoringPanelTextNoteR'] ?? null;
|
||||
|
||||
$stmt->close();
|
||||
|
||||
$guest->close();
|
||||
|
||||
$jsonUrlconfig = '/externe-geraete/json/config.json';
|
||||
$jsonUrl = '/externe-geraete/json/display-id-' . $geraetId . '.json';
|
||||
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?= $wkName ?> Anzeigen</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="stylesheet" href="/externe-geraete/css/display.css">
|
||||
<script src="/intern/js/gsap/gsap.min.js"></script>
|
||||
<script src="/intern/js/gsap/MorphSVGPlugin.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
|
||||
/* Brand */
|
||||
--display-panelBgLogo: <?= $cleanColor ?>;
|
||||
--display-text-color: <?= $cleanColorText ?>;
|
||||
|
||||
/* Backgrounds */
|
||||
--display-bg: <?= $displayColorScoringBg ?>;
|
||||
--display-bg-soft: <?= $displayColorScoringBgSoft ?>;
|
||||
--display-panel: <?= $displayColorScoringPanel ?>;
|
||||
--display-panel-soft: <?= $displayColorScoringPanelSoft ?>;
|
||||
|
||||
/* Shadow */
|
||||
--display-shadow: <?= $displayColorScoringShadowColor ?>;
|
||||
|
||||
/* Border */
|
||||
--display-border: <?= $displayColorScoringBorderColor ?>;
|
||||
|
||||
/* Typography */
|
||||
--display-text-main: <?= $displayColorScoringPanelText ?>;
|
||||
--display-text-muted: <?= $displayColorScoringPanelTextSoft ?>;
|
||||
|
||||
/* Noten */
|
||||
--display-color-note-l: <?= $displayColorScoringPanelTextNoteL ?>;
|
||||
--display-color-note-r: <?= $displayColorScoringPanelTextNoteR ?>;
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="displayDiv">
|
||||
<div class="errorws">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="errorSvg" viewBox="0 0 800 200">
|
||||
<defs>
|
||||
<filter id="a" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="myLinearGradient" x1="0%" y1="5%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#22c55e00" />
|
||||
<stop offset="100%" stop-color="#22c55e90" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M150 100h200m100 0h200" id="errorLine" opacity=".3" stroke="#3b82f6" stroke-width="4" stroke-dasharray="10,15" stroke-linecap="round"/>
|
||||
<g transform="translate(150 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
|
||||
<rect x="-30" y="-19" width="60" height="38" rx="2" fill="#334155"/>
|
||||
<rect x="-28" y="-17" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="-5" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="7" width="56" height="10" rx="2" fill="#555"/>
|
||||
<text x="-26" y="-11" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">Test WKVS</text>
|
||||
|
||||
<text x="-26" y="-1" fill="#94a3b8" font-size="2.5" font-family="monospace" letter-spacing="0">WKVS,</text>
|
||||
<text x="-26" y="2.5" fill="#94a3b8" font-size="2" font-family="monospace" letter-spacing="0">EP</text>
|
||||
<rect x="0" y="-5" width="28" rx="2" height="10" fill="url(#myLinearGradient)" />
|
||||
|
||||
<text x="10" y="1" fill="#22c55e" font-size="4" font-family="monospace" letter-spacing="0.5">START</text>
|
||||
|
||||
<text x="-26" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">D 1.40</text>
|
||||
<text x="15" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">9.40</text>
|
||||
|
||||
|
||||
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">Anzeige</text>
|
||||
</g>
|
||||
<g transform="translate(650 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<rect x="-18" y="-15" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<rect x="-18" y="2" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<circle cx="10" cy="-10" r="2" fill="#10b981"/>
|
||||
<circle cx="10" cy="7" r="2" fill="#10b981"/>
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">WKVS Server</text>
|
||||
</g>
|
||||
<path fill="#f43f5e" class="opacity0gsap" opacity=".4" d="m-14.142-70.71 5.657 5.656-5.657 5.657-5.657-5.657z" transform="translate(400 100)"/>
|
||||
<path fill="#f43f5e" class="opacity0gsap" opacity=".4" d="m42.67 39.74 5.869 1.247-1.248 5.87-5.868-1.248z" transform="translate(400 100)"/>
|
||||
<path fill="#3b82f6" class="opacity0gsap" opacity=".4" d="m-50.98 28.301 8.66 5-5 8.66-8.66-5z" transform="translate(400 100)"/>
|
||||
<g filter="url(#a)" transform="translate(400 100)">
|
||||
<path id="cross"
|
||||
d="M-30,-30 L30,30 M30,-30 L-30,30"
|
||||
stroke="#f43f5e"
|
||||
stroke-width="8"
|
||||
stroke-linecap="round"
|
||||
fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="okSvg" viewBox="0 0 800 200" style="display: none">
|
||||
<defs>
|
||||
<filter id="a" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="myLinearGradient" x1="0%" y1="5%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#22c55e00" />
|
||||
<stop offset="100%" stop-color="#22c55e90" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M150 100 h500" opacity=".3" stroke="#3b82f6" stroke-width="4" stroke-dasharray="10, 15" stroke-linecap="round"/>
|
||||
<g transform="translate(150 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
|
||||
<rect x="-30" y="-19" width="60" height="38" rx="2" fill="#334155"/>
|
||||
<rect x="-28" y="-17" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="-5" width="56" height="10" rx="2" fill="#555"/>
|
||||
<rect x="-28" y="7" width="56" height="10" rx="2" fill="#555"/>
|
||||
<text x="-26" y="-11" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">Test WKVS</text>
|
||||
|
||||
<text x="-26" y="-1" fill="#94a3b8" font-size="2.5" font-family="monospace" letter-spacing="0">WKVS,</text>
|
||||
<text x="-26" y="2.5" fill="#94a3b8" font-size="2" font-family="monospace" letter-spacing="0">EP</text>
|
||||
<rect x="0" y="-5" width="28" rx="2" height="10" fill="url(#myLinearGradient)" />
|
||||
|
||||
<text x="10" y="1" fill="#22c55e" font-size="4" font-family="monospace" letter-spacing="0.5">START</text>
|
||||
|
||||
<text x="-26" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">D 1.40</text>
|
||||
<text x="15" y="13" fill="#94a3b8" font-size="4" font-family="monospace" letter-spacing="0.5">9.40</text>
|
||||
|
||||
|
||||
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">Anzeige</text>
|
||||
</g>
|
||||
<g transform="translate(650 100)">
|
||||
<circle r="45" fill="#1e293b" stroke="#3b82f6" stroke-width="3"/>
|
||||
<rect x="-18" y="-15" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<rect x="-18" y="2" width="36" height="10" rx="2" fill="#334155"/>
|
||||
<circle cx="10" cy="-10" r="2" fill="#10b981"/>
|
||||
<circle cx="10" cy="7" r="2" fill="#10b981"/>
|
||||
<text y="70" text-anchor="middle" fill="#94a3b8" font-size="12" font-family="monospace" letter-spacing="1.5">WKVS Server</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#a)" transform="translate(400 100)">
|
||||
<path d="m-15-60 15 30 m30-60 -30 60" stroke="#0e0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
<div class="divErrorTextWs">
|
||||
<p class="errortext">Keine WebSocket Verbindung</p>
|
||||
<p class="errortextSmall">Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noWsConnection">
|
||||
<div class="rotator">
|
||||
<p>Keine WebSocket Verbindung, Syncronisation via FETCH</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logobg">
|
||||
<img id="jsImgLogo" class="logoimg <?= (!$displayCTextDisplayLogo) ? 'cTextHidden' : '' ?>" src="/intern/img/logo-normal.png">
|
||||
<p class="logotext <?= (!$displayCTextDisplayWKName) ? 'cTextHidden' : '' ?>"><?= $wkName ?></p>
|
||||
<p class="logoctext"></p>
|
||||
</div>
|
||||
|
||||
<div class="pagediv">
|
||||
<div class="display-row row1">
|
||||
<p class="row1text"></p>
|
||||
</div>
|
||||
<div class="display-row row2">
|
||||
<div class="row2text">
|
||||
<p class="row2_1text sds"></p>
|
||||
<p class="row2_2text"></p>
|
||||
</div>
|
||||
<div class="start_div">
|
||||
<p class="start_text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="display-row row3">
|
||||
<p class="row3_1text"></p>
|
||||
<p class="row3_2text"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
const userAccess = "<?php echo $lastSegment; ?>_display";
|
||||
const jsonUrl = "<?php echo $jsonUrl; ?>";
|
||||
const jsonUrlconfig = "<?php echo $jsonUrlconfig; ?>";
|
||||
|
||||
// --- State Management ---
|
||||
let ws;
|
||||
let filedConectCount = 1;
|
||||
const fallbackConectCount = 5;
|
||||
const RETRY_DELAY = 3000;
|
||||
const FALLBACK_POLL_INTERVAL = 3000; // Fetch every 3 seconds if WS is dead
|
||||
const LONG_TERM_RETRY_DELAY_MULTIPLIER = 5;
|
||||
|
||||
let morphOnConnect = false;
|
||||
let morphOk = true;
|
||||
|
||||
const WSaccesstype = "<?= $WSaccesstype ?>";
|
||||
const WSaccess = "<?= $WSaccess ?>";
|
||||
const csrf_token = "<?= $csrf_token ?>";
|
||||
const WSaccessPermission = "<?= $WSaccessPermission ?>";
|
||||
let firstConnect = true;
|
||||
let longTermConnection = false;
|
||||
const urlAjaxNewWSToken = '/intern/scripts/ajax-create-ws-token.php';
|
||||
|
||||
let fallbackTimer = null;
|
||||
|
||||
// Hold our current data in memory
|
||||
let displayConfig = { type: 'logo', ctext: '' };
|
||||
let currentScore = {};
|
||||
let lastUniqueId = null;
|
||||
|
||||
const logobg = document.querySelector('.logobg');
|
||||
const ctext = document.querySelector('.logoctext');
|
||||
|
||||
// UI Elements
|
||||
let counterTriesEl = document.getElementById('counterTries');
|
||||
let counterMaxTriesEl = document.getElementById('counterMaxTries');
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
if(counterMaxTriesEl) counterMaxTriesEl.innerHTML = fallbackConectCount;
|
||||
|
||||
// --- Fullscreen Listener ---
|
||||
const fs = document.documentElement;
|
||||
document.body.addEventListener('click', () => {
|
||||
if (fs.requestFullscreen) fs.requestFullscreen();
|
||||
else if (fs.webkitRequestFullscreen) fs.webkitRequestFullscreen();
|
||||
else if (fs.mozRequestFullScreen) fs.mozRequestFullScreen();
|
||||
else if (fs.msRequestFullscreen) fs.msRequestFullscreen();
|
||||
});
|
||||
|
||||
async function fetchNewWSToken(accesstype, access, accessPermission) {
|
||||
try {
|
||||
const response = await fetch(urlAjaxNewWSToken, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrf_token,
|
||||
accesstype,
|
||||
access,
|
||||
accessPermission
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.success ? data.token : null;
|
||||
} catch (error) {
|
||||
console.error("Token fetch failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebSocket Logic ---
|
||||
async function startWebSocket() {
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
let token;
|
||||
if (firstConnect) {
|
||||
token = '<?= generateWSReadToken($WSaccesstype, $WSaccess) ?>';
|
||||
} else {
|
||||
token = await fetchNewWSToken(WSaccesstype, WSaccess, WSaccessPermission);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error("No valid token available. Retrying...");
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=token&token=${token}`);
|
||||
} catch (err) {
|
||||
console.error("Malformed WebSocket URL", err);
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
|
||||
if (morphOnConnect) {
|
||||
morph();
|
||||
} else {
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
document.querySelector('.noWsConnection').style.display = 'none';
|
||||
document.querySelector('.pagediv').classList.remove("manuel");
|
||||
}
|
||||
|
||||
filedConectCount = 1;
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
|
||||
firstConnect = false;
|
||||
longTermConnection = false;
|
||||
morphOnConnect = false;
|
||||
morphOk = true;
|
||||
|
||||
// Stop fallback polling since WS is alive
|
||||
stopFallbackPolling();
|
||||
|
||||
// Do ONE initial fetch to sync current state
|
||||
fetchFullState();
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
if (!longTermConnection) {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
document.querySelector('.errorws').style.opacity = '1';
|
||||
}
|
||||
morphOk = false;
|
||||
restore();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!longTermConnection) {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
document.querySelector('.errorws').style.opacity = '1';
|
||||
}
|
||||
morphOnConnect = true;
|
||||
morphOk = false;
|
||||
restore();
|
||||
scheduleRetry();
|
||||
};
|
||||
|
||||
ws.addEventListener("message", msg => {
|
||||
let msgJSON;
|
||||
try {
|
||||
msgJSON = JSON.parse(msg.data);
|
||||
} catch (error) {
|
||||
return; // Ignore malformed messages
|
||||
}
|
||||
|
||||
// Route the incoming push data
|
||||
switch (msgJSON.type) {
|
||||
case "EINSTELLUNGEN_DISPLAY_UPDATE":
|
||||
updateSettings(msgJSON.payload.key, msgJSON.payload.value);
|
||||
break;
|
||||
case "UPDATE_DISPLAYCONTROL":
|
||||
// Expecting payload: { type: 'logo'|'ctext'|'scoring', ctext: '...' }
|
||||
displayConfig = msgJSON.payload;
|
||||
renderDOM();
|
||||
break;
|
||||
case "UPDATE_SCORE":
|
||||
// Expecting payload: the exact same structure as your jsonUrl outputs
|
||||
currentScore = msgJSON.payload;
|
||||
renderDOM();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
|
||||
if (longTermConnection) {
|
||||
setTimeout(startWebSocket, RETRY_DELAY * LONG_TERM_RETRY_DELAY_MULTIPLIER);
|
||||
} else {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
|
||||
if (filedConectCount >= fallbackConectCount) {
|
||||
// MAX RETRIES REACHED -> Enter Fallback Mode
|
||||
longTermConnection = true;
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
document.querySelector('.noWsConnection').style.display = 'flex';
|
||||
document.querySelector('.pagediv').classList.add("manuel");
|
||||
startFallbackPolling();
|
||||
} else {
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
setTimeout(startWebSocket, RETRY_DELAY);
|
||||
filedConectCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fallback Polling Logic ---
|
||||
function startFallbackPolling() {
|
||||
if (fallbackTimer === null) {
|
||||
console.warn("Starting JSON fallback polling...");
|
||||
fetchFullState(); // Fetch immediately once
|
||||
fallbackTimer = setInterval(fetchFullState, FALLBACK_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
function stopFallbackPolling() {
|
||||
if (fallbackTimer !== null) {
|
||||
clearInterval(fallbackTimer);
|
||||
fallbackTimer = null;
|
||||
console.log("Stopped JSON fallback polling.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Fetching (Initial Sync & Fallback) ---
|
||||
async function fetchFullState() {
|
||||
try {
|
||||
// Fetch both config and score simultaneously
|
||||
const [resConfig, resScore] = await Promise.all([
|
||||
fetch(jsonUrlconfig + '?t=' + Date.now(), { cache: "no-store" }),
|
||||
fetch(jsonUrl + '?t=' + Date.now(), { cache: "no-store" })
|
||||
]);
|
||||
|
||||
if (resConfig.ok) displayConfig = await resConfig.json();
|
||||
if (resScore.ok) currentScore = await resScore.json();
|
||||
|
||||
renderDOM();
|
||||
} catch (err) {
|
||||
console.error("Error fetching JSON:", err);
|
||||
const container = document.getElementById('score');
|
||||
if(container) {
|
||||
container.innerHTML = "";
|
||||
container.style.backgroundColor = "black";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- The Master Renderer ---
|
||||
function renderDOM() {
|
||||
|
||||
if (displayConfig.type === 'logo' || displayConfig.type === 'ctext') {
|
||||
// LOGO OR CUSTOM TEXT MODE
|
||||
if (logobg) {
|
||||
logobg.style.opacity = "1";
|
||||
|
||||
if (displayConfig.type === 'logo') logobg.classList.remove('ctext');
|
||||
if (displayConfig.type === 'ctext') logobg.classList.add('ctext');
|
||||
}
|
||||
if (ctext) {
|
||||
ctext.innerText = (displayConfig.type === 'ctext') ? (displayConfig.ctext || '') : '';
|
||||
resizeCText();
|
||||
}
|
||||
|
||||
} else if (displayConfig.type === 'scoring') {
|
||||
// SCORING MODE
|
||||
if (logobg) logobg.style.opacity = "0";
|
||||
|
||||
if (currentScore.uniqueid !== lastUniqueId) {
|
||||
lastUniqueId = currentScore.uniqueid;
|
||||
// Reset any animation/repeat logic here if needed
|
||||
}
|
||||
|
||||
const safeText = (selector, text) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.innerText = text !== undefined ? text : '';
|
||||
};
|
||||
|
||||
safeText('.row1text', `${currentScore.vorname || ''} ${currentScore.name || ''}`);
|
||||
safeText('.row2_1text', currentScore.verein ? `${currentScore.verein}, ` : '');
|
||||
safeText('.row2_2text', currentScore.programm || '');
|
||||
|
||||
const starttext = document.querySelector('.start_text');
|
||||
const row2El = document.querySelector('.row2');
|
||||
if (starttext && row2El) {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const dangerColor = rootStyles.getPropertyValue('--display-danger').trim();
|
||||
const successColor = rootStyles.getPropertyValue('--display-success').trim();
|
||||
|
||||
if (currentScore.start === true) {
|
||||
row2El.style.setProperty('--display-colorStartDiv', successColor);
|
||||
starttext.innerHTML = 'Start';
|
||||
} else {
|
||||
row2El.style.setProperty('--display-colorStartDiv', dangerColor);
|
||||
starttext.innerHTML = 'Stop';
|
||||
}
|
||||
}
|
||||
|
||||
safeText('.row3_1text', currentScore.noteLinks);
|
||||
safeText('.row3_2text', currentScore.noteRechts);
|
||||
}
|
||||
|
||||
fitTextAll();
|
||||
}
|
||||
|
||||
// --- Settings & UI Handlers ---
|
||||
function updateSettings(type, value) {
|
||||
|
||||
const sanitizeHex = (val) => val.replace(/[^0-9a-fA-F#]/g, '');
|
||||
|
||||
switch (type) {
|
||||
case 'wkName':
|
||||
const logotext = document.querySelector('.logotext');
|
||||
if (logotext) logotext.innerHTML = value;
|
||||
break;
|
||||
case 'displayColourLogo':
|
||||
document.documentElement.style.setProperty('--display-panelBgLogo', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayTextColourLogo':
|
||||
document.documentElement.style.setProperty('--display-text-color', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayCTextLogo':
|
||||
document.querySelector('.logoimg').classList.toggle('cTextHidden');
|
||||
break;
|
||||
case 'displayCTextWKName':
|
||||
document.querySelector('.logotext').classList.toggle('cTextHidden');
|
||||
break;
|
||||
case 'displayColorScoringBg':
|
||||
document.documentElement.style.setProperty('--display-bg', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBgSoft':
|
||||
document.documentElement.style.setProperty('--display-bg-soft', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringShadowColor':
|
||||
document.documentElement.style.setProperty('--display-shadow', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBorderColor':
|
||||
document.documentElement.style.setProperty('--display-border', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanel':
|
||||
document.documentElement.style.setProperty('--display-panel', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelSoft':
|
||||
document.documentElement.style.setProperty('--display-panel-soft', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelText':
|
||||
document.documentElement.style.setProperty('--display-text-main', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextSoft':
|
||||
document.documentElement.style.setProperty('--display-text-muted', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteL': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--display-color-note-l', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteR': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--display-color-note-r', sanitizeHex(value));
|
||||
break;
|
||||
case 'logo-normal':
|
||||
const jsImgLogo = document.getElementById('jsImgLogo');
|
||||
if(jsImgLogo) jsImgLogo.src = '/intern/img/logo-normal.png?' + Date.now();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Text Resizing Engine ---
|
||||
function isOverflown(parent, elem, heightscale, widthscale, paddingtext) {
|
||||
return (
|
||||
(elem.scrollWidth + paddingtext) > (parent.clientWidth / widthscale) ||
|
||||
(elem.scrollHeight + paddingtext) > (parent.clientHeight / heightscale)
|
||||
);
|
||||
}
|
||||
|
||||
function fitTextElement(elem, { minSize = 10, maxSize = 1000, step = 1, unit = 'px' } = {}) {
|
||||
if (!elem) return;
|
||||
const parent = elem.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
// FIXED: Declare variables properly so they don't leak into the global scope
|
||||
let heightscale = 1;
|
||||
let widthscale = 1;
|
||||
let paddingtext = 60;
|
||||
|
||||
if (parent.classList.contains('row2text')) {
|
||||
heightscale = 2;
|
||||
paddingtext = 0;
|
||||
}
|
||||
if (parent.classList.contains('row3')) {
|
||||
widthscale = 2;
|
||||
}
|
||||
|
||||
let size = minSize;
|
||||
elem.style.whiteSpace = 'nowrap';
|
||||
elem.style.fontSize = size + unit;
|
||||
|
||||
while (size < maxSize && !isOverflown(parent, elem, heightscale, widthscale, paddingtext)) {
|
||||
size += step;
|
||||
elem.style.fontSize = size + unit;
|
||||
}
|
||||
|
||||
elem.style.fontSize = (size - step) + unit;
|
||||
}
|
||||
|
||||
function resizeCText() {
|
||||
ctext.style.fontSize = '';
|
||||
if (isOverflown(logobg, ctext, 1, 1, 0)) fitTextElement(ctext);
|
||||
}
|
||||
|
||||
function fitTextAll() {
|
||||
const paragraphs = document.querySelectorAll('.pagediv p');
|
||||
paragraphs.forEach(p => fitTextElement(p));
|
||||
resizeCText();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', fitTextAll);
|
||||
|
||||
// --- Initialize ---
|
||||
startWebSocket();
|
||||
|
||||
async function morph() {
|
||||
gsap.to("#cross", {
|
||||
duration: 1,
|
||||
attr: {
|
||||
d: "M-15,-60 L0,-30 M30,-90 L0,-30"
|
||||
},
|
||||
stroke: "#0e0",
|
||||
ease: "power2.inOut"
|
||||
});
|
||||
|
||||
gsap.to(".opacity0gsap", {
|
||||
duration: 1,
|
||||
opacity: "0",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
const pathStart = "M150 100 h200 m100 0 h200";
|
||||
|
||||
const pathEnd = "M150 100 h250 m0 0 h250";
|
||||
|
||||
gsap.to("#errorLine", {
|
||||
duration: 1,
|
||||
attr: { d: pathEnd },
|
||||
ease: "power2.inOut"
|
||||
});
|
||||
|
||||
gsap.to(".errortext", {
|
||||
duration: 1,
|
||||
textContent: "Verbindung Wiederhergestellt",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
gsap.to(".errortextSmall", {
|
||||
duration: 1,
|
||||
textContent: "Stelle auf Wettkampfbetrieb um...",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (morphOk) {
|
||||
gsap.to('.errorws', {
|
||||
opacity: 0,
|
||||
duration: 0.5,
|
||||
overwrite: "auto"
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
setTimeout(() => {
|
||||
if (morphOk) {
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
}
|
||||
}, 3800);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
// 1. Reset the Cross/Check path and color
|
||||
gsap.set("#cross", {
|
||||
attr: { d: "M-30,-30 L30,30 M30,-30 L-30,30" },
|
||||
stroke: "#f43f5e"
|
||||
});
|
||||
|
||||
// 2. Reset opacity for elements with that class
|
||||
gsap.set(".opacity0gsap", {
|
||||
opacity: ".4"
|
||||
});
|
||||
|
||||
// 3. Reset the Error Line path
|
||||
gsap.set("#errorLine", {
|
||||
attr: { d: "M150 100 h200 m100 0 h200" }
|
||||
});
|
||||
|
||||
gsap.to(".errortext", {
|
||||
duration: 1,
|
||||
textContent: "Keine WebSocket Verbindung",
|
||||
ease: "back.out(1.7)"
|
||||
});
|
||||
|
||||
document.querySelector(".errortextSmall").innerHTML = 'Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)';
|
||||
|
||||
counterTriesEl = document.getElementById('counterTries');
|
||||
counterMaxTriesEl = document.getElementById('counterMaxTries');
|
||||
|
||||
if(counterTriesEl) counterTriesEl.innerHTML = filedConectCount;
|
||||
if(counterMaxTriesEl) counterMaxTriesEl.innerHTML = fallbackConectCount;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user