jellytau/src/lib/components/player/NextEpisodePopup.svelte

171 lines
4.7 KiB
Svelte

<script lang="ts">
import {
nextEpisode,
isNextEpisodePopupVisible,
nextEpisodeItem,
countdownSeconds,
initialCountdownSeconds,
isCountdownActive,
} from "$lib/stores/nextEpisode";
import {
cancelAutoPlay,
watchNextManually,
} from "$lib/services/nextEpisodeService";
import { auth } from "$lib/stores/auth";
import CachedImage from "../common/CachedImage.svelte";
// Use series primary image for better visual consistency
const imageId = $derived($nextEpisodeItem ? ($nextEpisodeItem.seriesId || $nextEpisodeItem.id) : null);
// Format episode info (S1:E5)
const episodeInfo = $derived.by(() => {
const episode = $nextEpisodeItem;
if (!episode) return "";
const season = episode.parentIndexNumber;
const epNum = episode.indexNumber;
if (season !== undefined && epNum !== undefined) {
return `S${season}:E${epNum}`;
}
return "";
});
// Calculate progress for the countdown bar (1 to 0)
const countdownProgress = $derived.by(() => {
const initial = $initialCountdownSeconds;
const current = $countdownSeconds;
if (initial <= 0) return 0;
return current / initial;
});
function handlePlayNow() {
if ($nextEpisodeItem) {
watchNextManually($nextEpisodeItem);
}
}
function handleCancel() {
cancelAutoPlay();
}
// Note: Countdown pause/resume on hover is not implemented
// Backend controls countdown timing via CountdownTick events
function handleMouseEnter() {
// TODO: Could add visual feedback on hover
}
function handleMouseLeave() {
// TODO: Could remove visual feedback
}
</script>
{#if $isNextEpisodePopupVisible && $nextEpisodeItem}
<div
class="fixed bottom-24 right-6 z-50 max-w-sm animate-slide-up"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
role="dialog"
aria-label="Next episode"
tabindex="-1"
>
<div
class="bg-[var(--color-surface)] rounded-xl shadow-2xl overflow-hidden border border-gray-700"
>
<!-- Episode Card -->
<div class="flex gap-4 p-4">
<!-- Thumbnail -->
<div
class="relative flex-shrink-0 w-28 h-16 rounded-lg overflow-hidden bg-gray-800"
>
{#if imageId && $nextEpisodeItem.primaryImageTag}
<CachedImage
itemId={imageId}
imageType="Primary"
tag={$nextEpisodeItem.primaryImageTag}
maxHeight={200}
alt={$nextEpisodeItem.name}
class="w-full h-full object-cover"
/>
{/if}
<!-- Play icon overlay -->
<div
class="absolute inset-0 flex items-center justify-center bg-black/30"
>
<svg
class="w-8 h-8 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
<!-- Episode Info -->
<div class="flex-1 min-w-0">
<p class="text-xs text-gray-400 mb-1">Up Next</p>
<h3 class="text-sm font-semibold text-white truncate">
{$nextEpisodeItem.name}
</h3>
<p class="text-xs text-gray-400">
{$nextEpisodeItem.seriesName}
{episodeInfo}
</p>
</div>
</div>
<!-- Actions -->
<div class="px-4 pb-4 flex gap-3">
<!-- Cancel button -->
<button
onclick={handleCancel}
class="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm font-medium text-white transition-colors"
>
Cancel
</button>
<!-- Play now button with countdown -->
<button
onclick={handlePlayNow}
class="flex-1 px-4 py-2 bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin-dark)] rounded-lg text-sm font-medium text-white transition-colors relative overflow-hidden"
>
{#if $isCountdownActive}
<!-- Countdown progress bar -->
<div
class="absolute inset-0 bg-white/20 origin-left transition-transform duration-1000 ease-linear"
style="transform: scaleX({countdownProgress})"
></div>
{/if}
<span class="relative">
{#if $isCountdownActive}
Play in {$countdownSeconds}s
{:else}
Play Now
{/if}
</span>
</button>
</div>
</div>
</div>
{/if}
<style>
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-up {
animation: slide-up 0.3s ease-out forwards;
}
</style>