From 8cfce3ab8871462252e5a3d5e7964ccd8c020cb8 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 3 May 2026 15:02:14 +0200 Subject: [PATCH] feat: add episode range dropdown with search and filler/recap indicators --- static/player.ts | 111 +++++++++++++++++++++++++++++++++++++++++ templates/renderer.go | 43 ++++++++++++---- templates/watch.gohtml | 64 +++++++++++++++++++++--- 3 files changed, 201 insertions(+), 17 deletions(-) diff --git a/static/player.ts b/static/player.ts index 64ad528..a04b18f 100644 --- a/static/player.ts +++ b/static/player.ts @@ -1174,6 +1174,117 @@ const initPlayer = (): void => { updateAutoplayButton() showControls() + // Episode search and range dropdown logic + const episodeSearchInput = document.querySelector('[data-episode-search]') as HTMLInputElement | null + const episodeDropdown = document.querySelector('[data-episode-dropdown]') as HTMLElement | null + const episodeGrid = document.querySelector('[data-episode-grid]') as HTMLElement | null + const episodeList = document.querySelector('[data-episode-list]') as HTMLElement | null + let searchDebounceTimer: number | undefined + + const updateEpisodeHighlight = (episodeNum: number): void => { + if (episodeGrid) { + episodeGrid.querySelectorAll('[data-episode-id]').forEach((el) => { + el.classList.remove('ring-2', 'ring-accent', 'text-accent') + const epNum = parseInt(el.getAttribute('data-episode-id') || '0', 10) + const isCurrent = el.classList.contains('bg-accent/20') + if (!isCurrent) { + el.classList.remove('bg-accent/20') + } + }) + const targetEp = episodeGrid.querySelector(`[data-episode-id="${episodeNum}"]`) as HTMLElement | null + if (targetEp) { + targetEp.classList.add('ring-2', 'ring-accent') + targetEp.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + } + if (episodeList) { + episodeList.querySelectorAll('[data-episode-id]').forEach((el) => { + el.classList.remove('ring-2', 'ring-accent') + }) + const targetEp = episodeList.querySelector(`[data-episode-id="${episodeNum}"]`) as HTMLElement | null + if (targetEp) { + targetEp.classList.add('ring-2', 'ring-accent') + targetEp.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + } + } + + const switchEpisodeRange = (rangeIndex: number): void => { + if (!episodeGrid || !episodeDropdown) return + const rangeBtns = episodeDropdown.querySelectorAll('.episode-range-btn') as NodeListOf + const allRanges = Array.from(rangeBtns) + const targetRange = allRanges[rangeIndex] + if (!targetRange) return + const rangeStart = parseInt(targetRange.getAttribute('data-range-start') || '1', 10) + const rangeEnd = parseInt(targetRange.getAttribute('data-range-end') || '100', 10) + const dropdownLabel = episodeDropdown.querySelector('[data-dropdown-label]') as HTMLElement | null + if (dropdownLabel) { + const startStr = rangeStart.toString().padStart(2, '0') + const endStr = rangeEnd.toString().padStart(2, '0') + dropdownLabel.textContent = `${startStr}-${endStr}` + } + episodeGrid.querySelectorAll('[data-episode-id]').forEach((el) => { + const epNum = parseInt(el.getAttribute('data-episode-id') || '0', 10) + if (epNum >= rangeStart && epNum <= rangeEnd) { + el.classList.remove('hidden') + } else { + el.classList.add('hidden') + } + }) + const dropdown = episodeDropdown as unknown as { close: () => void } + if (dropdown.close) dropdown.close() + } + + if (episodeSearchInput) { + episodeSearchInput.addEventListener('input', () => { + clearTimeout(searchDebounceTimer) + searchDebounceTimer = window.setTimeout(() => { + const inputVal = episodeSearchInput.value.replace(/\D/g, '') + if (inputVal === '') { + const currentEpNum = parseInt(currentEpisode, 10) + if (episodeGrid) { + switchEpisodeRange(Math.floor((currentEpNum - 1) / 100)) + episodeGrid.querySelectorAll('[data-episode-id]').forEach((el) => { + el.classList.remove('ring-2', 'ring-accent') + }) + } + if (episodeList) { + episodeList.querySelectorAll('[data-episode-id]').forEach((el) => { + el.classList.remove('ring-2', 'ring-accent') + }) + } + updateEpisodeHighlight(currentEpNum) + return + } + const episodeNum = parseInt(inputVal, 10) + if (Number.isNaN(episodeNum) || episodeNum <= 0) return + const maxEp = totalEpisodes > 0 ? totalEpisodes : 500 + const clampedEp = Math.min(episodeNum, maxEp) + if (episodeGrid) { + const rangeIndex = Math.floor((clampedEp - 1) / 100) + switchEpisodeRange(rangeIndex) + episodeSearchInput.value = clampedEp.toString() + } + updateEpisodeHighlight(clampedEp) + }, 300) + }) + } + + if (episodeDropdown) { + const rangeBtns = episodeDropdown.querySelectorAll('.episode-range-btn') + rangeBtns.forEach((btn) => { + btn.addEventListener('click', () => { + const rangeIndex = parseInt(btn.getAttribute('data-range-index') || '0', 10) + switchEpisodeRange(rangeIndex) + }) + }) + } + + // Initialize grid to show first range + if (episodeGrid && totalEpisodes > 100) { + switchEpisodeRange(0) + } + playerInitialized = true // Fetch thumbnails and metadata in the background diff --git a/templates/renderer.go b/templates/renderer.go index b249b2e..19564b5 100644 --- a/templates/renderer.go +++ b/templates/renderer.go @@ -63,15 +63,40 @@ func GetRenderer() *Renderer { "sub": func(a, b int) int { return a - b }, - "mul": func(a, b float64) float64 { - return a * b - }, - "div": func(a, b float64) float64 { - if b == 0 { - return 0 - } - return a / b - }, +"mul": func(a, b float64) float64 { + return a * b + }, + "imul": func(a, b int) int { + return a * b + }, +"div": func(a, b float64) float64 { + if b == 0 { + return 0 + } + return a / b + }, + "ceilDiv": func(a, b int) int { + if b == 0 { + return 0 + } + return (a + b - 1) / b + }, + "toFloat": func(a int) float64 { + return float64(a) + }, + "seq": func(start, end int) []int { + res := make([]int, 0, end-start) + for i := start; i < end; i++ { + res = append(res, i) + } + return res + }, + "min": func(a, b int) int { + if a < b { + return a + } + return b + }, "percent": func(current, total float64) float64 { if total == 0 { return 0 diff --git a/templates/watch.gohtml b/templates/watch.gohtml index 98cc847..15c12b5 100644 --- a/templates/watch.gohtml +++ b/templates/watch.gohtml @@ -3,6 +3,7 @@ {{$anime := .Anime}} {{$episodes := .Episodes}} {{$currentEpID := .CurrentEpID}} +{{$totalEpisodes := len $episodes}}
@@ -12,21 +13,68 @@
- {{if eq (len $episodes) 0}} + {{if eq $totalEpisodes 0}}

No episodes found

{{else}} -
- {{range $episodes}} - {{$isCurrent := eq (printf "%v" .MalID) $currentEpID}} - - EP{{.MalID}} - {{.Title}} - +
+
+ Episodes + {{if gt $totalEpisodes 100}} + + {{end}} +
+ + {{if gt $totalEpisodes 100}} + +
+ +
+ +
{{end}}
+ + {{if gt $totalEpisodes 100}} +
+ {{range $episodes}} + {{$isCurrent := eq (printf "%v" .MalID) $currentEpID}} + {{$isFiller := .Filler}} + {{$isRecap := .Recap}} + + {{.MalID}} + + {{end}} +
+ {{else}} +
+ {{range $episodes}} + {{$isCurrent := eq (printf "%v" .MalID) $currentEpID}} + {{$isFiller := .Filler}} + {{$isRecap := .Recap}} + + EP{{.MalID}} + {{.Title}} + + {{end}} +
+ {{end}} {{end}}