my-ai-songs / static /js /audio-player.js
Sebastiankay's picture
Upload 15 files
ff05e25
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