chore: format player progress quality keyboard
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user