refactor(templates): simplify watch page using extracted components
This commit is contained in:
@@ -1,52 +1,16 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"mal/internal/jikan"
|
"mal/integrations/jikan"
|
||||||
"mal/internal/shared/ui"
|
"mal/web/components"
|
||||||
"net/url"
|
"mal/web/components/ui"
|
||||||
"strconv"
|
"mal/web/components/watch"
|
||||||
|
"mal/web/components/watchlist"
|
||||||
|
"mal/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WatchPageData holds the data needed for the watch page
|
templ WatchPage(anime jikan.Anime, data shared.WatchPageData) {
|
||||||
type WatchPageData struct {
|
|
||||||
MalID int
|
|
||||||
Title string
|
|
||||||
TitleEnglish string
|
|
||||||
TitleJapanese string
|
|
||||||
ImageURL string
|
|
||||||
Airing bool
|
|
||||||
CurrentEpisode string
|
|
||||||
TotalEpisodes int
|
|
||||||
StartTimeSeconds float64
|
|
||||||
CurrentStatus string
|
|
||||||
InitialMode string
|
|
||||||
AvailableModes []string
|
|
||||||
ModeSources map[string]ModeSource
|
|
||||||
Segments []SkipSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModeSource represents a stream source for a specific mode (dub/sub)
|
|
||||||
type ModeSource struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
Subtitles []SubtitleItem `json:"subtitles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubtitleItem represents a subtitle track
|
|
||||||
type SubtitleItem struct {
|
|
||||||
Lang string `json:"lang"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SkipSegment represents a skippable segment (intro/outro)
|
|
||||||
type SkipSegment struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Start float64 `json:"start"`
|
|
||||||
End float64 `json:"end"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ WatchPage(anime jikan.Anime, data WatchPageData) {
|
|
||||||
@Layout(fmt.Sprintf("%s - episode %s", anime.DisplayTitle(), data.CurrentEpisode), true) {
|
@Layout(fmt.Sprintf("%s - episode %s", anime.DisplayTitle(), data.CurrentEpisode), true) {
|
||||||
<div class="w-full overflow-x-clip">
|
<div class="w-full overflow-x-clip">
|
||||||
<div class="mx-auto grid w-full gap-4 lg:gap-5 lg:grid-cols-[220px_minmax(0,1fr)_250px] xl:grid-cols-[240px_minmax(0,1fr)_280px]">
|
<div class="mx-auto grid w-full gap-4 lg:gap-5 lg:grid-cols-[220px_minmax(0,1fr)_250px] xl:grid-cols-[240px_minmax(0,1fr)_280px]">
|
||||||
@@ -61,18 +25,18 @@ templ WatchPage(anime jikan.Anime, data WatchPageData) {
|
|||||||
hx-trigger="load"
|
hx-trigger="load"
|
||||||
class="overflow-y-auto flex-1 [&::-webkit-scrollbar]:hidden"
|
class="overflow-y-auto flex-1 [&::-webkit-scrollbar]:hidden"
|
||||||
>
|
>
|
||||||
@LoadingIndicatorSmall()
|
@ui.LoadingIndicatorSmall()
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main content: Video and Controls -->
|
<!-- Main content: Video and Controls -->
|
||||||
<div class="order-1 flex min-w-0 flex-1 flex-col gap-4 sm:gap-5 lg:order-2">
|
<div class="order-1 flex min-w-0 flex-1 flex-col gap-4 sm:gap-5 lg:order-2">
|
||||||
@VideoPlayer(data)
|
@watch.VideoPlayer(data)
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 sm:justify-end">
|
<div class="flex flex-wrap items-center justify-between gap-2 sm:justify-end">
|
||||||
if canGoPrevEpisode(data.CurrentEpisode) {
|
if shared.CanGoPrevEpisode(data.CurrentEpisode) {
|
||||||
<a
|
<a
|
||||||
href={ templ.URL(episodeWithOffsetURL(anime.MalID, data.CurrentEpisode, -1)) }
|
href={ templ.URL(shared.EpisodeWithOffsetURL(anime.MalID, data.CurrentEpisode, -1)) }
|
||||||
class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) hover:no-underline"
|
class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) hover:no-underline"
|
||||||
>
|
>
|
||||||
◀ Prev
|
◀ Prev
|
||||||
@@ -80,9 +44,9 @@ templ WatchPage(anime jikan.Anime, data WatchPageData) {
|
|||||||
} else {
|
} else {
|
||||||
<span class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text-faint) opacity-50">◀ Prev</span>
|
<span class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text-faint) opacity-50">◀ Prev</span>
|
||||||
}
|
}
|
||||||
if canGoNextEpisode(data.CurrentEpisode, anime.Episodes) {
|
if shared.CanGoNextEpisode(data.CurrentEpisode, anime.Episodes) {
|
||||||
<a
|
<a
|
||||||
href={ templ.URL(episodeWithOffsetURL(anime.MalID, data.CurrentEpisode, 1)) }
|
href={ templ.URL(shared.EpisodeWithOffsetURL(anime.MalID, data.CurrentEpisode, 1)) }
|
||||||
class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) hover:no-underline"
|
class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) hover:no-underline"
|
||||||
>
|
>
|
||||||
Next ▶
|
Next ▶
|
||||||
@@ -91,13 +55,13 @@ templ WatchPage(anime jikan.Anime, data WatchPageData) {
|
|||||||
<span class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text-faint) opacity-50">Next ▶</span>
|
<span class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text-faint) opacity-50">Next ▶</span>
|
||||||
}
|
}
|
||||||
<span id="watch-status-dropdown">
|
<span id="watch-status-dropdown">
|
||||||
@WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), data.CurrentStatus, anime.Airing)
|
@watchlist.WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), data.CurrentStatus, anime.Airing)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<h3 class="mb-3 text-lg font-semibold tracking-wide text-(--text)">Watch more seasons of this anime</h3>
|
<h3 class="mb-3 text-lg font-semibold tracking-wide text-(--text)">Watch more seasons of this anime</h3>
|
||||||
<div hx-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/relations", anime.MalID))) } hx-trigger="load">
|
<div hx-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/relations", anime.MalID))) } hx-trigger="load">
|
||||||
@ui.LoadingIndicator("Loading relations")
|
@components.LoadingIndicator("Loading relations")
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,10 +160,10 @@ templ EpisodeItem(episode jikan.Episode, currentEpisode string, animeID int) {
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ VideoPlayer(data WatchPageData) {
|
templ VideoPlayer(data shared.WatchPageData) {
|
||||||
{{ streamToken := modeToken(data.InitialMode, data.ModeSources) }}
|
{{ streamToken := shared.ModeToken(data.InitialMode, data.ModeSources) }}
|
||||||
{{ hasDub := modeAvailable(data.AvailableModes, "dub") }}
|
{{ hasDub := shared.ModeAvailable(data.AvailableModes, "dub") }}
|
||||||
{{ hasSub := modeAvailable(data.AvailableModes, "sub") }}
|
{{ hasSub := shared.ModeAvailable(data.AvailableModes, "sub") }}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col gap-4"
|
class="flex w-full flex-col gap-4"
|
||||||
data-mal-id={ fmt.Sprintf("%d", data.MalID) }
|
data-mal-id={ fmt.Sprintf("%d", data.MalID) }
|
||||||
@@ -215,9 +179,9 @@ templ VideoPlayer(data WatchPageData) {
|
|||||||
data-start-time-seconds={ fmt.Sprintf("%.3f", data.StartTimeSeconds) }
|
data-start-time-seconds={ fmt.Sprintf("%.3f", data.StartTimeSeconds) }
|
||||||
data-initial-mode={ data.InitialMode }
|
data-initial-mode={ data.InitialMode }
|
||||||
data-stream-token={ streamToken }
|
data-stream-token={ streamToken }
|
||||||
data-available-modes={ toJSON(data.AvailableModes) }
|
data-available-modes={ shared.ToJSON(data.AvailableModes) }
|
||||||
data-mode-sources={ toJSON(data.ModeSources) }
|
data-mode-sources={ shared.ToJSON(data.ModeSources) }
|
||||||
data-segments={ toJSON(data.Segments) }
|
data-segments={ shared.ToJSON(data.Segments) }
|
||||||
>
|
>
|
||||||
<div class="group relative aspect-video w-full overflow-hidden bg-black">
|
<div class="group relative aspect-video w-full overflow-hidden bg-black">
|
||||||
<video
|
<video
|
||||||
@@ -293,7 +257,7 @@ templ VideoPlayer(data WatchPageData) {
|
|||||||
"flex h-9 w-9 items-center justify-center text-white sm:h-10 sm:w-10",
|
"flex h-9 w-9 items-center justify-center text-white sm:h-10 sm:w-10",
|
||||||
templ.KV("opacity-50 cursor-not-allowed", !hasDub),
|
templ.KV("opacity-50 cursor-not-allowed", !hasDub),
|
||||||
}
|
}
|
||||||
title={ modeButtonTitle("Dub", hasDub) }
|
title={ shared.ModeButtonTitle("Dub", hasDub) }
|
||||||
disabled?={ !hasDub }
|
disabled?={ !hasDub }
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
@@ -306,7 +270,7 @@ templ VideoPlayer(data WatchPageData) {
|
|||||||
"flex h-9 w-9 items-center justify-center text-white sm:h-10 sm:w-10",
|
"flex h-9 w-9 items-center justify-center text-white sm:h-10 sm:w-10",
|
||||||
templ.KV("opacity-50 cursor-not-allowed", !hasSub),
|
templ.KV("opacity-50 cursor-not-allowed", !hasSub),
|
||||||
}
|
}
|
||||||
title={ modeButtonTitle("Sub", hasSub) }
|
title={ shared.ModeButtonTitle("Sub", hasSub) }
|
||||||
disabled?={ !hasSub }
|
disabled?={ !hasSub }
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
@@ -342,83 +306,5 @@ func buildStreamURL(mode string, token string) string {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("/watch/proxy/stream?mode=%s&token=%s", url.QueryEscape(mode), url.QueryEscape(token))
|
return fmt.Sprintf("/watch/proxy/stream?mode=%s&token=%s", url.QueryEscape(mode), url.QueryEscape(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
func modeToken(mode string, modeSources map[string]ModeSource) string {
|
|
||||||
normalizedMode := mode
|
|
||||||
if _, ok := modeSources[normalizedMode]; !ok {
|
|
||||||
if _, ok := modeSources["dub"]; ok {
|
|
||||||
normalizedMode = "dub"
|
|
||||||
} else if _, ok := modeSources["sub"]; ok {
|
|
||||||
normalizedMode = "sub"
|
|
||||||
} else {
|
|
||||||
for key := range modeSources {
|
|
||||||
normalizedMode = key
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
source, ok := modeSources[normalizedMode]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return source.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
func toJSON(v interface{}) string {
|
|
||||||
b, _ := json.Marshal(v)
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func episodeWithOffsetURL(animeID int, currentEpisode string, offset int) string {
|
|
||||||
episodeID, err := strconv.Atoi(currentEpisode)
|
|
||||||
if err != nil {
|
|
||||||
episodeID = 1
|
|
||||||
}
|
|
||||||
nextEpisode := episodeID + offset
|
|
||||||
if nextEpisode < 1 {
|
|
||||||
nextEpisode = 1
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("/watch/%d/%d", animeID, nextEpisode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func canGoPrevEpisode(currentEpisode string) bool {
|
|
||||||
episodeID, err := strconv.Atoi(currentEpisode)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return episodeID > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func canGoNextEpisode(currentEpisode string, totalEpisodes int) bool {
|
|
||||||
if totalEpisodes <= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
episodeID, err := strconv.Atoi(currentEpisode)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return episodeID < totalEpisodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func modeAvailable(modes []string, mode string) bool {
|
|
||||||
for _, value := range modes {
|
|
||||||
if value == mode {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func modeButtonTitle(label string, enabled bool) string {
|
|
||||||
if enabled {
|
|
||||||
return label
|
|
||||||
}
|
|
||||||
|
|
||||||
return label + " unavailable for this episode"
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user