feat: add prev/next buttons and watchlist dropdown below video player
This commit is contained in:
@@ -74,6 +74,19 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
user := middleware.GetUser(r.Context())
|
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")
|
currentEpID := r.URL.Query().Get("ep")
|
||||||
if currentEpID == "" {
|
if currentEpID == "" {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
@@ -146,12 +159,14 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "watch.gohtml", map[string]any{
|
if err := templates.GetRenderer().ExecuteTemplate(r.Context(), w, "watch.gohtml", map[string]any{
|
||||||
"Anime": anime,
|
"Anime": anime,
|
||||||
"Episodes": allEpisodes,
|
"Episodes": allEpisodes,
|
||||||
"WatchData": watchData,
|
"WatchData": watchData,
|
||||||
"User": user,
|
"User": user,
|
||||||
"CurrentPath": r.URL.Path,
|
"CurrentPath": r.URL.Path,
|
||||||
"CurrentEpID": currentEpID,
|
"CurrentEpID": currentEpID,
|
||||||
|
"WatchlistIDs": watchlistIDs,
|
||||||
|
"WatchlistStatus": watchlistStatus,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("render error: %v", err)
|
log.Printf("render error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -91,12 +92,27 @@ func GetRenderer() *Renderer {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
"min": func(a, b int) int {
|
"min": func(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
return b
|
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 {
|
"percent": func(current, total float64) float64 {
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -1,15 +1,62 @@
|
|||||||
{{define "title"}}Watch {{.Anime.Title}} - MyAnimeList{{end}}
|
{{define "title"}}Watch {{.Anime.Title}} - MyAnimeList{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
{{if .WatchlistIDs}}<script>initWatchlist({{.WatchlistIDs}})</script>{{end}}
|
||||||
{{$anime := .Anime}}
|
{{$anime := .Anime}}
|
||||||
{{$episodes := .Episodes}}
|
{{$episodes := .Episodes}}
|
||||||
{{$currentEpID := .CurrentEpID}}
|
{{$currentEpID := .CurrentEpID}}
|
||||||
{{$totalEpisodes := len $episodes}}
|
{{$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 class="flex-1 min-w-0">
|
||||||
<div id="video-player-container">
|
<div id="video-player-container">
|
||||||
{{template "video_player" dict "WatchData" .WatchData "TotalEpisodes" $anime.Episodes}}
|
{{template "video_player" dict "WatchData" .WatchData "TotalEpisodes" $anime.Episodes}}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="w-full lg:w-80 xl:w-96 shrink-0">
|
<div class="w-full lg:w-80 xl:w-96 shrink-0">
|
||||||
@@ -74,8 +121,67 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{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>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
Reference in New Issue
Block a user