feat: add prev/next buttons and watchlist dropdown below video player

This commit is contained in:
2026-05-03 15:24:04 +02:00
committed by Mikkel Elvers
parent 126fc3b9e3
commit 80e6894b96
3 changed files with 152 additions and 15 deletions

View File

@@ -74,6 +74,19 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r.Context())
var watchlistIDs []int64
var watchlistStatus string
if user != nil {
watchlist, _ := h.svc.db.GetUserWatchList(r.Context(), user.ID)
watchlistIDs = make([]int64, len(watchlist))
for i, entry := range watchlist {
watchlistIDs[i] = entry.AnimeID
if entry.AnimeID == int64(id) {
watchlistStatus = entry.Status
}
}
}
currentEpID := r.URL.Query().Get("ep")
if currentEpID == "" {
if user != nil {
@@ -152,6 +165,8 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
"User": user,
"CurrentPath": r.URL.Path,
"CurrentEpID": currentEpID,
"WatchlistIDs": watchlistIDs,
"WatchlistStatus": watchlistStatus,
}); err != nil {
log.Printf("render error: %v", err)
}

View File

@@ -9,6 +9,7 @@ import (
"log"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
)
@@ -96,6 +97,21 @@ func GetRenderer() *Renderer {
return a
}
return b
},
"int": func(v any) int {
switch n := v.(type) {
case int:
return n
case int64:
return int(n)
case float64:
return int(n)
case string:
i, _ := strconv.Atoi(n)
return i
default:
return 0
}
},
"percent": func(current, total float64) float64 {
if total == 0 {

View File

@@ -1,15 +1,62 @@
{{define "title"}}Watch {{.Anime.Title}} - MyAnimeList{{end}}
{{define "content"}}
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
{{$anime := .Anime}}
{{$episodes := .Episodes}}
{{$currentEpID := .CurrentEpID}}
{{$totalEpisodes := len $episodes}}
<div class="flex flex-col gap-8 pb-12 lg:flex-row lg:gap-6">
<div class="flex flex-col gap-6 pb-12 lg:flex-row lg:gap-6">
<div class="flex-1 min-w-0">
<div id="video-player-container">
{{template "video_player" dict "WatchData" .WatchData "TotalEpisodes" $anime.Episodes}}
</div>
<div class="flex items-center justify-between mt-4">
<div class="flex gap-2">
{{$prevEp := sub (int $currentEpID) 1}}
{{if ge $prevEp 1}}
<a href="/anime/{{$anime.MalID}}/watch?ep={{$prevEp}}" class="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 text-sm text-neutral-300 transition-colors">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
Prev
</a>
{{end}}
{{$nextEp := add (int $currentEpID) 1}}
{{if le $nextEp $anime.Episodes}}
<a href="/anime/{{$anime.MalID}}/watch?ep={{$nextEp}}" class="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 text-sm text-neutral-300 transition-colors">
Next
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</a>
{{end}}
</div>
<ui-dropdown class="relative block" data-align="right" data-width="min-w-[160px]">
<div data-trigger class="cursor-pointer">
<button type="button" class="bg-white/5 hover:bg-white/10 flex items-center justify-between gap-2 px-4 py-2 text-sm text-neutral-300 transition-colors">
<span id="watchlist-status-display-{{$anime.MalID}}">
{{if .WatchlistStatus}}{{if eq .WatchlistStatus "watching"}}Watching{{else if eq .WatchlistStatus "completed"}}Completed{{else if eq .WatchlistStatus "plan_to_watch"}}Plan to Watch{{else if eq .WatchlistStatus "dropped"}}Dropped{{end}}{{else}}Add to Watchlist{{end}}
</span>
<svg class="w-4 h-4 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 9 6 6 6-6" /></svg>
</button>
</div>
<div data-content class="hidden absolute z-50 min-w-[160px] bg-background-button shadow-2xl right-0 top-full mt-2">
<div class="flex flex-col py-1">
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'watching', 'Watching', this)">
<span class="text-sm text-white">Watching</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'completed', 'Completed', this)">
<span class="text-sm text-white">Completed</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'plan_to_watch', 'Plan to Watch', this)">
<span class="text-sm text-white">Plan to Watch</span>
</button>
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-white/10" onclick="updateWatchlist({{$anime.MalID}}, 'dropped', 'Dropped', this)">
<span class="text-sm text-white">Dropped</span>
</button>
</div>
</div>
</ui-dropdown>
</div>
</div>
<div class="w-full lg:w-80 xl:w-96 shrink-0">
@@ -76,6 +123,65 @@
</div>
{{end}}
{{end}}
<script>
function updateWatchlist(id, status, display, btn) {
fetch('/api/watchlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ animeId: id, status: status })
}).then(res => {
if (res.ok) {
watchlistIds.add(id);
document.getElementById('watchlist-status-display-' + id).textContent = display;
// Update all watchlist icons on the page
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
const button = icon.closest('button')
if (button) {
const malId = button.dataset.malId
if (malId && parseInt(malId) === id) {
button.classList.add('in-watchlist')
}
}
});
// Close dropdown after a small delay to let click event finish
requestAnimationFrame(() => {
const dropdown = btn.closest('ui-dropdown');
if (dropdown) dropdown.close();
});
}
});
}
function removeWatchlist(id) {
fetch('/api/watchlist/' + id, { method: 'DELETE' }).then(res => {
if (res.ok) {
watchlistIds.delete(id);
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
// Update all watchlist icons on the page
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
const button = icon.closest('button')
if (button) {
const malId = button.dataset.malId
if (malId && parseInt(malId) === id) {
button.classList.remove('in-watchlist')
}
}
});
// Close dropdown
const btn = document.getElementById('watchlist-status-display-' + id);
if (btn) {
const dropdown = btn.closest('ui-dropdown');
if (dropdown) dropdown.close();
}
}
});
}
</script>
</div>
</div>
{{end}}