Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DE Radio Stream | Ultimate Player</title> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <!-- FontAwesome für Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: rgba(30, 41, 59, 0.7); | |
| --accent-color: #3b82f6; | |
| --accent-glow: rgba(59, 130, 246, 0.5); | |
| --text-main: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| --glass-border: 1px solid rgba(255, 255, 255, 0.1); | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.15) 0%, transparent 40%), | |
| radial-gradient(circle at 90% 80%, rgba(236, 72, 153, 0.15) 0%, transparent 40%); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| padding: 1.5rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| backdrop-filter: blur(10px); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| border-bottom: var(--glass-border); | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .brand i { | |
| font-size: 1.8rem; | |
| color: var(--accent-color); | |
| filter: drop-shadow(0 0 10px var(--accent-glow)); | |
| } | |
| .brand h1 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| transition: var(--transition); | |
| border: var(--glass-border); | |
| } | |
| .anycoder-link:hover { | |
| color: var(--text-main); | |
| background: rgba(255, 255, 255, 0.1); | |
| transform: translateY(-2px); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 400px; | |
| gap: 2rem; | |
| padding: 2rem; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| padding: 1rem; | |
| } | |
| } | |
| /* --- Player Section --- */ | |
| .player-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| .visualizer-card { | |
| background: var(--card-bg); | |
| border-radius: 24px; | |
| padding: 2rem; | |
| border: var(--glass-border); | |
| backdrop-filter: blur(20px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| min-height: 400px; | |
| box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.5); | |
| } | |
| canvas#audioVisualizer { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| opacity: 0.6; | |
| } | |
| .now-playing-info { | |
| z-index: 2; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .station-logo { | |
| width: 150px; | |
| height: 150px; | |
| border-radius: 50%; | |
| object-fit: cover; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| margin-bottom: 1.5rem; | |
| border: 4px solid rgba(255,255,255,0.1); | |
| animation: rotateDisk 10s linear infinite; | |
| animation-play-state: paused; | |
| } | |
| .station-logo.playing { | |
| animation-play-state: running; | |
| border-color: var(--accent-color); | |
| } | |
| @keyframes rotateDisk { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .station-name { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| text-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| } | |
| .station-meta { | |
| color: var(--text-secondary); | |
| font-size: 1rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| } | |
| .live-badge { | |
| background: var(--danger); | |
| color: white; | |
| font-size: 0.7rem; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .controls { | |
| z-index: 2; | |
| display: flex; | |
| align-items: center; | |
| gap: 2rem; | |
| background: rgba(0,0,0,0.2); | |
| padding: 1rem 2rem; | |
| border-radius: 50px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .btn-control { | |
| background: none; | |
| border: none; | |
| color: var(--text-main); | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .btn-control:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--accent-color); | |
| } | |
| .btn-play { | |
| background: var(--accent-color); | |
| color: white; | |
| font-size: 1.8rem; | |
| width: 70px; | |
| height: 70px; | |
| box-shadow: 0 0 20px var(--accent-glow); | |
| } | |
| .btn-play:hover { | |
| transform: scale(1.1); | |
| background: #2563eb; | |
| color: white; | |
| } | |
| .volume-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| z-index: 2; | |
| margin-top: 1rem; | |
| width: 100%; | |
| max-width: 300px; | |
| } | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: var(--text-main); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(255,255,255,0.5); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 2px; | |
| } | |
| /* --- Playlist Section --- */ | |
| .playlist-section { | |
| background: var(--card-bg); | |
| border-radius: 24px; | |
| border: var(--glass-border); | |
| backdrop-filter: blur(20px); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| height: calc(100vh - 140px); | |
| position: sticky; | |
| top: 100px; | |
| } | |
| @media (max-width: 900px) { | |
| .playlist-section { | |
| height: 600px; | |
| position: static; | |
| } | |
| } | |
| .playlist-header { | |
| padding: 1.5rem; | |
| border-bottom: var(--glass-border); | |
| } | |
| .search-box { | |
| position: relative; | |
| width: 100%; | |
| } | |
| .search-box i { | |
| position: absolute; | |
| left: 15px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-secondary); | |
| } | |
| .search-box input { | |
| width: 100%; | |
| padding: 12px 12px 12px 45px; | |
| border-radius: 12px; | |
| border: var(--glass-border); | |
| background: rgba(0,0,0,0.2); | |
| color: var(--text-main); | |
| font-size: 1rem; | |
| outline: none; | |
| transition: var(--transition); | |
| } | |
| .search-box input:focus { | |
| border-color: var(--accent-color); | |
| background: rgba(0,0,0,0.4); | |
| } | |
| .station-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0.5rem; | |
| } | |
| /* Scrollbar Styling */ | |
| .station-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .station-list::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .station-list::-webkit-scrollbar-thumb { | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 3px; | |
| } | |
| .station-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 1rem; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| margin-bottom: 0.5rem; | |
| border: 1px solid transparent; | |
| } | |
| .station-item:hover { | |
| background: rgba(255,255,255,0.05); | |
| } | |
| .station-item.active { | |
| background: rgba(59, 130, 246, 0.15); | |
| border-color: var(--accent-color); | |
| } | |
| .station-item img { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 10px; | |
| object-fit: cover; | |
| margin-right: 1rem; | |
| } | |
| .station-details { | |
| flex: 1; | |
| } | |
| .station-details h4 { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| } | |
| .station-details span { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| background: rgba(255,255,255,0.05); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| } | |
| .play-indicator { | |
| opacity: 0; | |
| color: var(--accent-color); | |
| transition: var(--transition); | |
| } | |
| .station-item.active .play-indicator { | |
| opacity: 1; | |
| } | |
| .fav-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 8px; | |
| transition: var(--transition); | |
| } | |
| .fav-btn:hover, .fav-btn.active { | |
| color: #ec4899; | |
| } | |
| /* --- Footer --- */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| } | |
| /* Status Toast */ | |
| .status-toast { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background: var(--card-bg); | |
| border: var(--glass-border); | |
| padding: 12px 24px; | |
| border-radius: 50px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| z-index: 1000; | |
| backdrop-filter: blur(10px); | |
| } | |
| .status-toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: var(--text-secondary); | |
| } | |
| .status-toast.success .status-dot { background: var(--success); box-shadow: 0 0 10px var(--success); } | |
| .status-toast.error .status-dot { background: var(--danger); box-shadow: 0 0 10px var(--danger); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-radio"></i> | |
| <h1>DE Radio<span style="color:var(--accent-color)">.Stream</span></h1> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square" style="font-size: 0.7em; margin-left:5px;"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Player Section --> | |
| <section class="player-section"> | |
| <div class="visualizer-card"> | |
| <canvas id="audioVisualizer"></canvas> | |
| <div class="now-playing-info"> | |
| <img id="currentLogo" src="https://picsum.photos/seed/radio/300/300" alt="Station Logo" class="station-logo"> | |
| <h2 id="currentName" class="station-name">Wähle einen Sender</h2> | |
| <div class="station-meta"> | |
| <span id="currentGenre" class="genre">Pop</span> | |
| <span class="live-badge" id="liveBadge" style="display:none;">LIVE</span> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn-control" id="prevBtn" title="Vorheriger"> | |
| <i class="fa-solid fa-backward-step"></i> | |
| </button> | |
| <button class="btn-control btn-play" id="playBtn" title="Play/Pause"> | |
| <i class="fa-solid fa-play" id="playIcon"></i> | |
| </button> | |
| <button class="btn-control" id="nextBtn" title="Nächster"> | |
| <i class="fa-solid fa-forward-step"></i> | |
| </button> | |
| </div> | |
| <div class="volume-container"> | |
| <i class="fa-solid fa-volume-low" style="color: var(--text-secondary)"></i> | |
| <input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="0.8"> | |
| <i class="fa-solid fa-volume-high" style="color: var(--text-secondary)"></i> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Playlist Section --> | |
| <section class="playlist-section"> | |
| <div class="playlist-header"> | |
| <div class="search-box"> | |
| <i class="fa-solid fa-magnifying-glass"></i> | |
| <input type="text" id="searchInput" placeholder="Sender suchen..."> | |
| </div> | |
| </div> | |
| <ul class="station-list" id="stationList"> | |
| <!-- Stations werden hier per JS eingefügt --> | |
| </ul> | |
| </section> | |
| </main> | |
| <footer> | |
| © 2023 DE Radio Stream. Designed for modern browsers. | |
| </footer> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="status-toast"> | |
| <div class="status-dot"></div> | |
| <span id="toastMessage">Nachricht</span> | |
| </div> | |
| <script> | |
| // --- Radio Station Data --- | |
| // Verwendet öffentliche Stream-URLs. Einige erfordern CORS-Header, die wir im Audio-Objekt setzen. | |
| const stations = [ | |
| { | |
| id: 1, | |
| name: "Antenne Bayern", | |
| genre: "Pop & Hits", | |
| url: "https://mp3channels.webradio.antenne.de/antenne-bayern", | |
| imgSeed: "antenne" | |
| }, | |
| { | |
| id: 2, | |
| name: "1LIVE", | |
| genre: "Pop & Dance", | |
| url: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3", | |
| imgSeed: "1live" | |
| }, | |
| { | |
| id: 3, | |
| name: "Bayern 3", | |
| genre: "Pop & Rock", | |
| url: "https://br-br3-live.cast.addradio.de/br/br3/live/mp3/128/stream.mp3", | |
| imgSeed: "bayern3" | |
| }, | |
| { | |
| id: 4, | |
| name: "SWR3", | |
| genre: "Pop", | |
| url: "https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3", | |
| imgSeed: "swr3" | |
| }, | |
| { | |
| id: 5, | |
| name: "Deutschlandfunk", | |
| genre: "Nachrichten & Info", | |
| url: "https://stmdsl.dlf.de/dlf/01/128/mp3/stream.mp3", | |
| imgSeed: "dlf" | |
| }, | |
| { | |
| id: 6, | |
| name: "Bremen Vier", | |
| genre: "Pop & Alternative", | |
| url: "https://rb-bremenvier-live.cast.addradio.de/rb/bremenvier/live/mp3/128/stream.mp3", | |
| imgSeed: "bremen4" | |
| }, | |
| { | |
| id: 7, | |
| name: "NDR 2", | |
| genre: "Pop", | |
| url: "https://ndr-ndr2-niedersachsen.cast.addradio.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3", | |
| imgSeed: "ndr2" | |
| }, | |
| { | |
| id: 8, | |
| name: "YOU FM", | |
| genre: "Dance & Pop", | |
| url: "https://hr-youfm-live.cast.addradio.de/hr/youfm/live/mp3/128/stream.mp3", | |
| imgSeed: "youfm" | |
| }, | |
| { | |
| id: 9, | |
| name: "Radio Paloma", | |
| genre: "Schlager", | |
| url: "https://stream.radio-paloma.de/radio-paloma/mp3-192", | |
| imgSeed: "paloma" | |
| }, | |
| { | |
| id: 10, | |
| name: "JAM FM", | |
| genre: "Black & Urban", | |
| url: "https://stream.jam.fm/jamfm-live/mp3-128", | |
| imgSeed: "jamfm" | |
| }, | |
| { | |
| id: 11, | |
| name: "Klassik Radio", | |
| genre: "Klassik", | |
| url: "https://stream.klassikradio.de/live/mp3-192", | |
| imgSeed: "klassik" | |
| }, | |
| { | |
| id: 12, | |
| name: "FFN", | |
| genre: "Pop & Rock", | |
| url: "https://ffn-live.cast.addradio.de/ffn/live/mp3/128/stream.mp3", | |
| imgSeed: "ffn" | |
| } | |
| ]; | |
| // --- State Management --- | |
| let currentStationIndex = -1; | |
| let isPlaying = false; | |
| let audio = new Audio(); | |
| audio.crossOrigin = "anonymous"; // Wichtig für Visualizer | |
| // Visualizer Context | |
| let audioContext; | |
| let analyser; | |
| let dataArray; | |
| let source; | |
| let isAudioContextSetup = false; | |
| // --- DOM Elements --- | |
| const playBtn = document.getElementById('playBtn'); | |
| const playIcon = document.getElementById('playIcon'); | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| const stationListEl = document.getElementById('stationList'); | |
| const searchInput = document.getElementById('searchInput'); | |
| // Player UI Elements | |
| const currentLogo = document.getElementById('currentLogo'); | |
| const currentName = document.getElementById('currentName'); | |
| const currentGenre = document.getElementById('currentGenre'); | |
| const liveBadge = document.getElementById('liveBadge'); | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| // Canvas | |
| const canvas = document.getElementById('audioVisualizer'); | |
| const canvasCtx = canvas.getContext('2d'); | |
| // --- Initialization --- | |
| function init() { | |
| renderList(stations); | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Volume init | |
| audio.volume = volumeSlider.value; | |
| } | |
| // --- Playlist Rendering --- | |
| function renderList(data) { | |
| stationListEl.innerHTML = ''; | |
| data.forEach((station, index) => { | |
| // Finde den echten Index im ursprünglichen Array | |
| const originalIndex = stations.findIndex(s => s.id === station.id); | |
| const li = document.createElement('li'); | |
| li.className = `station-item ${originalIndex === currentStationIndex ? 'active' : ''}`; | |
| li.onclick = () => loadStation(originalIndex); | |
| li.innerHTML = ` | |
| <img src="https://picsum.photos/seed/${station.imgSeed}/100/100" alt="${station.name}"> | |
| <div class="station-details"> | |
| <h4>${station.name}</h4> | |
| <span>${station.genre}</span> | |
| </div> | |
| <div class="play-indicator"> | |
| <i class="fa-solid fa-chart-simple"></i> | |
| </div> | |
| `; | |
| stationListEl.appendChild(li); | |
| }); | |
| } | |
| // --- Audio Logic --- | |
| function loadStation(index) { | |
| // Wenn wir auf den aktiven Sender klicken, nur Play/Pause toggeln | |
| if (index === currentStationIndex) { | |
| togglePlay(); | |
| return; | |
| } | |
| currentStationIndex = index; | |
| const station = stations[index]; | |
| // UI Update | |
| currentName.innerText = station.name; | |
| currentGenre.innerText = station.genre; | |
| currentLogo.src = `https://picsum.photos/seed/${station.imgSeed}/300/300`; | |
| liveBadge.style.display = 'none'; | |
| // Audio Source | |
| audio.src = station.url; | |
| audio.load(); | |
| playAudio(); | |
| renderList(stations); // Update active state in list | |
| showToast(`Lade: ${station.name}`, 'neutral'); | |
| } | |
| function togglePlay() { | |
| if (currentStationIndex === -1) { | |
| loadStation(0); // Starte ersten Sender wenn noch keiner gewählt | |
| return; | |
| } | |
| if (isPlaying) { | |
| pauseAudio(); | |
| } else { | |
| playAudio(); | |
| } | |
| } | |
| function playAudio() { | |
| // AudioContext muss nach User-Geste gestartet werden | |
| setupAudioContext(); | |
| const playPromise = audio.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.then(_ => { | |
| isPlaying = true; | |
| updatePlayButton(); | |
| liveBadge.style.display = 'inline-block'; | |
| animateVisualizer(); | |
| showToast('Live Stream gestartet', 'success'); | |
| }) | |
| .catch(error => { | |
| console.error("Play Error:", error); | |
| showToast('Fehler beim Starten des Streams', 'error'); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| }); | |
| } | |
| } | |
| function pauseAudio() { | |
| audio.pause(); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| liveBadge.style.display = 'none'; | |
| } | |
| function updatePlayButton() { | |
| if (isPlaying) { | |
| playIcon.classList.remove('fa-play'); | |
| playIcon.classList.add('fa-pause'); | |
| currentLogo.classList.add('playing'); | |
| } else { | |
| playIcon.classList.remove('fa-pause'); | |
| playIcon.classList.add('fa-play'); | |
| currentLogo.classList.remove('playing'); | |
| } | |
| } | |
| // --- Web Audio API Visualizer --- | |
| function setupAudioContext() { | |
| if (isAudioContextSetup) return; | |
| try { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| source = audioContext.createMediaElementSource(audio); | |
| source.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| analyser.fftSize = 256; | |
| const bufferLength = analyser.frequencyBinCount; | |
| dataArray = new Uint8Array(bufferLength); | |
| isAudioContextSetup = true; | |
| } catch (e) { | |
| console.warn("Audio Context setup failed (CORS or browser restriction)", e); | |
| } | |
| } | |
| function resizeCanvas() { | |
| canvas.width = canvas.parentElement.offsetWidth; | |
| canvas.height = canvas.parentElement.offsetHeight; | |
| } | |
| function animateVisualizer() { | |
| if (!isPlaying) { | |
| canvasCtx.clearRect(0, 0, canvas.width, canvas.height); | |
| return; | |
| } | |
| requestAnimationFrame(animateVisualizer); | |
| if(analyser) { | |
| analyser.getByteFrequencyData(dataArray); | |
| canvasCtx.clearRect(0, 0, canvas.width, canvas.height); | |
| const barWidth = (canvas.width / dataArray.length) * 2.5; | |
| let barHeight; | |
| let x = 0; | |
| for (let i = 0; i < dataArray.length; i++) { | |
| barHeight = dataArray[i] / 2; | |
| // Gradient Color | |
| const gradient = canvasCtx.createLinearGradient(0, canvas.height, 0, 0); | |
| gradient.addColorStop(0, '#3b82f6'); | |
| gradient.addColorStop(1, '#ec4899'); | |
| canvasCtx.fillStyle = gradient; | |
| // Draw rounded bars from bottom | |
| canvasCtx.beginPath(); | |
| canvasCtx.roundRect(x, canvas.height - barHeight, barWidth, barHeight, 5); | |
| canvasCtx.fill(); | |
| x += barWidth + 2; | |
| } | |
| } | |
| } | |
| // --- Event Listeners --- | |
| playBtn.addEventListener('click', togglePlay); | |
| prevBtn.addEventListener('click', () => { | |
| let newIndex = currentStationIndex - 1; | |
| if (newIndex < 0) newIndex = stations.length - 1; | |
| loadStation(newIndex); | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| let newIndex = currentStationIndex + 1; | |
| if (newIndex >= stations.length) newIndex = 0; | |
| loadStation(newIndex); | |
| }); | |
| volumeSlider.addEventListener('input', (e) => { | |
| audio.volume = e.target.value; | |
| }); | |
| searchInput.addEventListener('input', (e) => { | |
| const query = e.target.value.toLowerCase(); | |
| const filtered = stations.filter(s => | |
| s.name.toLowerCase().includes(query) || | |
| s.genre.toLowerCase().includes(query) | |
| ); | |
| renderList(filtered); | |
| }); | |
| // Audio Error Handling | |
| audio.addEventListener('error', (e) => { | |
| console.error("Stream Error", e); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| showToast('Stream nicht verfügbar', 'error'); | |
| }); | |
| // --- Toast Helper --- | |
| function showToast(msg, type = 'neutral') { | |
| toastMessage.innerText = msg; | |
| toast.className = 'status-toast show'; | |
| if(type === 'success') toast.classList.add('success'); | |
| if(type === 'error') toast.classList.add('error'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Start App | |
| init(); | |
| </script> | |
| </body> | |
| </html> |