anycoder-872e5823 / index.html
Multimedix's picture
Upload folder using huggingface_hub
16bb77a verified
<!DOCTYPE html>
<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>