From 3384ac1ecc858f0e95fb73ac2490e7267a48022d Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 19 Apr 2026 01:10:50 +0200 Subject: [PATCH] refactor: simplify player interaction logic --- static/player.ts | 163 ++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/static/player.ts b/static/player.ts index 74de089..bd35db7 100644 --- a/static/player.ts +++ b/static/player.ts @@ -93,13 +93,15 @@ const initPlayer = (): void => { let transitionEpisode: number | null = null let completionSent = false let completionAttempts = 0 + const watchProgressURL = '/api/watch-progress' const previewPopover = container.querySelector('[data-preview-popover]') as HTMLElement const previewTime = container.querySelector('[data-preview-time]') as HTMLElement + const encodedModeState = encodeURIComponent(JSON.stringify(modeSources)) const streamUrlForMode = (mode: string): string => { const modeParam = encodeURIComponent(mode) - const stateParam = encodeURIComponent(JSON.stringify(modeSources)) + const stateParam = encodedModeState return `${streamURL}?mode=${modeParam}&state=${stateParam}` } @@ -261,6 +263,24 @@ const initPlayer = (): void => { previewPopover.style.left = `${clampedOffset}px` } + const buildWatchProgressPayload = (episodeNumber: number, timeSeconds: number): string => { + return JSON.stringify({ + mal_id: malID, + episode: episodeNumber, + time_seconds: timeSeconds, + }) + } + + const sendWatchProgressBeacon = (payload: string): boolean => { + if (!navigator.sendBeacon) { + return false + } + + const blob = new Blob([payload], { type: 'application/json' }) + navigator.sendBeacon(watchProgressURL, blob) + return true + } + const saveProgress = async (): Promise => { if (!Number.isInteger(malID) || malID <= 0) return if (!video.duration || !Number.isFinite(video.duration)) return @@ -272,17 +292,15 @@ const initPlayer = (): void => { return } + const payload = buildWatchProgressPayload(episodeNumber, safeTime) + try { - const response = await fetch('/api/watch-progress', { + const response = await fetch(watchProgressURL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - mal_id: malID, - episode: episodeNumber, - time_seconds: safeTime, - }), + body: payload, }) if (!response.ok) return lastSavedProgress = { @@ -326,20 +344,13 @@ const initPlayer = (): void => { if (!Number.isInteger(episodeNumber) || episodeNumber <= 0) return transitionEpisode = episodeNumber + const payload = buildWatchProgressPayload(episodeNumber, 0) - const payload = JSON.stringify({ - mal_id: malID, - episode: episodeNumber, - time_seconds: 0, - }) - - if (navigator.sendBeacon) { - const blob = new Blob([payload], { type: 'application/json' }) - navigator.sendBeacon('/api/watch-progress', blob) + if (sendWatchProgressBeacon(payload)) { return } - fetch('/api/watch-progress', { + fetch(watchProgressURL, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -400,19 +411,22 @@ const initPlayer = (): void => { } } + const hideSubtitleText = (): void => { + if (!subtitleText) return + subtitleText.textContent = '' + subtitleText.classList.remove('block') + subtitleText.classList.add('hidden') + } + const updateSubtitleRender = (currentTime: number): void => { if (!subtitleText) return if (!activeSubtitles.length) { - subtitleText.textContent = '' - subtitleText.classList.remove('block') - subtitleText.classList.add('hidden') + hideSubtitleText() return } const cue = activeSubtitles.find(item => currentTime >= item.start && currentTime <= item.end) if (!cue) { - subtitleText.textContent = '' - subtitleText.classList.remove('block') - subtitleText.classList.add('hidden') + hideSubtitleText() return } subtitleText.textContent = cue.text @@ -437,11 +451,7 @@ const initPlayer = (): void => { }) subtitleSelect.style.display = currentSubtitleTracks.length > 0 ? 'block' : 'none' activeSubtitles = [] - if (subtitleText) { - subtitleText.textContent = '' - subtitleText.classList.remove('block') - subtitleText.classList.add('hidden') - } + hideSubtitleText() } const modeDub = container.querySelector('[data-mode-dub]') as HTMLButtonElement @@ -537,13 +547,29 @@ const initPlayer = (): void => { }, 2000) } + const togglePlayPause = (): void => { + if (video.paused) { + video.play() + return + } + + video.pause() + } + + const toggleFullscreen = (): void => { + if (document.fullscreenElement) { + document.exitFullscreen() + return + } + + container.requestFullscreen() + } + // Initialize updateSubtitleOptions() updateModeButtons(currentMode) if (video) { - video.src = streamUrlForMode(currentMode) - video.addEventListener('loadedmetadata', () => { if (loading) loading.style.display = 'none' resolveActiveSegments() @@ -608,15 +634,15 @@ const initPlayer = (): void => { if (pathParts.length < 4) return const animeID = pathParts[2] - const currentEpisode = Number.parseInt(pathParts[3], 10) - if (Number.isNaN(currentEpisode)) return + const currentEpisodeNumber = Number.parseInt(pathParts[3], 10) + if (Number.isNaN(currentEpisodeNumber)) return - if (Number.isInteger(totalEpisodes) && totalEpisodes > 0 && currentEpisode >= totalEpisodes) { - completeAnime(currentEpisode) + if (Number.isInteger(totalEpisodes) && totalEpisodes > 0 && currentEpisodeNumber >= totalEpisodes) { + completeAnime(currentEpisodeNumber) return } - const nextEpisode = currentEpisode + 1 + const nextEpisode = currentEpisodeNumber + 1 markEpisodeTransition(nextEpisode) const nextUrl = `/watch/${animeID}/${nextEpisode}` @@ -698,20 +724,12 @@ const initPlayer = (): void => { } playPause?.addEventListener('click', () => { - if (video.paused) { - video.play() - } else { - video.pause() - } + togglePlayPause() showControls() }) video.addEventListener('click', () => { - if (video.paused) { - video.play() - } else { - video.pause() - } + togglePlayPause() showControls() }) @@ -774,11 +792,7 @@ const initPlayer = (): void => { forwardBtn?.addEventListener('click', () => seekBy(10)) fullscreenBtn?.addEventListener('click', () => { - if (document.fullscreenElement) { - document.exitFullscreen() - } else { - container.requestFullscreen() - } + toggleFullscreen() showControls() }) @@ -802,11 +816,7 @@ const initPlayer = (): void => { const selected = subtitleSelect.value if (selected === 'none') { activeSubtitles = [] - if (subtitleText) { - subtitleText.textContent = '' - subtitleText.classList.remove('block') - subtitleText.classList.add('hidden') - } + hideSubtitleText() return } const idx = Number(selected) @@ -840,13 +850,19 @@ const initPlayer = (): void => { hidePreviewPopover() }) - container.querySelectorAll('a[href]').forEach((element: Element) => { - element.addEventListener('click', (event: Event) => { - if (!(event.currentTarget instanceof HTMLAnchorElement)) return - const nextEpisode = parseEpisodeFromWatchHref(event.currentTarget.href) - if (nextEpisode === null) return - markEpisodeTransition(nextEpisode) - }) + container.addEventListener('click', (event: Event) => { + const target = event.target + if (!(target instanceof Node)) return + + const targetElement = target instanceof Element ? target : target.parentElement + if (!targetElement) return + + const anchor = targetElement.closest('a[href]') + if (!(anchor instanceof HTMLAnchorElement)) return + + const nextEpisode = parseEpisodeFromWatchHref(anchor.href) + if (nextEpisode === null) return + markEpisodeTransition(nextEpisode) }) window.addEventListener('mouseup', () => { @@ -870,21 +886,13 @@ const initPlayer = (): void => { document.addEventListener('keydown', (event) => { if (event.code === 'Space') { event.preventDefault() - if (video.paused) { - video.play() - } else { - video.pause() - } + togglePlayPause() } if (event.code === 'ArrowLeft') seekBy(-10) if (event.code === 'ArrowRight') seekBy(10) if (event.code === 'KeyM') video.muted = !video.muted if (event.code === 'KeyF') { - if (document.fullscreenElement) { - document.exitFullscreen() - } else { - container.requestFullscreen() - } + toggleFullscreen() } showControls() }) @@ -897,15 +905,8 @@ const initPlayer = (): void => { const episodeNumber = Number.parseInt(currentEpisode, 10) if (!Number.isInteger(episodeNumber) || episodeNumber <= 0) return const safeTime = Math.max(0, Math.min(video.currentTime, video.duration)) - const payload = JSON.stringify({ - mal_id: malID, - episode: episodeNumber, - time_seconds: safeTime, - }) - if (navigator.sendBeacon) { - const blob = new Blob([payload], { type: 'application/json' }) - navigator.sendBeacon('/api/watch-progress', blob) - } + const payload = buildWatchProgressPayload(episodeNumber, safeTime) + sendWatchProgressBeacon(payload) }) updatePlayPauseIcons(false)