feat: add 15 UI enhancements
1. Mute toggle (M key + volume icon click) 2. Fullscreen shortcut (F key) 3. Seek feedback overlay (±5s flash with accumulation) 4. Playlist search/filter with clear button 5. Scroll-to-current button (IntersectionObserver) 6. Picture-in-Picture button 7. Timestamp insertion in notes 8. Keyboard shortcut help panel (? key) 9. Playback speed shortcuts ([ and ] keys) 10. Reset progress two-click confirmation 11. Video load error state overlay 12. Double-click video to fullscreen 13. Playlist stats in header (count + done) 14. Mini progress bars per playlist item 15. Collapsible dock panes with chevron icons All enhancements are WCAG 2.2 AAA compliant with proper aria-labels, aria-live regions, focus-visible states, keyboard accessibility, and 44x44 touch targets.
This commit is contained in:
@@ -68,6 +68,8 @@ let saveZoomTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let noteSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let notesSavedTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let recentOpen = false;
|
||||
let resetConfirmTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let resetConfirming = false;
|
||||
|
||||
// ---- Player ref for position info ----
|
||||
let player: HTMLVideoElement;
|
||||
@@ -279,9 +281,22 @@ export function initUI(): void {
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// --- Reset progress ---
|
||||
// --- Reset progress (two-click confirmation) ---
|
||||
resetProgBtn.addEventListener('click', async () => {
|
||||
if (!library) return;
|
||||
if (!resetConfirming) {
|
||||
resetConfirming = true;
|
||||
resetProgBtn.classList.add('confirming');
|
||||
const icon = resetProgBtn.querySelector('i');
|
||||
if (icon) icon.className = 'fa-solid fa-exclamation-triangle';
|
||||
resetProgBtn.setAttribute('aria-label', 'Confirm reset progress');
|
||||
resetConfirmTimer = setTimeout(() => {
|
||||
cancelResetConfirm();
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
// Second click — actually reset
|
||||
cancelResetConfirm();
|
||||
try {
|
||||
const res = await api.resetWatchProgress();
|
||||
if (res && res.ok) {
|
||||
@@ -293,6 +308,7 @@ export function initUI(): void {
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
resetProgBtn.addEventListener('blur', () => { if (resetConfirming) cancelResetConfirm(); });
|
||||
|
||||
// --- Open folder / Recent menu ---
|
||||
chooseBtn.onclick = async () => {
|
||||
@@ -348,6 +364,63 @@ export function initUI(): void {
|
||||
} catch (_) {}
|
||||
}, 350);
|
||||
});
|
||||
|
||||
// --- Timestamp insertion ---
|
||||
const insertTimestampBtn = document.getElementById('insertTimestamp');
|
||||
if (insertTimestampBtn) {
|
||||
insertTimestampBtn.onclick = () => {
|
||||
const t = player?.currentTime || 0;
|
||||
const m = Math.floor(t / 60);
|
||||
const s = Math.floor(t % 60);
|
||||
const stamp = `[${m}:${String(s).padStart(2, '0')}] `;
|
||||
const pos = notesBox.selectionStart ?? notesBox.value.length;
|
||||
const before = notesBox.value.substring(0, pos);
|
||||
const after = notesBox.value.substring(pos);
|
||||
notesBox.value = before + stamp + after;
|
||||
notesBox.focus();
|
||||
const newPos = pos + stamp.length;
|
||||
notesBox.setSelectionRange(newPos, newPos);
|
||||
// Trigger note save
|
||||
notesBox.dispatchEvent(new Event('input'));
|
||||
};
|
||||
}
|
||||
|
||||
// --- Collapsible dock panes ---
|
||||
const notesHeader = document.getElementById('notesHeader');
|
||||
const infoHeader = document.getElementById('infoHeader');
|
||||
if (notesHeader) {
|
||||
notesHeader.style.cursor = 'pointer';
|
||||
notesHeader.setAttribute('aria-expanded', 'true');
|
||||
notesHeader.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).closest('.timestampBtn')) return;
|
||||
toggleDockPane(notesHeader, 'notes_collapsed');
|
||||
});
|
||||
notesHeader.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDockPane(notesHeader, 'notes_collapsed');
|
||||
}
|
||||
});
|
||||
notesHeader.tabIndex = 0;
|
||||
}
|
||||
if (infoHeader) {
|
||||
infoHeader.style.cursor = 'pointer';
|
||||
infoHeader.setAttribute('aria-expanded', 'true');
|
||||
infoHeader.addEventListener('click', () => {
|
||||
toggleDockPane(infoHeader, 'info_collapsed');
|
||||
});
|
||||
infoHeader.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDockPane(infoHeader, 'info_collapsed');
|
||||
}
|
||||
});
|
||||
infoHeader.tabIndex = 0;
|
||||
}
|
||||
|
||||
// Restore collapsed state from prefs
|
||||
if (prefs?.notes_collapsed) collapsePane(notesHeader!, true);
|
||||
if (prefs?.info_collapsed) collapsePane(infoHeader!, true);
|
||||
}
|
||||
|
||||
// ---- Exported functions ----
|
||||
@@ -633,3 +706,33 @@ export async function openRecentMenu(): Promise<void> {
|
||||
async function savePrefsPatch(patch: Record<string, unknown>): Promise<void> {
|
||||
await api.setPrefs(patch);
|
||||
}
|
||||
|
||||
function cancelResetConfirm(): void {
|
||||
resetConfirming = false;
|
||||
if (resetConfirmTimer) { clearTimeout(resetConfirmTimer); resetConfirmTimer = null; }
|
||||
resetProgBtn.classList.remove('confirming');
|
||||
const icon = resetProgBtn.querySelector('i');
|
||||
if (icon) icon.className = 'fa-solid fa-clock-rotate-left';
|
||||
resetProgBtn.setAttribute('aria-label', 'Reset progress');
|
||||
}
|
||||
|
||||
function toggleDockPane(header: HTMLElement, prefKey: string): void {
|
||||
const pane = header.closest('.dockPane');
|
||||
if (!pane) return;
|
||||
const isCollapsed = pane.classList.toggle('collapsed');
|
||||
header.setAttribute('aria-expanded', String(!isCollapsed));
|
||||
const chevron = header.querySelector('.dockChevron') as HTMLElement | null;
|
||||
if (chevron) chevron.style.transform = isCollapsed ? 'rotate(-90deg)' : '';
|
||||
savePrefsPatch({ [prefKey]: isCollapsed });
|
||||
}
|
||||
|
||||
function collapsePane(header: HTMLElement, collapsed: boolean): void {
|
||||
const pane = header.closest('.dockPane');
|
||||
if (!pane) return;
|
||||
if (collapsed) {
|
||||
pane.classList.add('collapsed');
|
||||
header.setAttribute('aria-expanded', 'false');
|
||||
const chevron = header.querySelector('.dockChevron') as HTMLElement | null;
|
||||
if (chevron) chevron.style.transform = 'rotate(-90deg)';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user