chore: format player progress quality keyboard

This commit is contained in:
2026-05-28 11:28:46 +02:00
committed by Milas Holsting
parent 2172d32dc6
commit 4c2c54229b
3 changed files with 47 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
import { state } from './state'; import { state } from "./state";
import { absoluteTimeFromRatio, getBounds } from './timeline'; import { absoluteTimeFromRatio, getBounds } from "./timeline";
import { import {
showControls, showControls,
toggleMute, toggleMute,
@@ -7,54 +7,54 @@ import {
toggleFullscreen, toggleFullscreen,
seekBy, seekBy,
setVolume, setVolume,
} from './controls'; } from "./controls";
import { saveProgress } from './progress'; import { saveProgress } from "./progress";
/** /**
* Sets up keyboard shortcuts for player control. * Sets up keyboard shortcuts for player control.
* Ignores input/textarea to allow typing. * Ignores input/textarea to allow typing.
*/ */
export const setupKeyboard = (): void => { export const setupKeyboard = (): void => {
document.addEventListener('keydown', e => { document.addEventListener("keydown", (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
// ignore when typing in form fields // ignore when typing in form fields
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)
return; return;
switch (e.code) { switch (e.code) {
case 'Space': case "Space":
case 'KeyK': case "KeyK":
e.preventDefault(); e.preventDefault();
togglePlayPause(); togglePlayPause();
showControls(); showControls();
void saveProgress(); void saveProgress();
break; break;
case 'ArrowLeft': case "ArrowLeft":
case 'KeyJ': case "KeyJ":
e.preventDefault(); e.preventDefault();
seekBy(-10); seekBy(-10);
break; break;
case 'ArrowRight': case "ArrowRight":
case 'KeyL': case "KeyL":
e.preventDefault(); e.preventDefault();
seekBy(10); seekBy(10);
break; break;
case 'ArrowUp': case "ArrowUp":
e.preventDefault(); e.preventDefault();
setVolume(state.video.volume + 0.05); setVolume(state.video.volume + 0.05);
showControls(); showControls();
break; break;
case 'ArrowDown': case "ArrowDown":
e.preventDefault(); e.preventDefault();
setVolume(state.video.volume - 0.05); setVolume(state.video.volume - 0.05);
showControls(); showControls();
break; break;
case 'KeyM': case "KeyM":
e.preventDefault(); e.preventDefault();
toggleMute(); toggleMute();
showControls(); showControls();
break; break;
case 'KeyF': case "KeyF":
e.preventDefault(); e.preventDefault();
toggleFullscreen(); toggleFullscreen();
showControls(); showControls();

View File

@@ -1,5 +1,5 @@
import { state } from './state'; import { state } from "./state";
import { displayTimeFromAbsolute } from './timeline'; import { displayTimeFromAbsolute } from "./timeline";
// builds JSON payload for progress API // builds JSON payload for progress API
const buildPayload = (episode: number, seconds: number) => const buildPayload = (episode: number, seconds: number) =>
@@ -12,7 +12,7 @@ const buildPayload = (episode: number, seconds: number) =>
// sends progress via beacon (survives page unload) // sends progress via beacon (survives page unload)
const sendBeacon = (payload: string) => { const sendBeacon = (payload: string) => {
if (!navigator.sendBeacon) return false; if (!navigator.sendBeacon) return false;
navigator.sendBeacon('/api/watch-progress', new Blob([payload], { type: 'application/json' })); navigator.sendBeacon("/api/watch-progress", new Blob([payload], { type: "application/json" }));
return true; return true;
}; };
@@ -41,9 +41,9 @@ export const saveProgress = async (): Promise<void> => {
const payload = buildPayload(episode, safeTime); const payload = buildPayload(episode, safeTime);
try { try {
const res = await fetch('/api/watch-progress', { const res = await fetch("/api/watch-progress", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: payload, body: payload,
}); });
if (!res.ok) return; if (!res.ok) return;
@@ -87,9 +87,9 @@ export const markEpisodeTransition = (episodeNumber: number): void => {
const payload = buildPayload(episodeNumber, 0); const payload = buildPayload(episodeNumber, 0);
// beacon falls back to fetch with keepalive // beacon falls back to fetch with keepalive
if (!sendBeacon(payload)) { if (!sendBeacon(payload)) {
fetch('/api/watch-progress', { fetch("/api/watch-progress", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
keepalive: true, keepalive: true,
body: payload, body: payload,
}).catch(() => undefined); }).catch(() => undefined);
@@ -101,25 +101,25 @@ export const markEpisodeTransition = (episodeNumber: number): void => {
*/ */
export const setupProgress = (): void => { export const setupProgress = (): void => {
// periodic save during playback // periodic save during playback
state.video.addEventListener('timeupdate', () => { state.video.addEventListener("timeupdate", () => {
scheduleProgressSave(); scheduleProgressSave();
}); });
// immediate save on pause // immediate save on pause
state.video.addEventListener('pause', () => { state.video.addEventListener("pause", () => {
window.clearTimeout(state.progressSaveTimer); window.clearTimeout(state.progressSaveTimer);
state.progressSaveTimer = undefined; state.progressSaveTimer = undefined;
saveProgress(); saveProgress();
}); });
// save after scrubbing // save after scrubbing
window.addEventListener('mouseup', () => { window.addEventListener("mouseup", () => {
state.isScrubbing = false; state.isScrubbing = false;
saveProgress(); saveProgress();
}); });
// save on page close // save on page close
window.addEventListener('beforeunload', () => { window.addEventListener("beforeunload", () => {
if (state.transitionEpisode !== null || state.completionSent || !state.malID) { if (state.transitionEpisode !== null || state.completionSent || !state.malID) {
return; return;
} }

View File

@@ -1,13 +1,13 @@
import { state } from './state'; import { state } from "./state";
import { displayTimeFromAbsolute } from './timeline'; import { displayTimeFromAbsolute } from "./timeline";
import { safeLocalStorage } from './storage'; import { safeLocalStorage } from "./storage";
// same as mode.ts - could be extracted to shared util // same as mode.ts - could be extracted to shared util
const streamUrlForMode = (mode: string, quality?: string): string => { const streamUrlForMode = (mode: string, quality?: string): string => {
const src = state.modeSources[mode]; const src = state.modeSources[mode];
if (!src?.token) return ''; if (!src?.token) return "";
let url = `${state.streamURL}?mode=${encodeURIComponent(mode)}&token=${encodeURIComponent(src.token)}`; 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; return url;
}; };
@@ -30,7 +30,7 @@ const loadVideo = (url: string): void => {
export const switchQuality = (quality: string): void => { export const switchQuality = (quality: string): void => {
const url = streamUrlForMode(state.currentMode, quality); const url = streamUrlForMode(state.currentMode, quality);
if (!url) return; if (!url) return;
safeLocalStorage.setItem('mal:preferred-quality', quality); safeLocalStorage.setItem("mal:preferred-quality", quality);
loadVideo(url); loadVideo(url);
}; };
@@ -39,38 +39,38 @@ export const switchQuality = (quality: string): void => {
* Shows/hides dropdown based on availability. * Shows/hides dropdown based on availability.
*/ */
export const updateQualityOptions = (): void => { export const updateQualityOptions = (): void => {
const select = state.container.querySelector('[data-quality-select]') as HTMLSelectElement | null; const select = state.container.querySelector("[data-quality-select]") as HTMLSelectElement | null;
if (!select) return; if (!select) return;
const qualities = state.modeSources[state.currentMode]?.qualities ?? []; const qualities = state.modeSources[state.currentMode]?.qualities ?? [];
select.innerHTML = ''; select.innerHTML = "";
const best = document.createElement('option'); const best = document.createElement("option");
best.value = 'best'; best.value = "best";
best.textContent = 'Auto / Best'; best.textContent = "Auto / Best";
select.appendChild(best); select.appendChild(best);
qualities.forEach(q => { qualities.forEach((q) => {
const opt = document.createElement('option'); const opt = document.createElement("option");
opt.value = q; opt.value = q;
opt.textContent = q; opt.textContent = q;
select.appendChild(opt); select.appendChild(opt);
}); });
// restore saved preference // restore saved preference
const preferred = safeLocalStorage.getItem('mal:preferred-quality') || 'best'; const preferred = safeLocalStorage.getItem("mal:preferred-quality") || "best";
select.value = qualities.includes(preferred) ? preferred : 'best'; select.value = qualities.includes(preferred) ? preferred : "best";
// hide if no quality options // hide if no quality options
const wrapper = select.parentElement; const wrapper = select.parentElement;
wrapper?.classList.toggle('hidden', qualities.length === 0); wrapper?.classList.toggle("hidden", qualities.length === 0);
}; };
/** /**
* Binds quality select change handler. * Binds quality select change handler.
*/ */
export const setupQuality = (): void => { export const setupQuality = (): void => {
const select = state.container.querySelector('[data-quality-select]') as HTMLSelectElement | null; const select = state.container.querySelector("[data-quality-select]") as HTMLSelectElement | null;
select?.addEventListener('change', e => { select?.addEventListener("change", (e) => {
switchQuality((e.target as HTMLSelectElement).value); switchQuality((e.target as HTMLSelectElement).value);
}); });
}; };