Spaces:
Sleeping
Sleeping
| 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 | |