import chroma from "https://unpkg.com/chroma-js@3.0.0/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) => ``) .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 = '
Keine Musikdateien gefunden.
'; // 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) => ``) .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 = `${file.title}