Files
mal/web/templates/watch.templ

203 lines
6.4 KiB
Plaintext

package templates
import (
"fmt"
"net/url"
"mal/integrations/jikan"
"mal/web/components"
"mal/web/components/ui"
"mal/web/components/watch"
"mal/web/components/watchlist"
"mal/web/shared"
)
templ WatchPage(anime jikan.Anime, data shared.WatchPageData) {
@Layout(fmt.Sprintf("%s - episode %s", anime.DisplayTitle(), data.CurrentEpisode), true) {
<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]">
<!-- Left sidebar: Episodes -->
<aside class="order-2 w-full min-w-0 lg:order-1">
<div class="flex h-full max-h-[320px] flex-col sm:max-h-[420px] lg:max-h-[800px]">
<div class="p-3 flex items-center justify-between">
<h3 class="text-sm font-semibold tracking-wide text-(--text)">Episodes</h3>
</div>
<div
hx-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/episodes?current=%s", anime.MalID, data.CurrentEpisode))) }
hx-trigger="load"
class="overflow-y-auto flex-1 [&::-webkit-scrollbar]:hidden"
>
@ui.LoadingIndicatorSmall()
</div>
</div>
</aside>
<!-- 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">
@watch.VideoPlayer(data)
<div class="flex flex-wrap items-center justify-between gap-2 sm:justify-end">
if shared.CanGoPrevEpisode(data.CurrentEpisode) {
<a
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"
>
◀ Prev
</a>
} else {
<span class="inline-flex h-8 items-center bg-(--panel-soft) px-2 text-xs text-(--text-faint) opacity-50">
◀ Prev
</span>
}
if shared.CanGoNextEpisode(data.CurrentEpisode, anime.Episodes) {
<a
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"
>
Next ▶
</a>
} else {
<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">
@watchlist.WatchlistDropdown(
anime.MalID,
anime.Title,
anime.TitleEnglish,
anime.TitleJapanese,
anime.ImageURL(),
data.CurrentStatus,
anime.Airing,
)
</span>
</div>
<section>
<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"
>
@components.LoadingIndicator("Loading relations")
</div>
</section>
</div>
<!-- Right sidebar: Anime Info -->
<aside class="order-3 w-full min-w-0 flex flex-col gap-4 lg:order-3">
<img
src={ anime.Images.Webp.LargeImageURL }
alt={ anime.Title }
class="mx-auto w-full max-w-sm object-cover shadow-lg lg:max-w-none"
/>
<div>
<h2 class="text-xl font-bold text-(--text)">{ anime.DisplayTitle() }</h2>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-(--text-muted)">
if anime.ShortRating() != "" {
<span>{ anime.ShortRating() }</span>
<span>•</span>
}
<span>HD</span>
<span>•</span>
<span>{ anime.Type }</span>
<span>•</span>
if anime.ShortDuration() != "" {
<span>{ anime.ShortDuration() }</span>
} else {
<span>{ anime.Duration }</span>
}
</div>
<p class="text-sm text-(--text-muted) mt-4 line-clamp-6">
{ anime.Synopsis }
</p>
</div>
<a
href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) }
class="inline-flex h-9 items-center justify-center bg-(--panel-soft) px-4 text-sm font-semibold text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) transition-colors"
>
View detail
</a>
</aside>
</div>
</div>
}
}
templ LoadingIndicatorSmall() {
<div class="flex items-center justify-center py-8">
<div class="h-5 w-5 animate-spin border-2 border-(--panel-soft) border-t-(--accent)"></div>
</div>
}
templ EpisodeList(episodes []jikan.Episode, currentEpisode string, animeID int) {
if len(episodes) == 0 {
<p class="py-4 text-center text-sm text-(--text-muted)">No episodes available</p>
} else {
<div class="flex flex-col">
for _, ep := range episodes {
@EpisodeItem(ep, currentEpisode, animeID)
}
</div>
}
}
templ EpisodeItem(episode jikan.Episode, currentEpisode string, animeID int) {
{{ isCurrent := fmt.Sprintf("%d", episode.MalID) == currentEpisode }}
<a
href={ templ.URL(fmt.Sprintf("/watch/%d/%d", animeID, episode.MalID)) }
class={
"flex items-center gap-3 px-3 py-2.5 text-sm no-underline transition-colors border-b border-(--panel-soft) last:border-0",
templ.KV("bg-white/5 text-white", isCurrent),
templ.KV("text-(--text-muted) hover:bg-white/5 hover:text-(--text)", !isCurrent),
}
>
<span
class={
"flex shrink-0 items-center justify-center font-medium w-6",
templ.KV("text-(--text)", isCurrent),
templ.KV("text-(--text-faint)", !isCurrent),
}
>
{ fmt.Sprintf("%d", episode.MalID) }
</span>
<span class="min-w-0 truncate font-medium">
if episode.Title != "" {
{ episode.Title }
} else {
Episode { fmt.Sprintf("%d", episode.MalID) }
}
</span>
<div class="ml-auto flex items-center gap-2">
if episode.Filler {
<span class="shrink-0 px-1.5 py-0.5 text-[9px] uppercase tracking-wider bg-yellow-900/50 text-yellow-400">
Filler
</span>
}
if episode.Recap {
<span class="shrink-0 px-1.5 py-0.5 text-[9px] uppercase tracking-wider bg-blue-900/50 text-blue-400">
Recap
</span>
}
if isCurrent {
<svg
class="h-4 w-4 shrink-0 text-white"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
}
</div>
</a>
}
func buildStreamURL(mode string, token string) string {
if token == "" {
return ""
}
return fmt.Sprintf("/watch/proxy/stream?mode=%s&token=%s", url.QueryEscape(mode), url.QueryEscape(token))
}