First working POC
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user