style: format static/player/controls.ts
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import { state } from "./state";
|
|
||||||
import { saveProgress } from "./progress";
|
import { saveProgress } from "./progress";
|
||||||
|
import { state } from "./state";
|
||||||
import { safeLocalStorage } from "./storage";
|
import { safeLocalStorage } from "./storage";
|
||||||
|
|
||||||
export const formatTime = (seconds: number): string => {
|
export const formatTime = (seconds: number): string => {
|
||||||
if (!Number.isFinite(seconds) || seconds < 0) return "00:00";
|
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||||
|
return "00:00";
|
||||||
|
}
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** Shows the controls overlay and schedules auto-hide after 2s if playing. */
|
||||||
* Shows the controls overlay and schedules auto-hide after 2s if playing.
|
|
||||||
*/
|
|
||||||
export const showControls = (): void => {
|
export const showControls = (): void => {
|
||||||
state.elements.container.classList.add("show-controls");
|
state.elements.container.classList.add("show-controls");
|
||||||
window.clearTimeout(state.timers.playerControlsTimeout);
|
window.clearTimeout(state.timers.playerControlsTimeout);
|
||||||
@@ -24,7 +24,9 @@ export const showControls = (): void => {
|
|||||||
|
|
||||||
// seek relative to current position
|
// seek relative to current position
|
||||||
export const seekBy = (delta: number): void => {
|
export const seekBy = (delta: number): void => {
|
||||||
if (state.elements.video.duration <= 0) return;
|
if (state.elements.video.duration <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.elements.video.currentTime = Math.max(
|
state.elements.video.currentTime = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(state.elements.video.duration, state.elements.video.currentTime + delta),
|
Math.min(state.elements.video.duration, state.elements.video.currentTime + delta),
|
||||||
@@ -59,7 +61,9 @@ export const toggleMute = (): void => {
|
|||||||
export const setVolume = (value: number): void => {
|
export const setVolume = (value: number): void => {
|
||||||
state.elements.video.volume = Math.max(0, Math.min(1, value));
|
state.elements.video.volume = Math.max(0, Math.min(1, value));
|
||||||
state.elements.video.muted = value === 0;
|
state.elements.video.muted = value === 0;
|
||||||
if (value > 0) state.playback.lastKnownVolume = value;
|
if (value > 0) {
|
||||||
|
state.playback.lastKnownVolume = value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleFullscreen = (): void => {
|
export const toggleFullscreen = (): void => {
|
||||||
@@ -78,40 +82,54 @@ const syncVolumeUI = (): void => {
|
|||||||
volumeRange.value = String(value);
|
volumeRange.value = String(value);
|
||||||
volumeRange.style.setProperty("--volume-percent", `${value}%`);
|
volumeRange.style.setProperty("--volume-percent", `${value}%`);
|
||||||
}
|
}
|
||||||
if (volumeUnderline) volumeUnderline.style.height = `${value}%`;
|
if (volumeUnderline) {
|
||||||
|
volumeUnderline.style.height = `${value}%`;
|
||||||
|
}
|
||||||
updateMuteIcons(state.elements.video.muted || state.elements.video.volume === 0);
|
updateMuteIcons(state.elements.video.muted || state.elements.video.volume === 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const VOLUME_STORAGE_KEY = "player-volume";
|
const VOLUME_STORAGE_KEY = "player-volume";
|
||||||
|
|
||||||
const parseStoredVolume = (raw: string | null): number | null => {
|
const parseStoredVolume = (raw: string | null): number | null => {
|
||||||
if (!raw) return null;
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const v = Number.parseFloat(raw);
|
const v = Number.parseFloat(raw);
|
||||||
if (!Number.isFinite(v)) return null;
|
if (!Number.isFinite(v)) {
|
||||||
if (v < 0 || v > 1) return null;
|
return null;
|
||||||
|
}
|
||||||
|
if (v < 0 || v > 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyStoredVolume = (): void => {
|
const applyStoredVolume = (): void => {
|
||||||
const stored = parseStoredVolume(safeLocalStorage.getItem(VOLUME_STORAGE_KEY));
|
const stored = parseStoredVolume(safeLocalStorage.getItem(VOLUME_STORAGE_KEY));
|
||||||
if (stored === null) return;
|
if (stored === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.elements.video.volume = stored;
|
state.elements.video.volume = stored;
|
||||||
state.elements.video.muted = stored === 0;
|
state.elements.video.muted = stored === 0;
|
||||||
if (stored > 0) state.playback.lastKnownVolume = stored;
|
if (stored > 0) {
|
||||||
|
state.playback.lastKnownVolume = stored;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let volumeSaveTimer: number | undefined;
|
let volumeSaveTimer: number | undefined;
|
||||||
const schedulePersistVolume = (): void => {
|
const schedulePersistVolume = (): void => {
|
||||||
window.clearTimeout(volumeSaveTimer);
|
window.clearTimeout(volumeSaveTimer);
|
||||||
volumeSaveTimer = window.setTimeout(() => {
|
volumeSaveTimer = window.setTimeout(() => {
|
||||||
if (!Number.isFinite(state.elements.video.volume)) return;
|
if (!Number.isFinite(state.elements.video.volume)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const clamped = Math.max(0, Math.min(1, state.elements.video.volume));
|
const clamped = Math.max(0, Math.min(1, state.elements.video.volume));
|
||||||
safeLocalStorage.setItem(VOLUME_STORAGE_KEY, clamped.toFixed(3));
|
safeLocalStorage.setItem(VOLUME_STORAGE_KEY, clamped.toFixed(3));
|
||||||
}, 250);
|
}, 250);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Controls {
|
type Controls = {
|
||||||
playPause: HTMLButtonElement | null;
|
playPause: HTMLButtonElement | null;
|
||||||
muteBtn: HTMLButtonElement | null;
|
muteBtn: HTMLButtonElement | null;
|
||||||
volumePanel: HTMLElement | null;
|
volumePanel: HTMLElement | null;
|
||||||
@@ -127,12 +145,14 @@ interface Controls {
|
|||||||
skipSegmentBtn: HTMLButtonElement | null;
|
skipSegmentBtn: HTMLButtonElement | null;
|
||||||
subtitleText: HTMLElement | null;
|
subtitleText: HTMLElement | null;
|
||||||
autoplayBtn: HTMLInputElement | null;
|
autoplayBtn: HTMLInputElement | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
let controlsCache: Controls | null = null;
|
let controlsCache: Controls | null = null;
|
||||||
|
|
||||||
const getControls = (): Controls => {
|
const getControls = (): Controls => {
|
||||||
if (controlsCache) return controlsCache;
|
if (controlsCache) {
|
||||||
|
return controlsCache;
|
||||||
|
}
|
||||||
const c = state.elements.container;
|
const c = state.elements.container;
|
||||||
controlsCache = {
|
controlsCache = {
|
||||||
playPause: c.querySelector("[data-play-pause]"),
|
playPause: c.querySelector("[data-play-pause]"),
|
||||||
@@ -166,10 +186,7 @@ const updateMuteIcons = (isMuted: boolean): void => {
|
|||||||
iconMuted?.classList.toggle("hidden", !isMuted);
|
iconMuted?.classList.toggle("hidden", !isMuted);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** Binds click handlers to player control buttons. Sets up video event listeners for icon sync. */
|
||||||
* Binds click handlers to player control buttons.
|
|
||||||
* Sets up video event listeners for icon sync.
|
|
||||||
*/
|
|
||||||
export const setupControls = (): void => {
|
export const setupControls = (): void => {
|
||||||
applyStoredVolume();
|
applyStoredVolume();
|
||||||
|
|
||||||
@@ -209,8 +226,12 @@ export const setupControls = (): void => {
|
|||||||
volumeRange?.addEventListener("pointerdown", () => volumePanel?.classList.add("is-dragging"));
|
volumeRange?.addEventListener("pointerdown", () => volumePanel?.classList.add("is-dragging"));
|
||||||
window.addEventListener("pointerup", () => volumePanel?.classList.remove("is-dragging"));
|
window.addEventListener("pointerup", () => volumePanel?.classList.remove("is-dragging"));
|
||||||
|
|
||||||
backwardBtn?.addEventListener("click", () => seekBy(-10));
|
backwardBtn?.addEventListener("click", () => {
|
||||||
forwardBtn?.addEventListener("click", () => seekBy(10));
|
seekBy(-10);
|
||||||
|
});
|
||||||
|
forwardBtn?.addEventListener("click", () => {
|
||||||
|
seekBy(10);
|
||||||
|
});
|
||||||
|
|
||||||
fullscreenBtn?.addEventListener("click", () => {
|
fullscreenBtn?.addEventListener("click", () => {
|
||||||
toggleFullscreen();
|
toggleFullscreen();
|
||||||
@@ -219,16 +240,20 @@ export const setupControls = (): void => {
|
|||||||
|
|
||||||
// skip intro/outro button
|
// skip intro/outro button
|
||||||
skipSegmentBtn?.addEventListener("click", () => {
|
skipSegmentBtn?.addEventListener("click", () => {
|
||||||
if (!state.skip.activeSegment) return;
|
if (!state.skip.activeSegment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.elements.video.currentTime = state.skip.activeSegment.end + 0.01;
|
state.elements.video.currentTime = state.skip.activeSegment.end + 0.01;
|
||||||
showControls();
|
showControls();
|
||||||
});
|
});
|
||||||
|
|
||||||
// fullscreen change handler
|
// fullscreen change handler
|
||||||
document.addEventListener("fullscreenchange", () => {
|
document.addEventListener("fullscreenchange", () => {
|
||||||
state.ui.isFullscreen = !!document.fullscreenElement;
|
state.ui.isFullscreen = Boolean(document.fullscreenElement);
|
||||||
state.elements.container.classList.toggle("fullscreen", state.ui.isFullscreen);
|
state.elements.container.classList.toggle("fullscreen", state.ui.isFullscreen);
|
||||||
if (state.ui.isFullscreen) showControls();
|
if (state.ui.isFullscreen) {
|
||||||
|
showControls();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// icon sync on state changes
|
// icon sync on state changes
|
||||||
|
|||||||
Reference in New Issue
Block a user