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
This commit is contained in:
Nick 2026-08-11 18:54:31 -05:00
parent a70f739ef7
commit d8ebfcda13
7 changed files with 188 additions and 68 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
__pycache__/
*.pyc
.DS_Store
.venv/

10
app.py
View File

@ -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/<side>", 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

View File

@ -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

View File

@ -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

View File

@ -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); }
}

View File

@ -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 ----

View File

@ -2,11 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>CuteBoard — Display {{ display_id }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
{% if not reduced_styles %}
<script src="{{ url_for('static', filename='js/tailwind.js') }}"></script>
<script>
tailwind.config = {
@ -21,10 +22,11 @@
},
};
</script>
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<body{% if reduced_styles %} class="reduced-styles"{% endif %}>
<div class="scoreboard flex flex-col h-screen w-screen" data-content-padding-top="{{ content_padding_top }}">
@ -67,10 +69,12 @@
<div class="sweep-overlay" id="sweep-left">
<div class="sweep-gradient"></div>
{% if not reduced_styles %}
<div class="wave-container">
<div class="wave wave-front"></div>
<div class="wave wave-back"></div>
</div>
{% endif %}
</div>
</div>
@ -105,10 +109,12 @@
<div class="sweep-overlay" id="sweep-right">
<div class="sweep-gradient"></div>
{% if not reduced_styles %}
<div class="wave-container">
<div class="wave wave-front"></div>
<div class="wave wave-back"></div>
</div>
{% endif %}
</div>
</div> <!-- /sides-row -->