First working POC
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
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">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Sleep Timer Indicator -->
|
||||
<SleepTimerIndicator onClick={onSleepTimerClick} />
|
||||
|
||||
<!-- 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>
|
||||
Reference in New Issue
Block a user