Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>StreamFlow - Online Radio</title> | |
| <!-- Importiere Icons (FontAwesome) --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --accent-glow: rgba(99, 102, 241, 0.5); | |
| --glass-bg: rgba(30, 41, 59, 0.7); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--glass-border); | |
| padding: 1rem 2rem; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: var(--primary-color); | |
| } | |
| .logo i { | |
| font-size: 1.8rem; | |
| } | |
| .search-container { | |
| flex: 1; | |
| max-width: 400px; | |
| position: relative; | |
| } | |
| .search-container input { | |
| width: 100%; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--glass-border); | |
| padding: 0.75rem 1rem 0.75rem 2.5rem; | |
| border-radius: 99px; | |
| color: var(--text-main); | |
| font-size: 0.95rem; | |
| transition: var(--transition); | |
| } | |
| .search-container input:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px var(--accent-glow); | |
| } | |
| .search-container i { | |
| position: absolute; | |
| left: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: var(--transition); | |
| background: rgba(255,255,255,0.05); | |
| padding: 0.4rem 0.8rem; | |
| border-radius: 6px; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--text-main); | |
| background: rgba(255,255,255,0.1); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| gap: 2rem; | |
| padding: 2rem; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| /* --- Player Section --- */ | |
| .player-card { | |
| background: var(--bg-card); | |
| border-radius: 24px; | |
| padding: 2rem; | |
| text-align: center; | |
| border: 1px solid var(--glass-border); | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: fit-content; | |
| position: sticky; | |
| top: 6rem; | |
| } | |
| .album-art { | |
| width: 200px; | |
| height: 200px; | |
| border-radius: 50%; | |
| overflow: hidden; | |
| margin-bottom: 1.5rem; | |
| position: relative; | |
| box-shadow: 0 0 20px var(--accent-glow); | |
| transition: var(--transition); | |
| } | |
| .album-art img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transition: transform 10s linear; | |
| } | |
| .album-art.playing img { | |
| transform: rotate(360deg); | |
| animation: spin 10s linear infinite; | |
| } | |
| @keyframes spin { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .station-info h2 { | |
| font-size: 1.5rem; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-main); | |
| } | |
| .station-info p { | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| margin-bottom: 2rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .visualizer { | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-end; | |
| gap: 4px; | |
| height: 30px; | |
| margin-bottom: 2rem; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .visualizer.active { | |
| opacity: 1; | |
| } | |
| .bar { | |
| width: 6px; | |
| background: var(--primary-color); | |
| border-radius: 3px; | |
| animation: bounce 0s infinite ease-in-out; | |
| } | |
| .visualizer.active .bar { | |
| animation-duration: 0.8s; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { height: 5px; } | |
| 50% { height: 25px; } | |
| } | |
| /* Stagger animations for bars */ | |
| .bar:nth-child(1) { animation-delay: 0.1s; } | |
| .bar:nth-child(2) { animation-delay: 0.3s; } | |
| .bar:nth-child(3) { animation-delay: 0.5s; } | |
| .bar:nth-child(4) { animation-delay: 0.2s; } | |
| .bar:nth-child(5) { animation-delay: 0.4s; } | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .btn-control { | |
| background: none; | |
| border: none; | |
| color: var(--text-main); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .btn-control:hover { | |
| color: var(--primary-color); | |
| } | |
| .btn-play { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| color: white; | |
| font-size: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 4px 15px var(--accent-glow); | |
| } | |
| .btn-play:hover { | |
| background: var(--primary-hover); | |
| transform: scale(1.05); | |
| color: white; | |
| } | |
| .volume-container { | |
| width: 100%; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| color: var(--text-muted); | |
| } | |
| .volume-slider { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 4px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| } | |
| .volume-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--text-main); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .volume-slider::-webkit-slider-thumb:hover { | |
| background: var(--primary-color); | |
| } | |
| /* --- Station List --- */ | |
| .stations-section h3 { | |
| margin-bottom: 1.5rem; | |
| font-size: 1.2rem; | |
| border-left: 4px solid var(--primary-color); | |
| padding-left: 1rem; | |
| } | |
| .stations-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .station-card { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 16px; | |
| padding: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .station-card:hover { | |
| transform: translateY(-3px); | |
| background: rgba(255,255,255,0.08); | |
| border-color: rgba(255,255,255,0.2); | |
| } | |
| .station-card.active { | |
| border-color: var(--primary-color); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| .station-card.active::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 4px; | |
| height: 100%; | |
| background: var(--primary-color); | |
| } | |
| .station-thumb { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 12px; | |
| object-fit: cover; | |
| flex-shrink: 0; | |
| } | |
| .station-details { | |
| flex: 1; | |
| min-width: 0; /* Text truncation fix */ | |
| } | |
| .station-name { | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .station-genre { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .play-indicator { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| background: rgba(255,255,255,0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: var(--transition); | |
| color: var(--text-main); | |
| } | |
| .station-card:hover .play-indicator { | |
| background: var(--primary-color); | |
| color: white; | |
| } | |
| .station-card.active .play-indicator { | |
| background: var(--primary-color); | |
| color: white; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } | |
| } | |
| /* --- Toast Notification --- */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .toast { | |
| background: var(--bg-card); | |
| border: 1px solid var(--glass-border); | |
| padding: 1rem 1.5rem; | |
| border-radius: 12px; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| animation: slideIn 0.3s ease-out forwards; | |
| min-width: 250px; | |
| } | |
| .toast.error { border-left: 4px solid #ef4444; } | |
| .toast.success { border-left: 4px solid #22c55e; } | |
| .toast.info { border-left: 4px solid var(--primary-color); } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes fadeOut { | |
| to { transform: translateX(100%); opacity: 0; } | |
| } | |
| /* --- Responsive --- */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| .player-card { | |
| position: relative; | |
| top: 0; | |
| margin-bottom: 2rem; | |
| } | |
| .album-art { | |
| width: 150px; | |
| height: 150px; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| header { | |
| flex-direction: column; | |
| align-items: stretch; | |
| padding: 1rem; | |
| } | |
| .search-container { | |
| max-width: 100%; | |
| order: 3; | |
| } | |
| .logo { | |
| justify-content: center; | |
| } | |
| .anycoder-link { | |
| align-self: flex-end; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-radio"></i> | |
| StreamFlow | |
| </div> | |
| <div class="search-container"> | |
| <i class="fa-solid fa-search"></i> | |
| <input type="text" id="searchInput" placeholder="Sender suchen..."> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Player Section --> | |
| <section class="player-card"> | |
| <div class="album-art" id="albumArt"> | |
| <img src="https://picsum.photos/seed/music/300/300" alt="Album Art" id="currentImage"> | |
| </div> | |
| <div class="station-info"> | |
| <h2 id="currentStationName">Wähle einen Sender</h2> | |
| <p id="currentGenre">Bereit zum Abspielen</p> | |
| </div> | |
| <!-- CSS Visualizer --> | |
| <div class="visualizer" id="visualizer"> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| <div class="bar"></div> | |
| </div> | |
| <div class="controls"> | |
| <!-- Prev Button (Visual only for this demo) --> | |
| <button class="btn-control" title="Vorheriger"> | |
| <i class="fa-solid fa-backward-step fa-lg"></i> | |
| </button> | |
| <button class="btn-play" id="playPauseBtn" title="Play/Pause"> | |
| <i class="fa-solid fa-play" id="playIcon"></i> | |
| </button> | |
| <!-- Next Button (Visual only for this demo) --> | |
| <button class="btn-control" title="Nächster"> | |
| <i class="fa-solid fa-forward-step fa-lg"></i> | |
| </button> | |
| </div> | |
| <div class="volume-container"> | |
| <i class="fa-solid fa-volume-low"></i> | |
| <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.05" value="0.8"> | |
| <i class="fa-solid fa-volume-high"></i> | |
| </div> | |
| </section> | |
| <!-- Stations List Section --> | |
| <section class="stations-section"> | |
| <h3>Verfügbare Sender</h3> | |
| <div class="stations-grid" id="stationsGrid"> | |
| <!-- Stations will be injected here via JS --> | |
| </div> | |
| </section> | |
| </main> | |
| <div id="toast-container"></div> | |
| <script> | |
| // --- Data: Radio Stations --- | |
| // Using reliable public MP3 streams. | |
| const stations = [ | |
| { | |
| name: "Antenne Bayern", | |
| genre: "Pop", | |
| url: "https://stream.antenne.de/antenne", | |
| image: "https://picsum.photos/seed/antenne/200/200" | |
| }, | |
| { | |
| name: "NDR 1 Niedersachsen", | |
| genre: "Information & Pop", | |
| url: "https://ndr-ndr1-niedersachsen-ndr.akamaized.net/ndr/ndr1/niedersachsen/playlist.m3u8", // HLS fallback logic needed usually, but browsers support it natively mostly or we use mp3 | |
| // Fallback to direct MP3 for broader compatibility in this demo | |
| url: "https://icecast.ndr.de/ndr/ndr1/niedersachsen/mp3/128/stream.mp3", | |
| image: "https://picsum.photos/seed/ndr/200/200" | |
| }, | |
| { | |
| name: "1LIVE", | |
| genre: "Rock & Pop", | |
| url: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3", | |
| image: "https://picsum.photos/seed/1live/200/200" | |
| }, | |
| { | |
| name: "SWR3", | |
| genre: "Pop & Hits", | |
| url: "https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3", | |
| image: "https://picsum.photos/seed/swr3/200/200" | |
| }, | |
| { | |
| name: "Deutschlandfunk", | |
| genre: "Nachrichten & Kultur", | |
| url: "https://st01.dlf.de/dlf/01/128/mp3/stream.mp3", | |
| image: "https://picsum.photos/seed/dlf/200/200" | |
| }, | |
| { | |
| name: "SRF 3", | |
| genre: "Pop & Rock", | |
| url: "https://stream.srg-ssr.ch/m/drs3/mp3_128", | |
| image: "https://picsum.photos/seed/srf3/200/200" | |
| }, | |
| { | |
| name: "BBC World Service", | |
| genre: "International News", | |
| url: "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service", | |
| image: "https://picsum.photos/seed/bbc/200/200" | |
| }, | |
| { | |
| name: "Ibiza Global Radio", | |
| genre: "Electronic", | |
| url: "http://ibizaglobalradio.streaming-pro.com:8024/;stream.mp3", | |
| image: "https://picsum.photos/seed/ibiza/200/200" | |
| }, | |
| { | |
| name: "Classic FM", | |
| genre: "Classical", | |
| url: "http://media-the.musicradio.com/ClassicFMMP3", | |
| image: "https://picsum.photos/seed/classic/200/200" | |
| }, | |
| { | |
| name: "Radio Paradise", | |
| genre: "Eclectic Rock", | |
| url: "http://stream.radioparadise.com/mp3-192", | |
| image: "https://picsum.photos/seed/paradise/200/200" | |
| } | |
| ]; | |
| // --- DOM Elements --- | |
| const audio = new Audio(); | |
| const stationsGrid = document.getElementById('stationsGrid'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const playIcon = document.getElementById('playIcon'); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const currentStationName = document.getElementById('currentStationName'); | |
| const currentGenre = document.getElementById('currentGenre'); | |
| const currentImage = document.getElementById('currentImage'); | |
| const albumArt = document.getElementById('albumArt'); | |
| const visualizer = document.getElementById('visualizer'); | |
| const toastContainer = document.getElementById('toast-container'); | |
| // --- State --- | |
| let isPlaying = false; | |
| let currentStation = null; | |
| // --- Initialization --- | |
| function init() { | |
| renderStations(stations); | |
| audio.volume = volumeSlider.value; | |
| } | |
| // --- Render Functions --- | |
| function renderStations(list) { | |
| stationsGrid.innerHTML = ''; | |
| if (list.length === 0) { | |
| stationsGrid.innerHTML = '<p style="color:var(--text-muted); grid-column: 1/-1;">Keine Sender gefunden.</p>'; | |
| return; | |
| } | |
| list.forEach(station => { | |
| const card = document.createElement('div'); | |
| card.className = `station-card ${currentStation && currentStation.name === station.name ? 'active' : ''}`; | |
| card.onclick = () => loadStation(station); | |
| card.innerHTML = ` | |
| <img src="${station.image}" alt="${station.name}" class="station-thumb"> | |
| <div class="station-details"> | |
| <div class="station-name">${station.name}</div> | |
| <div class="station-genre"><i class="fa-solid fa-music"></i> ${station.genre}</div> | |
| </div> | |
| <div class="play-indicator"> | |
| <i class="fa-solid ${currentStation && currentStation.name === station.name && isPlaying ? 'fa-chart-simple' : 'fa-play'}"></i> | |
| </div> | |
| `; | |
| stationsGrid.appendChild(card); | |
| }); | |
| } | |
| // --- Player Logic --- | |
| function loadStation(station) { | |
| if (currentStation && currentStation.name === station.name) { | |
| togglePlay(); | |
| return; | |
| } | |
| currentStation = station; | |
| audio.src = station.url; | |
| // Update UI | |
| currentStationName.textContent = station.name; | |
| currentGenre.textContent = station.genre; | |
| currentImage.src = station.image; | |
| // Update List UI to show active state | |
| renderStations(stations.filter(s => s.name.toLowerCase().includes(searchInput.value.toLowerCase()))); | |
| playAudio(); | |
| } | |
| function togglePlay() { | |
| if (!currentStation) { | |
| showToast('Bitte wähle zuerst einen Sender aus.', 'info'); | |
| return; | |
| } | |
| if (isPlaying) { | |
| pauseAudio(); | |
| } else { | |
| playAudio(); | |
| } | |
| } | |
| function playAudio() { | |
| const playPromise = audio.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.then(_ => { | |
| isPlaying = true; | |
| updatePlayerUI(); | |
| }) | |
| .catch(error => { | |
| console.error('Playback failed:', error); | |
| isPlaying = false; | |
| updatePlayerUI(); | |
| showToast('Fehler beim Laden des Streams. Versuche einen anderen Sender.', 'error'); | |
| }); | |
| } | |
| } | |
| function pauseAudio() { | |
| audio.pause(); | |
| isPlaying = false; | |
| updatePlayerUI(); | |
| } | |
| function updatePlayerUI() { | |
| if (isPlaying) { | |
| playIcon.classList.remove('fa-play'); | |
| playIcon.classList.add('fa-pause'); | |
| albumArt.classList.add('playing'); | |
| visualizer.classList.add('active'); | |
| } else { | |
| playIcon.classList.remove('fa-pause'); | |
| playIcon.classList.add('fa-play'); | |
| albumArt.classList.remove('playing'); | |
| visualizer.classList.remove('active'); | |
| } | |
| // Refresh list icons | |
| renderStations(stations.filter(s => s.name.toLowerCase().includes(searchInput.value.toLowerCase()))); | |
| } | |
| // --- Event Listeners --- | |
| playPauseBtn.addEventListener('click', togglePlay); | |
| volumeSlider.addEventListener('input', (e) => { | |
| audio.volume = e.target.value; | |
| }); | |
| searchInput.addEventListener('input', (e) => { | |
| const query = e.target.value.toLowerCase(); | |
| const filtered = stations.filter(station => | |
| station.name.toLowerCase().includes(query) || | |
| station.genre.toLowerCase().includes(query) | |
| ); | |
| renderStations(filtered); | |
| }); | |
| // Handle audio errors (e.g., stream offline) | |
| audio.addEventListener('error', (e) => { | |
| if(currentStation) { | |
| showToast(`Stream "${currentStation.name}" ist nicht verfügbar.`, 'error'); | |
| isPlaying = false; | |
| updatePlayerUI(); | |
| } | |
| }); | |
| // --- Toast Notification System --- | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| let iconClass = 'fa-info-circle'; | |
| if (type === 'error') iconClass = 'fa-exclamation-circle'; | |
| if (type === 'success') iconClass = 'fa-check-circle'; | |
| toast.innerHTML = ` | |
| <i class="fa-solid ${iconClass}"></i> | |
| <span>${message}</span> | |
| `; | |
| toastContainer.appendChild(toast); | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| toast.style.animation = 'fadeOut 0.3s ease-out forwards'; | |
| toast.addEventListener('animationend', () => { | |
| toast.remove(); | |
| }); | |
| }, 3000); | |
| } | |
| // Run init | |
| init(); | |
| </script> | |
| </body> | |
| </html> |