fix: WCAG AAA contrast compliance, speed menu z-index, custom app icon
- Fix all text colors to meet WCAG 2.2 AAA 7:1 contrast ratios against dark backgrounds (--textMuted, --textDim, hover states across playlist, player, panels, tooltips) - Fix speed menu rendering behind seek bar by correcting z-index stacking context (.controls z-index:10, .miniCtl z-index:3, .seek z-index:2) - Replace default Tauri icons with custom TutorialVault icon across all required sizes (32-512px PNGs, ICO, ICNS, Windows Square logos) - Update README: Fraunces → Bricolage Grotesque font reference - Add collapsible dock pane persistence and keyboard-adjustable dividers
This commit is contained in:
@@ -27,6 +27,7 @@ let recentMenu: HTMLElement;
|
||||
let refreshBtn: HTMLElement;
|
||||
let resetProgBtn: HTMLElement;
|
||||
let notesBox: HTMLTextAreaElement;
|
||||
let notesBackdrop: HTMLElement;
|
||||
let notesSaved: HTMLElement;
|
||||
let nowTitle: HTMLElement;
|
||||
let nowSub: HTMLElement;
|
||||
@@ -93,6 +94,32 @@ export function initUI(): void {
|
||||
resetProgBtn = document.getElementById('resetProgBtn')!;
|
||||
notesBox = document.getElementById('notesBox') as HTMLTextAreaElement;
|
||||
notesSaved = document.getElementById('notesSaved')!;
|
||||
|
||||
// Create notes highlight backdrop for timestamp coloring
|
||||
notesBackdrop = document.createElement('div');
|
||||
notesBackdrop.className = 'notesBackdrop';
|
||||
notesBackdrop.setAttribute('aria-hidden', 'true');
|
||||
notesBox.parentElement!.insertBefore(notesBackdrop, notesBox);
|
||||
|
||||
// Scroll sync between textarea and backdrop
|
||||
notesBox.addEventListener('scroll', () => {
|
||||
notesBackdrop.scrollTop = notesBox.scrollTop;
|
||||
notesBackdrop.scrollLeft = notesBox.scrollLeft;
|
||||
});
|
||||
|
||||
// Forward hover/focus state to backdrop for visual effects
|
||||
notesBox.addEventListener('mouseenter', () => notesBackdrop.classList.add('hover'));
|
||||
notesBox.addEventListener('mouseleave', () => notesBackdrop.classList.remove('hover'));
|
||||
notesBox.addEventListener('focus', () => notesBackdrop.classList.add('focus'));
|
||||
notesBox.addEventListener('blur', () => notesBackdrop.classList.remove('focus'));
|
||||
|
||||
// Show pointer cursor when hovering over timestamps
|
||||
notesBox.addEventListener('mousemove', (e) => {
|
||||
notesBox.style.pointerEvents = 'none';
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
notesBox.style.pointerEvents = '';
|
||||
notesBox.style.cursor = (el && el.classList.contains('tsLink')) ? 'pointer' : '';
|
||||
});
|
||||
nowTitle = document.getElementById('nowTitle')!;
|
||||
nowSub = document.getElementById('nowSub')!;
|
||||
overallBar = document.getElementById('overallBar')!;
|
||||
@@ -348,6 +375,7 @@ export function initUI(): void {
|
||||
|
||||
// --- Notes ---
|
||||
notesBox.addEventListener('input', () => {
|
||||
updateNotesHighlight();
|
||||
if (!library) return;
|
||||
const it = currentItem();
|
||||
if (!it) return;
|
||||
@@ -385,6 +413,37 @@ export function initUI(): void {
|
||||
};
|
||||
}
|
||||
|
||||
// --- Clickable timestamps in notes ---
|
||||
notesBox.addEventListener('click', () => {
|
||||
const pos = notesBox.selectionStart;
|
||||
if (pos === null || pos === undefined) return;
|
||||
// Only act on single click without selection
|
||||
if (notesBox.selectionStart !== notesBox.selectionEnd) return;
|
||||
const text = notesBox.value;
|
||||
// Match [H:MM:SS] or [M:SS] patterns
|
||||
const re = /\[(\d{1,3}):(\d{2})(?::(\d{2}))?\]/g;
|
||||
let match;
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
if (pos >= start && pos <= end) {
|
||||
let totalSeconds: number;
|
||||
if (match[3] !== undefined) {
|
||||
// [H:MM:SS]
|
||||
totalSeconds = parseInt(match[1], 10) * 3600 + parseInt(match[2], 10) * 60 + parseInt(match[3], 10);
|
||||
} else {
|
||||
// [M:SS]
|
||||
totalSeconds = parseInt(match[1], 10) * 60 + parseInt(match[2], 10);
|
||||
}
|
||||
if (player && Number.isFinite(player.duration) && player.duration > 0) {
|
||||
player.currentTime = clamp(totalSeconds, 0, player.duration);
|
||||
cb.notify?.(`Jumped to ${match[0]}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Collapsible dock panes ---
|
||||
const notesHeader = document.getElementById('notesHeader');
|
||||
const infoHeader = document.getElementById('infoHeader');
|
||||
@@ -578,11 +637,12 @@ export async function refreshCurrentVideoMeta(): Promise<void> {
|
||||
|
||||
export async function loadNoteForCurrent(): Promise<void> {
|
||||
const it = currentItem();
|
||||
if (!it) { notesBox.value = ''; return; }
|
||||
if (!it) { notesBox.value = ''; updateNotesHighlight(); return; }
|
||||
try {
|
||||
const res = await api.getNote(it.fid);
|
||||
notesBox.value = (res && res.ok) ? (res.note || '') : '';
|
||||
} catch (_) { notesBox.value = ''; }
|
||||
updateNotesHighlight();
|
||||
}
|
||||
|
||||
export function setOnTopChecked(v: boolean): void { onTopChk.checked = v; }
|
||||
@@ -726,6 +786,21 @@ function toggleDockPane(header: HTMLElement, prefKey: string): void {
|
||||
savePrefsPatch({ [prefKey]: isCollapsed });
|
||||
}
|
||||
|
||||
function updateNotesHighlight(): void {
|
||||
if (!notesBackdrop) return;
|
||||
const text = notesBox.value;
|
||||
const escaped = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
const highlighted = escaped.replace(
|
||||
/\[(\d{1,3}:\d{2}(?::\d{2})?)\]/g,
|
||||
'<span class="tsLink">[$1]</span>'
|
||||
);
|
||||
// Trailing newline matches textarea's extra line space
|
||||
notesBackdrop.innerHTML = highlighted + '\n';
|
||||
}
|
||||
|
||||
function collapsePane(header: HTMLElement, collapsed: boolean): void {
|
||||
const pane = header.closest('.dockPane');
|
||||
if (!pane) return;
|
||||
|
||||
Reference in New Issue
Block a user