Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
162 lines
4.6 KiB
Svelte
162 lines
4.6 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.imageId}
|
|
<CachedImage
|
|
itemId={imageId}
|
|
imageType="Primary"
|
|
tag={$nextEpisodeItem.imageId}
|
|
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>
|