635 lines
23 KiB
Svelte
635 lines
23 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import {
|
|
getCacheStats,
|
|
setCacheLimit,
|
|
clearCache,
|
|
formatBytes,
|
|
gbToBytes,
|
|
bytesToGb,
|
|
type ImageCacheStats,
|
|
} from "$lib/services/imageCache";
|
|
|
|
type VolumeLevel = "loud" | "normal" | "quiet";
|
|
|
|
interface AudioSettings {
|
|
crossfadeDuration: number;
|
|
gaplessPlayback: boolean;
|
|
normalizeVolume: boolean;
|
|
volumeLevel: VolumeLevel;
|
|
}
|
|
|
|
interface VideoSettings {
|
|
autoPlayNextEpisode: boolean;
|
|
autoPlayCountdownSeconds: number;
|
|
autoPlayMaxEpisodes: number;
|
|
}
|
|
|
|
const episodeLimitOptions = [
|
|
{ value: 0, label: "Unlimited" },
|
|
{ value: 1, label: "1" },
|
|
{ value: 2, label: "2" },
|
|
{ value: 3, label: "3" },
|
|
{ value: 5, label: "5" },
|
|
{ value: 10, label: "10" },
|
|
];
|
|
|
|
let settings = $state<AudioSettings>({
|
|
crossfadeDuration: 0,
|
|
gaplessPlayback: true,
|
|
normalizeVolume: false,
|
|
volumeLevel: "normal",
|
|
});
|
|
|
|
let videoSettings = $state<VideoSettings>({
|
|
autoPlayNextEpisode: true,
|
|
autoPlayCountdownSeconds: 10,
|
|
autoPlayMaxEpisodes: 0,
|
|
});
|
|
|
|
let loading = $state(true);
|
|
let saving = $state(false);
|
|
let saveMessage = $state("");
|
|
|
|
// Image cache state
|
|
let cacheStats = $state<ImageCacheStats | null>(null);
|
|
let cacheLoading = $state(false);
|
|
let clearingCache = $state(false);
|
|
|
|
// Cache limit options in bytes
|
|
const cacheLimitOptions = [
|
|
{ label: "500 MB", bytes: gbToBytes(0.5) },
|
|
{ label: "1 GB", bytes: gbToBytes(1), default: true },
|
|
{ label: "2 GB", bytes: gbToBytes(2) },
|
|
{ label: "5 GB", bytes: gbToBytes(5) },
|
|
{ label: "Unlimited", bytes: 0 },
|
|
];
|
|
|
|
onMount(async () => {
|
|
await loadSettings();
|
|
});
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
loading = true;
|
|
const [audioResult, videoResult] = await Promise.all([
|
|
invoke<AudioSettings>("player_get_audio_settings"),
|
|
invoke<VideoSettings>("player_get_video_settings"),
|
|
]);
|
|
settings = audioResult;
|
|
videoSettings = videoResult;
|
|
// Load cache stats in parallel but don't block on it
|
|
loadCacheStats();
|
|
} catch (e) {
|
|
console.error("Failed to load settings:", e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function loadCacheStats() {
|
|
try {
|
|
cacheLoading = true;
|
|
cacheStats = await getCacheStats();
|
|
} catch (e) {
|
|
console.error("Failed to load cache stats:", e);
|
|
} finally {
|
|
cacheLoading = false;
|
|
}
|
|
}
|
|
|
|
async function handleCacheLimitChange(limitBytes: number) {
|
|
try {
|
|
await setCacheLimit(limitBytes);
|
|
// Reload stats to reflect new limit
|
|
await loadCacheStats();
|
|
} catch (e) {
|
|
console.error("Failed to set cache limit:", e);
|
|
}
|
|
}
|
|
|
|
async function handleClearCache() {
|
|
try {
|
|
clearingCache = true;
|
|
await clearCache();
|
|
await loadCacheStats();
|
|
} catch (e) {
|
|
console.error("Failed to clear cache:", e);
|
|
} finally {
|
|
clearingCache = false;
|
|
}
|
|
}
|
|
|
|
// Get the current selected limit option
|
|
function isCurrentLimit(optionBytes: number): boolean {
|
|
if (!cacheStats) return false;
|
|
// Unlimited is 0
|
|
if (optionBytes === 0 && cacheStats.limitBytes === 0) return true;
|
|
// Allow small tolerance for floating point
|
|
return Math.abs(cacheStats.limitBytes - optionBytes) < 1000;
|
|
}
|
|
|
|
// Calculate cache usage percentage
|
|
function getCacheUsagePercent(): number {
|
|
if (!cacheStats || cacheStats.limitBytes === 0) return 0;
|
|
return Math.min(100, (cacheStats.totalSizeBytes / cacheStats.limitBytes) * 100);
|
|
}
|
|
|
|
async function saveSettings() {
|
|
try {
|
|
saving = true;
|
|
saveMessage = "";
|
|
await Promise.all([
|
|
invoke("player_set_audio_settings", { settings }),
|
|
invoke("player_set_video_settings", { settings: videoSettings }),
|
|
]);
|
|
saveMessage = "Settings saved successfully!";
|
|
setTimeout(() => {
|
|
saveMessage = "";
|
|
}, 3000);
|
|
} catch (e) {
|
|
console.error("Failed to save settings:", e);
|
|
saveMessage = "Failed to save settings";
|
|
} finally {
|
|
saving = false;
|
|
}
|
|
}
|
|
|
|
function handleCrossfadeChange(e: Event) {
|
|
const target = e.target as HTMLInputElement;
|
|
settings.crossfadeDuration = parseFloat(target.value);
|
|
}
|
|
|
|
function handleGaplessToggle() {
|
|
settings.gaplessPlayback = !settings.gaplessPlayback;
|
|
}
|
|
|
|
function handleNormalizeToggle() {
|
|
settings.normalizeVolume = !settings.normalizeVolume;
|
|
}
|
|
|
|
function handleVolumeLevelChange(level: VolumeLevel) {
|
|
settings.volumeLevel = level;
|
|
}
|
|
|
|
function handleAutoPlayToggle() {
|
|
videoSettings.autoPlayNextEpisode = !videoSettings.autoPlayNextEpisode;
|
|
}
|
|
|
|
function handleCountdownChange(e: Event) {
|
|
const target = e.target as HTMLInputElement;
|
|
videoSettings.autoPlayCountdownSeconds = parseInt(target.value, 10);
|
|
}
|
|
</script>
|
|
|
|
<div class="max-w-2xl mx-auto space-y-8 p-6 pb-24 h-full overflow-y-auto">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">Audio Settings</h1>
|
|
<p class="text-gray-400">Configure playback and audio processing</p>
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="text-center py-12 text-gray-400">
|
|
<p>Loading settings...</p>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-6">
|
|
<!-- Crossfade -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-white">Crossfade</h2>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Fade between tracks for seamless transitions
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="text-2xl font-bold text-[var(--color-jellyfin)]">
|
|
{settings.crossfadeDuration.toFixed(1)}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="12"
|
|
step="0.5"
|
|
value={settings.crossfadeDuration}
|
|
oninput={handleCrossfadeChange}
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]"
|
|
/>
|
|
<div class="flex justify-between text-xs text-gray-500 mt-2">
|
|
<span>0s (Off)</span>
|
|
<span>12s (Max)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gapless Playback -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-white">Gapless Playback</h2>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Eliminate silence between tracks in albums
|
|
</p>
|
|
</div>
|
|
<button
|
|
onclick={handleGaplessToggle}
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {settings.gaplessPlayback
|
|
? 'bg-[var(--color-jellyfin)]'
|
|
: 'bg-gray-600'}"
|
|
aria-label="Toggle gapless playback"
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {settings.gaplessPlayback
|
|
? 'translate-x-7'
|
|
: 'translate-x-1'}"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Volume Normalization -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-white">Volume Normalization</h2>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Automatically adjust volume levels for consistent playback
|
|
</p>
|
|
</div>
|
|
<button
|
|
onclick={handleNormalizeToggle}
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {settings.normalizeVolume
|
|
? 'bg-[var(--color-jellyfin)]'
|
|
: 'bg-gray-600'}"
|
|
aria-label="Toggle volume normalization"
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {settings.normalizeVolume
|
|
? 'translate-x-7'
|
|
: 'translate-x-1'}"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
|
|
{#if settings.normalizeVolume}
|
|
<div class="pt-4 border-t border-gray-700">
|
|
<p class="text-sm font-medium text-gray-300 mb-3">Target Volume Level</p>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<button
|
|
onclick={() => handleVolumeLevelChange("loud")}
|
|
class="py-3 px-4 rounded-lg transition-all {settings.volumeLevel ===
|
|
'loud'
|
|
? 'bg-[var(--color-jellyfin)] text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
|
>
|
|
<div class="font-semibold">Loud</div>
|
|
<div class="text-xs opacity-75">-11 LUFS</div>
|
|
</button>
|
|
<button
|
|
onclick={() => handleVolumeLevelChange("normal")}
|
|
class="py-3 px-4 rounded-lg transition-all {settings.volumeLevel ===
|
|
'normal'
|
|
? 'bg-[var(--color-jellyfin)] text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
|
>
|
|
<div class="font-semibold">Normal</div>
|
|
<div class="text-xs opacity-75">-14 LUFS</div>
|
|
</button>
|
|
<button
|
|
onclick={() => handleVolumeLevelChange("quiet")}
|
|
class="py-3 px-4 rounded-lg transition-all {settings.volumeLevel ===
|
|
'quiet'
|
|
? 'bg-[var(--color-jellyfin)] text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
|
>
|
|
<div class="font-semibold">Quiet</div>
|
|
<div class="text-xs opacity-75">-23 LUFS</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Video Playback Settings -->
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<h2 class="text-2xl font-bold text-white mb-4">Video Playback</h2>
|
|
|
|
<!-- Auto-play Next Episode -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-xl font-semibold text-white">Auto-play Next Episode</h3>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Automatically start the next episode when one finishes
|
|
</p>
|
|
</div>
|
|
<button
|
|
onclick={handleAutoPlayToggle}
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {videoSettings.autoPlayNextEpisode
|
|
? 'bg-[var(--color-jellyfin)]'
|
|
: 'bg-gray-600'}"
|
|
aria-label="Toggle auto-play next episode"
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {videoSettings.autoPlayNextEpisode
|
|
? 'translate-x-7'
|
|
: 'translate-x-1'}"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
|
|
{#if videoSettings.autoPlayNextEpisode}
|
|
<div class="pt-4 border-t border-gray-700">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-300">Countdown Duration</p>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Time before next episode starts automatically
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="text-2xl font-bold text-[var(--color-jellyfin)]">
|
|
{videoSettings.autoPlayCountdownSeconds}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="5"
|
|
max="30"
|
|
step="5"
|
|
value={videoSettings.autoPlayCountdownSeconds}
|
|
oninput={handleCountdownChange}
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]"
|
|
/>
|
|
<div class="flex justify-between text-xs text-gray-500 mt-2">
|
|
<span>5s</span>
|
|
<span>30s</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Episode Limit -->
|
|
<div class="pt-4 border-t border-gray-700">
|
|
<div class="mb-4">
|
|
<p class="text-sm font-medium text-gray-300">Episode Limit</p>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Stop auto-playing after this many consecutive episodes
|
|
</p>
|
|
</div>
|
|
<div class="grid grid-cols-3 md:grid-cols-6 gap-2">
|
|
{#each episodeLimitOptions as option}
|
|
<button
|
|
onclick={() => { videoSettings.autoPlayMaxEpisodes = option.value; }}
|
|
class="py-3 px-3 rounded-lg transition-all text-sm
|
|
{videoSettings.autoPlayMaxEpisodes === option.value
|
|
? 'bg-[var(--color-jellyfin)] text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
|
>
|
|
<div class="font-semibold">{option.label}</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Cache Settings -->
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<h2 class="text-2xl font-bold text-white mb-4">Image Cache</h2>
|
|
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 space-y-6">
|
|
<!-- Cache Usage -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="text-lg font-semibold text-white">Cache Usage</h3>
|
|
{#if cacheLoading}
|
|
<span class="text-sm text-gray-400">Loading...</span>
|
|
{:else if cacheStats}
|
|
<span class="text-sm text-gray-300">
|
|
{formatBytes(cacheStats.totalSizeBytes)} / {cacheStats.limitBytes === 0 ? "Unlimited" : formatBytes(cacheStats.limitBytes)}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if cacheStats && cacheStats.limitBytes > 0}
|
|
<!-- Progress bar -->
|
|
<div class="w-full bg-gray-700 rounded-full h-3 mb-2">
|
|
<div
|
|
class="h-3 rounded-full transition-all duration-300 {getCacheUsagePercent() > 90 ? 'bg-red-500' : getCacheUsagePercent() > 70 ? 'bg-yellow-500' : 'bg-[var(--color-jellyfin)]'}"
|
|
style="width: {getCacheUsagePercent()}%"
|
|
></div>
|
|
</div>
|
|
{/if}
|
|
|
|
<p class="text-sm text-gray-400">
|
|
{#if cacheStats}
|
|
{cacheStats.itemCount} images cached
|
|
{:else}
|
|
Thumbnails and artwork are cached locally for faster loading
|
|
{/if}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Cache Limit -->
|
|
<div class="pt-4 border-t border-gray-700">
|
|
<h3 class="text-lg font-semibold text-white mb-3">Storage Limit</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
{#each cacheLimitOptions as option}
|
|
<button
|
|
onclick={() => handleCacheLimitChange(option.bytes)}
|
|
class="py-2 px-3 rounded-lg transition-all text-sm {isCurrentLimit(option.bytes)
|
|
? 'bg-[var(--color-jellyfin)] text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
|
>
|
|
<div class="font-semibold">{option.label}</div>
|
|
{#if option.default}
|
|
<div class="text-xs opacity-75">Default</div>
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clear Cache -->
|
|
<div class="pt-4 border-t border-gray-700">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-white">Clear Image Cache</h3>
|
|
<p class="text-sm text-gray-400">Remove all cached thumbnails and artwork</p>
|
|
</div>
|
|
<button
|
|
onclick={handleClearCache}
|
|
disabled={clearingCache || (cacheStats?.itemCount ?? 0) === 0}
|
|
class="px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{clearingCache ? "Clearing..." : "Clear Cache"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Download Settings -->
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<h2 class="text-2xl font-bold text-white mb-4">Downloads</h2>
|
|
|
|
<!-- Storage Limit -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 mb-4">
|
|
<div class="mb-4">
|
|
<h3 class="text-xl font-semibold text-white">Storage Limit</h3>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Maximum storage for offline downloads
|
|
</p>
|
|
</div>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<button
|
|
class="py-3 px-4 rounded-lg transition-all bg-gray-700 text-gray-300 hover:bg-gray-600"
|
|
>
|
|
<div class="font-semibold">5 GB</div>
|
|
</button>
|
|
<button
|
|
class="py-3 px-4 rounded-lg transition-all bg-[var(--color-jellyfin)] text-white"
|
|
>
|
|
<div class="font-semibold">10 GB</div>
|
|
<div class="text-xs opacity-75">Default</div>
|
|
</button>
|
|
<button
|
|
class="py-3 px-4 rounded-lg transition-all bg-gray-700 text-gray-300 hover:bg-gray-600"
|
|
>
|
|
<div class="font-semibold">20 GB</div>
|
|
</button>
|
|
<button
|
|
class="py-3 px-4 rounded-lg transition-all bg-gray-700 text-gray-300 hover:bg-gray-600"
|
|
>
|
|
<div class="font-semibold">Unlimited</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Smart Caching -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 mb-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-xl font-semibold text-white">Smart Caching</h3>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Automatically download albums you're listening to
|
|
</p>
|
|
</div>
|
|
<button
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
|
aria-label="Toggle album affinity (coming soon)"
|
|
disabled
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Queue Pre-caching -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6 mb-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-xl font-semibold text-white">Queue Pre-caching</h3>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Download next 5 tracks in queue automatically
|
|
</p>
|
|
</div>
|
|
<button
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
|
aria-label="Toggle queue pre-caching (coming soon)"
|
|
disabled
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WiFi Only -->
|
|
<div class="bg-[var(--color-surface)] rounded-lg p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-xl font-semibold text-white">WiFi Only</h3>
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
Only download when connected to WiFi
|
|
</p>
|
|
</div>
|
|
<button
|
|
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
|
aria-label="Toggle WiFi only downloads (coming soon)"
|
|
disabled
|
|
>
|
|
<span
|
|
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
|
></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-sm">
|
|
{#if saveMessage}
|
|
<span
|
|
class="text-{saveMessage.includes('success')
|
|
? 'green'
|
|
: 'red'}-400"
|
|
>
|
|
{saveMessage}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
onclick={saveSettings}
|
|
disabled={saving}
|
|
class="px-6 py-3 bg-[var(--color-jellyfin)] text-white rounded-lg font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{saving ? "Saving..." : "Save Settings"}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Info Box -->
|
|
<div class="bg-blue-900/20 border border-blue-800 rounded-lg p-4">
|
|
<div class="flex gap-3">
|
|
<svg
|
|
class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
<div class="text-sm text-blue-300">
|
|
<p class="font-semibold mb-1">About these settings:</p>
|
|
<ul class="list-disc list-inside space-y-1 text-blue-200">
|
|
<li>
|
|
<strong>Crossfade</strong> smoothly blends the end of one track with the
|
|
beginning of the next
|
|
</li>
|
|
<li>
|
|
<strong>Gapless</strong> removes silence between tracks for continuous
|
|
album playback
|
|
</li>
|
|
<li>
|
|
<strong>Normalization</strong> uses ReplayGain tags and real-time
|
|
loudnorm filtering
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|