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:
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user