File size: 29,394 Bytes
728ff90
 
 
 
 
 
 
 
 
 
 
 
ff05e25
728ff90
 
 
 
 
 
 
 
ff05e25
728ff90
 
 
 
 
 
 
ff05e25
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
 
 
ff05e25
2949f9a
ff05e25
2949f9a
728ff90
 
2949f9a
ff05e25
2949f9a
728ff90
2949f9a
728ff90
2949f9a
 
728ff90
ff05e25
 
2949f9a
728ff90
 
2949f9a
728ff90
 
2949f9a
728ff90
2949f9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728ff90
 
2949f9a
728ff90
2949f9a
 
 
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff05e25
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
 
 
 
ff05e25
 
 
2949f9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff05e25
 
728ff90
 
 
 
 
 
 
 
2949f9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
 
 
 
 
 
 
 
ff05e25
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949f9a
 
 
 
 
728ff90
 
 
 
 
 
 
 
 
 
 
ff05e25
 
728ff90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff05e25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728ff90
 
 
 
 
ff05e25
 
 
 
 
 
 
728ff90
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
import chroma from "https://unpkg.com/[email protected]/index.js";

(function () {
  "use strict"; // Hilft, häufige Fehler zu vermeiden

  // DOM Elemente sicher auswählen
  const audioPlayerWrapper = document.getElementById("audio_player_wrapper");
  const playPauseButton = document.getElementById("play-pause-button");
  const volumeSlider = document.getElementById("volume-slider");
  const volumeIcon = document.getElementById("volume-icon"); // Get the volume icon element
  const playerCover = document.getElementById("player-cover");
  const playerTitle = document.getElementById("player-title");
  const playerGenres = document.getElementById("player-genres"); // Für Genre/Artist Info
  const waveformContainer = document.getElementById("waveform");
  const audioControlBar = document.getElementById("audio-control-bar");
  const siteTitle = document.getElementById("site-title");
  const siteSubtitle = document.getElementById("site-subtitle");
  const trackListSection = document.getElementById("track-list");
  const timePlayedSpan = document.getElementById("timePlayed"); // Get time played span
  const timeCompleteSpan = document.getElementById("timeComplete"); // Get time complete span
  const musicCardsContainer = document.getElementById("music-cards-container"); // Container für die Karten
  const playerInfoContainer = document.getElementById("player-info-container"); // Container for player info (cover, title, etc.)

  // Wavesurfer Instanz und Zustand
  let wavesurfer = null;
  let currentTrackInfo = null; // Speichert Infos zum aktuellen Track { url, cover, title, ..., element }
  let currentlyPlayingElement = null; // Speichert das .card-image Element des spielenden Tracks
  let isLoading = false; // Verhindert Klicks während des Ladens

  const isTouchDevice = window.matchMedia("(pointer: coarse)").matches; // Check for touch capability
  // --- Hilfsfunktionen ---

  // Formatiert Sekunden in MM:SS
  function formatTime(seconds) {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = Math.floor(seconds % 60);
    const formattedTime = `${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}`;
    return formattedTime;
  }

  // Aktualisiert das Lautstärke-Icon basierend auf dem Wert
  function updateVolumeIcon(volume) {
    volumeIcon.classList.remove("fa-volume-high", "fa-volume-low", "fa-volume-off"); // Sicherstellen, dass alte Klassen entfernt werden
    if (volume <= 0) volumeIcon.classList.add("fa-volume-off");
    else if (volume < 0.5) volumeIcon.classList.add("fa-volume-low"); // Adjusted threshold for better distribution
    else volumeIcon.classList.add("fa-volume-high");
  }

  // Aktualisiert den visuellen Zustand einer Track-Karte und des globalen Players
  function updatePlayerState(element, state) {
    // state: 'playing', 'pause', 'loading', 'idle'
    console.log(`Updating state: ${state}`, element); // Mehr Logging

    // Alten Zustand vom vorherigen Element entfernen (nur wenn es ein anderes Element ist)
    // Auch .is-in-view entfernen, wenn der Zustand wechselt (wird ggf. vom Observer wieder hinzugefügt)
    if (currentlyPlayingElement && currentlyPlayingElement !== element) {
      currentlyPlayingElement.classList.remove("playing", "pause", "loading", "is-in-view");
      console.log(`Removed state from previous element:`, currentlyPlayingElement);
    }

    // Setze das aktuell aktive Element (kann null sein bei idle)
    // Wenn idle, auch .is-in-view entfernen
    currentlyPlayingElement = state === "idle" ? null : element;

    // Neuen Zustand auf das aktuelle Element setzen (falls vorhanden und nicht idle)
    if (element && state !== "idle") {
      // Stelle sicher, dass andere Zustände entfernt werden, bevor der neue gesetzt wird
      element.classList.remove("playing", "pause", "loading");
      element.classList.add(state);
      // Wenn ein Track aktiv wird (playing/pause/loading), soll der reine "in-view" Effekt nicht gelten
      element.classList.remove("is-in-view");
      console.log(`Set state '${state}' on element:`, element);
    }

    // Globalen Player-Wrapper und Play/Pause-Button aktualisieren
    if (state === "playing") {
      playPauseButton.setAttribute("data-state", "playing");
      audioPlayerWrapper.classList.add("active"); // Sicherstellen, dass aktiv
      trackListSection.classList.add("player-active");
      console.log("Player wrapper set to active (playing)");
    } else if (state === "pause") {
      playPauseButton.setAttribute("data-state", "pause");
      audioPlayerWrapper.classList.add("active"); // Bleibt aktiv bei Pause
      trackListSection.classList.add("player-active");
      console.log("Player wrapper remains active (paused)");
    } else if (state === "loading") {
      playPauseButton.setAttribute("data-state", "pause"); // Zeigt Pause-Icon während des Ladens
      audioPlayerWrapper.classList.add("active"); // Bleibt aktiv während des Ladens
      trackListSection.classList.add("player-active");
      console.log("Player wrapper remains active (loading)");
    } else if (state === "idle") {
      playPauseButton.setAttribute("data-state", "pause"); // Zeigt Pause-Icon im Idle-Zustand
      audioPlayerWrapper.classList.remove("active"); // Nur bei IDLE inaktiv setzen
      trackListSection.classList.remove("player-active");
      console.log("Player wrapper set to inactive (idle)");
    }

    // Ladezustand für Klicks (wird jetzt auch im Player-Wrapper gesetzt)
    isLoading = state === "loading";
    const loadingState = state === "loading";
    trackListSection.classList.toggle("player-loading", loadingState);
    audioPlayerWrapper.classList.toggle("player-loading", loadingState);
    console.log(`Loading flag set to: ${isLoading}`);
  }

  // Holt Track-Daten aus dem localStorage
  function getStoredTrackPeaks(encodedTitle) {
    try {
      const tracklist = JSON.parse(localStorage.getItem("tracklist")) || [];
      const found = tracklist.find((track) => track.encoded_title === encodedTitle);
      return found ? found.peaks : null;
    } catch (e) {
      console.error("Fehler beim Lesen des localStorage:", e);
      // Optional: localStorage leeren bei Fehler
      // localStorage.removeItem("tracklist");
      return null;
    }
  }

  // Speichert Track-Daten (inkl. Peaks) im localStorage
  function saveTrackPeaks(trackData) {
    if (!trackData || !trackData.encoded_title || !trackData.peaks) return;
    try {
      let tracklist = JSON.parse(localStorage.getItem("tracklist")) || [];
      // Prüfen, ob Track schon existiert (um Duplikate zu vermeiden)
      const existingIndex = tracklist.findIndex((track) => track.encoded_title === trackData.encoded_title);
      if (existingIndex > -1) {
        // Nur Peaks aktualisieren, falls nötig (oder einfach überschreiben)
        tracklist[existingIndex].peaks = trackData.peaks;
      } else {
        // Nur relevante Daten speichern, um localStorage zu schonen
        tracklist.push({
          encoded_title: trackData.encoded_title,
          peaks: trackData.peaks,
          // Optional: Weitere Daten wie Titel speichern, falls nützlich
          // title: trackData.title
        });
      }
      // Optional: Alte Einträge entfernen, wenn Liste zu groß wird (LRU Cache o.ä.)
      localStorage.setItem("tracklist", JSON.stringify(tracklist));
    } catch (e) {
      console.error("Fehler beim Schreiben in den localStorage:", e);
    }
  }

  // Funktion zum Kopieren der URL
  function copyURLToClipboard(url) {
    if (!url) return;
    // Sicherstellen, dass die URL absolut ist (bereits im HTML mit _external=True)
    const absoluteUrl = encodeURI(url); // URLs sollten bereits encoded sein, aber sicherheitshalber nochmal
    navigator.clipboard
      .writeText(absoluteUrl)
      .then(() => {
        console.log("URL in die Zwischenablage kopiert:", absoluteUrl);
        // Optional: Visuelles Feedback für den Benutzer
        // alert('URL kopiert!');
      })
      .catch((err) => {
        console.error("Fehler beim Kopieren der URL: ", err);
        // Optional: Fehlermeldung für den Benutzer
        // alert('Konnte URL nicht kopieren.');
      });
  }

  // --- Wavesurfer Initialisierung und Steuerung ---
  function initWaveSurfer(progressColor = "#7c3aed", peakData = null) {
    // Bestehende Instanz zerstören, falls vorhanden
    if (wavesurfer) {
      wavesurfer.destroy();
    }

    wavesurfer = WaveSurfer.create({
      container: waveformContainer,
      waveColor: "rgba(255, 255, 255, 0.8)", // Etwas transparenter vielleicht?
      progressColor: progressColor,
      height: 60,
      cursorWidth: 0,
      barWidth: 6, // Etwas schmaler
      barGap: 2,
      barRadius: 3,
      peaks: peakData, // Vorgefertigte Peaks verwenden, falls vorhanden
      normalize: true, // Normalisiert die Wellenform-Höhe
      // responsive: true, // Passt sich an Containergröße an (ggf. Performance beachten)
      // hideScrollbar: true
    });

    // --- Wavesurfer Event Listener ---
    wavesurfer.on("ready", () => {
      const duration = wavesurfer.getDuration();
      console.log("Wavesurfer ready, playing track.");
      wavesurfer.play();
      // Peaks extrahieren und speichern (nachdem sie generiert wurden)
      if (currentTrackInfo && !currentTrackInfo.peaks) {
        // Nur speichern, wenn nicht aus Cache geladen
        currentTrackInfo.peaks = wavesurfer.exportPeaks();
        saveTrackPeaks(currentTrackInfo);
        console.log("Peaks generiert und gespeichert für:", currentTrackInfo.title);
      }
      // Update total time display
      timeCompleteSpan.textContent = formatTime(duration);
      updatePlayerState(currentTrackInfo.element, "playing"); // Zustand auf 'playing' setzen
      // Remove blur from player info container when ready
      playerInfoContainer?.classList.remove("is-blurring");
    });

    wavesurfer.on("play", () => {
      console.log("Wavesurfer play event");
      if (currentTrackInfo) updatePlayerState(currentTrackInfo.element, "playing");
    });

    wavesurfer.on("pause", () => {
      console.log("Wavesurfer pause event");
      if (currentTrackInfo) updatePlayerState(currentTrackInfo.element, "pause");
    });

    wavesurfer.on("finish", () => {
      console.log("Track finished");
      const finishedTrackElement = currentTrackInfo ? currentTrackInfo.element : null;
      const finishedTrackUrl = currentTrackInfo ? currentTrackInfo.url : null;
      // currentTrackInfo wird erst zurückgesetzt, NACHDEM der nächste Track geladen wird oder wenn keiner gefunden wird.

      // Add blur to player info container on finish
      playerInfoContainer?.classList.add("is-blurring");

      // --- Autoplay Random Song ---
      const allTrackTriggers = Array.from(musicCardsContainer.querySelectorAll(".track-play-trigger"));

      if (allTrackTriggers.length > 0) {
        let nextTrackElement;
        if (allTrackTriggers.length === 1) {
          nextTrackElement = allTrackTriggers[0]; // Play the only track again
        } else {
          // Filter out the track that just finished, if possible
          const potentialNextTracks = allTrackTriggers.filter((el) => el.dataset.trackUrl !== finishedTrackUrl);
          const tracksToChooseFrom = potentialNextTracks.length > 0 ? potentialNextTracks : allTrackTriggers; // Fallback if only one track exists or filtering failed
          const randomIndex = Math.floor(Math.random() * tracksToChooseFrom.length);
          nextTrackElement = tracksToChooseFrom[randomIndex];
        }
        console.log("Playing random next track:", nextTrackElement.dataset.trackTitle);
        // Wichtig: Player bleibt aktiv, loadTrackFromElement setzt den 'loading' Zustand
        loadTrackFromElement(nextTrackElement); // Use helper to load
      } else {
        currentTrackInfo = null; // Jetzt zurücksetzen, da kein nächster Track kommt
        updatePlayerState(finishedTrackElement, "idle"); // Player inaktiv setzen, wenn keine Tracks mehr da sind
      }
      // --- End Autoplay ---
    });

    wavesurfer.on("loading", (percent) => {
      // console.log('Loading: ' + percent + '%'); // Kann für Ladeanzeige genutzt werden
    });

    // Update current time display during playback
    wavesurfer.on("audioprocess", (currentTime) => {
      // console.log("Current time:", currentTime); // Debugging
      timePlayedSpan.textContent = formatTime(currentTime);
      audioControlBar.style.setProperty("--progress", `${(currentTime / wavesurfer.getDuration()) * 100}%`);
    });

    wavesurfer.on("error", (error) => {
      console.error("Wavesurfer error:", error);
      updatePlayerState(currentTrackInfo ? currentTrackInfo.element : null, "idle"); // Zustand bei Fehler zurücksetzen
      alert(`Fehler beim Laden des Tracks: ${currentTrackInfo?.title || "Unbekannt"}`);
      currentTrackInfo = null;
      // Remove blur from player info container on error
      playerInfoContainer?.classList.remove("is-blurring");
      isLoading = false; // Loading Flag zurücksetzen
      trackListSection.classList.remove("player-loading");
      audioPlayerWrapper.classList.remove("player-loading");
    });

    // Initial Lautstärke setzen (aus Slider)
    if (wavesurfer) wavesurfer.setVolume(parseFloat(volumeSlider.value));
  }
  // Helper function to extract data and load track from an element
  function loadTrackFromElement(element) {
    if (!element) return;
    const trackData = {
      url: element.dataset.trackUrl,
      cover: element.dataset.trackCover,
      title: element.dataset.trackTitle,
      encoded_title: element.dataset.trackEncodedTitle,
      genre: element.dataset.trackGenre,
      dominantColor: element.dataset.trackDominantColor,
      complementaryColor: element.dataset.trackComplementaryColor,
      // peaks: null // Will be loaded/set in loadTrack
    };
    loadTrack(trackData, element);
  }

  // --- Hauptfunktion zum Laden eines Tracks ---
  function loadTrack(trackData, targetElement) {
    if (isLoading) {
      console.log("Player is already loading a track.");
      return;
    }

    // Prüfen, ob derselbe Track geklickt wurde (Play/Pause Umschaltung)
    if (wavesurfer && currentTrackInfo && currentTrackInfo.url === trackData.url && currentTrackInfo.element === targetElement) {
      console.log("Toggling play/pause for the current track.");
      wavesurfer.playPause();
      // Der Zustand wird durch die 'play'/'pause' Events aktualisiert
      return;
    }

    console.log("Loading new track:", trackData.title);
    isLoading = true;
    currentTrackInfo = { ...trackData, element: targetElement }; // Alle Infos + Element speichern
    updatePlayerState(targetElement, "loading");

    // Player UI aktualisieren
    const coverUrl = encodeURI(trackData.cover); // Sicherstellen, dass URL encoded ist
    audioPlayerWrapper.style.setProperty("--audio-player-background-cover", `url('${coverUrl}')`);
    audioPlayerWrapper.style.setProperty("--color-accent", chroma(trackData.complementaryColor).rgb());
    //audioPlayerWrapper.style.setProperty("--color-dominant", chroma(trackData.dominantColor));

    playerCover.src = coverUrl;
    playerCover.alt = `Cover von ${trackData.title}`; // Besserer Alt-Text
    playerTitle.textContent = trackData.title;

    let tdg = "";
    if (trackData.genre) {
      tdg = trackData.genre
        .split(",")
        .sort() // <-- Add sorting here
        .map((g) => `<span class="button is-small">${g.trim()}</span>`)
        .join(" ");
    }
    playerGenres.innerHTML = tdg || "Unbekanntes Genre"; // Genre als Fallback anzeigen
    // Peaks aus localStorage holen
    currentTrackInfo.peaks = getStoredTrackPeaks(trackData.encoded_title);
    if (currentTrackInfo.peaks) {
      console.log("Found stored peak data.");
    } else {
      console.log("No stored peak data found, will generate.");
    }

    // Wavesurfer (neu) initialisieren
    if (wavesurfer) {
      wavesurfer.destroy();
    }

    initWaveSurfer(chroma(trackData.complementaryColor), currentTrackInfo.peaks);

    // Track laden (URL-Prüfung)
    let trackUrl = trackData.url;
    if (trackUrl.startsWith("http://")) {
      trackUrl = trackUrl.replace("http://", "https://");
    }
    console.log("Loading URL:", trackUrl);
    wavesurfer.load(trackUrl);
  }

  // --- Event Listener ---

  // Event Delegation für Klicks auf Tracks und Copy-Buttons
  trackListSection.addEventListener("click", (event) => {
    const trackTrigger = event.target.closest(".track-play-trigger");
    const copyButton = event.target.closest(".copy-url-button");

    if (trackTrigger && !isLoading) {
      // Track-Daten aus data-* Attributen sammeln
      const trackData = {
        url: trackTrigger.dataset.trackUrl,
        cover: trackTrigger.dataset.trackCover,
        title: trackTrigger.dataset.trackTitle,
        encoded_title: trackTrigger.dataset.trackEncodedTitle,
        genre: trackTrigger.dataset.trackGenre,
        dominantColor: trackTrigger.dataset.trackDominantColor,
        complementaryColor: trackTrigger.dataset.trackComplementaryColor,
        // peaks: null // Wird später geladen oder gesetzt
      };
      loadTrack(trackData, trackTrigger);
    } else if (copyButton && !isLoading) {
      // Also prevent copy during loading
      copyURLToClipboard(copyButton.dataset.url);
    }
  });

  // Globaler Play/Pause Button
  playPauseButton.addEventListener("click", () => {
    if (wavesurfer && currentTrackInfo) {
      // Nur wenn ein Track geladen ist/war
      wavesurfer.playPause();
    } else if (!isLoading && trackListSection.querySelector(".track-play-trigger")) {
      // Optional: Ersten Track abspielen, wenn nichts geladen ist
      // const firstTrackElement = trackListSection.querySelector('.track-play-trigger');
      // firstTrackElement.click();
      console.log("Kein Track geladen zum Abspielen/Pausieren.");
    }
  });

  // Lautstärkeregler
  volumeSlider.addEventListener("input", (e) => {
    const volume = parseFloat(e.target.value);
    if (wavesurfer) {
      wavesurfer.setVolume(volume);
    }
    // Update icon
    updateVolumeIcon(volume);
    // Save volume to localStorage
    localStorage.setItem("playerVolume", volume);
    console.log("Volume saved:", volume); // Debugging
  });

  // Titel-Hover-Animation (optimiert)
  trackListSection.addEventListener("mouseover", (event) => {
    const titleElement = event.target.closest(".track-title");
    if (titleElement) {
      // Prüfen, ob Text tatsächlich überläuft
      const isOverflowing = titleElement.scrollWidth > titleElement.offsetWidth;
      if (isOverflowing) {
        titleElement.classList.add("is-overflowing");
      }
    }
  });

  trackListSection.addEventListener("mouseout", (event) => {
    const titleElement = event.target.closest(".track-title");
    if (titleElement) {
      // Animation stoppen/Klasse entfernen
      titleElement.addEventListener("transitionend", () => {
        titleElement.classList.remove("is-overflowing");
      });
    }
  });
  // --- Funktion zum Erstellen und Anzeigen der Musikkarten ---
  function displayMusicList(musicFiles) {
    if (!musicCardsContainer) {
      console.error("Music cards container not found!");
      return;
    }

    // Leere den Container, bevor neue Karten hinzugefügt werden
    musicCardsContainer.innerHTML = "";

    if (!musicFiles || musicFiles.length === 0) {
      musicCardsContainer.innerHTML = '<p class="column">Keine Musikdateien gefunden.</p>'; // Nachricht, wenn keine Dateien vorhanden sind
      return;
    }

    musicFiles.forEach((file) => {
      // 1. Erstelle das äußere Column-Div
      const columnDiv = document.createElement("div");
      columnDiv.className = "column is-one-quarter";

      // 2. Erstelle das Card-Div
      const cardDiv = document.createElement("div");
      cardDiv.className = "card";
      cardDiv.style.setProperty("--color-accent", `rgba(${file.complementary_color}, 1)`);
      cardDiv.style.setProperty("--color-dominant", `rgba(${file.dominant_color}, 1)`); // Keep original dominant color

      // 3. Erstelle das Card-Image-Div (Trigger)
      const cardImageDiv = document.createElement("div");

      // --- Added Color Adjustment ---
      let complementaryColor = chroma(`rgb(${file.complementary_color})`);
      // Adjust saturation and lightness for brighter and more saturated color
      const colorLightDark = chroma(complementaryColor).get("lab.l") < 50 ? "dark" : chroma(complementaryColor).get("lab.l") > 70 ? "light" : null;
      if (colorLightDark === "light") {
        complementaryColor = chroma(complementaryColor).saturate(2).brighten(0.5).hex();
      } else {
        complementaryColor = chroma(complementaryColor).saturate(2).brighten(2).hex();
      }
      cardDiv.style.setProperty("--color-accent", complementaryColor); // Update CSS variable with adjusted color
      // --- End Added Color Adjustment ---

      cardImageDiv.className = "card-image track-play-trigger";
      cardImageDiv.style.backgroundImage = `url('/static/coverarts/${file.cover_art}')`;
      // Setze data-* Attribute
      cardImageDiv.dataset.trackUrl = `/files/music/${file.filename}`;
      cardImageDiv.dataset.trackCover = `/static/coverarts/${file.cover_art}`;
      cardImageDiv.dataset.trackTitle = file.title;
      cardImageDiv.dataset.trackEncodedTitle = file.encoded_title;
      cardImageDiv.dataset.trackGenre = file.genre || ""; // Stelle sicher, dass Genre existiert
      cardImageDiv.dataset.trackComplementaryColor = complementaryColor; // Use adjusted color hex
      cardImageDiv.dataset.trackDominantColor = file.dominant_color; // Keep original dominant color string

      // 4. Erstelle das zweite Card-Image-Div (für den Effekt)
      const cardImage2Div = document.createElement("div");
      cardImage2Div.className = "card-image-2";
      cardImage2Div.style.backgroundImage = `url('/static/coverarts/${file.cover_art}')`;

      // 5. Erstelle das Card-Content-Div
      const cardContentDiv = document.createElement("div");
      cardContentDiv.className = "card-content";

      // Formatierte Dauer
      const minutes = Math.floor(file.duration / 60);
      const seconds = String(file.duration % 60).padStart(2, "0");
      const formattedDuration = `${minutes}:${seconds}`;

      // Genre-Buttons (falls Genre vorhanden ist)
      let genreHtml = "";
      if (file.genre) {
        genreHtml = file.genre
          .split(",")
          .sort() // <-- Add sorting here
          .map((g) => `<span class="button is-small">${g.trim()}</span>`)
          .join(" ");
      }

      // InnerHTML für Card-Content (vereinfacht die Struktur)
      // Wichtig: encodeURIComponent für den Dateinamen im Download-Link
      // Wichtig: Die URL für den Copy-Button muss serverseitig generiert werden, wenn sie komplex ist.
      //          Hier nehmen wir an, dass die API die benötigte URL direkt liefert oder wir sie konstruieren können.
      //          Wenn url_for('music', ...) benötigt wird, muss diese URL in den JSON-Daten enthalten sein.
      //          Wir verwenden hier den einfachen Dateipfad als Beispiel.
      const copyUrl = `/files/music/${file.filename}`; // Beispiel: Einfache URL-Konstruktion

      cardContentDiv.innerHTML = `

        <div class="media mb-3">

          <div class="media-content">

            <p class="title is-5 track-title" data-title="${file.title}">${file.title}</p>

          </div>

        </div>

        <div class="content content is-flex is-flex-direction-column is-align-items-flex-start is-justify-content-space-around">

          <div class="mb-4">${genreHtml}</div>

          <a class="button is-small is-primary" href="/files/music/${encodeURIComponent(file.filename)}" download><i class="fa-solid fa-download"></i></a>

          <!--button class="button is-small copy-url-button" data-url="${copyUrl}"><i class="fa-solid fa-clipboard"></i></button-->

        <span class="track-duration button is-small">${formattedDuration}</span>

        </div>

      `;

      // 6. Füge die Teile zusammen
      cardDiv.appendChild(cardImageDiv);
      cardDiv.appendChild(cardImage2Div);
      cardDiv.appendChild(cardContentDiv);
      columnDiv.appendChild(cardDiv);

      // 7. Füge die fertige Karte zum Container hinzu
      musicCardsContainer.appendChild(columnDiv);
      // Observe the new card if Intersection Observer is supported
      if (cardIntersectionObserver) cardIntersectionObserver.observe(cardDiv);
    });
  }

  // Function to fetch music data
  async function fetchMusicData() {
    try {
      // Make a GET request to the /api/music endpoint
      const response = await fetch("/api/music");

      // Check if the request was successful (status code 200-299)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      // Parse the JSON response body
      const musicFiles = await response.json();

      // Now you have the music_files data in the 'musicFiles' variable
      console.log("Successfully fetched music data:", musicFiles);

      // You can now use the musicFiles array to update your UI, etc.
      // Display the music list using the fetched data
      displayMusicList(musicFiles);
    } catch (error) {
      // Handle any errors that occurred during the fetch
      console.error("Could not fetch music data:", error);
    }
  }

  // Call the function to fetch the data when needed (e.g., on page load)
  fetchMusicData();

  // --- Initial Volume Setup ---
  function initializeVolume() {
    const savedVolume = localStorage.getItem("playerVolume");
    const initialVolume = savedVolume !== null ? parseFloat(savedVolume) : 1.0; // Default to 1.0 if nothing saved
    volumeSlider.value = initialVolume;
    updateVolumeIcon(initialVolume); // Set initial icon
    if (wavesurfer) wavesurfer.setVolume(initialVolume); // Set wavesurfer volume if already initialized
    console.log("Initial volume set to:", initialVolume); // Debugging
  }
  initializeVolume(); // Call this function on script load

  // --- Title Gradient Angle based on Mouse Position ---
  function updateTitleGradient(event) {
    if (!siteTitle) return; // Ensure siteTitle exists

    const centerX = window.innerWidth / 2;
    const centerY = window.innerHeight / 2;
    const mouseX = event.clientX;
    const mouseY = event.clientY;

    const deltaX = mouseX - centerX;
    const deltaY = mouseY - centerY;

    const angleRad = Math.atan2(deltaY, deltaX);
    const angleDeg = (angleRad * 180) / Math.PI + 180; // Convert to degrees (0-360 range)

    siteTitle.style.setProperty("--title-gradient-deg", `${angleDeg.toFixed(2)}deg`);
    console.log(angleDeg.toFixed(2));
  }

  particlesJS.load("particles-js", "static/particles.json", function () {
    console.log("callback - particles.js config loaded");
  });

  // Add listener to the body for mouse movement
  document.body.addEventListener("mousemove", updateTitleGradient);

  // --- Intersection Observer for Card View ---
  let cardIntersectionObserver = null;

  function setupIntersectionObserver() {
    const options = {
      root: null, // relative to document viewport
      rootMargin: "0px",
      threshold: 0.1, // Trigger when 10% of the element is visible
    };

    const handleIntersection = (entries, observer) => {
      entries.forEach((entry) => {
        const cardImage = entry.target.querySelector(".card-image.track-play-trigger");
        if (!cardImage) return;

        // Only apply is-in-view if the card is NOT actively playing, paused or loading
        const isActive = cardImage.classList.contains("playing") || cardImage.classList.contains("pause") || cardImage.classList.contains("loading");

        if (entry.isIntersecting && !isActive) {
          cardImage.classList.add("is-in-view");
        } else {
          cardImage.classList.remove("is-in-view");
        }
      });
    };
    cardIntersectionObserver = new IntersectionObserver(handleIntersection, options);
  }
  // --- Initialisierung ---
  console.log("Audio Player Script initialized.");
  // Optional: Letzten Track beim Laden der Seite wiederherstellen?
  // const lastTrack = JSON.parse(localStorage.getItem("lastPlayedTrack"));
  // if(lastTrack) { /* Lade Logik */ }

  // Setup observer only on touch devices (or universally if desired)
  if (isTouchDevice && "IntersectionObserver" in window) {
    console.log("Setting up Intersection Observer for cards (touch device).");
    setupIntersectionObserver();
    // Initial observation happens in displayMusicList after cards are created
  }
})(); // Ende der IIFE