204 lines
6.0 KiB
Svelte
204 lines
6.0 KiB
Svelte
<!-- TRACES: UR-005 | DR-009 -->
|
|
<script lang="ts">
|
|
import { untrack } from "svelte";
|
|
import { sleepTimerActive } from "$lib/stores/sleepTimer";
|
|
import SleepTimerIndicator from "./SleepTimerIndicator.svelte";
|
|
|
|
interface Props {
|
|
isPlaying?: boolean;
|
|
hasPrevious?: boolean;
|
|
hasNext?: boolean;
|
|
shuffle?: boolean;
|
|
repeat?: "off" | "all" | "one";
|
|
onPlayPause?: () => void;
|
|
onPrevious?: () => void;
|
|
onNext?: () => void;
|
|
onToggleShuffle?: () => void;
|
|
onCycleRepeat?: () => void;
|
|
onSleepTimerClick?: () => void;
|
|
}
|
|
|
|
let {
|
|
isPlaying = false,
|
|
hasPrevious = false,
|
|
hasNext = false,
|
|
shuffle = false,
|
|
repeat = "off",
|
|
onPlayPause,
|
|
onPrevious,
|
|
onNext,
|
|
onToggleShuffle,
|
|
onCycleRepeat,
|
|
onSleepTimerClick,
|
|
}: Props = $props();
|
|
|
|
// Local optimistic state for instant button feedback
|
|
let optimisticIsPlaying = $state(false);
|
|
let optimisticTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
// Sync with prop changes (initializes and updates on prop change)
|
|
$effect(() => {
|
|
optimisticIsPlaying = isPlaying;
|
|
// Clear timeout when prop updates (state confirmed)
|
|
if (optimisticTimeout) {
|
|
clearTimeout(optimisticTimeout);
|
|
optimisticTimeout = null;
|
|
}
|
|
});
|
|
|
|
// Cleanup on unmount
|
|
$effect(() => {
|
|
return () => {
|
|
if (optimisticTimeout) {
|
|
clearTimeout(optimisticTimeout);
|
|
}
|
|
};
|
|
});
|
|
|
|
function handlePlayPause() {
|
|
// Immediately toggle optimistic state for instant visual feedback
|
|
optimisticIsPlaying = !optimisticIsPlaying;
|
|
|
|
// Clear any pending timeout
|
|
if (optimisticTimeout) {
|
|
clearTimeout(optimisticTimeout);
|
|
}
|
|
|
|
// Reset optimistic state after a delay if prop doesn't update
|
|
optimisticTimeout = setTimeout(() => {
|
|
optimisticIsPlaying = isPlaying;
|
|
}, 1000);
|
|
|
|
// Call the actual handler
|
|
untrack(() => onPlayPause?.());
|
|
}
|
|
</script>
|
|
|
|
<div class="flex items-center gap-4">
|
|
<!-- Sleep Timer (above shuffle) -->
|
|
{#if !$sleepTimerActive}
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onSleepTimerClick?.();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
class="p-2 rounded-full text-gray-400 hover:text-white transition-colors"
|
|
title="Sleep timer"
|
|
aria-label="Sleep timer"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
</svg>
|
|
</button>
|
|
{:else}
|
|
<SleepTimerIndicator onClick={onSleepTimerClick} />
|
|
{/if}
|
|
|
|
<!-- Shuffle -->
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleShuffle?.();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
class="p-2 rounded-full transition-colors {shuffle
|
|
? 'text-[var(--color-jellyfin)]'
|
|
: 'text-gray-400 hover:text-white'}"
|
|
title="Shuffle"
|
|
>
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Previous -->
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onPrevious?.();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
disabled={!hasPrevious}
|
|
class="p-2 rounded-full text-white hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
title="Previous"
|
|
>
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Play/Pause -->
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
handlePlayPause();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
class="p-3 rounded-full bg-white text-black hover:scale-105 transition-transform"
|
|
title={optimisticIsPlaying ? "Pause" : "Play"}
|
|
>
|
|
{#if optimisticIsPlaying}
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
|
</svg>
|
|
{:else}
|
|
<svg class="w-6 h-6 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Next -->
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onNext?.();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
disabled={!hasNext}
|
|
class="p-2 rounded-full text-white hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
title="Next"
|
|
>
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Repeat -->
|
|
<button
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
onCycleRepeat?.();
|
|
}}
|
|
ontouchstart={(e) => e.stopPropagation()}
|
|
ontouchmove={(e) => e.stopPropagation()}
|
|
ontouchend={(e) => e.stopPropagation()}
|
|
class="p-2 rounded-full transition-colors {repeat !== 'off'
|
|
? 'text-[var(--color-jellyfin)]'
|
|
: 'text-gray-400 hover:text-white'}"
|
|
title="Repeat: {repeat}"
|
|
>
|
|
{#if repeat === "one"}
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4zm-4-2V9h-1l-2 1v1h1.5v4H13z" />
|
|
</svg>
|
|
{:else}
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
</div>
|