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