Add stopwatch fill button and asset cache-busting

- Stopwatch ⏱ button appears in add/quick-book rows when sw >= 1s, fills input with floor(seconds)
- AppController passes md5 hashes of app.js/style.css to template for automatic cache-busting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kühn 2026-04-30 11:00:54 +02:00
parent f0cbe5b5d0
commit a8f6692de4
3 changed files with 23 additions and 6 deletions

View file

@ -715,18 +715,23 @@ document.addEventListener('visibilitychange',function(){
} }
}); });
(function(){ var sw = (function(){
var swEl = document.getElementById('sw'); var swEl = document.getElementById('sw');
var state = 0; // 0=stopped, 1=running, 2=paused var state = 0; // 0=stopped, 1=running, 2=paused
var start = 0, elapsed = 0, raf = null; var start = 0, elapsed = 0, raf = null;
function fmt(ms){ function getMs(){ return state === 1 ? elapsed + (Date.now() - start) : elapsed; }
var s = ms / 1000;
return s.toFixed(2) + 's'; function updateFillBtns(){
var show = getMs() >= 1000;
document.querySelectorAll('.btn-sw-fill').forEach(function(b){ b.style.display = show ? '' : 'none'; });
} }
function fmt(ms){ return (ms / 1000).toFixed(2) + 's'; }
function tick(){ function tick(){
swEl.textContent = fmt(Date.now() - start + elapsed); swEl.textContent = fmt(Date.now() - start + elapsed);
updateFillBtns();
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
} }
@ -740,12 +745,20 @@ document.addEventListener('visibilitychange',function(){
elapsed += Date.now() - start; elapsed += Date.now() - start;
swEl.textContent = fmt(elapsed); swEl.textContent = fmt(elapsed);
swEl.classList.remove('running'); swEl.classList.remove('running');
state = 2; state = 2; updateFillBtns();
} else { } else {
cancelAnimationFrame(raf); cancelAnimationFrame(raf);
elapsed = 0; swEl.textContent = '0.00s'; elapsed = 0; swEl.textContent = '0.00s';
swEl.classList.remove('running'); swEl.classList.remove('running');
state = 0; state = 0; updateFillBtns();
} }
}); });
document.addEventListener('click', function(e){
if(!e.target.classList.contains('btn-sw-fill')) return;
var inp = e.target.closest('.add-row, .qb-row').querySelector('.num-in');
if(inp) inp.value = Math.floor(getMs() / 1000);
});
return { getMs: getMs };
})(); })();

View file

@ -73,6 +73,8 @@ body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min
.ulbl{font-size:12px;color:var(--text3)} .ulbl{font-size:12px;color:var(--text3)}
.btn-as{flex:1;padding:8px;border-radius:var(--rs);background:var(--blue-bg);color:var(--blue);border:1px solid rgba(37,99,235,.2);cursor:pointer;font-family:'DM Sans',sans-serif;font-size:13px;font-weight:600} .btn-as{flex:1;padding:8px;border-radius:var(--rs);background:var(--blue-bg);color:var(--blue);border:1px solid rgba(37,99,235,.2);cursor:pointer;font-family:'DM Sans',sans-serif;font-size:13px;font-weight:600}
.btn-as:active{opacity:.7} .btn-as:active{opacity:.7}
.btn-sw-fill{flex:none;padding:7px 9px;border-radius:var(--rs);background:var(--bg3);color:var(--text2);border:1px solid var(--border2);cursor:pointer;font-size:13px;line-height:1}
.btn-sw-fill:active{opacity:.7}
.nosets{font-size:12px;color:var(--text3);padding:4px 0 8px} .nosets{font-size:12px;color:var(--text3);padding:4px 0 8px}
.card-foot{padding:8px 16px 12px;display:flex;justify-content:flex-end} .card-foot{padding:8px 16px 12px;display:flex;justify-content:flex-end}
.btn-del{background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer;padding:4px 0;font-family:'DM Sans',sans-serif} .btn-del{background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer;padding:4px 0;font-family:'DM Sans',sans-serif}

View file

@ -55,6 +55,7 @@
<div class="add-row"> <div class="add-row">
<input class="num-in" type="number" min="1"/> <input class="num-in" type="number" min="1"/>
<span class="ulbl"></span> <span class="ulbl"></span>
<button class="btn-sw-fill" style="display:none">⏱</button>
<button class="btn-as">Eintragen</button> <button class="btn-as">Eintragen</button>
</div> </div>
</template> </template>
@ -74,6 +75,7 @@
<div class="qb-name"></div> <div class="qb-name"></div>
<div class="qb-stat"></div> <div class="qb-stat"></div>
<input class="num-in" type="number" min="1"/> <input class="num-in" type="number" min="1"/>
<button class="btn-sw-fill" style="display:none">⏱</button>
<button class="btn-as">Eintragen</button> <button class="btn-as">Eintragen</button>
</div> </div>
</template> </template>