style: format static/player/main.ts
This commit is contained in:
@@ -1,46 +1,52 @@
|
|||||||
import { state, initState, showEndState, hideEndState } from "./state";
|
import { onHtmxLoad, onReady } from "../utils";
|
||||||
import { invalidateBounds, updateTimeline } from "./timeline";
|
|
||||||
import { setupControls, showControls } from "./controls";
|
import { setupControls, showControls } from "./controls";
|
||||||
|
import { formatTime } from "./controls";
|
||||||
|
import { goToNextEpisode } from "./episodes/nav";
|
||||||
|
import { setupThumbnails } from "./episodes/thumbnails";
|
||||||
|
import { setupAutoplayButton, updateEpisodeHighlight, switchEpisodeRange } from "./episodes/ui";
|
||||||
import { setupKeyboard } from "./keyboard";
|
import { setupKeyboard } from "./keyboard";
|
||||||
import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles";
|
|
||||||
import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip";
|
|
||||||
import { setupQuality, updateQualityOptions } from "./quality";
|
|
||||||
import {
|
import {
|
||||||
ensurePreferredModeSource,
|
ensurePreferredModeSource,
|
||||||
hydrateAlternateMode,
|
hydrateAlternateMode,
|
||||||
setupMode,
|
setupMode,
|
||||||
updateModeButtons,
|
updateModeButtons,
|
||||||
} from "./mode";
|
} from "./mode";
|
||||||
import { setupAutoplayButton, updateEpisodeHighlight, switchEpisodeRange } from "./episodes/ui";
|
|
||||||
import { goToNextEpisode } from "./episodes/nav";
|
|
||||||
import { resolveActiveSegments, renderSegments } from "./skip/segments";
|
|
||||||
import { setupSegmentEditor } from "./skip/editor";
|
|
||||||
import { setupThumbnails } from "./episodes/thumbnails";
|
|
||||||
import { markEpisodeTransition, saveEndedProgress, setupProgress } from "./progress";
|
import { markEpisodeTransition, saveEndedProgress, setupProgress } from "./progress";
|
||||||
|
import { setupQuality, updateQualityOptions } from "./quality";
|
||||||
|
import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip";
|
||||||
|
import { setupSegmentEditor } from "./skip/editor";
|
||||||
|
import { resolveActiveSegments, renderSegments } from "./skip/segments";
|
||||||
|
import { state, initState, showEndState, hideEndState } from "./state";
|
||||||
import { safeLocalStorage } from "./storage";
|
import { safeLocalStorage } from "./storage";
|
||||||
import { destroyVideoSource, loadVideoSource } from "./video";
|
import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles";
|
||||||
|
import { invalidateBounds, updateTimeline } from "./timeline";
|
||||||
import {
|
import {
|
||||||
absoluteTimeFromDisplay,
|
absoluteTimeFromDisplay,
|
||||||
absoluteTimeFromRatio,
|
absoluteTimeFromRatio,
|
||||||
getBounds,
|
getBounds,
|
||||||
displayTimeFromAbsolute,
|
displayTimeFromAbsolute,
|
||||||
} from "./timeline";
|
} from "./timeline";
|
||||||
import { formatTime } from "./controls";
|
import { destroyVideoSource, loadVideoSource } from "./video";
|
||||||
import { onHtmxLoad, onReady } from "../utils";
|
|
||||||
|
|
||||||
let currentContainer: HTMLElement | null = null;
|
let currentContainer: HTMLElement | null = null;
|
||||||
let cleanup: (() => void) | null = null;
|
let cleanup: (() => void) | null = null;
|
||||||
|
|
||||||
type ClosableDropdown = HTMLElement & { close: () => void };
|
type ClosableDropdown = HTMLElement & { close: () => void };
|
||||||
const isClosableDropdown = (el: Element | null): el is ClosableDropdown => {
|
const isClosableDropdown = (el: Element | null): el is ClosableDropdown => {
|
||||||
if (!el) return false;
|
if (!el) {
|
||||||
if (!(el instanceof HTMLElement)) return false;
|
return false;
|
||||||
|
}
|
||||||
|
if (!(el instanceof HTMLElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const maybe = el as Partial<{ close: unknown }>;
|
const maybe = el as Partial<{ close: unknown }>;
|
||||||
return typeof maybe.close === "function";
|
return typeof maybe.close === "function";
|
||||||
};
|
};
|
||||||
|
|
||||||
const hidePreviewPopover = (): void => {
|
const hidePreviewPopover = (): void => {
|
||||||
if (!state.elements.previewPopover) return;
|
if (!state.elements.previewPopover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.elements.previewPopover.classList.add("hidden");
|
state.elements.previewPopover.classList.add("hidden");
|
||||||
state.elements.previewPopover.classList.add("opacity-0");
|
state.elements.previewPopover.classList.add("opacity-0");
|
||||||
state.elements.previewPopover.classList.remove("opacity-100");
|
state.elements.previewPopover.classList.remove("opacity-100");
|
||||||
@@ -48,7 +54,9 @@ const hidePreviewPopover = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showPreviewPopover = (): void => {
|
const showPreviewPopover = (): void => {
|
||||||
if (!state.elements.previewPopover) return;
|
if (!state.elements.previewPopover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.elements.previewPopover.classList.remove("hidden");
|
state.elements.previewPopover.classList.remove("hidden");
|
||||||
state.elements.previewPopover.classList.remove("opacity-0");
|
state.elements.previewPopover.classList.remove("opacity-0");
|
||||||
state.elements.previewPopover.classList.add("opacity-100");
|
state.elements.previewPopover.classList.add("opacity-100");
|
||||||
@@ -95,8 +103,12 @@ const updatePreviewUI = (ratio: number): void => {
|
|||||||
|
|
||||||
const initPlayer = async (): Promise<void> => {
|
const initPlayer = async (): Promise<void> => {
|
||||||
const container = document.querySelector("[data-video-player]") as HTMLElement | null;
|
const container = document.querySelector("[data-video-player]") as HTMLElement | null;
|
||||||
if (!container) return;
|
if (!container) {
|
||||||
if (container === currentContainer) return;
|
return;
|
||||||
|
}
|
||||||
|
if (container === currentContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
teardownPlayer();
|
teardownPlayer();
|
||||||
|
|
||||||
if (!initState(container)) {
|
if (!initState(container)) {
|
||||||
@@ -105,14 +117,16 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
currentContainer = container;
|
currentContainer = container;
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const { signal } = abortController;
|
||||||
cleanup = null;
|
cleanup = null;
|
||||||
|
|
||||||
const loading = container.querySelector("[data-loading]") as HTMLElement | null;
|
const loading = container.querySelector("[data-loading]") as HTMLElement | null;
|
||||||
const progressWrap = container.querySelector("[data-progress-wrap]") as HTMLElement | null;
|
const progressWrap = container.querySelector("[data-progress-wrap]") as HTMLElement | null;
|
||||||
|
|
||||||
const scrubToPointer = (clientX: number, shouldShowControls: boolean): void => {
|
const scrubToPointer = (clientX: number, shouldShowControls: boolean): void => {
|
||||||
if (!progressWrap) return;
|
if (!progressWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const rect = progressWrap.getBoundingClientRect();
|
const rect = progressWrap.getBoundingClientRect();
|
||||||
state.elements.video.currentTime = absoluteTimeFromRatio(
|
state.elements.video.currentTime = absoluteTimeFromRatio(
|
||||||
Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
|
Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
|
||||||
@@ -159,7 +173,9 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
const resumeAfterModeSwitch = (() => {
|
const resumeAfterModeSwitch = (() => {
|
||||||
try {
|
try {
|
||||||
const raw = sessionStorage.getItem("mal:resume-after-mode-switch");
|
const raw = sessionStorage.getItem("mal:resume-after-mode-switch");
|
||||||
if (raw === null) return null;
|
if (raw === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
sessionStorage.removeItem("mal:resume-after-mode-switch");
|
sessionStorage.removeItem("mal:resume-after-mode-switch");
|
||||||
const parsed = Number(raw);
|
const parsed = Number(raw);
|
||||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||||
@@ -303,13 +319,15 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
"pointerdown",
|
"pointerdown",
|
||||||
(e) => {
|
(e) => {
|
||||||
// ignore right/middle click
|
// ignore right/middle click
|
||||||
if ("button" in e && e.button !== 0) return;
|
if ("button" in e && e.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.ui.isScrubbing = true;
|
state.ui.isScrubbing = true;
|
||||||
try {
|
try {
|
||||||
(e.currentTarget as HTMLElement).setPointerCapture((e as PointerEvent).pointerId);
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error("failed to capture pointer:", e);
|
console.error("failed to capture pointer:", error);
|
||||||
throw e;
|
throw error;
|
||||||
}
|
}
|
||||||
scrubToPointer(e.clientX, true);
|
scrubToPointer(e.clientX, true);
|
||||||
},
|
},
|
||||||
@@ -331,7 +349,9 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
"pointerup",
|
"pointerup",
|
||||||
() => {
|
() => {
|
||||||
// ensure we finish the seek even if no window mousemove fired
|
// ensure we finish the seek even if no window mousemove fired
|
||||||
if (!progressWrap) return;
|
if (!progressWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.ui.isScrubbing = false;
|
state.ui.isScrubbing = false;
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
@@ -341,7 +361,9 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"pointermove",
|
"pointermove",
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!state.ui.isScrubbing || !progressWrap) return;
|
if (!state.ui.isScrubbing || !progressWrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
scrubToPointer(e.clientX, false);
|
scrubToPointer(e.clientX, false);
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
@@ -351,18 +373,30 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(e) => {
|
(e) => {
|
||||||
const target = e.target;
|
const { target } = e;
|
||||||
if (!(target instanceof Element)) return;
|
if (!(target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const anchor = target.closest("a[href]");
|
const anchor = target.closest("a[href]");
|
||||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
if (!(anchor instanceof HTMLAnchorElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = new URL(anchor.href, location.origin);
|
const url = new URL(anchor.href, location.origin);
|
||||||
if (url.origin !== location.origin) return;
|
if (url.origin !== location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parts = url.pathname.split("/").filter(Boolean);
|
const parts = url.pathname.split("/").filter(Boolean);
|
||||||
if (parts[0] !== "anime" || parts[2] !== "watch") return;
|
if (parts[0] !== "anime" || parts[2] !== "watch") {
|
||||||
if (Number.parseInt(parts[1], 10) !== state.episode.malID) return;
|
return;
|
||||||
|
}
|
||||||
|
if (Number.parseInt(parts[1], 10) !== state.episode.malID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const nextEpisode = Number.parseInt(url.searchParams.get("ep") ?? "1", 10);
|
const nextEpisode = Number.parseInt(url.searchParams.get("ep") ?? "1", 10);
|
||||||
const currentEpisode = Number.parseInt(state.episode.current, 10);
|
const currentEpisode = Number.parseInt(state.episode.current, 10);
|
||||||
if (nextEpisode === currentEpisode + 1) markEpisodeTransition(nextEpisode);
|
if (nextEpisode === currentEpisode + 1) {
|
||||||
|
markEpisodeTransition(nextEpisode);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
@@ -380,7 +414,7 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
clearTimeout(searchDebounce);
|
clearTimeout(searchDebounce);
|
||||||
// debounce to avoid excessive range switches while typing
|
// debounce to avoid excessive range switches while typing
|
||||||
searchDebounce = window.setTimeout(() => {
|
searchDebounce = window.setTimeout(() => {
|
||||||
const val = searchInput.value.replace(/\D/g, "");
|
const val = searchInput.value.replaceAll(/\D/g, "");
|
||||||
if (!val) {
|
if (!val) {
|
||||||
// clear: jump to current episode range
|
// clear: jump to current episode range
|
||||||
const cur = Number.parseInt(state.episode.current, 10);
|
const cur = Number.parseInt(state.episode.current, 10);
|
||||||
@@ -389,7 +423,9 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ep = Number.parseInt(val, 10);
|
const ep = Number.parseInt(val, 10);
|
||||||
if (!ep || ep <= 0) return;
|
if (!ep || ep <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const maxEp = state.episode.total > 0 ? state.episode.total : 500;
|
const maxEp = state.episode.total > 0 ? state.episode.total : 500;
|
||||||
const clamped = Math.min(ep, maxEp);
|
const clamped = Math.min(ep, maxEp);
|
||||||
searchInput.value = String(clamped);
|
searchInput.value = String(clamped);
|
||||||
@@ -412,7 +448,9 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
const idx = Number.parseInt((btn as HTMLElement).dataset.rangeIndex ?? "0", 10);
|
const idx = Number.parseInt((btn as HTMLElement).dataset.rangeIndex ?? "0", 10);
|
||||||
switchEpisodeRange(idx);
|
switchEpisodeRange(idx);
|
||||||
const dd = btn.closest("ui-dropdown");
|
const dd = btn.closest("ui-dropdown");
|
||||||
if (isClosableDropdown(dd)) dd.close();
|
if (isClosableDropdown(dd)) {
|
||||||
|
dd.close();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user