Replace single toast with stacked toast queue

Multiple toasts can appear simultaneously, capped at 3. Oldest
toast auto-evicts when cap is reached. Each toast auto-dismisses
with a fade-up exit animation.

- frontend/app.js: toast() now creates DOM elements in toastContainer
- frontend/index.html: single #toast replaced with #toastContainer
- frontend/styles.css: .toast-container flexbox row, .toast-exit animation
This commit is contained in:
Jonathan Sykes
2026-06-14 14:36:07 +08:00
parent ddfa1494c6
commit a4d6b825d5
7 changed files with 51 additions and 11 deletions

View File

@@ -20,7 +20,7 @@ bottom as they come up.
- [x] Keyboard-shortcut help overlay (press `?`), listing space / arrows / f / m. - [x] Keyboard-shortcut help overlay (press `?`), listing space / arrows / f / m.
- [x] Download/preload progress indicator on cards (percent or a bar, not just a pulse). - [x] Download/preload progress indicator on cards (percent or a bar, not just a pulse).
- [x] Remember and restore the last playback position per video. - [x] Remember and restore the last playback position per video.
- [ ] Toast queue (stack multiple toasts instead of replacing). - [x] Toast queue (stack multiple toasts instead of replacing).
## Features ## Features
- [ ] Drag-to-reorder videos within a playlist. - [ ] Drag-to-reorder videos within a playlist.

View File

@@ -109,12 +109,29 @@ function fmtTime(sec) {
const mm = h ? String(m).padStart(2, '0') : String(m); const mm = h ? String(m).padStart(2, '0') : String(m);
return (h ? h + ':' : '') + mm + ':' + String(s).padStart(2, '0'); return (h ? h + ':' : '') + mm + ':' + String(s).padStart(2, '0');
} }
function toast(msg) { function toast(msg, { duration = 2200 } = {}) {
const t = $('toast'); const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg; t.textContent = msg;
t.classList.remove('hidden'); const container = $('toastContainer');
clearTimeout(toast._t); container.appendChild(t);
toast._t = setTimeout(() => t.classList.add('hidden'), 2200); // Trigger layout for animation
t.style.animation = 'none';
t.offsetHeight; // force reflow
t.style.animation = '';
// Auto-remove after duration
setTimeout(() => {
t.classList.add('toast-exit');
setTimeout(() => t.remove(), 280);
}, duration);
// Cap at 3 toasts — remove oldest
while (container.children.length > 3) {
const first = container.firstChild;
first.classList.add('toast-exit');
setTimeout(() => first.remove(), 280);
}
} }
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); } function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
function fmtBytes(n) { function fmtBytes(n) {

View File

@@ -162,7 +162,7 @@
</div> </div>
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toastContainer" class="toast-container"></div>
<!-- Keyboard shortcuts help overlay --> <!-- Keyboard shortcuts help overlay -->
<div id="shortcutHelp" class="shortcut-overlay hidden"> <div id="shortcutHelp" class="shortcut-overlay hidden">

View File

@@ -922,19 +922,42 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
.btn.danger:hover { background: rgba(255,75,50,0.12); } .btn.danger:hover { background: rgba(255,75,50,0.12); }
/* ===================== Toast ===================== */ /* ===================== Toast ===================== */
.toast-container {
position: fixed;
bottom: 26px;
left: 50%;
transform: translateX(-50%);
z-index: 200;
display: flex;
flex-direction: column-reverse;
align-items: center;
gap: 8px;
pointer-events: none;
}
.toast { .toast {
position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%);
background: var(--bg-3); background: var(--bg-3);
border: 1px solid var(--line); border: 1px solid var(--line);
color: var(--text); color: var(--text);
padding: 13px 20px; padding: 13px 20px;
border-radius: 11px; border-radius: 11px;
box-shadow: var(--shadow); box-shadow: var(--shadow);
z-index: 200; font-size: 13.5px;
font-size: 13.5px; font-weight: 500; font-weight: 500;
white-space: nowrap;
animation: toastIn 0.3s var(--ease); animation: toastIn 0.3s var(--ease);
pointer-events: auto;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.toast-exit {
animation: toastOut 0.25s var(--ease) forwards;
}
@keyframes toastOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-8px); }
} }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
/* ===================== Keyboard shortcut overlay ===================== */ /* ===================== Keyboard shortcut overlay ===================== */
.shortcut-overlay { .shortcut-overlay {

Binary file not shown.