chore: format player mode and state
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { state } from './state';
|
||||
import { displayTimeFromAbsolute } from './timeline';
|
||||
import { showControls } from './controls';
|
||||
import { updateSubtitleOptions } from './subtitles';
|
||||
import { updateQualityOptions } from './quality';
|
||||
import { safeLocalStorage } from './storage';
|
||||
import { state } from "./state";
|
||||
import { displayTimeFromAbsolute } from "./timeline";
|
||||
import { showControls } from "./controls";
|
||||
import { updateSubtitleOptions } from "./subtitles";
|
||||
import { updateQualityOptions } from "./quality";
|
||||
import { safeLocalStorage } from "./storage";
|
||||
|
||||
// builds stream URL with mode, token, and optional quality param
|
||||
const streamUrlForMode = (mode: string, quality?: string): string => {
|
||||
const src = state.modeSources[mode];
|
||||
if (!src?.token) return '';
|
||||
if (!src?.token) return "";
|
||||
let url = `${state.streamURL}?mode=${encodeURIComponent(mode)}&token=${encodeURIComponent(src.token)}`;
|
||||
if (quality && quality !== 'best') url += `&quality=${encodeURIComponent(quality)}`;
|
||||
if (quality && quality !== "best") url += `&quality=${encodeURIComponent(quality)}`;
|
||||
return url;
|
||||
};
|
||||
|
||||
@@ -34,9 +34,9 @@ const loadVideo = (url: string): void => {
|
||||
export const switchMode = (mode: string): void => {
|
||||
if (!state.availableModes.includes(mode) || mode === state.currentMode) return;
|
||||
state.currentMode = mode;
|
||||
safeLocalStorage.setItem('player-audio-mode', mode);
|
||||
safeLocalStorage.setItem("player-audio-mode", mode);
|
||||
const qualitySelect = state.container.querySelector(
|
||||
'[data-quality-select]'
|
||||
"[data-quality-select]",
|
||||
) as HTMLSelectElement | null;
|
||||
loadVideo(streamUrlForMode(mode, qualitySelect?.value));
|
||||
updateSubtitleOptions();
|
||||
@@ -49,24 +49,24 @@ export const switchMode = (mode: string): void => {
|
||||
* Disables unavailable modes.
|
||||
*/
|
||||
export const updateModeButtons = (): void => {
|
||||
const dub = state.container.querySelector('[data-mode-dub]') as HTMLButtonElement | null;
|
||||
const sub = state.container.querySelector('[data-mode-sub]') as HTMLButtonElement | null;
|
||||
const dub = state.container.querySelector("[data-mode-dub]") as HTMLButtonElement | null;
|
||||
const sub = state.container.querySelector("[data-mode-sub]") as HTMLButtonElement | null;
|
||||
const m = state.currentMode;
|
||||
|
||||
dub?.classList.toggle('text-accent', m === 'dub');
|
||||
dub?.classList.toggle('text-foreground', m !== 'dub');
|
||||
dub?.classList.toggle('opacity-50', !state.availableModes.includes('dub'));
|
||||
dub?.classList.toggle('cursor-not-allowed', !state.availableModes.includes('dub'));
|
||||
dub?.classList.toggle("text-accent", m === "dub");
|
||||
dub?.classList.toggle("text-foreground", m !== "dub");
|
||||
dub?.classList.toggle("opacity-50", !state.availableModes.includes("dub"));
|
||||
dub?.classList.toggle("cursor-not-allowed", !state.availableModes.includes("dub"));
|
||||
if (dub) {
|
||||
dub.disabled = !state.availableModes.includes('dub');
|
||||
dub.disabled = !state.availableModes.includes("dub");
|
||||
}
|
||||
|
||||
sub?.classList.toggle('text-accent', m === 'sub');
|
||||
sub?.classList.toggle('text-foreground', m !== 'sub');
|
||||
sub?.classList.toggle('opacity-50', !state.availableModes.includes('sub'));
|
||||
sub?.classList.toggle('cursor-not-allowed', !state.availableModes.includes('sub'));
|
||||
sub?.classList.toggle("text-accent", m === "sub");
|
||||
sub?.classList.toggle("text-foreground", m !== "sub");
|
||||
sub?.classList.toggle("opacity-50", !state.availableModes.includes("sub"));
|
||||
sub?.classList.toggle("cursor-not-allowed", !state.availableModes.includes("sub"));
|
||||
if (sub) {
|
||||
sub.disabled = !state.availableModes.includes('sub');
|
||||
sub.disabled = !state.availableModes.includes("sub");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,27 +74,27 @@ export const updateModeButtons = (): void => {
|
||||
* Binds click handlers for mode buttons and autoplay toggle.
|
||||
*/
|
||||
export const setupMode = (): void => {
|
||||
const dub = state.container.querySelector('[data-mode-dub]') as HTMLButtonElement | null;
|
||||
const sub = state.container.querySelector('[data-mode-sub]') as HTMLButtonElement | null;
|
||||
const dub = state.container.querySelector("[data-mode-dub]") as HTMLButtonElement | null;
|
||||
const sub = state.container.querySelector("[data-mode-sub]") as HTMLButtonElement | null;
|
||||
|
||||
dub?.addEventListener('click', () => {
|
||||
if (state.availableModes.includes('dub')) {
|
||||
switchMode('dub');
|
||||
dub?.addEventListener("click", () => {
|
||||
if (state.availableModes.includes("dub")) {
|
||||
switchMode("dub");
|
||||
showControls();
|
||||
}
|
||||
});
|
||||
sub?.addEventListener('click', () => {
|
||||
if (state.availableModes.includes('sub')) {
|
||||
switchMode('sub');
|
||||
sub?.addEventListener("click", () => {
|
||||
if (state.availableModes.includes("sub")) {
|
||||
switchMode("sub");
|
||||
showControls();
|
||||
}
|
||||
});
|
||||
|
||||
const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLInputElement | null;
|
||||
autoplayBtn?.addEventListener('change', e => {
|
||||
const autoplayBtn = document.querySelector("[data-autoplay]") as HTMLInputElement | null;
|
||||
autoplayBtn?.addEventListener("change", (e) => {
|
||||
safeLocalStorage.setItem(
|
||||
'mal:autoplay-enabled',
|
||||
(e.target as HTMLInputElement).checked ? 'true' : 'false'
|
||||
"mal:autoplay-enabled",
|
||||
(e.target as HTMLInputElement).checked ? "true" : "false",
|
||||
);
|
||||
showControls();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from './types';
|
||||
import { q, qs, dataset } from '../q';
|
||||
import { safeLocalStorage } from './storage';
|
||||
import type { ModeSource, SkipSegment, SubtitleCue, SubtitleTrack, ActiveSegment } from "./types";
|
||||
import { q, qs, dataset } from "../q";
|
||||
import { safeLocalStorage } from "./storage";
|
||||
|
||||
export interface PlayerState {
|
||||
container: HTMLElement;
|
||||
@@ -44,22 +44,22 @@ export interface PlayerState {
|
||||
}
|
||||
|
||||
const createInitialState = (): PlayerState => ({
|
||||
container: document.createElement('div'),
|
||||
video: document.createElement('video'),
|
||||
progress: document.createElement('div'),
|
||||
scrubber: document.createElement('div'),
|
||||
buffered: document.createElement('div'),
|
||||
timeDisplay: document.createElement('div'),
|
||||
durationDisplay: document.createElement('div'),
|
||||
container: document.createElement("div"),
|
||||
video: document.createElement("video"),
|
||||
progress: document.createElement("div"),
|
||||
scrubber: document.createElement("div"),
|
||||
buffered: document.createElement("div"),
|
||||
timeDisplay: document.createElement("div"),
|
||||
durationDisplay: document.createElement("div"),
|
||||
modeSources: {},
|
||||
availableModes: [],
|
||||
currentMode: 'dub',
|
||||
modeSwitchedFrom: '',
|
||||
currentEpisode: '1',
|
||||
currentMode: "dub",
|
||||
modeSwitchedFrom: "",
|
||||
currentEpisode: "1",
|
||||
totalEpisodes: 0,
|
||||
malID: 0,
|
||||
streamURL: '/watch/proxy/stream',
|
||||
initialStreamToken: '',
|
||||
streamURL: "/watch/proxy/stream",
|
||||
initialStreamToken: "",
|
||||
startTimeSeconds: 0,
|
||||
shouldAutoPlay: false,
|
||||
parsedSegments: [],
|
||||
@@ -76,7 +76,7 @@ const createInitialState = (): PlayerState => ({
|
||||
transitionEpisode: null,
|
||||
completionSent: false,
|
||||
completionAttempts: 0,
|
||||
lastSavedProgress: { episode: '1', seconds: -1 },
|
||||
lastSavedProgress: { episode: "1", seconds: -1 },
|
||||
episodeGrid: null,
|
||||
episodeList: null,
|
||||
previewPopover: null,
|
||||
@@ -98,7 +98,7 @@ interface RequiredPlayerElements {
|
||||
const findElement = <T extends Element>(
|
||||
container: HTMLElement,
|
||||
selector: string,
|
||||
elementType: new () => T
|
||||
elementType: new () => T,
|
||||
): T | null => {
|
||||
const element = container.querySelector(selector);
|
||||
if (element instanceof elementType) return element;
|
||||
@@ -106,12 +106,12 @@ const findElement = <T extends Element>(
|
||||
};
|
||||
|
||||
const requiredPlayerElements = (container: HTMLElement): RequiredPlayerElements | null => {
|
||||
const video = findElement(container, 'video', HTMLVideoElement);
|
||||
const progress = findElement(container, '[data-progress]', HTMLElement);
|
||||
const scrubber = findElement(container, '[data-scrubber]', HTMLElement);
|
||||
const buffered = findElement(container, '[data-buffered]', HTMLElement);
|
||||
const timeDisplay = findElement(container, '[data-time]', HTMLElement);
|
||||
const durationDisplay = findElement(container, '[data-duration]', HTMLElement);
|
||||
const video = findElement(container, "video", HTMLVideoElement);
|
||||
const progress = findElement(container, "[data-progress]", HTMLElement);
|
||||
const scrubber = findElement(container, "[data-scrubber]", HTMLElement);
|
||||
const buffered = findElement(container, "[data-buffered]", HTMLElement);
|
||||
const timeDisplay = findElement(container, "[data-time]", HTMLElement);
|
||||
const durationDisplay = findElement(container, "[data-duration]", HTMLElement);
|
||||
|
||||
if (!video || !progress || !scrubber || !buffered || !timeDisplay || !durationDisplay) {
|
||||
return null;
|
||||
@@ -136,43 +136,43 @@ export const initState = (c: HTMLElement): boolean => {
|
||||
state.buffered = elements.buffered;
|
||||
state.timeDisplay = elements.timeDisplay;
|
||||
state.durationDisplay = elements.durationDisplay;
|
||||
state.previewPopover = q<HTMLElement>(c, '[data-preview-popover]');
|
||||
state.previewTime = q<HTMLElement>(c, '[data-preview-time]');
|
||||
state.videoOverlay = q<HTMLElement>(c, '[data-video-overlay]');
|
||||
state.previewPopover = q<HTMLElement>(c, "[data-preview-popover]");
|
||||
state.previewTime = q<HTMLElement>(c, "[data-preview-time]");
|
||||
state.videoOverlay = q<HTMLElement>(c, "[data-video-overlay]");
|
||||
|
||||
// data attributes from server
|
||||
state.malID = Number.parseInt(dataset(c, 'malId'), 10);
|
||||
state.currentEpisode = dataset(c, 'currentEpisode') || '1';
|
||||
state.totalEpisodes = Number.parseInt(dataset(c, 'totalEpisodes'), 10);
|
||||
state.streamURL = dataset(c, 'streamUrl') || '/watch/proxy/stream';
|
||||
state.initialStreamToken = dataset(c, 'streamToken') || '';
|
||||
state.startTimeSeconds = Number.parseFloat(dataset(c, 'startTimeSeconds') || '0') || 0;
|
||||
state.malID = Number.parseInt(dataset(c, "malId"), 10);
|
||||
state.currentEpisode = dataset(c, "currentEpisode") || "1";
|
||||
state.totalEpisodes = Number.parseInt(dataset(c, "totalEpisodes"), 10);
|
||||
state.streamURL = dataset(c, "streamUrl") || "/watch/proxy/stream";
|
||||
state.initialStreamToken = dataset(c, "streamToken") || "";
|
||||
state.startTimeSeconds = Number.parseFloat(dataset(c, "startTimeSeconds") || "0") || 0;
|
||||
// from session: previous page set this when autoplay triggered
|
||||
state.shouldAutoPlay = sessionStorage.getItem('mal:autoplay-next') === 'true';
|
||||
sessionStorage.removeItem('mal:autoplay-next');
|
||||
state.shouldAutoPlay = sessionStorage.getItem("mal:autoplay-next") === "true";
|
||||
sessionStorage.removeItem("mal:autoplay-next");
|
||||
|
||||
// global elements (not inside player container)
|
||||
state.episodeGrid = qs<HTMLElement>('[data-episode-grid]');
|
||||
state.episodeList = qs<HTMLElement>('[data-episode-list]');
|
||||
state.episodeGrid = qs<HTMLElement>("[data-episode-grid]");
|
||||
state.episodeList = qs<HTMLElement>("[data-episode-list]");
|
||||
|
||||
const safeJsonUnknown = (raw: string | undefined): unknown => {
|
||||
try {
|
||||
return JSON.parse(raw ?? '');
|
||||
return JSON.parse(raw ?? "");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
|
||||
const isStringArray = (v: unknown): v is string[] =>
|
||||
Array.isArray(v) && v.every(item => typeof item === 'string');
|
||||
Array.isArray(v) && v.every((item) => typeof item === "string");
|
||||
|
||||
const isSubtitleItemArray = (v: unknown): v is { lang: string; token: string }[] =>
|
||||
Array.isArray(v) &&
|
||||
v.every(
|
||||
item => isRecord(item) && typeof item.lang === 'string' && typeof item.token === 'string'
|
||||
(item) => isRecord(item) && typeof item.lang === "string" && typeof item.token === "string",
|
||||
);
|
||||
|
||||
const parseModeSources = (v: unknown): Record<string, ModeSource> => {
|
||||
@@ -180,7 +180,7 @@ export const initState = (c: HTMLElement): boolean => {
|
||||
const out: Record<string, ModeSource> = {};
|
||||
for (const [key, value] of Object.entries(v)) {
|
||||
if (!isRecord(value)) continue;
|
||||
if (typeof value.token !== 'string' || value.token === '') continue;
|
||||
if (typeof value.token !== "string" || value.token === "") continue;
|
||||
if (!isSubtitleItemArray(value.subtitles)) continue;
|
||||
const qualities = value.qualities;
|
||||
out[key] = {
|
||||
@@ -199,10 +199,10 @@ export const initState = (c: HTMLElement): boolean => {
|
||||
const out: SkipSegment[] = [];
|
||||
for (const item of v) {
|
||||
if (!isRecord(item)) continue;
|
||||
const type = typeof item.type === 'string' ? item.type : '';
|
||||
const start = typeof item.start === 'number' ? item.start : Number(item.start);
|
||||
const end = typeof item.end === 'number' ? item.end : Number(item.end);
|
||||
const source = typeof item.source === 'string' ? item.source : undefined;
|
||||
const type = typeof item.type === "string" ? item.type : "";
|
||||
const start = typeof item.start === "number" ? item.start : Number(item.start);
|
||||
const end = typeof item.end === "number" ? item.end : Number(item.end);
|
||||
const source = typeof item.source === "string" ? item.source : undefined;
|
||||
if (!type || !Number.isFinite(start) || !Number.isFinite(end)) continue;
|
||||
out.push({ type, start, end, source });
|
||||
}
|
||||
@@ -210,25 +210,25 @@ export const initState = (c: HTMLElement): boolean => {
|
||||
};
|
||||
|
||||
// mode sources = { sub: { token, subtitles, qualities }, dub: { ... } }
|
||||
state.modeSources = parseModeSources(safeJsonUnknown(dataset(c, 'modeSources')));
|
||||
state.availableModes = parseAvailableModes(safeJsonUnknown(dataset(c, 'availableModes')));
|
||||
state.modeSources = parseModeSources(safeJsonUnknown(dataset(c, "modeSources")));
|
||||
state.availableModes = parseAvailableModes(safeJsonUnknown(dataset(c, "availableModes")));
|
||||
|
||||
// resolve initial mode: localStorage > backend default > first available > 'dub'
|
||||
const backendInitialMode = dataset(c, 'initialMode') || 'dub';
|
||||
state.modeSwitchedFrom = dataset(c, 'modeSwitchedFrom') || '';
|
||||
const storedMode = safeLocalStorage.getItem('player-audio-mode');
|
||||
const backendInitialMode = dataset(c, "initialMode") || "dub";
|
||||
state.modeSwitchedFrom = dataset(c, "modeSwitchedFrom") || "";
|
||||
const storedMode = safeLocalStorage.getItem("player-audio-mode");
|
||||
const initialMode =
|
||||
storedMode && state.availableModes.includes(storedMode) ? storedMode : backendInitialMode;
|
||||
const fallbackMode = Object.keys(state.modeSources).find(m => state.modeSources[m]?.token);
|
||||
const fallbackMode = Object.keys(state.modeSources).find((m) => state.modeSources[m]?.token);
|
||||
state.currentMode = state.modeSources[initialMode]?.token
|
||||
? initialMode
|
||||
: (fallbackMode ?? state.availableModes[0] ?? 'dub');
|
||||
: (fallbackMode ?? state.availableModes[0] ?? "dub");
|
||||
|
||||
// parse skip segments from data attribute
|
||||
const segments = parseSegments(safeJsonUnknown(dataset(c, 'segments')));
|
||||
const segments = parseSegments(safeJsonUnknown(dataset(c, "segments")));
|
||||
state.parsedSegments = segments
|
||||
.map(s => ({ ...s, start: Number(s.start) || 0, end: Number(s.end) || 0 }))
|
||||
.filter(s => s.end > s.start);
|
||||
.map((s) => ({ ...s, start: Number(s.start) || 0, end: Number(s.end) || 0 }))
|
||||
.filter((s) => s.end > s.start);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user