From d8ebfcda13eeb3b09f6af4bf6d8975d78e0ec2a2 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 11 Aug 2026 18:54:31 -0500 Subject: [PATCH] Add reduced-styles toggle, decrement, and kiosk improvements - Add reduced_styles config to switch between Tailwind CDN and inline CSS - Restore liquid wave animations in full-styles mode - Add score decrement endpoint and staged hold-to-reset (decrement/arm/reset) - Improve start.sh: prefer .venv, cleanup stale processes, X display fallback - Scope inline utility classes to body.reduced-styles --- .gitignore | 1 + app.py | 10 ++++ config.toml | 19 +++---- start.sh | 30 ++++++++++- static/css/style.css | 119 ++++++++++++++++++++++++++++--------------- static/js/main.js | 67 +++++++++++++++++++----- templates/index.html | 10 +++- 7 files changed, 188 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index b908d4c..124727f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ *.pyc .DS_Store +.venv/ diff --git a/app.py b/app.py index a1cc37e..e679c85 100644 --- a/app.py +++ b/app.py @@ -123,6 +123,7 @@ def index(): return render_template( "index.html", display_id=config["display_id"], + reduced_styles=config.get("reduced_styles", True), header=config.get("header", ""), header_font_size=config.get("header_font_size", 0), content_padding_top=config.get("content_padding_top", 0), @@ -171,6 +172,7 @@ def api_status(): "header_font_size": config.get("header_font_size", 0), "content_padding_top": config.get("content_padding_top", 0), "content_gap": config.get("content_gap", 1), + "reduced_styles": config.get("reduced_styles", True), "ip": get_local_ip(), "port": PORT, "scores": dict(scores), @@ -288,6 +290,14 @@ def increment(side): return jsonify(scores) +@app.route("/decrement/", methods=["GET", "POST"]) +def decrement(side): + if side in scores and scores[side] > 0: + scores[side] -= 1 + broadcast({"type": "score", "scores": dict(scores), "side": side}) + return jsonify(scores) + + @app.route("/reset", methods=["GET", "POST"]) def reset(): scores["left"] = 0 diff --git a/config.toml b/config.toml index 7c3081f..eecd342 100644 --- a/config.toml +++ b/config.toml @@ -1,22 +1,23 @@ -display_id = 1 -display_name = "Test" -header = "Grand Finals" +display_id = 6 +display_name = "Table 6" +header = "carom sports" header_font_size = 4 content_padding_top = 4 content_gap = 1 +reduced_styles = false [left] -name = "Tim" -subtitle = "Test 456" -color = "#dc2626" +name = " Phil" +subtitle = "" +color = "#2c41dd" name_font_size = 3.75 subtitle_font_size = 1.25 score_font_size = 30 [right] -name = "Nick" -subtitle = "Test 123" -color = "#1a5fb4" +name = " Nick" +subtitle = "" +color = "#e708ac" name_font_size = 3.75 subtitle_font_size = 1.25 score_font_size = 30 diff --git a/start.sh b/start.sh index 548c302..d40852d 100755 --- a/start.sh +++ b/start.sh @@ -3,11 +3,37 @@ set -e cd "$(dirname "$0")" -python3 app.py & +# Clean up previous instances so restarts are safe +fuser -k 50743/tcp 2>/dev/null || true +pkill -x chromium 2>/dev/null || true +pkill -x firefox 2>/dev/null || true +sleep 1 + +if [ -x ".venv/bin/python3" ]; then + PYTHON=".venv/bin/python3" +else + PYTHON="python3" +fi + +"$PYTHON" app.py & APP_PID=$! sleep 2 -chromium-browser --kiosk http://localhost:50743 & +BROWSER_FLAGS="--kiosk http://localhost:50743 \ + --enable-gpu-rasterization \ + --ignore-gpu-blocklist \ + --no-first-run \ + --no-default-browser-check \ + --disable-dev-shm-usage \ + --disable-infobars" + +if command -v chromium >/dev/null 2>&1; then + export DISPLAY="${DISPLAY:-:0}" + export XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}" + chromium $BROWSER_FLAGS & +else + firefox --kiosk http://localhost:50743 & +fi wait $APP_PID diff --git a/static/css/style.css b/static/css/style.css index c3f5cc1..cf55f1b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -11,18 +11,41 @@ html, body { height: 100vh; overflow: hidden; background: #0a0a0f; + cursor: none; } +/* ============ TAILWIND UTILITY REPLACEMENTS ============ */ +/* Only used when reduced_styles = true (no Tailwind CDN). */ +.reduced-styles .flex { display: flex; } +.reduced-styles .flex-col { flex-direction: column; } +.reduced-styles .flex-1 { flex: 1 1 0%; } +.reduced-styles .h-screen { height: 100vh; } +.reduced-styles .w-screen { width: 100vw; } +.reduced-styles .text-center { text-align: center; } +.reduced-styles .pt-6 { padding-top: 1.5rem; } +.reduced-styles .pointer-events-none { pointer-events: none; } +.reduced-styles .items-center { align-items: center; } +.reduced-styles .justify-center { justify-content: center; } +.reduced-styles .relative { position: relative; } +.reduced-styles .tracking-widest { letter-spacing: 0.1em; } +.reduced-styles .uppercase { text-transform: uppercase; } +.reduced-styles .whitespace-nowrap { white-space: nowrap; } +.reduced-styles .leading-none { line-height: 1; } +.reduced-styles .font-black { font-weight: 900; } +.reduced-styles .font-bold { font-weight: 700; } +.reduced-styles .italic { font-style: italic; } +.reduced-styles .font-cinzel { font-family: 'Cinzel', serif; } +.reduced-styles .font-orbitron { font-family: 'Orbitron', sans-serif; } +.reduced-styles .font-playfair { font-family: 'Playfair Display', serif; } + /* ============ SIDES ============ */ .side { - cursor: pointer; position: relative; touch-action: none; - transition: filter 0.15s ease; } -.side:active { - filter: brightness(1.15); +.side:active .side-glow { + opacity: 1; } /* Hold to reset progress bar */ @@ -36,31 +59,34 @@ html, body { pointer-events: none; background: var(--team-color); box-shadow: 0 0 12px var(--team-color); - width: 0; + width: 100%; + transform: scaleX(0); + transform-origin: left center; transition: none; } .side.holding::after { - animation: hold-progress 1.5s linear forwards; + animation: hold-progress 4s linear forwards; +} + +.side.hold-reset-arm::after { + background: #ef4444; + box-shadow: 0 0 12px #ef4444; } .side.hold-cancel::after { animation: hold-fade 0.2s ease-out forwards; - width: 100%; + transform: scaleX(1); } @keyframes hold-progress { - 0% { width: 0; } - 100% { width: 100%; } + 0% { transform: scaleX(0); } + 100% { transform: scaleX(1); } } @keyframes hold-fade { 0% { opacity: 1; } - 100% { opacity: 0; width: 100%; } -} - -.side.holding { - filter: brightness(0.7) !important; + 100% { opacity: 0; } } .side-glow { @@ -71,6 +97,9 @@ html, body { height: 100vh; pointer-events: none; z-index: 0; + opacity: 0.7; + transition: opacity 0.15s ease; + will-change: opacity; } .side-content { @@ -84,8 +113,7 @@ html, body { text-transform: uppercase; color: #c9a84c; text-shadow: - 0 0 20px rgba(201, 168, 76, 0.3), - 0 0 40px rgba(201, 168, 76, 0.1); + 0 0 10px rgba(201, 168, 76, 0.35); } /* ============ TEAM SUBTITLE ============ */ @@ -103,9 +131,8 @@ html, body { position: relative; color: inherit; text-shadow: - 0 0 0.04em currentColor, - 0 0 0.1em currentColor, - 0 0 0.2em color-mix(in srgb, currentColor 40%, transparent); + 0 0 0.05em currentColor, + 0 0 0.12em currentColor; transition: text-shadow 0.3s ease; } @@ -180,9 +207,7 @@ html, body { background: linear-gradient( 90deg, var(--team-color) 0%, - var(--team-color) 40%, var(--team-color) 60%, - color-mix(in srgb, var(--team-color) 40%, transparent) 85%, transparent 100% ); } @@ -191,20 +216,18 @@ html, body { background: linear-gradient( 90deg, transparent 0%, - color-mix(in srgb, var(--team-color) 40%, transparent) 15%, var(--team-color) 40%, - var(--team-color) 60%, var(--team-color) 100% ); } -/* ============ LIQUID WAVES ============ */ -.wave-container { +/* ============ LIQUID WAVES (full styles only) ============ */ +body:not(.reduced-styles) .wave-container { position: absolute; inset: 0; } -.wave { +body:not(.reduced-styles) .wave { position: absolute; left: -25%; width: 150%; @@ -213,7 +236,7 @@ html, body { will-change: transform; } -.wave-front { +body:not(.reduced-styles) .wave-front { height: 160%; top: -15%; background: var(--team-color); @@ -221,7 +244,7 @@ html, body { animation-duration: 3s; } -.wave-back { +body:not(.reduced-styles) .wave-back { height: 200%; top: -40%; background: var(--team-color); @@ -238,18 +261,28 @@ html, body { /* ============ HAND WAVE COUNTDOWN ============ */ .side.counting { box-shadow: - 0 0 60px rgba(59, 130, 246, 0.9), - 0 0 120px rgba(59, 130, 246, 0.5), - 0 0 200px rgba(59, 130, 246, 0.25), - inset 0 0 80px rgba(59, 130, 246, 0.2); - animation: counting-pulse 0.6s ease-in-out infinite alternate; + 0 0 40px rgba(59, 130, 246, 0.4), + inset 0 0 60px rgba(59, 130, 246, 0.15); outline: 4px solid rgba(59, 130, 246, 0.7); outline-offset: -4px; } +.side.counting::before { + content: ''; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + box-shadow: + 0 0 50px rgba(59, 130, 246, 0.55), + inset 0 0 90px rgba(59, 130, 246, 0.2); + animation: counting-pulse 0.6s ease-in-out infinite alternate; + will-change: opacity; +} + @keyframes counting-pulse { - 0% { box-shadow: 0 0 50px rgba(59,130,246,0.7), 0 0 100px rgba(59,130,246,0.3), 0 0 160px rgba(59,130,246,0.15), inset 0 0 60px rgba(59,130,246,0.12); outline-color: rgba(59,130,246,0.5); } - 100% { box-shadow: 0 0 80px rgba(59,130,246,1.0), 0 0 160px rgba(59,130,246,0.6), 0 0 260px rgba(59,130,246,0.35), inset 0 0 120px rgba(59,130,246,0.3); outline-color: rgba(59,130,246,0.9); } + 0% { opacity: 0.4; } + 100% { opacity: 1; } } .hw-bar { @@ -261,7 +294,9 @@ html, body { pointer-events: none; background: #60a5fa; box-shadow: 0 0 20px #3b82f6, 0 0 40px #3b82f6; - width: 0; + width: 100%; + transform: scaleX(0); + transform-origin: left center; } .hw-bar.active { @@ -269,8 +304,8 @@ html, body { } @keyframes hw-progress { - 0% { width: 0; } - 100% { width: 100%; } + 0% { transform: scaleX(0); } + 100% { transform: scaleX(1); } } /* ============ RESET CONFIRM OVERLAY ============ */ @@ -312,7 +347,9 @@ html, body { height: 8px; background: white; box-shadow: 0 0 20px white, 0 0 40px white; - width: 0; + width: 100%; + transform: scaleX(0); + transform-origin: left center; } #reset-confirm.show .confirm-bar { @@ -320,6 +357,6 @@ html, body { } @keyframes confirm-progress { - 0% { width: 0; } - 100% { width: 100%; } + 0% { transform: scaleX(0); } + 100% { transform: scaleX(1); } } diff --git a/static/js/main.js b/static/js/main.js index 1aa30f3..fd27d28 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -44,6 +44,7 @@ } function fitAll() { + scoreFontCache = {}; var headerRow = document.querySelector('.header-row'); if (headerRow) { var span = headerRow.querySelector('span'); @@ -104,33 +105,52 @@ // ---- Tap / Hold handlers ---- document.querySelectorAll('.side').forEach(function (side) { var sideName = side.dataset.side; - var holdTimer = null; var holdFadeTimer = null; + var holdDecTimer = null; + var holdResetArmTimer = null; + var holdResetTimer = null; var holdFired = false; + function clearHoldTimers() { + if (holdFadeTimer) { clearTimeout(holdFadeTimer); holdFadeTimer = null; } + if (holdDecTimer) { clearTimeout(holdDecTimer); holdDecTimer = null; } + if (holdResetArmTimer) { clearTimeout(holdResetArmTimer); holdResetArmTimer = null; } + if (holdResetTimer) { clearTimeout(holdResetTimer); holdResetTimer = null; } + } + function onStart(e) { e.preventDefault(); if (debounce) return; holdFired = false; side.classList.remove('hold-cancel'); + side.classList.remove('hold-reset-arm'); holdFadeTimer = setTimeout(function () { void side.offsetWidth; side.classList.add('holding'); - }, 500); + }, 300); - holdTimer = setTimeout(function () { + holdDecTimer = setTimeout(function () { + holdFired = true; + decrement(sideName); + }, 2000); + + holdResetArmTimer = setTimeout(function () { + side.classList.add('hold-reset-arm'); + }, 2500); + + holdResetTimer = setTimeout(function () { holdFired = true; side.classList.remove('holding'); - side.classList.remove('hold-cancel'); + side.classList.remove('hold-reset-arm'); resetSide(sideName); - }, 2000); + }, 4000); } function onEnd(e) { - if (holdFadeTimer) { clearTimeout(holdFadeTimer); holdFadeTimer = null; } - if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } + clearHoldTimers(); side.classList.remove('holding'); + side.classList.remove('hold-reset-arm'); if (holdFired) { side.classList.remove('hold-cancel'); return; @@ -144,10 +164,10 @@ } function onCancel() { - if (holdFadeTimer) { clearTimeout(holdFadeTimer); holdFadeTimer = null; } - if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } + clearHoldTimers(); side.classList.remove('holding'); side.classList.remove('hold-cancel'); + side.classList.remove('hold-reset-arm'); } side.addEventListener('pointerdown', onStart); @@ -172,7 +192,7 @@ var bar = s.querySelector('.hw-bar'); if (bar) { bar.classList.remove('active'); - bar.style.width = '0'; + bar.style.transform = 'scaleX(0)'; } }); } @@ -185,7 +205,7 @@ if (el) el.classList.add('counting'); var bar = document.querySelector('.side-' + side + ' .hw-bar'); if (bar) { - bar.style.width = '0'; + bar.style.transform = 'scaleX(0)'; void bar.offsetWidth; bar.classList.add('active'); } @@ -290,7 +310,13 @@ fetch('/increment/' + side, { method: 'POST' }).catch(function () {}); } + // ---- Decrement ---- + function decrement(side) { + fetch('/decrement/' + side, { method: 'POST' }).catch(function () {}); + } + // ---- Update display ---- + var scoreFontCache = {}; function updateDisplay(side, value) { var el = document.getElementById('score-' + side); el.textContent = value; @@ -299,9 +325,15 @@ if (sfs > 0) { el.style.fontSize = sfs + 'rem'; } else { - var sideEl = document.querySelector('.side-' + side); - var rect = sideEl.getBoundingClientRect(); - fitTextHeight(el, rect.height * 0.55); + var key = side + '-' + String(value).length; + if (scoreFontCache[key]) { + el.style.fontSize = scoreFontCache[key] + 'px'; + } else { + var sideEl = document.querySelector('.side-' + side); + var rect = sideEl.getBoundingClientRect(); + fitTextHeight(el, rect.height * 0.55); + scoreFontCache[key] = parseFloat(el.style.fontSize) || 0; + } } el.classList.remove('score-pop'); @@ -310,6 +342,7 @@ } // ---- Trigger sweep animation ---- + var sweepTimers = {}; function triggerSweep(side) { var overlay = document.getElementById('sweep-' + side); if (!overlay) return; @@ -319,6 +352,12 @@ void overlay.offsetHeight; overlay.style.animation = ''; overlay.classList.add('sweeping'); + + if (sweepTimers[side]) clearTimeout(sweepTimers[side]); + sweepTimers[side] = setTimeout(function () { + overlay.classList.remove('sweeping'); + sweepTimers[side] = null; + }, 1100); } // ---- Reset single side ---- diff --git a/templates/index.html b/templates/index.html index 927d607..d62844e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,11 +2,12 @@ - + CuteBoard — Display {{ display_id }} + {% if not reduced_styles %} + {% endif %} - +
@@ -67,10 +69,12 @@
+ {% if not reduced_styles %}
+ {% endif %}
@@ -105,10 +109,12 @@
+ {% if not reduced_styles %}
+ {% endif %}