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:
parent
a70f739ef7
commit
d8ebfcda13
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.venv/
|
||||
|
||||
10
app.py
10
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/<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
|
||||
|
||||
17
config.toml
17
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"
|
||||
subtitle = ""
|
||||
color = "#e708ac"
|
||||
name_font_size = 3.75
|
||||
subtitle_font_size = 1.25
|
||||
score_font_size = 30
|
||||
|
||||
30
start.sh
30
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
|
||||
|
||||
@ -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); }
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -298,10 +324,16 @@
|
||||
var sfs = parseFloat(el.dataset.scoreFontSize);
|
||||
if (sfs > 0) {
|
||||
el.style.fontSize = sfs + 'rem';
|
||||
} else {
|
||||
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 ----
|
||||
|
||||
@ -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 -->
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user