All checks were successful
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
162 lines
5.8 KiB
Svelte
162 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { playerController } from "$lib/player";
|
|
import { volume, isMuted, mergedVolume } from "$lib/stores/player";
|
|
import { isRemoteMode } from "$lib/stores/playbackMode";
|
|
import { selectedSession, sessions } from "$lib/stores/sessions";
|
|
import { platform } from "@tauri-apps/plugin-os";
|
|
|
|
interface Props {
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
let { size = "md" }: Props = $props();
|
|
|
|
// On Android, volume is controlled by system volume buttons (not a slider)
|
|
const isAndroid = platform() === "android";
|
|
|
|
let showSlider = $state(false);
|
|
let sliderValue = $state($mergedVolume);
|
|
|
|
// Sync slider with merged volume (handles both local and remote)
|
|
$effect(() => {
|
|
sliderValue = $mergedVolume;
|
|
});
|
|
|
|
async function handleVolumeChange(e: Event) {
|
|
const target = e.target as HTMLInputElement;
|
|
const newVolume = parseFloat(target.value);
|
|
sliderValue = newVolume;
|
|
|
|
if ($isRemoteMode && $selectedSession) {
|
|
// Remote mode: send volume as 0-100 integer to remote session
|
|
await sessions.sendVolume($selectedSession.id, Math.round(newVolume * 100));
|
|
} else {
|
|
await playerController.setVolume(newVolume);
|
|
}
|
|
}
|
|
|
|
async function toggleMute() {
|
|
if ($isRemoteMode && $selectedSession) {
|
|
await sessions.sendToggleMute($selectedSession.id);
|
|
} else {
|
|
await playerController.toggleMute();
|
|
}
|
|
}
|
|
|
|
function toggleSlider() {
|
|
showSlider = !showSlider;
|
|
}
|
|
|
|
// Icon sizes based on prop (use $derived for reactivity)
|
|
const iconSize = $derived(size === "sm" ? "w-4 h-4" : size === "md" ? "w-5 h-5" : "w-6 h-6");
|
|
const buttonPadding = $derived(size === "sm" ? "p-1" : size === "md" ? "p-2" : "p-3");
|
|
</script>
|
|
|
|
{#if !isAndroid}
|
|
<div class="relative flex items-center gap-1">
|
|
<!-- Volume Icon Button (click to toggle slider) -->
|
|
<button
|
|
onclick={toggleSlider}
|
|
class="{buttonPadding} rounded-full hover:bg-white/10 transition-colors"
|
|
title="Volume"
|
|
>
|
|
{#if $isMuted || sliderValue === 0}
|
|
<!-- Muted Icon -->
|
|
<svg class={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"
|
|
/>
|
|
</svg>
|
|
{:else if sliderValue < 0.33}
|
|
<!-- Low Volume Icon -->
|
|
<svg class={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15.536 8.464a5 5 0 010 7.072m-9.95-9.193L4 8.929V5.071a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586z"
|
|
/>
|
|
</svg>
|
|
{:else if sliderValue < 0.66}
|
|
<!-- Medium Volume Icon -->
|
|
<svg class={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15.536 8.464a5 5 0 010 7.072M6.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h2.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L6.586 15z"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<!-- High Volume Icon -->
|
|
<svg class={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Volume Slider (toggle on click) -->
|
|
{#if showSlider}
|
|
<div
|
|
class="absolute left-full ml-2 bg-[var(--color-surface)] rounded-lg shadow-lg p-3 z-[70] flex items-center gap-2"
|
|
role="group"
|
|
aria-label="Volume controls"
|
|
>
|
|
<!-- Mute button inside slider popup -->
|
|
<button
|
|
onclick={toggleMute}
|
|
class="p-1 rounded hover:bg-white/10 transition-colors"
|
|
title={$isMuted ? "Unmute" : "Mute"}
|
|
>
|
|
{#if $isMuted || sliderValue === 0}
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15.536 8.464a5 5 0 010 7.072M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={sliderValue}
|
|
oninput={handleVolumeChange}
|
|
class="w-24 h-1 accent-[var(--color-jellyfin)] cursor-pointer"
|
|
/>
|
|
<span class="text-xs text-gray-400 w-8 text-right">{Math.round(sliderValue * 100)}%</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Click outside to close volume slider -->
|
|
{#if showSlider}
|
|
<button
|
|
class="fixed inset-0 z-[65]"
|
|
onclick={() => showSlider = false}
|
|
aria-label="Close volume"
|
|
></button>
|
|
{/if}
|