First working POC
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
<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;
|
||||
}
|
||||
|
||||
let settings = $state<AudioSettings>({
|
||||
crossfadeDuration: 0,
|
||||
gaplessPlayback: true,
|
||||
normalizeVolume: false,
|
||||
volumeLevel: "normal",
|
||||
});
|
||||
|
||||
let videoSettings = $state<VideoSettings>({
|
||||
autoPlayNextEpisode: true,
|
||||
autoPlayCountdownSeconds: 10,
|
||||
});
|
||||
|
||||
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">
|
||||
<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>
|
||||
{/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>
|
||||
Reference in New Issue
Block a user