|
|
|
@@ -4,7 +4,9 @@ import (
|
|
|
|
"encoding/json"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"mal/internal/jikan"
|
|
|
|
"mal/internal/jikan"
|
|
|
|
|
|
|
|
"mal/internal/shared/ui"
|
|
|
|
"net/url"
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
"strconv"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// WatchPageData holds the data needed for the watch page
|
|
|
|
// WatchPageData holds the data needed for the watch page
|
|
|
|
@@ -12,6 +14,7 @@ type WatchPageData struct {
|
|
|
|
MalID int
|
|
|
|
MalID int
|
|
|
|
Title string
|
|
|
|
Title string
|
|
|
|
CurrentEpisode string
|
|
|
|
CurrentEpisode string
|
|
|
|
|
|
|
|
CurrentStatus string
|
|
|
|
InitialMode string
|
|
|
|
InitialMode string
|
|
|
|
AvailableModes []string
|
|
|
|
AvailableModes []string
|
|
|
|
ModeSources map[string]ModeSource
|
|
|
|
ModeSources map[string]ModeSource
|
|
|
|
@@ -41,48 +44,95 @@ type SkipSegment struct {
|
|
|
|
|
|
|
|
|
|
|
|
templ WatchPage(anime jikan.Anime, data WatchPageData) {
|
|
|
|
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="grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
|
|
<div class="flex flex-col lg:flex-row gap-5 max-w-full">
|
|
|
|
<div class="grid min-w-0 gap-5">
|
|
|
|
<!-- Left sidebar: Episodes -->
|
|
|
|
@VideoPlayer(data)
|
|
|
|
<aside class="w-full lg:w-[320px] shrink-0">
|
|
|
|
<div class="grid gap-2">
|
|
|
|
<div class="flex flex-col h-full lg:max-h-[800px]">
|
|
|
|
<h1 class="text-lg font-semibold tracking-wide text-(--text)">
|
|
|
|
<div class="p-3 flex items-center justify-between">
|
|
|
|
{ anime.DisplayTitle() }
|
|
|
|
<h3 class="text-sm font-semibold tracking-wide text-(--text)">Episodes</h3>
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
<p class="text-sm text-(--text-muted)">
|
|
|
|
|
|
|
|
Episode { data.CurrentEpisode }
|
|
|
|
|
|
|
|
if anime.Episodes > 0 {
|
|
|
|
|
|
|
|
<span class="text-(--text-faint)"> / { fmt.Sprintf("%d", anime.Episodes) }</span>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
|
|
|
|
|
|
<a
|
|
|
|
|
|
|
|
href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) }
|
|
|
|
|
|
|
|
class="inline-flex h-8 items-center bg-(--panel-soft) px-3 text-xs text-(--text) no-underline hover:bg-(--panel) hover:text-(--text) hover:no-underline"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
← Back to anime
|
|
|
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<aside class="grid gap-4">
|
|
|
|
|
|
|
|
<div class="bg-(--panel) p-3">
|
|
|
|
|
|
|
|
<h3 class="mb-3 text-base 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-get={ string(templ.URL(fmt.Sprintf("/api/anime/%d/episodes?current=%s", anime.MalID, data.CurrentEpisode))) }
|
|
|
|
hx-trigger="load"
|
|
|
|
hx-trigger="load"
|
|
|
|
class="max-h-[600px] overflow-y-auto"
|
|
|
|
class="watch-episodes-scroll overflow-y-auto flex-1"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
@LoadingIndicatorSmall()
|
|
|
|
@LoadingIndicatorSmall()
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</aside>
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main content: Video and Controls -->
|
|
|
|
|
|
|
|
<div class="flex-1 min-w-0 flex flex-col gap-5">
|
|
|
|
|
|
|
|
@VideoPlayer(data)
|
|
|
|
|
|
|
|
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
|
|
|
|
|
|
if canGoPrevEpisode(data.CurrentEpisode) {
|
|
|
|
|
|
|
|
<a
|
|
|
|
|
|
|
|
href={ templ.URL(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 canGoNextEpisode(data.CurrentEpisode, anime.Episodes) {
|
|
|
|
|
|
|
|
<a
|
|
|
|
|
|
|
|
href={ templ.URL(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>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
@WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), data.CurrentStatus, anime.Airing)
|
|
|
|
|
|
|
|
</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">
|
|
|
|
|
|
|
|
@ui.LoadingIndicator("Loading relations")
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Right sidebar: Anime Info -->
|
|
|
|
|
|
|
|
<aside class="w-full lg:w-[320px] shrink-0 flex flex-col gap-4">
|
|
|
|
|
|
|
|
<img src={ anime.Images.Webp.LargeImageURL } alt={ anime.Title } class="w-full object-cover shadow-lg" />
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
|
|
<h2 class="text-xl font-bold text-(--text)">{ anime.DisplayTitle() }</h2>
|
|
|
|
|
|
|
|
<div class="flex items-center gap-2 mt-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() {
|
|
|
|
templ LoadingIndicatorSmall() {
|
|
|
|
<div class="flex items-center justify-center py-8">
|
|
|
|
<div class="flex items-center justify-center py-8">
|
|
|
|
<div class="h-5 w-5 animate-spin rounded-full border-2 border-(--panel-soft) border-t-(--accent)"></div>
|
|
|
|
<div class="h-5 w-5 animate-spin border-2 border-(--panel-soft) border-t-(--accent)"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -90,7 +140,7 @@ templ EpisodeList(episodes []jikan.Episode, currentEpisode string, animeID int)
|
|
|
|
if len(episodes) == 0 {
|
|
|
|
if len(episodes) == 0 {
|
|
|
|
<p class="py-4 text-center text-sm text-(--text-muted)">No episodes available</p>
|
|
|
|
<p class="py-4 text-center text-sm text-(--text-muted)">No episodes available</p>
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
<div class="grid gap-1">
|
|
|
|
<div class="flex flex-col">
|
|
|
|
for _, ep := range episodes {
|
|
|
|
for _, ep := range episodes {
|
|
|
|
@EpisodeItem(ep, currentEpisode, animeID)
|
|
|
|
@EpisodeItem(ep, currentEpisode, animeID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -103,40 +153,45 @@ templ EpisodeItem(episode jikan.Episode, currentEpisode string, animeID int) {
|
|
|
|
<a
|
|
|
|
<a
|
|
|
|
href={ templ.URL(fmt.Sprintf("/watch/%d/%d", animeID, episode.MalID)) }
|
|
|
|
href={ templ.URL(fmt.Sprintf("/watch/%d/%d", animeID, episode.MalID)) }
|
|
|
|
class={
|
|
|
|
class={
|
|
|
|
"flex items-center gap-3 px-2 py-2 text-xs no-underline transition-colors",
|
|
|
|
"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-(--panel-soft) text-(--text)", isCurrent),
|
|
|
|
templ.KV("bg-white/5 text-white", isCurrent),
|
|
|
|
templ.KV("text-(--text-muted) hover:bg-(--panel-soft) hover:text-(--text)", !isCurrent),
|
|
|
|
templ.KV("text-(--text-muted) hover:bg-white/5 hover:text-(--text)", !isCurrent),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<span
|
|
|
|
<span
|
|
|
|
class={
|
|
|
|
class={
|
|
|
|
"flex h-6 w-6 shrink-0 items-center justify-center text-[10px] font-semibold",
|
|
|
|
"flex shrink-0 items-center justify-center font-medium w-6",
|
|
|
|
templ.KV("bg-(--accent) text-white", isCurrent),
|
|
|
|
templ.KV("text-(--text)", isCurrent),
|
|
|
|
templ.KV("bg-(--panel) text-(--text-faint)", !isCurrent),
|
|
|
|
templ.KV("text-(--text-faint)", !isCurrent),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{ fmt.Sprintf("%d", episode.MalID) }
|
|
|
|
{ fmt.Sprintf("%d", episode.MalID) }
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
<span class="min-w-0 truncate">
|
|
|
|
<span class="min-w-0 truncate font-medium">
|
|
|
|
if episode.Title != "" {
|
|
|
|
if episode.Title != "" {
|
|
|
|
{ episode.Title }
|
|
|
|
{ episode.Title }
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Episode { fmt.Sprintf("%d", episode.MalID) }
|
|
|
|
Episode { fmt.Sprintf("%d", episode.MalID) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
|
|
|
|
<div class="ml-auto flex items-center gap-2">
|
|
|
|
if episode.Filler {
|
|
|
|
if episode.Filler {
|
|
|
|
<span class="ml-auto shrink-0 rounded px-1.5 py-0.5 text-[9px] uppercase tracking-wider bg-yellow-900/50 text-yellow-400">Filler</span>
|
|
|
|
<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 {
|
|
|
|
if episode.Recap {
|
|
|
|
<span class="ml-auto shrink-0 rounded px-1.5 py-0.5 text-[9px] uppercase tracking-wider bg-blue-900/50 text-blue-400">Recap</span>
|
|
|
|
<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>
|
|
|
|
</a>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
templ VideoPlayer(data WatchPageData) {
|
|
|
|
templ VideoPlayer(data WatchPageData) {
|
|
|
|
{{ streamURL := buildStreamURL(data.InitialMode, data.ModeSources) }}
|
|
|
|
{{ streamURL := buildStreamURL(data.InitialMode, data.ModeSources) }}
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
class="group relative aspect-video w-full overflow-hidden bg-black"
|
|
|
|
class="flex flex-col gap-4 w-full"
|
|
|
|
data-video-player
|
|
|
|
data-video-player
|
|
|
|
data-stream-url="/watch/proxy/stream"
|
|
|
|
data-stream-url="/watch/proxy/stream"
|
|
|
|
data-initial-mode={ data.InitialMode }
|
|
|
|
data-initial-mode={ data.InitialMode }
|
|
|
|
@@ -144,6 +199,7 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
data-mode-sources={ toJSON(data.ModeSources) }
|
|
|
|
data-mode-sources={ toJSON(data.ModeSources) }
|
|
|
|
data-segments={ toJSON(data.Segments) }
|
|
|
|
data-segments={ toJSON(data.Segments) }
|
|
|
|
>
|
|
|
|
>
|
|
|
|
|
|
|
|
<div class="group relative aspect-video w-full overflow-hidden bg-black">
|
|
|
|
<video
|
|
|
|
<video
|
|
|
|
class="h-full w-full"
|
|
|
|
class="h-full w-full"
|
|
|
|
preload="metadata"
|
|
|
|
preload="metadata"
|
|
|
|
@@ -152,7 +208,7 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
src={ streamURL }
|
|
|
|
src={ streamURL }
|
|
|
|
></video>
|
|
|
|
></video>
|
|
|
|
<div data-loading class="absolute inset-0 flex items-center justify-center bg-black/50">
|
|
|
|
<div data-loading class="absolute inset-0 flex items-center justify-center bg-black/50">
|
|
|
|
<div class="h-8 w-8 animate-spin rounded-full border-2 border-(--panel-soft) border-t-(--accent)"></div>
|
|
|
|
<div class="h-8 w-8 animate-spin border-2 border-(--panel-soft) border-t-(--accent)"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div data-subtitle-text class="absolute bottom-20 left-1/2 z-20 hidden max-w-[88vw] -translate-x-1/2 px-4 text-center text-xl font-semibold text-white drop-shadow-lg"></div>
|
|
|
|
<div data-subtitle-text class="absolute bottom-20 left-1/2 z-20 hidden max-w-[88vw] -translate-x-1/2 px-4 text-center text-xl font-semibold text-white drop-shadow-lg"></div>
|
|
|
|
<button data-skip class="absolute bottom-24 right-5 z-20 hidden border border-white bg-transparent px-4 py-2 text-base font-semibold text-white transition-opacity hover:opacity-85">
|
|
|
|
<button data-skip class="absolute bottom-24 right-5 z-20 hidden border border-white bg-transparent px-4 py-2 text-base font-semibold text-white transition-opacity hover:opacity-85">
|
|
|
|
@@ -162,7 +218,7 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
<div data-progress-wrap class="group/progress relative mb-5 h-1 cursor-pointer bg-white/30">
|
|
|
|
<div data-progress-wrap class="group/progress relative mb-5 h-1 cursor-pointer bg-white/30">
|
|
|
|
<div data-segments class="pointer-events-none absolute inset-0 z-20"></div>
|
|
|
|
<div data-segments class="pointer-events-none absolute inset-0 z-20"></div>
|
|
|
|
<div data-progress class="pointer-events-none absolute inset-y-0 left-0 z-10 bg-blue-500"></div>
|
|
|
|
<div data-progress class="pointer-events-none absolute inset-y-0 left-0 z-10 bg-blue-500"></div>
|
|
|
|
<div data-scrubber class="pointer-events-none absolute -top-1.5 z-30 h-5 w-5 -translate-x-1/2 rounded-full bg-blue-500 opacity-0 transition-opacity group-hover/progress:opacity-100" style="left: 0%"></div>
|
|
|
|
<div data-scrubber class="pointer-events-none absolute -top-1.5 z-30 h-5 w-5 -translate-x-1/2 bg-blue-500 opacity-0 transition-opacity group-hover/progress:opacity-100" style="left: 0%"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
<div class="flex items-center justify-between gap-4">
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
@@ -205,7 +261,6 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
<span data-time class="text-base text-white tabular-nums">00:00 / 00:00</span>
|
|
|
|
<span data-time class="text-base text-white tabular-nums">00:00 / 00:00</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
<div data-mode-switch class="flex items-center gap-1">
|
|
|
|
|
|
|
|
<button data-mode-dub class="flex h-10 w-10 items-center justify-center text-white" title="Dub">
|
|
|
|
<button data-mode-dub class="flex h-10 w-10 items-center justify-center text-white" title="Dub">
|
|
|
|
<svg class="h-6 w-6" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
<svg class="h-6 w-6" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
<path d="M6 9h6M6 15h4M12 9v6M17 7.5c2.2 2 2.2 7 0 9M19.2 5.5c3.4 3.2 3.4 10 0 13" stroke="white" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
|
|
<path d="M6 9h6M6 15h4M12 9v6M17 7.5c2.2 2 2.2 7 0 9M19.2 5.5c3.4 3.2 3.4 10 0 13" stroke="white" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
|
|
@@ -217,7 +272,6 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
<path d="M8 11.5h8M8 14.5h5" stroke="white" stroke-width="1.85" stroke-linecap="round"/>
|
|
|
|
<path d="M8 11.5h8M8 14.5h5" stroke="white" stroke-width="1.85" stroke-linecap="round"/>
|
|
|
|
</svg>
|
|
|
|
</svg>
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button data-backward class="flex h-10 w-10 items-center justify-center text-white" title="-10s">
|
|
|
|
<button data-backward class="flex h-10 w-10 items-center justify-center text-white" title="-10s">
|
|
|
|
<svg class="h-6 w-6" viewBox="0 0 50 50" aria-hidden="true">
|
|
|
|
<svg class="h-6 w-6" viewBox="0 0 50 50" aria-hidden="true">
|
|
|
|
<path d="M29.9199 45H25.2051V26.5391L20.6064 28.3154V24.3975L29.4219 20.7949H29.9199V45ZM48.1013 35.0059C48.1013 38.3483 47.4926 40.9049 46.2751 42.6758C45.0687 44.4466 43.3422 45.332 41.0954 45.332C38.8708 45.332 37.1498 44.4743 35.9323 42.7588C34.726 41.0322 34.1006 38.5641 34.0564 35.3545V30.7891C34.0564 27.4577 34.6596 24.9121 35.8659 23.1523C37.0723 21.3815 38.8044 20.4961 41.0622 20.4961C43.32 20.4961 45.0521 21.3704 46.2585 23.1191C47.4649 24.8678 48.0792 27.3636 48.1013 30.6064V35.0059ZM43.3864 30.1084C43.3864 28.2048 43.1983 26.777 42.822 25.8252C42.4457 24.8734 41.8591 24.3975 41.0622 24.3975C39.5681 24.3975 38.7933 26.1406 38.738 29.627V35.6533C38.738 37.6012 38.9262 39.0511 39.3025 40.0029C39.6898 40.9548 40.2875 41.4307 41.0954 41.4307C41.8591 41.4307 42.4236 40.988 42.7888 40.1025C43.1651 39.2061 43.3643 37.8392 43.3864 36.002V30.1084Z" fill="white"/>
|
|
|
|
<path d="M29.9199 45H25.2051V26.5391L20.6064 28.3154V24.3975L29.4219 20.7949H29.9199V45ZM48.1013 35.0059C48.1013 38.3483 47.4926 40.9049 46.2751 42.6758C45.0687 44.4466 43.3422 45.332 41.0954 45.332C38.8708 45.332 37.1498 44.4743 35.9323 42.7588C34.726 41.0322 34.1006 38.5641 34.0564 35.3545V30.7891C34.0564 27.4577 34.6596 24.9121 35.8659 23.1523C37.0723 21.3815 38.8044 20.4961 41.0622 20.4961C43.32 20.4961 45.0521 21.3704 46.2585 23.1191C47.4649 24.8678 48.0792 27.3636 48.1013 30.6064V35.0059ZM43.3864 30.1084C43.3864 28.2048 43.1983 26.777 42.822 25.8252C42.4457 24.8734 41.8591 24.3975 41.0622 24.3975C39.5681 24.3975 38.7933 26.1406 38.738 29.627V35.6533C38.738 37.6012 38.9262 39.0511 39.3025 40.0029C39.6898 40.9548 40.2875 41.4307 41.0954 41.4307C41.8591 41.4307 42.4236 40.988 42.7888 40.1025C43.1651 39.2061 43.3643 37.8392 43.3864 36.002V30.1084Z" fill="white"/>
|
|
|
|
@@ -239,6 +293,7 @@ templ VideoPlayer(data WatchPageData) {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildStreamURL(mode string, modeSources map[string]ModeSource) string {
|
|
|
|
func buildStreamURL(mode string, modeSources map[string]ModeSource) string {
|
|
|
|
@@ -250,3 +305,34 @@ func toJSON(v interface{}) string {
|
|
|
|
b, _ := json.Marshal(v)
|
|
|
|
b, _ := json.Marshal(v)
|
|
|
|
return string(b)
|
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
|