Multimedix commited on
Commit
26f9e28
·
verified ·
1 Parent(s): 002d65a

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +904 -19
index.html CHANGED
@@ -1,19 +1,904 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DE Radio Stream | Ultimate Player</title>
7
+
8
+ <!-- Google Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- FontAwesome für Icons -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
15
+
16
+ <style>
17
+ :root {
18
+ --bg-color: #0f172a;
19
+ --card-bg: rgba(30, 41, 59, 0.7);
20
+ --accent-color: #3b82f6;
21
+ --accent-glow: rgba(59, 130, 246, 0.5);
22
+ --text-main: #f8fafc;
23
+ --text-secondary: #94a3b8;
24
+ --success: #10b981;
25
+ --danger: #ef4444;
26
+ --glass-border: 1px solid rgba(255, 255, 255, 0.1);
27
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
28
+ }
29
+
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ font-family: 'Outfit', sans-serif;
35
+ }
36
+
37
+ body {
38
+ background-color: var(--bg-color);
39
+ background-image:
40
+ radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.15) 0%, transparent 40%),
41
+ radial-gradient(circle at 90% 80%, rgba(236, 72, 153, 0.15) 0%, transparent 40%);
42
+ color: var(--text-main);
43
+ min-height: 100vh;
44
+ display: flex;
45
+ flex-direction: column;
46
+ overflow-x: hidden;
47
+ }
48
+
49
+ /* --- Header --- */
50
+ header {
51
+ padding: 1.5rem 2rem;
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ backdrop-filter: blur(10px);
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 100;
59
+ border-bottom: var(--glass-border);
60
+ }
61
+
62
+ .brand {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 10px;
66
+ }
67
+
68
+ .brand i {
69
+ font-size: 1.8rem;
70
+ color: var(--accent-color);
71
+ filter: drop-shadow(0 0 10px var(--accent-glow));
72
+ }
73
+
74
+ .brand h1 {
75
+ font-size: 1.5rem;
76
+ font-weight: 700;
77
+ letter-spacing: -0.5px;
78
+ }
79
+
80
+ .anycoder-link {
81
+ font-size: 0.9rem;
82
+ color: var(--text-secondary);
83
+ text-decoration: none;
84
+ background: rgba(255, 255, 255, 0.05);
85
+ padding: 8px 16px;
86
+ border-radius: 20px;
87
+ transition: var(--transition);
88
+ border: var(--glass-border);
89
+ }
90
+
91
+ .anycoder-link:hover {
92
+ color: var(--text-main);
93
+ background: rgba(255, 255, 255, 0.1);
94
+ transform: translateY(-2px);
95
+ }
96
+
97
+ /* --- Main Layout --- */
98
+ main {
99
+ flex: 1;
100
+ display: grid;
101
+ grid-template-columns: 1fr 400px;
102
+ gap: 2rem;
103
+ padding: 2rem;
104
+ max-width: 1400px;
105
+ margin: 0 auto;
106
+ width: 100%;
107
+ }
108
+
109
+ @media (max-width: 900px) {
110
+ main {
111
+ grid-template-columns: 1fr;
112
+ padding: 1rem;
113
+ }
114
+ }
115
+
116
+ /* --- Player Section --- */
117
+ .player-section {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 2rem;
121
+ }
122
+
123
+ .visualizer-card {
124
+ background: var(--card-bg);
125
+ border-radius: 24px;
126
+ padding: 2rem;
127
+ border: var(--glass-border);
128
+ backdrop-filter: blur(20px);
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ justify-content: center;
133
+ position: relative;
134
+ overflow: hidden;
135
+ min-height: 400px;
136
+ box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.5);
137
+ }
138
+
139
+ canvas#audioVisualizer {
140
+ position: absolute;
141
+ bottom: 0;
142
+ left: 0;
143
+ width: 100%;
144
+ height: 100%;
145
+ z-index: 1;
146
+ opacity: 0.6;
147
+ }
148
+
149
+ .now-playing-info {
150
+ z-index: 2;
151
+ text-align: center;
152
+ margin-bottom: 2rem;
153
+ }
154
+
155
+ .station-logo {
156
+ width: 150px;
157
+ height: 150px;
158
+ border-radius: 50%;
159
+ object-fit: cover;
160
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
161
+ margin-bottom: 1.5rem;
162
+ border: 4px solid rgba(255,255,255,0.1);
163
+ animation: rotateDisk 10s linear infinite;
164
+ animation-play-state: paused;
165
+ }
166
+
167
+ .station-logo.playing {
168
+ animation-play-state: running;
169
+ border-color: var(--accent-color);
170
+ }
171
+
172
+ @keyframes rotateDisk {
173
+ from { transform: rotate(0deg); }
174
+ to { transform: rotate(360deg); }
175
+ }
176
+
177
+ .station-name {
178
+ font-size: 2rem;
179
+ font-weight: 700;
180
+ margin-bottom: 0.5rem;
181
+ text-shadow: 0 2px 10px rgba(0,0,0,0.3);
182
+ }
183
+
184
+ .station-meta {
185
+ color: var(--text-secondary);
186
+ font-size: 1rem;
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ gap: 10px;
191
+ }
192
+
193
+ .live-badge {
194
+ background: var(--danger);
195
+ color: white;
196
+ font-size: 0.7rem;
197
+ padding: 2px 8px;
198
+ border-radius: 4px;
199
+ font-weight: 700;
200
+ text-transform: uppercase;
201
+ animation: pulse 2s infinite;
202
+ }
203
+
204
+ @keyframes pulse {
205
+ 0% { opacity: 1; }
206
+ 50% { opacity: 0.5; }
207
+ 100% { opacity: 1; }
208
+ }
209
+
210
+ .controls {
211
+ z-index: 2;
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 2rem;
215
+ background: rgba(0,0,0,0.2);
216
+ padding: 1rem 2rem;
217
+ border-radius: 50px;
218
+ backdrop-filter: blur(10px);
219
+ }
220
+
221
+ .btn-control {
222
+ background: none;
223
+ border: none;
224
+ color: var(--text-main);
225
+ font-size: 1.5rem;
226
+ cursor: pointer;
227
+ transition: var(--transition);
228
+ width: 50px;
229
+ height: 50px;
230
+ border-radius: 50%;
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ }
235
+
236
+ .btn-control:hover {
237
+ background: rgba(255,255,255,0.1);
238
+ color: var(--accent-color);
239
+ }
240
+
241
+ .btn-play {
242
+ background: var(--accent-color);
243
+ color: white;
244
+ font-size: 1.8rem;
245
+ width: 70px;
246
+ height: 70px;
247
+ box-shadow: 0 0 20px var(--accent-glow);
248
+ }
249
+
250
+ .btn-play:hover {
251
+ transform: scale(1.1);
252
+ background: #2563eb;
253
+ color: white;
254
+ }
255
+
256
+ .volume-container {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 10px;
260
+ z-index: 2;
261
+ margin-top: 1rem;
262
+ width: 100%;
263
+ max-width: 300px;
264
+ }
265
+
266
+ input[type=range] {
267
+ -webkit-appearance: none;
268
+ width: 100%;
269
+ background: transparent;
270
+ }
271
+
272
+ input[type=range]::-webkit-slider-thumb {
273
+ -webkit-appearance: none;
274
+ height: 16px;
275
+ width: 16px;
276
+ border-radius: 50%;
277
+ background: var(--text-main);
278
+ cursor: pointer;
279
+ margin-top: -6px;
280
+ box-shadow: 0 0 10px rgba(255,255,255,0.5);
281
+ }
282
+
283
+ input[type=range]::-webkit-slider-runnable-track {
284
+ width: 100%;
285
+ height: 4px;
286
+ cursor: pointer;
287
+ background: rgba(255,255,255,0.2);
288
+ border-radius: 2px;
289
+ }
290
+
291
+ /* --- Playlist Section --- */
292
+ .playlist-section {
293
+ background: var(--card-bg);
294
+ border-radius: 24px;
295
+ border: var(--glass-border);
296
+ backdrop-filter: blur(20px);
297
+ display: flex;
298
+ flex-direction: column;
299
+ overflow: hidden;
300
+ height: calc(100vh - 140px);
301
+ position: sticky;
302
+ top: 100px;
303
+ }
304
+
305
+ @media (max-width: 900px) {
306
+ .playlist-section {
307
+ height: 600px;
308
+ position: static;
309
+ }
310
+ }
311
+
312
+ .playlist-header {
313
+ padding: 1.5rem;
314
+ border-bottom: var(--glass-border);
315
+ }
316
+
317
+ .search-box {
318
+ position: relative;
319
+ width: 100%;
320
+ }
321
+
322
+ .search-box i {
323
+ position: absolute;
324
+ left: 15px;
325
+ top: 50%;
326
+ transform: translateY(-50%);
327
+ color: var(--text-secondary);
328
+ }
329
+
330
+ .search-box input {
331
+ width: 100%;
332
+ padding: 12px 12px 12px 45px;
333
+ border-radius: 12px;
334
+ border: var(--glass-border);
335
+ background: rgba(0,0,0,0.2);
336
+ color: var(--text-main);
337
+ font-size: 1rem;
338
+ outline: none;
339
+ transition: var(--transition);
340
+ }
341
+
342
+ .search-box input:focus {
343
+ border-color: var(--accent-color);
344
+ background: rgba(0,0,0,0.4);
345
+ }
346
+
347
+ .station-list {
348
+ flex: 1;
349
+ overflow-y: auto;
350
+ padding: 0.5rem;
351
+ }
352
+
353
+ /* Scrollbar Styling */
354
+ .station-list::-webkit-scrollbar {
355
+ width: 6px;
356
+ }
357
+ .station-list::-webkit-scrollbar-track {
358
+ background: transparent;
359
+ }
360
+ .station-list::-webkit-scrollbar-thumb {
361
+ background: rgba(255,255,255,0.2);
362
+ border-radius: 3px;
363
+ }
364
+
365
+ .station-item {
366
+ display: flex;
367
+ align-items: center;
368
+ padding: 1rem;
369
+ border-radius: 12px;
370
+ cursor: pointer;
371
+ transition: var(--transition);
372
+ margin-bottom: 0.5rem;
373
+ border: 1px solid transparent;
374
+ }
375
+
376
+ .station-item:hover {
377
+ background: rgba(255,255,255,0.05);
378
+ }
379
+
380
+ .station-item.active {
381
+ background: rgba(59, 130, 246, 0.15);
382
+ border-color: var(--accent-color);
383
+ }
384
+
385
+ .station-item img {
386
+ width: 50px;
387
+ height: 50px;
388
+ border-radius: 10px;
389
+ object-fit: cover;
390
+ margin-right: 1rem;
391
+ }
392
+
393
+ .station-details {
394
+ flex: 1;
395
+ }
396
+
397
+ .station-details h4 {
398
+ font-size: 1rem;
399
+ font-weight: 600;
400
+ margin-bottom: 4px;
401
+ }
402
+
403
+ .station-details span {
404
+ font-size: 0.8rem;
405
+ color: var(--text-secondary);
406
+ background: rgba(255,255,255,0.05);
407
+ padding: 2px 6px;
408
+ border-radius: 4px;
409
+ }
410
+
411
+ .play-indicator {
412
+ opacity: 0;
413
+ color: var(--accent-color);
414
+ transition: var(--transition);
415
+ }
416
+
417
+ .station-item.active .play-indicator {
418
+ opacity: 1;
419
+ }
420
+
421
+ .fav-btn {
422
+ background: none;
423
+ border: none;
424
+ color: var(--text-secondary);
425
+ cursor: pointer;
426
+ padding: 8px;
427
+ transition: var(--transition);
428
+ }
429
+
430
+ .fav-btn:hover, .fav-btn.active {
431
+ color: #ec4899;
432
+ }
433
+
434
+ /* --- Footer --- */
435
+ footer {
436
+ text-align: center;
437
+ padding: 2rem;
438
+ color: var(--text-secondary);
439
+ font-size: 0.9rem;
440
+ }
441
+
442
+ /* Status Toast */
443
+ .status-toast {
444
+ position: fixed;
445
+ bottom: 20px;
446
+ left: 50%;
447
+ transform: translateX(-50%) translateY(100px);
448
+ background: var(--card-bg);
449
+ border: var(--glass-border);
450
+ padding: 12px 24px;
451
+ border-radius: 50px;
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 10px;
455
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
456
+ transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
457
+ z-index: 1000;
458
+ backdrop-filter: blur(10px);
459
+ }
460
+
461
+ .status-toast.show {
462
+ transform: translateX(-50%) translateY(0);
463
+ }
464
+
465
+ .status-dot {
466
+ width: 10px;
467
+ height: 10px;
468
+ border-radius: 50%;
469
+ background: var(--text-secondary);
470
+ }
471
+
472
+ .status-toast.success .status-dot { background: var(--success); box-shadow: 0 0 10px var(--success); }
473
+ .status-toast.error .status-dot { background: var(--danger); box-shadow: 0 0 10px var(--danger); }
474
+ </style>
475
+ </head>
476
+ <body>
477
+
478
+ <header>
479
+ <div class="brand">
480
+ <i class="fa-solid fa-radio"></i>
481
+ <h1>DE Radio<span style="color:var(--accent-color)">.Stream</span></h1>
482
+ </div>
483
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
484
+ Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square" style="font-size: 0.7em; margin-left:5px;"></i>
485
+ </a>
486
+ </header>
487
+
488
+ <main>
489
+ <!-- Player Section -->
490
+ <section class="player-section">
491
+ <div class="visualizer-card">
492
+ <canvas id="audioVisualizer"></canvas>
493
+
494
+ <div class="now-playing-info">
495
+ <img id="currentLogo" src="https://picsum.photos/seed/radio/300/300" alt="Station Logo" class="station-logo">
496
+ <h2 id="currentName" class="station-name">Wähle einen Sender</h2>
497
+ <div class="station-meta">
498
+ <span id="currentGenre" class="genre">Pop</span>
499
+ <span class="live-badge" id="liveBadge" style="display:none;">LIVE</span>
500
+ </div>
501
+ </div>
502
+
503
+ <div class="controls">
504
+ <button class="btn-control" id="prevBtn" title="Vorheriger">
505
+ <i class="fa-solid fa-backward-step"></i>
506
+ </button>
507
+ <button class="btn-control btn-play" id="playBtn" title="Play/Pause">
508
+ <i class="fa-solid fa-play" id="playIcon"></i>
509
+ </button>
510
+ <button class="btn-control" id="nextBtn" title="Nächster">
511
+ <i class="fa-solid fa-forward-step"></i>
512
+ </button>
513
+ </div>
514
+
515
+ <div class="volume-container">
516
+ <i class="fa-solid fa-volume-low" style="color: var(--text-secondary)"></i>
517
+ <input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="0.8">
518
+ <i class="fa-solid fa-volume-high" style="color: var(--text-secondary)"></i>
519
+ </div>
520
+ </div>
521
+ </section>
522
+
523
+ <!-- Playlist Section -->
524
+ <section class="playlist-section">
525
+ <div class="playlist-header">
526
+ <div class="search-box">
527
+ <i class="fa-solid fa-magnifying-glass"></i>
528
+ <input type="text" id="searchInput" placeholder="Sender suchen...">
529
+ </div>
530
+ </div>
531
+ <ul class="station-list" id="stationList">
532
+ <!-- Stations werden hier per JS eingefügt -->
533
+ </ul>
534
+ </section>
535
+ </main>
536
+
537
+ <footer>
538
+ &copy; 2023 DE Radio Stream. Designed for modern browsers.
539
+ </footer>
540
+
541
+ <!-- Toast Notification -->
542
+ <div id="toast" class="status-toast">
543
+ <div class="status-dot"></div>
544
+ <span id="toastMessage">Nachricht</span>
545
+ </div>
546
+
547
+ <script>
548
+ // --- Radio Station Data ---
549
+ // Verwendet öffentliche Stream-URLs. Einige erfordern CORS-Header, die wir im Audio-Objekt setzen.
550
+ const stations = [
551
+ {
552
+ id: 1,
553
+ name: "Antenne Bayern",
554
+ genre: "Pop & Hits",
555
+ url: "https://mp3channels.webradio.antenne.de/antenne-bayern",
556
+ imgSeed: "antenne"
557
+ },
558
+ {
559
+ id: 2,
560
+ name: "1LIVE",
561
+ genre: "Pop & Dance",
562
+ url: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3",
563
+ imgSeed: "1live"
564
+ },
565
+ {
566
+ id: 3,
567
+ name: "Bayern 3",
568
+ genre: "Pop & Rock",
569
+ url: "https://br-br3-live.cast.addradio.de/br/br3/live/mp3/128/stream.mp3",
570
+ imgSeed: "bayern3"
571
+ },
572
+ {
573
+ id: 4,
574
+ name: "SWR3",
575
+ genre: "Pop",
576
+ url: "https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3",
577
+ imgSeed: "swr3"
578
+ },
579
+ {
580
+ id: 5,
581
+ name: "Deutschlandfunk",
582
+ genre: "Nachrichten & Info",
583
+ url: "https://stmdsl.dlf.de/dlf/01/128/mp3/stream.mp3",
584
+ imgSeed: "dlf"
585
+ },
586
+ {
587
+ id: 6,
588
+ name: "Bremen Vier",
589
+ genre: "Pop & Alternative",
590
+ url: "https://rb-bremenvier-live.cast.addradio.de/rb/bremenvier/live/mp3/128/stream.mp3",
591
+ imgSeed: "bremen4"
592
+ },
593
+ {
594
+ id: 7,
595
+ name: "NDR 2",
596
+ genre: "Pop",
597
+ url: "https://ndr-ndr2-niedersachsen.cast.addradio.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3",
598
+ imgSeed: "ndr2"
599
+ },
600
+ {
601
+ id: 8,
602
+ name: "YOU FM",
603
+ genre: "Dance & Pop",
604
+ url: "https://hr-youfm-live.cast.addradio.de/hr/youfm/live/mp3/128/stream.mp3",
605
+ imgSeed: "youfm"
606
+ },
607
+ {
608
+ id: 9,
609
+ name: "Radio Paloma",
610
+ genre: "Schlager",
611
+ url: "https://stream.radio-paloma.de/radio-paloma/mp3-192",
612
+ imgSeed: "paloma"
613
+ },
614
+ {
615
+ id: 10,
616
+ name: "JAM FM",
617
+ genre: "Black & Urban",
618
+ url: "https://stream.jam.fm/jamfm-live/mp3-128",
619
+ imgSeed: "jamfm"
620
+ },
621
+ {
622
+ id: 11,
623
+ name: "Klassik Radio",
624
+ genre: "Klassik",
625
+ url: "https://stream.klassikradio.de/live/mp3-192",
626
+ imgSeed: "klassik"
627
+ },
628
+ {
629
+ id: 12,
630
+ name: "FFN",
631
+ genre: "Pop & Rock",
632
+ url: "https://ffn-live.cast.addradio.de/ffn/live/mp3/128/stream.mp3",
633
+ imgSeed: "ffn"
634
+ }
635
+ ];
636
+
637
+ // --- State Management ---
638
+ let currentStationIndex = -1;
639
+ let isPlaying = false;
640
+ let audio = new Audio();
641
+ audio.crossOrigin = "anonymous"; // Wichtig für Visualizer
642
+
643
+ // Visualizer Context
644
+ let audioContext;
645
+ let analyser;
646
+ let dataArray;
647
+ let source;
648
+ let isAudioContextSetup = false;
649
+
650
+ // --- DOM Elements ---
651
+ const playBtn = document.getElementById('playBtn');
652
+ const playIcon = document.getElementById('playIcon');
653
+ const prevBtn = document.getElementById('prevBtn');
654
+ const nextBtn = document.getElementById('nextBtn');
655
+ const volumeSlider = document.getElementById('volumeSlider');
656
+ const stationListEl = document.getElementById('stationList');
657
+ const searchInput = document.getElementById('searchInput');
658
+
659
+ // Player UI Elements
660
+ const currentLogo = document.getElementById('currentLogo');
661
+ const currentName = document.getElementById('currentName');
662
+ const currentGenre = document.getElementById('currentGenre');
663
+ const liveBadge = document.getElementById('liveBadge');
664
+ const toast = document.getElementById('toast');
665
+ const toastMessage = document.getElementById('toastMessage');
666
+
667
+ // Canvas
668
+ const canvas = document.getElementById('audioVisualizer');
669
+ const canvasCtx = canvas.getContext('2d');
670
+
671
+ // --- Initialization ---
672
+ function init() {
673
+ renderList(stations);
674
+ resizeCanvas();
675
+ window.addEventListener('resize', resizeCanvas);
676
+
677
+ // Volume init
678
+ audio.volume = volumeSlider.value;
679
+ }
680
+
681
+ // --- Playlist Rendering ---
682
+ function renderList(data) {
683
+ stationListEl.innerHTML = '';
684
+ data.forEach((station, index) => {
685
+ // Finde den echten Index im ursprünglichen Array
686
+ const originalIndex = stations.findIndex(s => s.id === station.id);
687
+
688
+ const li = document.createElement('li');
689
+ li.className = `station-item ${originalIndex === currentStationIndex ? 'active' : ''}`;
690
+ li.onclick = () => loadStation(originalIndex);
691
+
692
+ li.innerHTML = `
693
+ <img src="https://picsum.photos/seed/${station.imgSeed}/100/100" alt="${station.name}">
694
+ <div class="station-details">
695
+ <h4>${station.name}</h4>
696
+ <span>${station.genre}</span>
697
+ </div>
698
+ <div class="play-indicator">
699
+ <i class="fa-solid fa-chart-simple"></i>
700
+ </div>
701
+ `;
702
+ stationListEl.appendChild(li);
703
+ });
704
+ }
705
+
706
+ // --- Audio Logic ---
707
+ function loadStation(index) {
708
+ // Wenn wir auf den aktiven Sender klicken, nur Play/Pause toggeln
709
+ if (index === currentStationIndex) {
710
+ togglePlay();
711
+ return;
712
+ }
713
+
714
+ currentStationIndex = index;
715
+ const station = stations[index];
716
+
717
+ // UI Update
718
+ currentName.innerText = station.name;
719
+ currentGenre.innerText = station.genre;
720
+ currentLogo.src = `https://picsum.photos/seed/${station.imgSeed}/300/300`;
721
+ liveBadge.style.display = 'none';
722
+
723
+ // Audio Source
724
+ audio.src = station.url;
725
+ audio.load();
726
+
727
+ playAudio();
728
+ renderList(stations); // Update active state in list
729
+ showToast(`Lade: ${station.name}`, 'neutral');
730
+ }
731
+
732
+ function togglePlay() {
733
+ if (currentStationIndex === -1) {
734
+ loadStation(0); // Starte ersten Sender wenn noch keiner gewählt
735
+ return;
736
+ }
737
+
738
+ if (isPlaying) {
739
+ pauseAudio();
740
+ } else {
741
+ playAudio();
742
+ }
743
+ }
744
+
745
+ function playAudio() {
746
+ // AudioContext muss nach User-Geste gestartet werden
747
+ setupAudioContext();
748
+
749
+ const playPromise = audio.play();
750
+
751
+ if (playPromise !== undefined) {
752
+ playPromise.then(_ => {
753
+ isPlaying = true;
754
+ updatePlayButton();
755
+ liveBadge.style.display = 'inline-block';
756
+ animateVisualizer();
757
+ showToast('Live Stream gestartet', 'success');
758
+ })
759
+ .catch(error => {
760
+ console.error("Play Error:", error);
761
+ showToast('Fehler beim Starten des Streams', 'error');
762
+ isPlaying = false;
763
+ updatePlayButton();
764
+ });
765
+ }
766
+ }
767
+
768
+ function pauseAudio() {
769
+ audio.pause();
770
+ isPlaying = false;
771
+ updatePlayButton();
772
+ liveBadge.style.display = 'none';
773
+ }
774
+
775
+ function updatePlayButton() {
776
+ if (isPlaying) {
777
+ playIcon.classList.remove('fa-play');
778
+ playIcon.classList.add('fa-pause');
779
+ currentLogo.classList.add('playing');
780
+ } else {
781
+ playIcon.classList.remove('fa-pause');
782
+ playIcon.classList.add('fa-play');
783
+ currentLogo.classList.remove('playing');
784
+ }
785
+ }
786
+
787
+ // --- Web Audio API Visualizer ---
788
+ function setupAudioContext() {
789
+ if (isAudioContextSetup) return;
790
+
791
+ try {
792
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
793
+ analyser = audioContext.createAnalyser();
794
+ source = audioContext.createMediaElementSource(audio);
795
+
796
+ source.connect(analyser);
797
+ analyser.connect(audioContext.destination);
798
+
799
+ analyser.fftSize = 256;
800
+ const bufferLength = analyser.frequencyBinCount;
801
+ dataArray = new Uint8Array(bufferLength);
802
+
803
+ isAudioContextSetup = true;
804
+ } catch (e) {
805
+ console.warn("Audio Context setup failed (CORS or browser restriction)", e);
806
+ }
807
+ }
808
+
809
+ function resizeCanvas() {
810
+ canvas.width = canvas.parentElement.offsetWidth;
811
+ canvas.height = canvas.parentElement.offsetHeight;
812
+ }
813
+
814
+ function animateVisualizer() {
815
+ if (!isPlaying) {
816
+ canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
817
+ return;
818
+ }
819
+
820
+ requestAnimationFrame(animateVisualizer);
821
+
822
+ if(analyser) {
823
+ analyser.getByteFrequencyData(dataArray);
824
+
825
+ canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
826
+
827
+ const barWidth = (canvas.width / dataArray.length) * 2.5;
828
+ let barHeight;
829
+ let x = 0;
830
+
831
+ for (let i = 0; i < dataArray.length; i++) {
832
+ barHeight = dataArray[i] / 2;
833
+
834
+ // Gradient Color
835
+ const gradient = canvasCtx.createLinearGradient(0, canvas.height, 0, 0);
836
+ gradient.addColorStop(0, '#3b82f6');
837
+ gradient.addColorStop(1, '#ec4899');
838
+
839
+ canvasCtx.fillStyle = gradient;
840
+
841
+ // Draw rounded bars from bottom
842
+ canvasCtx.beginPath();
843
+ canvasCtx.roundRect(x, canvas.height - barHeight, barWidth, barHeight, 5);
844
+ canvasCtx.fill();
845
+
846
+ x += barWidth + 2;
847
+ }
848
+ }
849
+ }
850
+
851
+ // --- Event Listeners ---
852
+ playBtn.addEventListener('click', togglePlay);
853
+
854
+ prevBtn.addEventListener('click', () => {
855
+ let newIndex = currentStationIndex - 1;
856
+ if (newIndex < 0) newIndex = stations.length - 1;
857
+ loadStation(newIndex);
858
+ });
859
+
860
+ nextBtn.addEventListener('click', () => {
861
+ let newIndex = currentStationIndex + 1;
862
+ if (newIndex >= stations.length) newIndex = 0;
863
+ loadStation(newIndex);
864
+ });
865
+
866
+ volumeSlider.addEventListener('input', (e) => {
867
+ audio.volume = e.target.value;
868
+ });
869
+
870
+ searchInput.addEventListener('input', (e) => {
871
+ const query = e.target.value.toLowerCase();
872
+ const filtered = stations.filter(s =>
873
+ s.name.toLowerCase().includes(query) ||
874
+ s.genre.toLowerCase().includes(query)
875
+ );
876
+ renderList(filtered);
877
+ });
878
+
879
+ // Audio Error Handling
880
+ audio.addEventListener('error', (e) => {
881
+ console.error("Stream Error", e);
882
+ isPlaying = false;
883
+ updatePlayButton();
884
+ showToast('Stream nicht verfügbar', 'error');
885
+ });
886
+
887
+ // --- Toast Helper ---
888
+ function showToast(msg, type = 'neutral') {
889
+ toastMessage.innerText = msg;
890
+ toast.className = 'status-toast show';
891
+ if(type === 'success') toast.classList.add('success');
892
+ if(type === 'error') toast.classList.add('error');
893
+
894
+ setTimeout(() => {
895
+ toast.classList.remove('show');
896
+ }, 3000);
897
+ }
898
+
899
+ // Start App
900
+ init();
901
+
902
+ </script>
903
+ </body>
904
+ </html>