First version, for githup; UNSTABLE, DO NOT USE!
This commit is contained in:
131
www/displays/audio.php
Normal file
131
www/displays/audio.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Remote Audioplayer</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<style>
|
||||
body {
|
||||
background: #000 !important;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const userAccess = "audio";
|
||||
|
||||
let ws;
|
||||
const RETRY_DELAY = 200; // ms
|
||||
|
||||
function startWebSocket() {
|
||||
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://` + window.location.hostname + `/ws/?access=${userAccess}`);
|
||||
} catch (err) {
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
document.querySelector('.errorws').style.display = 'none';
|
||||
|
||||
// SAFE: this executes only on successful connect
|
||||
ws.send(JSON.stringify({
|
||||
type: "SELF",
|
||||
payload: {}
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
document.querySelector('h1').innerText = 'WS ERROR';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
document.querySelector('h1').innerText = 'WS DISCONECTED';
|
||||
scheduleRetry();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
setTimeout(startWebSocket, RETRY_DELAY);
|
||||
}
|
||||
|
||||
startWebSocket();
|
||||
</script>
|
||||
<h1>Der Audioplayer startet durch das Berühren des Displays</h1>
|
||||
|
||||
<audio id="peep" preload="auto"></audio>
|
||||
<audio id="musicPlayer" preload="auto"></audio>
|
||||
|
||||
<script>
|
||||
const peep = document.getElementById('peep');
|
||||
const music = document.getElementById('musicPlayer');
|
||||
|
||||
let lastMusicUrl = null;
|
||||
let pollingStarted = false; // ensure we start polling only once
|
||||
|
||||
async function fetchAndHandleMusic() {
|
||||
try {
|
||||
const response = await fetch('/displays/json/audio.json?t=' + Date.now(), { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
|
||||
// Stop everything if musik is "stop"
|
||||
if (!data.musik || data.musik === 'nan' || data.start == false) {
|
||||
if (!music.paused) {
|
||||
music.pause();
|
||||
music.currentTime = 0;
|
||||
}
|
||||
if (!peep.paused) {
|
||||
peep.pause();
|
||||
peep.currentTime = 0;
|
||||
}
|
||||
lastMusicUrl = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Play only if new URL
|
||||
if (data.musik !== lastMusicUrl) {
|
||||
lastMusicUrl = data.musik;
|
||||
|
||||
// Play short peep first
|
||||
peep.src = '/files/music/piep.mp3';
|
||||
peep.play().then(() => {
|
||||
// After 2 seconds, play the main music
|
||||
setTimeout(() => {
|
||||
music.src = data.musik;
|
||||
music.play().catch(err => console.log('Music play error:', err));
|
||||
}, 2000);
|
||||
}).catch(err => console.log('Peep play error:', err));
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching JSON:', err);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', () => {
|
||||
if (!pollingStarted) {
|
||||
pollingStarted = true;
|
||||
ws.addEventListener("message", () => {
|
||||
fetchAndHandleMusic();
|
||||
});
|
||||
document.querySelector('h1').innerText = 'Player läuft'; // remove instruction
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
372
www/displays/css/display.css
Normal file
372
www/displays/css/display.css
Normal file
@@ -0,0 +1,372 @@
|
||||
:root {
|
||||
--bg: #111827;
|
||||
--panel: #020617;
|
||||
--panel-soft: #0b1120;
|
||||
--accent: #38bdf8;
|
||||
--accent-soft: rgba(56, 189, 248, 0.15);
|
||||
--text-main: #e5e7eb;
|
||||
--text-muted: #9ca3af;
|
||||
--danger: #ef4444;
|
||||
--success: #22c55e;
|
||||
--radius-lg: 18px;
|
||||
--border-subtle: 1px solid rgba(148, 163, 184, 0.35);
|
||||
--shadow-soft: 0 20px 45px rgba(15, 23, 42, 0.85);
|
||||
--transition-fast: 180ms ease-out;
|
||||
--colorStartDiv: #0b1120;
|
||||
--panelBgLogo: #4a2f96;
|
||||
--font-heading: clamp(1.5rem, 6vh, 4rem);
|
||||
--font-sub: clamp(1rem, 3.5vh, 2.2rem);
|
||||
--logo-size: clamp(150px, 40vh, 500px);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
head > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-main);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* WebSocket error overlay */
|
||||
.errorws {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 5vh 5vw;
|
||||
background: radial-gradient(circle at top, #020617 0, #020617 55%);
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.errorws .logoimg {
|
||||
max-width: var(--logo-size);
|
||||
height: auto;
|
||||
margin-bottom: 2vh;
|
||||
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.65));
|
||||
}
|
||||
|
||||
.errortext {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 8vw, 5rem);
|
||||
line-height: 1.1;
|
||||
max-width: 90vw;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-main);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.errortextSmall {
|
||||
margin: 10px;
|
||||
font-size: clamp(1rem, 3vw, 2rem);
|
||||
opacity: 0.8;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--text-main);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Startup logo overlay */
|
||||
.logobg {
|
||||
padding: 4vh;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background-color: var(--panelBgLogo);
|
||||
z-index: 1000;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.logoimg {
|
||||
width: auto;
|
||||
max-height: 45vh;
|
||||
max-width: 80vw;
|
||||
object-fit: contain;
|
||||
/* filter: drop-shadow(0 18px 40px rgba(15, 23, 42, 0.4)); */
|
||||
}
|
||||
|
||||
.logotext {
|
||||
margin: 0;
|
||||
font-size: clamp(1.8rem, 7vh, 6rem);
|
||||
text-align: center;
|
||||
width: 90vw;
|
||||
line-height: 1.2;
|
||||
/* Forces single line for names/titles to prevent overflow */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logotext, .logoctext {
|
||||
color: var(--text-color) !important;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
.logoctext {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 7vw, 12rem);
|
||||
}
|
||||
|
||||
/* 4. Responsive adjustments for different screen ratios */
|
||||
@media (aspect-ratio: 4/3), (aspect-ratio: 10/7) {
|
||||
/* Tablet specific tweaks (iPad/Android Tabs) */
|
||||
:root {
|
||||
--logo-size: 30vh;
|
||||
}
|
||||
.logotext {
|
||||
font-size: 5vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1921px) {
|
||||
/* Ultra-large TV/4K tweaks */
|
||||
.logotext {
|
||||
letter-spacing: 0.25em; /* Better legibility at distance */
|
||||
}
|
||||
}
|
||||
|
||||
/* Main scoreboard container – full screen */
|
||||
.pagediv {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2vh;
|
||||
padding: 2.2vh 2.4vw;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at top left, var(--bg-soft), transparent 60%),
|
||||
radial-gradient(circle at bottom right, var(--bg-soft), transparent 60%);
|
||||
}
|
||||
|
||||
.pagediv.manuel {
|
||||
padding-right: clamp(60px, 2.4vw, 2.4vw);
|
||||
}
|
||||
|
||||
/* Common row styling */
|
||||
.display-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(135deg, var(--panel-soft), var(--panel));
|
||||
border-bottom: var(--border-subtle);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.display-row::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top left, var(--bg-soft), transparent 55%);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row1text,
|
||||
.row2text p,
|
||||
.row3 > p,
|
||||
.start_text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Row 1: athlete name */
|
||||
.row1text {
|
||||
font-size: min(8vh, 4.6vw);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Row 2: club, program, start/stop */
|
||||
.row2 {
|
||||
justify-content: space-between;
|
||||
gap: 2vw;
|
||||
}
|
||||
|
||||
.row2::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at bottom right, var(--colorStartDiv), transparent 55%);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row2text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8vh;
|
||||
width: 50vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row2_1text {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.row2_2text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Start / stop pill */
|
||||
.start_div {
|
||||
width: 35vw;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.start_text {
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Row 3: D-score and total */
|
||||
.row3 {
|
||||
justify-content: space-between;
|
||||
gap: 2vw;
|
||||
}
|
||||
|
||||
.row3 > p {
|
||||
font-size: min(9vh, 5vw);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row3_1text {
|
||||
color: var(--color-note-l);
|
||||
}
|
||||
|
||||
.row3_2text {
|
||||
color: var(--color-note-r);
|
||||
}
|
||||
|
||||
/* Existing #score and char/row styles if you use them elsewhere */
|
||||
#score {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.char {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 40vw;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 20vw;
|
||||
}
|
||||
|
||||
/* Landscape / portrait logo scaling */
|
||||
@media (orientation: landscape) {
|
||||
.logoimg {
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
.logoimg {
|
||||
max-width: 60vw;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small screens – keep everything readable */
|
||||
@media (max-width: 768px) {
|
||||
.pagediv {
|
||||
padding: 1.6vh 3vw;
|
||||
gap: 1.4vh;
|
||||
}
|
||||
|
||||
.display-row {
|
||||
padding: 1.4vh 3vw;
|
||||
}
|
||||
|
||||
.row2 {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.start_div {
|
||||
align-self: flex-end;
|
||||
min-width: 40vw;
|
||||
}
|
||||
}
|
||||
|
||||
.noWsConnection {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
|
||||
transform: rotate(270deg);
|
||||
transform-origin: right bottom;
|
||||
}
|
||||
|
||||
.rotator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.noWsConnection img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.noWsConnection p {
|
||||
font-size: 20px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
letter-spacing: 1.1px;
|
||||
}
|
||||
441
www/displays/display.php
Normal file
441
www/displays/display.php
Normal file
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
|
||||
/*$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
|
||||
$fullDomain = $protocol . $_SERVER['HTTP_HOST'];*/
|
||||
|
||||
$lastSegment = strtolower($_GET['geraet']) ?? '';
|
||||
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'];
|
||||
|
||||
$token = "QQa2UMbEYW8oOL7wz9DjtqECVCikSZsDuSdmzxiadEXFsKyujEUyQOW1AYMD2OqU8VXxClIRweRuWLzvBrZpPYL41e89Rs96tM7Lq1KpjA5E2mg2UfgvztheGRV";
|
||||
|
||||
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` FROM $tableGeraete ORDER BY start_index ASC");
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$disciplines = array_map(
|
||||
'strtolower',
|
||||
array_column($result->fetch_all(MYSQLI_ASSOC), 'name')
|
||||
);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
// Fetch and Sanitize Brand Colors
|
||||
$wkName = db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['wkName']);
|
||||
$cleanColor = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColourLogo']));
|
||||
$cleanColorText = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayTextColourLogo']));
|
||||
|
||||
// Fetch and Sanitize Layout Colors
|
||||
$displayColorScoringBg = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringBg']));
|
||||
$displayColorScoringBgSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringBgSoft']));
|
||||
$displayColorScoringPanel = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanel']));
|
||||
$displayColorScoringPanelSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelSoft']));
|
||||
|
||||
// Fetch and Sanitize Text Colors
|
||||
$displayColorScoringPanelText = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelText']));
|
||||
$displayColorScoringPanelTextSoft = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextSoft']));
|
||||
|
||||
// Fetch and Sanitize Accent Colors (Note: fixed 'diplay' typo here)
|
||||
$displayColorScoringPanelTextNoteL = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextNoteL']));
|
||||
$displayColorScoringPanelTextNoteR = sanitize_hex(db_get_var($guest, "SELECT `value` FROM $tableVar WHERE `name` = ?", ['displayColorScoringPanelTextNoteR']));
|
||||
|
||||
$guest->close();
|
||||
|
||||
if (!isset($lastSegment) || !in_array($lastSegment, $disciplines)){
|
||||
echo 'kein Gerät';
|
||||
exit;
|
||||
}
|
||||
|
||||
$jsonUrlconfig = '/displays/json/config.json';
|
||||
$jsonUrl = '/displays/json/display_' . $lastSegment . '.json';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?= $wkName ?> Anzeigen</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="stylesheet" href="/displays/css/display.css">
|
||||
<style>
|
||||
:root {
|
||||
/* Brand */
|
||||
--panelBgLogo: <?= $cleanColor ?>;
|
||||
--text-color: <?= $cleanColorText ?>;
|
||||
|
||||
/* Backgrounds */
|
||||
--bg: <?= $displayColorScoringBg ?>;
|
||||
--bg-soft: <?= $displayColorScoringBgSoft ?>26;
|
||||
--panel: <?= $displayColorScoringPanel ?>;
|
||||
--panel-soft: <?= $displayColorScoringPanelSoft ?>;
|
||||
|
||||
/* Typography */
|
||||
--text-main: <?= $displayColorScoringPanelText ?>;
|
||||
--text-muted: <?= $displayColorScoringPanelTextSoft ?>;
|
||||
|
||||
/* Noten */
|
||||
--color-note-l: <?= $displayColorScoringPanelTextNoteL ?>;
|
||||
--color-note-r: <?= $displayColorScoringPanelTextNoteR ?>;
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="errorws">
|
||||
<img class="logoimg" src="https://cdn-icons-png.freepik.com/512/12890/12890341.png?ga=GA1.1.991281105.1761199359">
|
||||
<p class="errortext">Keine WebSocket Verbindung</p>
|
||||
<p class="errortextSmall">Versuche Verbindung... (Versuch <span id="counterTries"></span> / <span id="counterMaxTries"></span>)</p>
|
||||
</div>
|
||||
|
||||
<div class="noWsConnection">
|
||||
<div class="rotator">
|
||||
<img src="https://cdn-icons-png.freepik.com/512/12890/12890341.png?ga=GA1.1.991281105.1761199359">
|
||||
<p>Keine WebSocket Verbindung, Syncronisation via FETCH</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logobg">
|
||||
<img id="jsImgLogo" class="logoimg" src="/intern/img/logo-normal.png">
|
||||
<p class="logotext"><?= $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>
|
||||
|
||||
|
||||
<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 = 10;
|
||||
const RETRY_DELAY = 1000;
|
||||
const FALLBACK_POLL_INTERVAL = 3000; // Fetch every 3 seconds if WS is dead
|
||||
|
||||
let fallbackTimer = null;
|
||||
|
||||
// Hold our current data in memory
|
||||
let displayConfig = { type: 'logo', ctext: '' };
|
||||
let currentScore = {};
|
||||
let lastUniqueId = null;
|
||||
|
||||
// UI Elements
|
||||
const counterTriesEl = document.getElementById('counterTries');
|
||||
const 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();
|
||||
});
|
||||
|
||||
// --- WebSocket Logic ---
|
||||
function startWebSocket() {
|
||||
console.log("Attempting WebSocket connection...");
|
||||
|
||||
try {
|
||||
ws = new WebSocket(`wss://${window.location.hostname}/ws/?access=${userAccess}`);
|
||||
} catch (err) {
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected!");
|
||||
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;
|
||||
|
||||
// Stop fallback polling since WS is alive
|
||||
stopFallbackPolling();
|
||||
|
||||
// Do ONE initial fetch to sync current state
|
||||
fetchFullState();
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
document.querySelector('.errorws').style.display = 'flex';
|
||||
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() {
|
||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||
|
||||
if (filedConectCount >= fallbackConectCount) {
|
||||
// MAX RETRIES REACHED -> Enter Fallback Mode
|
||||
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);
|
||||
|
||||
// Optionally, keep trying to reconnect the WS slowly in the background
|
||||
setTimeout(startWebSocket, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
const logobg = document.querySelector('.logobg');
|
||||
const ctext = document.querySelector('.logoctext');
|
||||
|
||||
if (displayConfig.type === 'logo' || displayConfig.type === 'ctext') {
|
||||
// LOGO OR CUSTOM TEXT MODE
|
||||
if (logobg) logobg.style.opacity = "1";
|
||||
if (ctext) ctext.innerText = (displayConfig.type === 'ctext') ? (displayConfig.ctext || '') : '';
|
||||
|
||||
} 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('--danger').trim();
|
||||
const successColor = rootStyles.getPropertyValue('--success').trim();
|
||||
|
||||
if (currentScore.start === true) {
|
||||
row2El.style.setProperty('--colorStartDiv', successColor);
|
||||
starttext.innerHTML = 'Start';
|
||||
} else {
|
||||
row2El.style.setProperty('--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('--panelBgLogo', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayTextColourLogo':
|
||||
document.documentElement.style.setProperty('--text-color', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBg':
|
||||
document.documentElement.style.setProperty('--bg', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringBgSoft':
|
||||
document.documentElement.style.setProperty('--bg-soft', sanitizeHex(value) + "26");
|
||||
break;
|
||||
case 'displayColorScoringPanel':
|
||||
document.documentElement.style.setProperty('--panel', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelSoft':
|
||||
document.documentElement.style.setProperty('--panel-soft', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelText':
|
||||
document.documentElement.style.setProperty('--text-main', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextSoft':
|
||||
document.documentElement.style.setProperty('--text-muted', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteL': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--color-note-l', sanitizeHex(value));
|
||||
break;
|
||||
case 'displayColorScoringPanelTextNoteR': // Matching your PHP typo
|
||||
document.documentElement.style.setProperty('--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 fitTextAll() {
|
||||
const paragraphs = document.querySelectorAll('.pagediv p');
|
||||
paragraphs.forEach(p => fitTextElement(p));
|
||||
}
|
||||
|
||||
window.addEventListener('resize', fitTextAll);
|
||||
|
||||
// --- Initialize ---
|
||||
startWebSocket();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user