diff --git a/index.html b/index.html index 5b4b4b7..aa153e1 100644 --- a/index.html +++ b/index.html @@ -3428,6 +3428,7 @@ +
@@ -3665,6 +3666,9 @@
+ + +
Full-screen Visualizer @@ -3868,6 +3872,29 @@
+
+
+
+ Compact Artists + Show artist cards in a compact, horizontal layout +
+ +
+
+
+ Compact Albums + Show album cards in a compact, horizontal layout +
+ +
+
+ - -
-
- Compact Artists - Show artist cards in a compact, horizontal layout -
- -
-
-
- Compact Albums - Show album cards in a compact, horizontal layout -
- -
- -
-
+
+
@@ -4985,6 +4950,43 @@
+
+
+ Download Quality + Quality for track downloads +
+ +
+
+
+ Lossless Container + Container format for lossless downloads +
+ +
+
+
+ Cover Art Size + Size for downloaded/embedded cover art +
+ +
Filename Template @@ -5015,6 +5017,9 @@ placeholder="{albumTitle} - {albumArtist} - monochrome.tf" />
+
+ +
Generate M3U @@ -5067,6 +5072,9 @@
+
+ +
Relative Paths @@ -5092,6 +5100,36 @@
+ +
+
+
+
+
+ ADVANCED: Custom Database/Auth + Configure custom PocketBase and Firebase instances +
+ +
+
+ +
+
+
+
+ API Instances + Manage and prioritize API instances. +
+ +
+
    +
    +
    +
    +
    +
    @@ -5140,6 +5178,9 @@
    +
    + +
    Reset Local Data @@ -5160,6 +5201,9 @@ Clear Cloud Data
    +
    + +
    Backup & Restore @@ -5187,27 +5231,10 @@ />
    -
    -
    - ADVANCED: Custom Database/Auth - Configure custom PocketBase and Firebase instances -
    - -
    -
    -
    -
    - API Instances - Manage and prioritize API instances. -
    - -
    -
      -
      +
      -
      +
      +
      Blocked Content { - const speed = parseFloat(e.target.value) || 1.0; - playbackSpeedInput.value = speed; - player.setPlaybackSpeed(speed); - }); - - // Input allows full 0.01-100 range - const handleInputChange = () => { - const speed = parseFloat(playbackSpeedInput.value) || 1.0; - const validSpeed = Math.max(0.01, Math.min(100, speed)); + // Helper function to update both controls + const updatePlaybackSpeedControls = (speed) => { + const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0)); playbackSpeedInput.value = validSpeed; // Only update slider if value is within slider range if (validSpeed >= 0.25 && validSpeed <= 4.0) { playbackSpeedSlider.value = validSpeed; } - player.setPlaybackSpeed(validSpeed); + return validSpeed; }; - playbackSpeedInput.addEventListener('change', handleInputChange); - playbackSpeedInput.addEventListener('blur', handleInputChange); + // Initialize with current value + const currentSpeed = audioEffectsSettings.getSpeed(); + updatePlaybackSpeedControls(currentSpeed); + + playbackSpeedSlider.addEventListener('input', (e) => { + const speed = parseFloat(e.target.value); + playbackSpeedInput.value = speed; + audioEffectsSettings.setSpeed(speed); + player.setPlaybackSpeed(speed); + }); + + playbackSpeedInput.addEventListener('input', (e) => { + const speed = parseFloat(e.target.value); + if (!isNaN(speed) && speed >= 0.01 && speed <= 100) { + if (speed >= 0.25 && speed <= 4.0) { + playbackSpeedSlider.value = speed; + } + audioEffectsSettings.setSpeed(speed); + player.setPlaybackSpeed(speed); + } + }); + + playbackSpeedInput.addEventListener('change', (e) => { + const speed = parseFloat(e.target.value); + const validSpeed = updatePlaybackSpeedControls(speed); + audioEffectsSettings.setSpeed(validSpeed); + player.setPlaybackSpeed(validSpeed); + }); + + if (playbackSpeedReset) { + playbackSpeedReset.addEventListener('click', () => { + const defaultSpeed = audioEffectsSettings.resetSpeed(); + updatePlaybackSpeedControls(defaultSpeed); + player.setPlaybackSpeed(defaultSpeed); + }); + } } // ======================================== diff --git a/js/storage.js b/js/storage.js index c238f58..dfdaa98 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1352,6 +1352,11 @@ export const audioEffectsSettings = { localStorage.setItem(this.SPEED_KEY, validSpeed.toString()); }, + resetSpeed() { + this.setSpeed(1.0); + return 1.0; + }, + // Preserve pitch when changing speed (default true) isPreservePitchEnabled() { try { diff --git a/styles.css b/styles.css index 49ae84b..e779837 100644 --- a/styles.css +++ b/styles.css @@ -2617,6 +2617,65 @@ input[type='search']::-webkit-search-cancel-button { width: 100px; } +.playback-speed-control { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.playback-speed-slider { + appearance: none; + width: 150px; + height: 6px; + background: var(--border); + border-radius: var(--radius-full); + cursor: pointer; + outline: none; +} + +.playback-speed-slider::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + transition: transform 0.1s ease; +} + +.playback-speed-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +.playback-speed-number-input { + width: 80px; + padding: 0.25rem 0.5rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--input); + color: var(--foreground); + text-align: center; + font-size: 0.9rem; +} + +.playback-speed-unit { + font-size: 0.9rem; + color: var(--muted-foreground); + min-width: 1rem; +} + +.playback-speed-number-input:focus { + outline: none; + border-color: var(--primary); +} + +/* Hide arrows/spinners for number input */ +.playback-speed-number-input::-webkit-outer-spin-button, +.playback-speed-number-input::-webkit-inner-spin-button { + appearance: none; + margin: 0; +} + .template-input { width: 100%; max-width: 400px;