player: add quality selector and theater mode
This commit is contained in:
199
static/player.ts
199
static/player.ts
@@ -9,6 +9,7 @@ import DOMPurify from 'dompurify'
|
|||||||
interface ModeSource {
|
interface ModeSource {
|
||||||
token: string
|
token: string
|
||||||
subtitles: SubtitleItem[]
|
subtitles: SubtitleItem[]
|
||||||
|
qualities?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubtitleItem {
|
interface SubtitleItem {
|
||||||
@@ -79,9 +80,11 @@ const initPlayer = (): void => {
|
|||||||
const scrubber = container.querySelector('[data-scrubber]') as HTMLElement
|
const scrubber = container.querySelector('[data-scrubber]') as HTMLElement
|
||||||
const segmentsTrack = container.querySelector('[data-segments]') as HTMLElement
|
const segmentsTrack = container.querySelector('[data-segments]') as HTMLElement
|
||||||
const subtitleSelect = container.querySelector('[data-subtitle-select]') as HTMLSelectElement
|
const subtitleSelect = container.querySelector('[data-subtitle-select]') as HTMLSelectElement
|
||||||
|
const qualitySelect = container.querySelector('[data-quality-select]') as HTMLSelectElement
|
||||||
const backwardBtn = container.querySelector('[data-backward]') as HTMLButtonElement
|
const backwardBtn = container.querySelector('[data-backward]') as HTMLButtonElement
|
||||||
const forwardBtn = container.querySelector('[data-forward]') as HTMLButtonElement
|
const forwardBtn = container.querySelector('[data-forward]') as HTMLButtonElement
|
||||||
const fullscreenBtn = container.querySelector('[data-fullscreen]') as HTMLButtonElement
|
const fullscreenBtn = container.querySelector('[data-fullscreen]') as HTMLButtonElement
|
||||||
|
const theaterBtn = container.querySelector('[data-theater]') as HTMLButtonElement
|
||||||
const skipSegmentBtn = container.querySelector('[data-skip]') as HTMLButtonElement
|
const skipSegmentBtn = container.querySelector('[data-skip]') as HTMLButtonElement
|
||||||
const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLButtonElement
|
const autoplayBtn = document.querySelector('[data-autoplay]') as HTMLButtonElement
|
||||||
const subtitleText = container.querySelector('[data-subtitle-text]') as HTMLElement
|
const subtitleText = container.querySelector('[data-subtitle-text]') as HTMLElement
|
||||||
@@ -149,13 +152,17 @@ const initPlayer = (): void => {
|
|||||||
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 videoOverlay = container.querySelector('[data-video-overlay]') as HTMLElement
|
const videoOverlay = container.querySelector('[data-video-overlay]') as HTMLElement
|
||||||
const streamUrlForMode = (mode: string): string => {
|
const streamUrlForMode = (mode: string, quality?: string): string => {
|
||||||
const modeParam = encodeURIComponent(mode)
|
const modeParam = encodeURIComponent(mode)
|
||||||
const modeSource = modeSources[mode]
|
const modeSource = modeSources[mode]
|
||||||
const token = modeSource?.token
|
const token = modeSource?.token
|
||||||
if (!token) return ''
|
if (!token) return ''
|
||||||
const tokenParam = encodeURIComponent(token)
|
const tokenParam = encodeURIComponent(token)
|
||||||
return `${streamURL}?mode=${modeParam}&token=${tokenParam}`
|
let url = `${streamURL}?mode=${modeParam}&token=${tokenParam}`
|
||||||
|
if (quality && quality !== 'best') {
|
||||||
|
url += `&quality=${encodeURIComponent(quality)}`
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitleProxyURL = (track: SubtitleItem): string => {
|
const subtitleProxyURL = (track: SubtitleItem): string => {
|
||||||
@@ -178,6 +185,9 @@ const initPlayer = (): void => {
|
|||||||
const skipLabel = (segmentType: string): string => segmentType === 'ed' ? 'Skip outro' : 'Skip intro'
|
const skipLabel = (segmentType: string): string => segmentType === 'ed' ? 'Skip outro' : 'Skip intro'
|
||||||
|
|
||||||
const isAutoplayEnabled = (): boolean => localStorage.getItem('mal:autoplay-enabled') !== 'false'
|
const isAutoplayEnabled = (): boolean => localStorage.getItem('mal:autoplay-enabled') !== 'false'
|
||||||
|
const isAutoSkipEnabled = (): boolean => localStorage.getItem('mal:autoskip-enabled') === 'true'
|
||||||
|
const isTheaterMode = (): boolean => localStorage.getItem('mal:theater-mode') === 'true'
|
||||||
|
const getPreferredQuality = (): string => localStorage.getItem('mal:preferred-quality') || 'best'
|
||||||
|
|
||||||
const updateAutoplayButton = (): void => {
|
const updateAutoplayButton = (): void => {
|
||||||
if (!autoplayBtn) return
|
if (!autoplayBtn) return
|
||||||
@@ -186,6 +196,12 @@ const initPlayer = (): void => {
|
|||||||
checkbox.checked = enabled
|
checkbox.checked = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateAutoSkipButton = (): void => {
|
||||||
|
const autoSkipBtn = document.querySelector('[data-autoskip]') as HTMLInputElement | null
|
||||||
|
if (!autoSkipBtn) return
|
||||||
|
autoSkipBtn.checked = isAutoSkipEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
const timelineBounds = (): { start: number, end: number, duration: number } => {
|
const timelineBounds = (): { start: number, end: number, duration: number } => {
|
||||||
const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0
|
const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0
|
||||||
|
|
||||||
@@ -291,12 +307,21 @@ const initPlayer = (): void => {
|
|||||||
const activationTime = skipActivationTime(item)
|
const activationTime = skipActivationTime(item)
|
||||||
return currentDisplayTime >= activationTime && currentDisplayTime < item.end
|
return currentDisplayTime >= activationTime && currentDisplayTime < item.end
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!segment) {
|
if (!segment) {
|
||||||
activeSkipSegment = null
|
activeSkipSegment = null
|
||||||
skipSegmentBtn?.classList.add('hidden')
|
skipSegmentBtn?.classList.add('hidden')
|
||||||
skipSegmentBtn?.classList.remove('block')
|
skipSegmentBtn?.classList.remove('block')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-skip logic
|
||||||
|
if (isAutoSkipEnabled() && currentDisplayTime >= segment.start && currentDisplayTime < segment.end) {
|
||||||
|
const target = absoluteTimeFromDisplay(segment.end + 0.01)
|
||||||
|
video.currentTime = target
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
activeSkipSegment = segment
|
activeSkipSegment = segment
|
||||||
if (skipSegmentBtn) {
|
if (skipSegmentBtn) {
|
||||||
skipSegmentBtn.textContent = skipLabel(segment.type)
|
skipSegmentBtn.textContent = skipLabel(segment.type)
|
||||||
@@ -644,6 +669,34 @@ const initPlayer = (): void => {
|
|||||||
hideSubtitleText()
|
hideSubtitleText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateQualityOptions = (): void => {
|
||||||
|
if (!qualitySelect) return
|
||||||
|
const modeSource = modeSources[currentMode]
|
||||||
|
const qualities = modeSource?.qualities || []
|
||||||
|
|
||||||
|
qualitySelect.innerHTML = ''
|
||||||
|
const best = document.createElement('option')
|
||||||
|
best.value = 'best'
|
||||||
|
best.textContent = 'Auto / Best'
|
||||||
|
qualitySelect.appendChild(best)
|
||||||
|
|
||||||
|
qualities.forEach(q => {
|
||||||
|
const option = document.createElement('option')
|
||||||
|
option.value = q
|
||||||
|
option.textContent = q
|
||||||
|
qualitySelect.appendChild(option)
|
||||||
|
})
|
||||||
|
|
||||||
|
const preferred = getPreferredQuality()
|
||||||
|
if (qualities.includes(preferred)) {
|
||||||
|
qualitySelect.value = preferred
|
||||||
|
} else {
|
||||||
|
qualitySelect.value = 'best'
|
||||||
|
}
|
||||||
|
|
||||||
|
qualitySelect.style.display = qualities.length > 0 ? 'block' : 'none'
|
||||||
|
}
|
||||||
|
|
||||||
const modeDub = container.querySelector('[data-mode-dub]') as HTMLButtonElement
|
const modeDub = container.querySelector('[data-mode-dub]') as HTMLButtonElement
|
||||||
const modeSub = container.querySelector('[data-mode-sub]') as HTMLButtonElement
|
const modeSub = container.querySelector('[data-mode-sub]') as HTMLButtonElement
|
||||||
|
|
||||||
@@ -666,7 +719,7 @@ const initPlayer = (): void => {
|
|||||||
|
|
||||||
const switchMode = (mode: string): void => {
|
const switchMode = (mode: string): void => {
|
||||||
if (!availableModes.includes(mode) || mode === currentMode) return
|
if (!availableModes.includes(mode) || mode === currentMode) return
|
||||||
const nextURL = streamUrlForMode(mode)
|
const nextURL = streamUrlForMode(mode, qualitySelect?.value)
|
||||||
if (!nextURL) return
|
if (!nextURL) return
|
||||||
const wasPlaying = video.ended || !video.paused
|
const wasPlaying = video.ended || !video.paused
|
||||||
const previousTime = displayTimeFromAbsolute(video.currentTime)
|
const previousTime = displayTimeFromAbsolute(video.currentTime)
|
||||||
@@ -677,6 +730,7 @@ const initPlayer = (): void => {
|
|||||||
pendingSeekTime = previousTime
|
pendingSeekTime = previousTime
|
||||||
if (wasPlaying) video.play().catch(() => {})
|
if (wasPlaying) video.play().catch(() => {})
|
||||||
updateSubtitleOptions()
|
updateSubtitleOptions()
|
||||||
|
updateQualityOptions()
|
||||||
updateModeButtons(currentMode)
|
updateModeButtons(currentMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,11 +817,43 @@ const initPlayer = (): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleTheaterMode = (): void => {
|
||||||
|
const layout = document.getElementById('watch-layout')
|
||||||
|
if (!layout) return
|
||||||
|
|
||||||
|
const enabled = !isTheaterMode()
|
||||||
|
localStorage.setItem('mal:theater-mode', enabled ? 'true' : 'false')
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
layout.classList.remove('lg:flex-row')
|
||||||
|
layout.classList.add('lg:flex-col')
|
||||||
|
} else {
|
||||||
|
layout.classList.add('lg:flex-row')
|
||||||
|
layout.classList.remove('lg:flex-col')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth scroll back to player
|
||||||
|
container.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchQuality = (quality: string): void => {
|
||||||
|
const nextURL = streamUrlForMode(currentMode, quality)
|
||||||
|
if (!nextURL) return
|
||||||
|
const wasPlaying = video.ended || !video.paused
|
||||||
|
const previousTime = displayTimeFromAbsolute(video.currentTime)
|
||||||
|
hidePreviewPopover()
|
||||||
|
video.src = nextURL
|
||||||
|
video.load()
|
||||||
|
pendingSeekTime = previousTime
|
||||||
|
if (wasPlaying) video.play().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
updateSubtitleOptions()
|
updateSubtitleOptions()
|
||||||
|
updateQualityOptions()
|
||||||
updateModeButtons(currentMode)
|
updateModeButtons(currentMode)
|
||||||
|
|
||||||
const startingURL = streamUrlForMode(currentMode)
|
const startingURL = streamUrlForMode(currentMode, getPreferredQuality())
|
||||||
if (startingURL) {
|
if (startingURL) {
|
||||||
video.src = startingURL
|
video.src = startingURL
|
||||||
} else if (initialStreamToken) {
|
} else if (initialStreamToken) {
|
||||||
@@ -901,8 +987,8 @@ const initPlayer = (): void => {
|
|||||||
completionAttempts = 0
|
completionAttempts = 0
|
||||||
activeSubtitles = []
|
activeSubtitles = []
|
||||||
hideSubtitleText()
|
hideSubtitleText()
|
||||||
|
|
||||||
updateSubtitleOptions()
|
updateSubtitleOptions()
|
||||||
|
updateQualityOptions()
|
||||||
updateModeButtons(currentMode)
|
updateModeButtons(currentMode)
|
||||||
updateVideoOverlay(currentEpisode, data.episode_title || '')
|
updateVideoOverlay(currentEpisode, data.episode_title || '')
|
||||||
|
|
||||||
@@ -1087,6 +1173,11 @@ const initPlayer = (): void => {
|
|||||||
showControls()
|
showControls()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
theaterBtn?.addEventListener('click', () => {
|
||||||
|
toggleTheaterMode()
|
||||||
|
showControls()
|
||||||
|
})
|
||||||
|
|
||||||
skipSegmentBtn?.addEventListener('click', () => {
|
skipSegmentBtn?.addEventListener('click', () => {
|
||||||
if (!activeSkipSegment) return
|
if (!activeSkipSegment) return
|
||||||
|
|
||||||
@@ -1107,6 +1198,15 @@ const initPlayer = (): void => {
|
|||||||
showControls()
|
showControls()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (target.hasAttribute('data-autoskip')) {
|
||||||
|
const isChecked = (target as HTMLInputElement).checked
|
||||||
|
localStorage.setItem('mal:autoskip-enabled', isChecked ? 'true' : 'false')
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
subtitleSelect?.addEventListener('change', async () => {
|
subtitleSelect?.addEventListener('change', async () => {
|
||||||
const selected = subtitleSelect.value
|
const selected = subtitleSelect.value
|
||||||
if (selected === 'none') {
|
if (selected === 'none') {
|
||||||
@@ -1123,6 +1223,13 @@ const initPlayer = (): void => {
|
|||||||
activeSubtitles = await loadSubtitle(track.url)
|
activeSubtitles = await loadSubtitle(track.url)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
qualitySelect?.addEventListener('change', () => {
|
||||||
|
const selected = qualitySelect.value
|
||||||
|
localStorage.setItem('mal:preferred-quality', selected)
|
||||||
|
switchQuality(selected)
|
||||||
|
showControls()
|
||||||
|
})
|
||||||
|
|
||||||
progressWrap?.addEventListener('mousedown', (event) => {
|
progressWrap?.addEventListener('mousedown', (event) => {
|
||||||
isScrubbing = true
|
isScrubbing = true
|
||||||
const rect = progressWrap.getBoundingClientRect()
|
const rect = progressWrap.getBoundingClientRect()
|
||||||
@@ -1181,17 +1288,86 @@ const initPlayer = (): void => {
|
|||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return
|
||||||
if (event.code === 'Space') {
|
|
||||||
|
// Key codes
|
||||||
|
const code = event.code
|
||||||
|
const key = event.key
|
||||||
|
|
||||||
|
// Space or K: Toggle Play/Pause
|
||||||
|
if (code === 'Space' || code === 'KeyK') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
togglePlayPause()
|
togglePlayPause()
|
||||||
|
showControls()
|
||||||
}
|
}
|
||||||
if (event.code === 'ArrowLeft') seekBy(-10)
|
|
||||||
if (event.code === 'ArrowRight') seekBy(10)
|
// ArrowLeft or J: Seek Backward 10s
|
||||||
if (event.code === 'KeyM') video.muted = !video.muted
|
if (code === 'ArrowLeft' || code === 'KeyJ') {
|
||||||
if (event.code === 'KeyF') {
|
event.preventDefault()
|
||||||
|
seekBy(-10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrowRight or L: Seek Forward 10s
|
||||||
|
if (code === 'ArrowRight' || code === 'KeyL') {
|
||||||
|
event.preventDefault()
|
||||||
|
seekBy(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrowUp: Volume Up
|
||||||
|
if (code === 'ArrowUp') {
|
||||||
|
event.preventDefault()
|
||||||
|
const nextVolume = Math.min(1, video.volume + 0.05)
|
||||||
|
video.volume = nextVolume
|
||||||
|
video.muted = false
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrowDown: Volume Down
|
||||||
|
if (code === 'ArrowDown') {
|
||||||
|
event.preventDefault()
|
||||||
|
const nextVolume = Math.max(0, video.volume - 0.05)
|
||||||
|
video.volume = nextVolume
|
||||||
|
video.muted = nextVolume === 0
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyM: Toggle Mute
|
||||||
|
if (code === 'KeyM') {
|
||||||
|
event.preventDefault()
|
||||||
|
if (video.muted || video.volume === 0) {
|
||||||
|
const restoredVolume = lastKnownVolume > 0 ? lastKnownVolume : 1
|
||||||
|
video.muted = false
|
||||||
|
video.volume = restoredVolume
|
||||||
|
} else {
|
||||||
|
lastKnownVolume = video.volume > 0 ? video.volume : lastKnownVolume
|
||||||
|
video.muted = true
|
||||||
|
}
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyF: Toggle Fullscreen
|
||||||
|
if (code === 'KeyF') {
|
||||||
|
event.preventDefault()
|
||||||
toggleFullscreen()
|
toggleFullscreen()
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyT: Toggle Theater Mode
|
||||||
|
if (code === 'KeyT') {
|
||||||
|
event.preventDefault()
|
||||||
|
toggleTheaterMode()
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers 0-9: Jump to percentage (0% to 90%)
|
||||||
|
if (/^\d$/.test(key)) {
|
||||||
|
const bounds = timelineBounds()
|
||||||
|
if (bounds.duration > 0) {
|
||||||
|
event.preventDefault()
|
||||||
|
const percent = parseInt(key, 10) * 10
|
||||||
|
video.currentTime = absoluteTimeFromRatio(percent / 100)
|
||||||
|
showControls()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showControls()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
@@ -1214,6 +1390,7 @@ const initPlayer = (): void => {
|
|||||||
syncVolumeUI()
|
syncVolumeUI()
|
||||||
updateSkipButton(0)
|
updateSkipButton(0)
|
||||||
updateAutoplayButton()
|
updateAutoplayButton()
|
||||||
|
updateAutoSkipButton()
|
||||||
showControls()
|
showControls()
|
||||||
|
|
||||||
// Episode search and range dropdown logic
|
// Episode search and range dropdown logic
|
||||||
|
|||||||
@@ -61,6 +61,13 @@
|
|||||||
<div class="peer-checked:bg-accent peer h-5 w-9 rounded-full bg-white/20 transition-colors after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-all peer-checked:after:translate-x-full"></div>
|
<div class="peer-checked:bg-accent peer h-5 w-9 rounded-full bg-white/20 transition-colors after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-all peer-checked:after:translate-x-full"></div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between px-5 py-2.5">
|
||||||
|
<span class="text-[15px] font-medium text-white">Auto-skip</span>
|
||||||
|
<label class="relative inline-flex cursor-pointer items-center">
|
||||||
|
<input type="checkbox" data-autoskip class="peer sr-only" />
|
||||||
|
<div class="peer-checked:bg-accent peer h-5 w-9 rounded-full bg-white/20 transition-colors after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-all peer-checked:after:translate-x-full"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="my-1 h-px w-full bg-white/10"></div>
|
<div class="my-1 h-px w-full bg-white/10"></div>
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<span class="mb-1 block px-5 text-xs font-medium tracking-wider text-neutral-400 uppercase">Audio / Subtitles</span>
|
<span class="mb-1 block px-5 text-xs font-medium tracking-wider text-neutral-400 uppercase">Audio / Subtitles</span>
|
||||||
@@ -71,7 +78,14 @@
|
|||||||
<button data-mode-sub class="flex items-center justify-between px-5 py-2.5 text-left transition-colors hover:bg-white/10 text-white focus:outline-none">
|
<button data-mode-sub class="flex items-center justify-between px-5 py-2.5 text-left transition-colors hover:bg-white/10 text-white focus:outline-none">
|
||||||
<span class="text-sm font-medium">Japanese (Sub)</span>
|
<span class="text-sm font-medium">Japanese (Sub)</span>
|
||||||
</button>
|
</button>
|
||||||
<select data-subtitle-select class="mt-2 mx-4 bg-black/40 text-white text-xs border border-white/10 px-2 py-1 outline-none hidden"></select>
|
<div class="px-5 py-2 flex flex-col gap-1">
|
||||||
|
<span class="text-[10px] text-neutral-500 uppercase font-bold tracking-widest">Subtitles</span>
|
||||||
|
<select data-subtitle-select class="w-full bg-white/5 text-white text-xs border border-white/10 px-2 py-1.5 outline-none rounded focus:border-accent hidden"></select>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-2 flex flex-col gap-1">
|
||||||
|
<span class="text-[10px] text-neutral-500 uppercase font-bold tracking-widest">Quality</span>
|
||||||
|
<select data-quality-select class="w-full bg-white/5 text-white text-xs border border-white/10 px-2 py-1.5 outline-none rounded focus:border-accent hidden"></select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +95,10 @@
|
|||||||
<button data-fullscreen class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none">
|
<button data-fullscreen class="flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none">
|
||||||
<svg class="size-6 transition-colors duration-200" viewBox="0 0 240 240" aria-hidden="true"><path d="M143.7,53.9c-1.9-1.9-1.3-4,1.4-4.4l50.6-8.4c1.8-0.5,3.7,0.6,4.2,2.4c0.2,0.6,0.2,1.2,0,1.7l-8.4,50.6c-0.4,2.7-2.4,3.4-4.4,1.4l-14.5-14.5l-28.2,28.2l-14.3-14.3l28.2-28.2L143.7,53.9z M44.2,200.9l50.6-8.4c2.7-0.4,3.4-2.4,1.4-4.4l-14.5-14.5l28.2-28.2l-14.3-14.3l-28.2,28.2l-14.5-14.5c-1.9-1.9-4-1.3-4.4,1.4l-8.4,50.6c-0.5,1.8,0.6,3.6,2.4,4.2C43,201,43.6,201,44.2,200.9L44.2,200.9z" fill="currentColor"></path></svg>
|
<svg class="size-6 transition-colors duration-200" viewBox="0 0 240 240" aria-hidden="true"><path d="M143.7,53.9c-1.9-1.9-1.3-4,1.4-4.4l50.6-8.4c1.8-0.5,3.7,0.6,4.2,2.4c0.2,0.6,0.2,1.2,0,1.7l-8.4,50.6c-0.4,2.7-2.4,3.4-4.4,1.4l-14.5-14.5l-28.2,28.2l-14.3-14.3l28.2-28.2L143.7,53.9z M44.2,200.9l50.6-8.4c2.7-0.4,3.4-2.4,1.4-4.4l-14.5-14.5l28.2-28.2l-14.3-14.3l-28.2,28.2l-14.5-14.5c-1.9-1.9-4-1.3-4.4,1.4l-8.4,50.6c-0.5,1.8,0.6,3.6,2.4,4.2C43,201,43.6,201,44.2,200.9L44.2,200.9z" fill="currentColor"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button data-theater class="hidden lg:flex items-center justify-center text-white transition-opacity hover:opacity-80 focus:outline-none" title="Theater Mode">
|
||||||
|
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" /><line x1="2" y1="15" x2="22" y2="15" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{{if or (not $currentEpID) (eq (printf "%v" $currentEpID) "0") (eq (printf "%v" $currentEpID) "")}}{{$currentEpID = "1"}}{{end}}
|
{{if or (not $currentEpID) (eq (printf "%v" $currentEpID) "0") (eq (printf "%v" $currentEpID) "")}}{{$currentEpID = "1"}}{{end}}
|
||||||
{{$totalEpisodes := len $episodes}}
|
{{$totalEpisodes := len $episodes}}
|
||||||
|
|
||||||
<div class="flex flex-col gap-6 pb-12 lg:flex-row lg:gap-6">
|
<div id="watch-layout" class="flex flex-col gap-6 pb-12 lg:flex-row lg:gap-6">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<a href="/anime/{{$anime.MalID}}" class="inline-flex items-center gap-2 text-sm text-neutral-400 hover:text-white transition-colors">
|
<a href="/anime/{{$anime.MalID}}" class="inline-flex items-center gap-2 text-sm text-neutral-400 hover:text-white transition-colors">
|
||||||
|
|||||||
Reference in New Issue
Block a user