203 lines
6.4 KiB
Plaintext
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))
|
|
}
|