style: format static/player/skip/editor.ts

This commit is contained in:
2026-06-21 02:04:47 +02:00
committed by Milas Holsting
parent 445e37c2d8
commit 4815080ec9

View File

@@ -1,11 +1,9 @@
import { state } from "../state";
import { formatTime, showControls } from "../controls"; import { formatTime, showControls } from "../controls";
import { state } from "../state";
import { resolveActiveSegments, renderSegments } from "./segments"; import { resolveActiveSegments, renderSegments } from "./segments";
type SkipType = "op" | "ed"; type SkipType = "op" | "ed";
type ClosableDropdown = HTMLElement & { type ClosableDropdown = HTMLElement & { close: (options?: { restoreFocus?: boolean }) => void };
close: (options?: { restoreFocus?: boolean }) => void;
};
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;
@@ -15,11 +13,15 @@ const isClosableDropdown = (element: Element | null): element is ClosableDropdow
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]");
@@ -31,9 +33,9 @@ export const setupSegmentEditor = (): void => {
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 = [
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])",
@@ -47,7 +49,9 @@ export const setupSegmentEditor = (): void => {
let lastActiveElement: HTMLElement | null = null; let lastActiveElement: HTMLElement | null = null;
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 = "";
@@ -58,8 +62,12 @@ export const setupSegmentEditor = (): void => {
}; };
const updateLabels = (): void => { const updateLabels = (): void => {
if (startLabel) startLabel.textContent = startTime == null ? "—" : formatTime(startTime); if (startLabel) {
if (endLabel) endLabel.textContent = endTime == null ? "—" : formatTime(endTime); startLabel.textContent = startTime == null ? "—" : formatTime(startTime);
}
if (endLabel) {
endLabel.textContent = endTime == null ? "—" : formatTime(endTime);
}
}; };
const open = (): void => { const open = (): void => {
@@ -96,26 +104,34 @@ export const setupSegmentEditor = (): void => {
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") {
const focusables = Array.from(panel.querySelectorAll(focusableSelector)).filter( return;
}
const focusables = [...panel.querySelectorAll(focusableSelector)].filter(
(el) => (el) =>
el instanceof HTMLElement && el instanceof HTMLElement &&
!el.hasAttribute("disabled") && !el.hasAttribute("disabled") &&
!el.getAttribute("aria-hidden"), !el.getAttribute("aria-hidden"),
) as HTMLElement[]; ) as HTMLElement[];
if (focusables.length === 0) return; if (focusables.length === 0) {
return;
}
const first = focusables[0]; const first = focusables[0];
const last = focusables[focusables.length - 1]; const last = focusables[focusables.length - 1];
const active = document.activeElement; const active = document.activeElement;
if (!(active instanceof HTMLElement)) return; if (!(active instanceof HTMLElement)) {
return;
}
if (e.shiftKey && active === first) { if (e.shiftKey && active === first) {
e.preventDefault(); e.preventDefault();
@@ -131,24 +147,35 @@ 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;
if (content && content.contains(target)) return; if (content && content.contains(target)) {
return;
}
close(); close();
}); });
// 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.dataset.value || "ed") as SkipType;
if (typeValue) typeValue.value = v; if (typeValue) {
if (typeLabel) typeLabel.textContent = v === "op" ? "Opening (OP)" : "Ending (ED)"; typeValue.value = v;
}
if (typeLabel) {
typeLabel.textContent = v === "op" ? "Opening (OP)" : "Ending (ED)";
}
const dropdown = btn.closest("ui-dropdown"); const dropdown = btn.closest("ui-dropdown");
if (isClosableDropdown(dropdown)) { if (isClosableDropdown(dropdown)) {
dropdown.close({ restoreFocus: false }); dropdown.close({ restoreFocus: false });
@@ -167,7 +194,9 @@ export const setupSegmentEditor = (): void => {
markStartBtn?.addEventListener("click", () => { markStartBtn?.addEventListener("click", () => {
startTime = Math.max(0, state.elements.video.currentTime); startTime = Math.max(0, state.elements.video.currentTime);
if (endTime != null && startTime >= endTime) endTime = null; if (endTime != null && startTime >= endTime) {
endTime = null;
}
setError(null); setError(null);
updateLabels(); updateLabels();
showControls(); showControls();
@@ -217,7 +246,9 @@ export const setupSegmentEditor = (): void => {
} catch (error) { } catch (error) {
console.error("failed to parse response json:", error); console.error("failed to parse response json:", error);
} }
if (payload?.error) message = payload.error; if (payload?.error) {
message = payload.error;
}
setError(message); setError(message);
return; return;
} }
@@ -226,7 +257,9 @@ export const setupSegmentEditor = (): void => {
const normalizedType = skipType === "ed" ? "ending" : "opening"; const normalizedType = skipType === "ed" ? "ending" : "opening";
state.skip.parsedSegments = state.skip.parsedSegments.filter((s) => { state.skip.parsedSegments = state.skip.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.skip.parsedSegments.push({ state.skip.parsedSegments.push({