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