chore: format player skip editor

This commit is contained in:
2026-05-28 11:29:43 +02:00
committed by Milas Holsting
parent 95a434cd04
commit ea63544998

View File

@@ -1,40 +1,40 @@
import { state } from '../state'; import { state } from "../state";
import { formatTime, showControls } from '../controls'; import { formatTime, showControls } from "../controls";
import { resolveActiveSegments, renderSegments } from './segments'; import { resolveActiveSegments, renderSegments } from "./segments";
type SkipType = 'op' | 'ed'; type SkipType = "op" | "ed";
const qs = <T extends Element>(root: ParentNode, sel: string): T | null => const qs = <T extends Element>(root: ParentNode, sel: string): T | null =>
root.querySelector(sel) as T | null; root.querySelector(sel) as T | null;
export const setupSegmentEditor = (): void => { export const setupSegmentEditor = (): void => {
const root = document.querySelector('[data-segment-editor-root]') as HTMLElement | null; const root = document.querySelector("[data-segment-editor-root]") as HTMLElement | null;
if (!root) return; if (!root) return;
const toggleBtn = qs<HTMLButtonElement>(root, '[data-segment-editor-toggle]'); const toggleBtn = qs<HTMLButtonElement>(root, "[data-segment-editor-toggle]");
const panel = qs<HTMLElement>(root, '[data-segment-editor]'); const panel = qs<HTMLElement>(root, "[data-segment-editor]");
if (!toggleBtn || !panel) return; if (!toggleBtn || !panel) return;
const closeBtn = qs<HTMLButtonElement>(panel, '[data-segment-editor-close]'); const closeBtn = qs<HTMLButtonElement>(panel, "[data-segment-editor-close]");
const typeValue = qs<HTMLInputElement>(panel, '[data-segment-type-value]'); const typeValue = qs<HTMLInputElement>(panel, "[data-segment-type-value]");
const typeLabel = qs<HTMLElement>(panel, '[data-segment-type-label]'); const typeLabel = qs<HTMLElement>(panel, "[data-segment-type-label]");
const markStartBtn = qs<HTMLButtonElement>(panel, '[data-segment-mark-start]'); const markStartBtn = qs<HTMLButtonElement>(panel, "[data-segment-mark-start]");
const markEndBtn = qs<HTMLButtonElement>(panel, '[data-segment-mark-end]'); const markEndBtn = qs<HTMLButtonElement>(panel, "[data-segment-mark-end]");
const startLabel = qs<HTMLElement>(panel, '[data-segment-start]'); const startLabel = qs<HTMLElement>(panel, "[data-segment-start]");
const endLabel = qs<HTMLElement>(panel, '[data-segment-end]'); const endLabel = qs<HTMLElement>(panel, "[data-segment-end]");
const resetBtn = qs<HTMLButtonElement>(panel, '[data-segment-reset]'); const resetBtn = qs<HTMLButtonElement>(panel, "[data-segment-reset]");
const saveBtn = qs<HTMLButtonElement>(panel, '[data-segment-save]'); const saveBtn = qs<HTMLButtonElement>(panel, "[data-segment-save]");
const errorBox = qs<HTMLElement>(panel, '[data-segment-error]'); const errorBox = qs<HTMLElement>(panel, "[data-segment-error]");
const typeOptions = Array.from( const typeOptions = Array.from(
panel.querySelectorAll('[data-segment-type-option]') panel.querySelectorAll("[data-segment-type-option]"),
) as HTMLButtonElement[]; ) as HTMLButtonElement[];
const focusableSelector = [ const focusableSelector = [
'button:not([disabled])', "button:not([disabled])",
'input:not([disabled])', "input:not([disabled])",
'select:not([disabled])', "select:not([disabled])",
'textarea:not([disabled])', "textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])', '[tabindex]:not([tabindex="-1"])',
].join(','); ].join(",");
let startTime: number | null = null; let startTime: number | null = null;
let endTime: number | null = null; let endTime: number | null = null;
@@ -43,56 +43,58 @@ export const setupSegmentEditor = (): void => {
const setError = (msg: string | null): void => { const setError = (msg: string | null): void => {
if (!errorBox) return; if (!errorBox) return;
if (!msg) { if (!msg) {
errorBox.classList.add('hidden'); errorBox.classList.add("hidden");
errorBox.textContent = ''; errorBox.textContent = "";
return; return;
} }
errorBox.textContent = msg; errorBox.textContent = msg;
errorBox.classList.remove('hidden'); errorBox.classList.remove("hidden");
}; };
const updateLabels = (): void => { const updateLabels = (): void => {
if (startLabel) startLabel.textContent = startTime == null ? '—' : formatTime(startTime); if (startLabel) startLabel.textContent = startTime == null ? "—" : formatTime(startTime);
if (endLabel) endLabel.textContent = endTime == null ? '—' : formatTime(endTime); if (endLabel) endLabel.textContent = endTime == null ? "—" : formatTime(endTime);
}; };
const open = (): void => { const open = (): void => {
lastActiveElement = lastActiveElement =
document.activeElement instanceof HTMLElement ? document.activeElement : null; document.activeElement instanceof HTMLElement ? document.activeElement : null;
panel.classList.remove('hidden'); panel.classList.remove("hidden");
panel.classList.add('flex'); panel.classList.add("flex");
panel.setAttribute('aria-hidden', 'false'); panel.setAttribute("aria-hidden", "false");
setError(null); setError(null);
showControls(); showControls();
const firstFocusable = panel.querySelector(focusableSelector) as HTMLElement | null; const firstFocusable = panel.querySelector(focusableSelector) as HTMLElement | null;
firstFocusable?.focus(); firstFocusable?.focus();
}; };
const close = (): void => { const close = (): void => {
panel.classList.add('hidden'); panel.classList.add("hidden");
panel.classList.remove('flex'); panel.classList.remove("flex");
panel.setAttribute('aria-hidden', 'true'); panel.setAttribute("aria-hidden", "true");
setError(null); setError(null);
lastActiveElement?.focus(); lastActiveElement?.focus();
}; };
toggleBtn.addEventListener('click', () => { toggleBtn.addEventListener("click", () => {
if (panel.classList.contains('hidden')) open(); if (panel.classList.contains("hidden")) open();
else close(); else close();
}); });
closeBtn?.addEventListener('click', close); closeBtn?.addEventListener("click", close);
document.addEventListener('keydown', e => { document.addEventListener("keydown", (e) => {
if (panel.classList.contains('hidden')) return; if (panel.classList.contains("hidden")) return;
if (e.key === 'Escape') { if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
close(); close();
return; return;
} }
if (e.key !== 'Tab') return; if (e.key !== "Tab") return;
const focusables = Array.from(panel.querySelectorAll(focusableSelector)).filter( const focusables = Array.from(panel.querySelectorAll(focusableSelector)).filter(
el => (el) =>
el instanceof HTMLElement && !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden') el instanceof HTMLElement &&
!el.hasAttribute("disabled") &&
!el.getAttribute("aria-hidden"),
) as HTMLElement[]; ) as HTMLElement[];
if (focusables.length === 0) return; if (focusables.length === 0) return;
@@ -114,12 +116,12 @@ export const setupSegmentEditor = (): void => {
}); });
// close when clicking the backdrop outside the modal content // close when clicking the backdrop outside the modal content
document.addEventListener('pointerdown', e => { document.addEventListener("pointerdown", (e) => {
if (panel.classList.contains('hidden')) return; if (panel.classList.contains("hidden")) return;
const target = e.target as Node | null; const target = e.target as Node | null;
if (!target) return; if (!target) return;
if ( if (
(e.target as HTMLElement | null)?.closest('[data-segment-editor] [data-segment-editor-close]') (e.target as HTMLElement | null)?.closest("[data-segment-editor] [data-segment-editor-close]")
) )
return; return;
const content = panel.firstElementChild; const content = panel.firstElementChild;
@@ -128,15 +130,15 @@ export const setupSegmentEditor = (): void => {
}); });
// dropdown type selector (uses ui-dropdown for visuals; we manage the value) // dropdown type selector (uses ui-dropdown for visuals; we manage the value)
typeOptions.forEach(btn => { typeOptions.forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener("click", () => {
const v = (btn.getAttribute('data-value') || 'ed') as SkipType; const v = (btn.getAttribute("data-value") || "ed") as SkipType;
if (typeValue) typeValue.value = v; if (typeValue) typeValue.value = v;
if (typeLabel) typeLabel.textContent = v === 'op' ? 'Opening (OP)' : 'Ending (ED)'; if (typeLabel) typeLabel.textContent = v === "op" ? "Opening (OP)" : "Ending (ED)";
// close dropdown popover if it exists // close dropdown popover if it exists
const dropdown = btn.closest('ui-dropdown'); const dropdown = btn.closest("ui-dropdown");
const content = dropdown?.querySelector('[data-content]') as HTMLElement | null; const content = dropdown?.querySelector("[data-content]") as HTMLElement | null;
content?.classList.add('hidden'); content?.classList.add("hidden");
showControls(); showControls();
}); });
}); });
@@ -147,19 +149,19 @@ export const setupSegmentEditor = (): void => {
setError(null); setError(null);
updateLabels(); updateLabels();
}; };
resetBtn?.addEventListener('click', reset); resetBtn?.addEventListener("click", reset);
markStartBtn?.addEventListener('click', () => { markStartBtn?.addEventListener("click", () => {
startTime = Math.max(0, state.video.currentTime); startTime = Math.max(0, state.video.currentTime);
if (endTime != null && startTime >= endTime) endTime = null; if (endTime != null && startTime >= endTime) endTime = null;
setError(null); setError(null);
updateLabels(); updateLabels();
showControls(); showControls();
}); });
markEndBtn?.addEventListener('click', () => { markEndBtn?.addEventListener("click", () => {
endTime = Math.max(0, state.video.currentTime); endTime = Math.max(0, state.video.currentTime);
if (startTime != null && endTime <= startTime) { if (startTime != null && endTime <= startTime) {
setError('End must be after start.'); setError("End must be after start.");
return; return;
} }
setError(null); setError(null);
@@ -167,24 +169,24 @@ export const setupSegmentEditor = (): void => {
showControls(); showControls();
}); });
saveBtn?.addEventListener('click', async () => { saveBtn?.addEventListener("click", async () => {
const skipType = ((typeValue?.value || 'ed') as SkipType) ?? 'ed'; const skipType = ((typeValue?.value || "ed") as SkipType) ?? "ed";
if (startTime == null || endTime == null) { if (startTime == null || endTime == null) {
setError('Mark start and end first.'); setError("Mark start and end first.");
return; return;
} }
if (endTime <= startTime) { if (endTime <= startTime) {
setError('End must be after start.'); setError("End must be after start.");
return; return;
} }
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.classList.add('opacity-70'); saveBtn.classList.add("opacity-70");
setError(null); setError(null);
try { try {
const res = await fetch('/api/watch/segments', { const res = await fetch("/api/watch/segments", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
mal_id: state.malID, mal_id: state.malID,
episode: Number.parseInt(state.currentEpisode, 10), episode: Number.parseInt(state.currentEpisode, 10),
@@ -194,7 +196,7 @@ export const setupSegmentEditor = (): void => {
}), }),
}); });
if (!res.ok) { if (!res.ok) {
let message = res.status === 401 ? 'Login required.' : 'Failed to save segment.'; let message = res.status === 401 ? "Login required." : "Failed to save segment.";
try { try {
const payload = (await res.json()) as { error?: string }; const payload = (await res.json()) as { error?: string };
if (payload?.error) message = payload.error; if (payload?.error) message = payload.error;
@@ -204,28 +206,28 @@ export const setupSegmentEditor = (): void => {
} }
// Update local segments immediately so UI reflects the saved data. // Update local segments immediately so UI reflects the saved data.
const normalizedType = skipType === 'ed' ? 'ending' : 'opening'; const normalizedType = skipType === "ed" ? "ending" : "opening";
state.parsedSegments = (state.parsedSegments || []).filter(s => { state.parsedSegments = (state.parsedSegments || []).filter((s) => {
const t = (s.type || '').toLowerCase(); const t = (s.type || "").toLowerCase();
if (normalizedType === 'ending') return t !== 'ed' && t !== 'ending' && t !== 'outro'; if (normalizedType === "ending") return t !== "ed" && t !== "ending" && t !== "outro";
return t !== 'op' && t !== 'opening' && t !== 'intro'; return t !== "op" && t !== "opening" && t !== "intro";
}); });
state.parsedSegments.push({ state.parsedSegments.push({
type: normalizedType, type: normalizedType,
start: startTime, start: startTime,
end: endTime, end: endTime,
source: 'override', source: "override",
}); });
resolveActiveSegments(); resolveActiveSegments();
renderSegments(); renderSegments();
window.showToast?.({ message: 'Segment saved.' }); window.showToast?.({ message: "Segment saved." });
close(); close();
} catch { } catch {
setError('Failed to save segment.'); setError("Failed to save segment.");
} finally { } finally {
saveBtn.disabled = false; saveBtn.disabled = false;
saveBtn.classList.remove('opacity-70'); saveBtn.classList.remove("opacity-70");
} }
}); });