From 44ed8b3b49bc172885e4e380d0f923da054f7a94 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 10 May 2026 18:34:38 +0200 Subject: [PATCH] feat: extract quality switching and mode selection --- static/player/mode.ts | 64 ++++++++++++++++++++++++++++++++++++++++ static/player/quality.ts | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 static/player/mode.ts create mode 100644 static/player/quality.ts diff --git a/static/player/mode.ts b/static/player/mode.ts new file mode 100644 index 0000000..b5c0d86 --- /dev/null +++ b/static/player/mode.ts @@ -0,0 +1,64 @@ +import { state } from './state' +import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from './timeline' +import { showControls } from './controls' +import { ModeSource } from './types' + +const streamUrlForMode = (mode: string, quality?: string): string => { + const src = state.modeSources[mode] + if (!src?.token) return '' + let url = `${state.streamURL}?mode=${encodeURIComponent(mode)}&token=${encodeURIComponent(src.token)}` + if (quality && quality !== 'best') url += `&quality=${encodeURIComponent(quality)}` + return url +} + +const loadVideo = (url: string): void => { + if (!url) return + const wasPlaying = !state.video.paused + const prevTime = displayTimeFromAbsolute(state.video.currentTime) + state.video.src = url + state.video.load() + state.pendingSeekTime = prevTime + if (wasPlaying) state.video.play().catch(() => {}) +} + +export const switchMode = (mode: string): void => { + if (!state.availableModes.includes(mode) || mode === state.currentMode) return + state.currentMode = mode + localStorage.setItem('player-audio-mode', mode) + loadVideo(streamUrlForMode(mode, state.container.querySelector('[data-quality-select]')?.value)) + import('./subtitles').then(m => m.updateSubtitleOptions()) + import('./quality').then(m => m.updateQualityOptions()) + import('./mode').then(m => m.updateModeButtons()) +} + +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 m = state.currentMode + + dub?.classList.toggle('text-accent', m === 'dub') + dub?.classList.toggle('text-white', m !== 'dub') + dub?.classList.toggle('opacity-50', !state.availableModes.includes('dub')) + dub?.classList.toggle('cursor-not-allowed', !state.availableModes.includes('dub')) + dub && (dub.disabled = !state.availableModes.includes('dub')) + + sub?.classList.toggle('text-accent', m === 'sub') + sub?.classList.toggle('text-white', m !== 'sub') + sub?.classList.toggle('opacity-50', !state.availableModes.includes('sub')) + sub?.classList.toggle('cursor-not-allowed', !state.availableModes.includes('sub')) + sub && (sub.disabled = !state.availableModes.includes('sub')) +} + +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 + + dub?.addEventListener('click', () => { if (state.availableModes.includes('dub')) { switchMode('dub'); showControls() } }) + sub?.addEventListener('click', () => { if (state.availableModes.includes('sub')) { switchMode('sub'); showControls() } }) + + const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLInputElement | null + autoplayBtn?.addEventListener('change', (e) => { + localStorage.setItem('mal:autoplay-enabled', (e.target as HTMLInputElement).checked ? 'true' : 'false') + showControls() + }) +} diff --git a/static/player/quality.ts b/static/player/quality.ts new file mode 100644 index 0000000..01d641b --- /dev/null +++ b/static/player/quality.ts @@ -0,0 +1,59 @@ +import { state } from './state' +import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from './timeline' + +const streamUrlForMode = (mode: string, quality?: string): string => { + const src = state.modeSources[mode] + if (!src?.token) return '' + let url = `${state.streamURL}?mode=${encodeURIComponent(mode)}&token=${encodeURIComponent(src.token)}` + if (quality && quality !== 'best') url += `&quality=${encodeURIComponent(quality)}` + return url +} + +const loadVideo = (url: string): void => { + if (!url) return + const wasPlaying = !state.video.paused + const prevTime = displayTimeFromAbsolute(state.video.currentTime) + state.video.src = url + state.video.load() + state.pendingSeekTime = prevTime + if (wasPlaying) state.video.play().catch(() => {}) +} + +export const switchQuality = (quality: string): void => { + const url = streamUrlForMode(state.currentMode, quality) + if (!url) return + localStorage.setItem('mal:preferred-quality', quality) + loadVideo(url) +} + +export const updateQualityOptions = (): void => { + const select = state.container.querySelector('[data-quality-select]') as HTMLSelectElement | null + if (!select) return + const qualities = state.modeSources[state.currentMode]?.qualities ?? [] + select.innerHTML = '' + + const best = document.createElement('option') + best.value = 'best' + best.textContent = 'Auto / Best' + select.appendChild(best) + + qualities.forEach(q => { + const opt = document.createElement('option') + opt.value = q + opt.textContent = q + select.appendChild(opt) + }) + + const preferred = localStorage.getItem('mal:preferred-quality') || 'best' + select.value = qualities.includes(preferred) ? preferred : 'best' + + const wrapper = select.parentElement + wrapper?.classList.toggle('hidden', qualities.length === 0) +} + +export const setupQuality = (): void => { + const select = state.container.querySelector('[data-quality-select]') as HTMLSelectElement | null + select?.addEventListener('change', (e) => { + switchQuality((e.target as HTMLSelectElement).value) + }) +}