package templates
import (
"encoding/json"
"fmt"
"mal/internal/jikan"
"mal/internal/shared/ui"
"net/url"
"strconv"
)
// WatchPageData holds the data needed for the watch page
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 {
URL string `json:"url"`
Referer string `json:"referer"`
Subtitles []SubtitleItem `json:"subtitles"`
}
// SubtitleItem represents a subtitle track
type SubtitleItem struct {
Lang string `json:"lang"`
URL string `json:"url"`
Referer string `json:"referer"`
}
// 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) {
@VideoPlayer(data)
if canGoPrevEpisode(data.CurrentEpisode) {
◀ Prev
} else {
◀ Prev
}
if canGoNextEpisode(data.CurrentEpisode, anime.Episodes) {
Next ▶
} else {
Next ▶
}
@WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), data.CurrentStatus, anime.Airing)
Watch more seasons of this anime
@ui.LoadingIndicator("Loading relations")
}
}
templ LoadingIndicatorSmall() {
}
templ EpisodeList(episodes []jikan.Episode, currentEpisode string, animeID int) {
if len(episodes) == 0 {
No episodes available
} else {
for _, ep := range episodes {
@EpisodeItem(ep, currentEpisode, animeID)
}
}
}
templ EpisodeItem(episode jikan.Episode, currentEpisode string, animeID int) {
{{ isCurrent := fmt.Sprintf("%d", episode.MalID) == currentEpisode }}
{ fmt.Sprintf("%d", episode.MalID) }
if episode.Title != "" {
{ episode.Title }
} else {
Episode { fmt.Sprintf("%d", episode.MalID) }
}
if episode.Filler {
Filler
}
if episode.Recap {
Recap
}
if isCurrent {
}
}
templ VideoPlayer(data WatchPageData) {
{{ streamURL := buildStreamURL(data.InitialMode, data.ModeSources) }}
{{ hasDub := modeAvailable(data.AvailableModes, "dub") }}
{{ hasSub := modeAvailable(data.AvailableModes, "sub") }}
}
func buildStreamURL(mode string, modeSources map[string]ModeSource) string {
stateJSON, _ := json.Marshal(modeSources)
return fmt.Sprintf("/watch/proxy/stream?mode=%s&state=%s", url.QueryEscape(mode), url.QueryEscape(string(stateJSON)))
}
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"
}