Multimedix commited on
Commit
16bb77a
·
verified ·
1 Parent(s): 4a4fe03

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +789 -1187
index.html CHANGED
@@ -1,1195 +1,797 @@
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
- <meta name="description" content="Deutsches Online Radio - Alle öffentlich-rechtlichen Sender in einer modernen App">
7
- <meta name="theme-color" content="#0a0a1a">
8
- <title>Deutsches Online Radio | Öffentlich-rechtliche Sender</title>
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
- <style>
11
- /* Modern CSS Reset & Custom Properties */
12
- :root {
13
- --primary-color: #0a0a1a;
14
- --secondary-color: #1a1a2e;
15
- --accent-color: #00d4ff;
16
- --text-primary: #ffffff;
17
- --text-secondary: #b0b0d0;
18
- --glass-bg: rgba(255, 255, 255, 0.08);
19
- --glass-border: rgba(255, 255, 255, 0.1);
20
- --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
21
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
22
- --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
- --error-color: #ff4757;
24
- --success-color: #2ed573;
25
- --warning-color: #ffa502;
26
- }
27
-
28
- * {
29
- margin: 0;
30
- padding: 0;
31
- box-sizing: border-box;
32
- }
33
-
34
- body {
35
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
36
- background: var(--primary-color);
37
- color: var(--text-primary);
38
- min-height: 100vh;
39
- display: flex;
40
- flex-direction: column;
41
- line-height: 1.6;
42
- overflow-x: hidden;
43
- -webkit-tap-highlight-color: transparent;
44
- }
45
-
46
- /* Header */
47
- header {
48
- background: var(--glass-bg);
49
- backdrop-filter: blur(12px) saturate(180%);
50
- -webkit-backdrop-filter: blur(12px) saturate(180%);
51
- border-bottom: 1px solid var(--glass-border);
52
- padding: 1.5rem 2rem;
53
- display: flex;
54
- justify-content: space-between;
55
- align-items: center;
56
- position: sticky;
57
- top: 0;
58
- z-index: 100;
59
- box-shadow: var(--glass-shadow);
60
- }
61
-
62
- header h1 {
63
- font-size: clamp(1.2rem, 4vw, 1.8rem);
64
- background: var(--gradient);
65
- -webkit-background-clip: text;
66
- -webkit-text-fill-color: transparent;
67
- background-clip: text;
68
- display: flex;
69
- align-items: center;
70
- gap: 0.75rem;
71
- font-weight: 700;
72
- }
73
-
74
- .anycoder-link {
75
- color: var(--accent-color);
76
- text-decoration: none;
77
- font-size: 0.85rem;
78
- padding: 0.5rem 1rem;
79
- border-radius: 20px;
80
- background: var(--glass-bg);
81
- border: 1px solid var(--glass-border);
82
- transition: var(--transition);
83
- font-weight: 500;
84
- }
85
-
86
- .anycoder-link:hover {
87
- background: rgba(0, 212, 255, 0.15);
88
- transform: translateY(-2px);
89
- box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
90
- }
91
-
92
- /* Main Content */
93
- main {
94
- flex: 1;
95
- padding: 2rem;
96
- max-width: 1400px;
97
- margin: 0 auto;
98
- width: 100%;
99
- padding-bottom: 120px;
100
- }
101
-
102
- /* Search Section */
103
- .search-section {
104
- margin-bottom: 2rem;
105
- position: relative;
106
- }
107
-
108
- .search-input {
109
- width: 100%;
110
- max-width: 600px;
111
- padding: 1rem 1.5rem;
112
- font-size: 1rem;
113
- background: var(--glass-bg);
114
- border: 1px solid var(--glass-border);
115
- border-radius: 50px;
116
- color: var(--text-primary);
117
- outline: none;
118
- transition: var(--transition);
119
- backdrop-filter: blur(10px);
120
- -webkit-backdrop-filter: blur(10px);
121
- font-family: inherit;
122
- }
123
-
124
- .search-input::placeholder {
125
- color: var(--text-secondary);
126
- opacity: 0.7;
127
- }
128
-
129
- .search-input:focus {
130
- border-color: var(--accent-color);
131
- box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2);
132
- }
133
-
134
- .search-input:invalid {
135
- border-color: var(--error-color);
136
- }
137
-
138
- /* Filter Buttons */
139
- .filter-buttons {
140
- display: flex;
141
- gap: 0.5rem;
142
- margin-bottom: 2rem;
143
- flex-wrap: wrap;
144
- }
145
-
146
- .filter-btn {
147
- padding: 0.6rem 1.5rem;
148
- background: var(--glass-bg);
149
- border: 1px solid var(--glass-border);
150
- border-radius: 25px;
151
- color: var(--text-secondary);
152
- cursor: pointer;
153
- transition: var(--transition);
154
- font-size: 0.9rem;
155
- font-weight: 500;
156
- backdrop-filter: blur(10px);
157
- -webkit-backdrop-filter: blur(10px);
158
- user-select: none;
159
- }
160
-
161
- .filter-btn:hover {
162
- background: rgba(255, 255, 255, 0.15);
163
- color: var(--text-primary);
164
- transform: translateY(-2px);
165
- }
166
-
167
- .filter-btn.active {
168
- background: var(--accent-color);
169
- color: var(--primary-color);
170
- border-color: var(--accent-color);
171
- font-weight: 600;
172
- }
173
-
174
- .filter-btn:focus-visible {
175
- outline: 2px solid var(--accent-color);
176
- outline-offset: 2px;
177
- }
178
-
179
- /* Stations Grid */
180
- .stations-grid {
181
- display: grid;
182
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
183
- gap: 1.5rem;
184
- margin-bottom: 2rem;
185
- }
186
-
187
- /* Station Card */
188
- .station-card {
189
- background: var(--glass-bg);
190
- border: 1px solid var(--glass-border);
191
- border-radius: 20px;
192
- padding: 1.5rem;
193
- cursor: pointer;
194
- transition: var(--transition);
195
- position: relative;
196
- overflow: hidden;
197
- backdrop-filter: blur(10px);
198
- -webkit-backdrop-filter: blur(10px);
199
- display: flex;
200
- flex-direction: column;
201
- }
202
-
203
- .station-card::before {
204
- content: '';
205
- position: absolute;
206
- top: 0;
207
- left: 0;
208
- right: 0;
209
- height: 4px;
210
- background: var(--gradient);
211
- transform: scaleX(0);
212
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
213
- }
214
-
215
- .station-card:hover {
216
- transform: translateY(-5px);
217
- box-shadow: var(--glass-shadow);
218
- border-color: rgba(0, 212, 255, 0.3);
219
- }
220
-
221
- .station-card:hover::before {
222
- transform: scaleX(1);
223
- }
224
-
225
- .station-card.playing {
226
- border-color: var(--accent-color);
227
- background: rgba(0, 212, 255, 0.08);
228
- }
229
-
230
- .station-card.playing::before {
231
- transform: scaleX(1);
232
- background: var(--accent-color);
233
- }
234
-
235
- .station-header {
236
- display: flex;
237
- align-items: center;
238
- gap: 1rem;
239
- margin-bottom: 1rem;
240
- }
241
-
242
- .station-logo {
243
- width: 60px;
244
- height: 60px;
245
- border-radius: 12px;
246
- object-fit: cover;
247
- background: var(--secondary-color);
248
- border: 1px solid var(--glass-border);
249
- flex-shrink: 0;
250
- }
251
-
252
- .station-logo.error {
253
- display: flex;
254
- align-items: center;
255
- justify-content: center;
256
- font-size: 1.5rem;
257
- color: var(--text-secondary);
258
- }
259
-
260
- .station-info {
261
- flex: 1;
262
- min-width: 0;
263
- }
264
-
265
- .station-info h3 {
266
- font-size: 1.1rem;
267
- margin-bottom: 0.25rem;
268
- white-space: nowrap;
269
- overflow: hidden;
270
- text-overflow: ellipsis;
271
- }
272
-
273
- .station-genre {
274
- font-size: 0.8rem;
275
- color: var(--text-secondary);
276
- background: rgba(255, 255, 255, 0.05);
277
- padding: 0.25rem 0.75rem;
278
- border-radius: 12px;
279
- display: inline-block;
280
- text-transform: capitalize;
281
- }
282
-
283
- .station-description {
284
- font-size: 0.9rem;
285
- color: var(--text-secondary);
286
- margin-bottom: 1rem;
287
- display: -webkit-box;
288
- -webkit-line-clamp: 2;
289
- -webkit-box-orient: vertical;
290
- overflow: hidden;
291
- flex-grow: 1;
292
- }
293
-
294
- .station-actions {
295
- display: flex;
296
- justify-content: space-between;
297
- align-items: center;
298
- margin-top: auto;
299
- }
300
-
301
- .play-indicator {
302
- display: none;
303
- align-items: center;
304
- gap: 0.5rem;
305
- font-size: 0.8rem;
306
- color: var(--accent-color);
307
- font-weight: 500;
308
- }
309
-
310
- .station-card.playing .play-indicator {
311
- display: flex;
312
- }
313
-
314
- .equalizer {
315
- display: flex;
316
- gap: 2px;
317
- height: 16px;
318
- align-items: flex-end;
319
- }
320
-
321
- .equalizer-bar {
322
- width: 3px;
323
- background: var(--accent-color);
324
- border-radius: 2px;
325
- animation: equalize 1s ease-in-out infinite alternate;
326
- will-change: transform;
327
- }
328
-
329
- .equalizer-bar:nth-child(1) {
330
- animation-delay: 0s;
331
- height: 40%;
332
- }
333
-
334
- .equalizer-bar:nth-child(2) {
335
- animation-delay: 0.1s;
336
- height: 60%;
337
- }
338
-
339
- .equalizer-bar:nth-child(3) {
340
- animation-delay: 0.2s;
341
- height: 80%;
342
- }
343
-
344
- .equalizer-bar:nth-child(4) {
345
- animation-delay: 0.3s;
346
- height: 50%;
347
- }
348
-
349
- @keyframes equalize {
350
- 0% {
351
- transform: scaleY(0.5);
352
- }
353
- 100% {
354
- transform: scaleY(1);
355
- }
356
- }
357
-
358
- .favorite-btn {
359
- background: none;
360
- border: none;
361
- color: var(--text-secondary);
362
- font-size: 1.2rem;
363
- cursor: pointer;
364
- transition: var(--transition);
365
- padding: 0.5rem;
366
- border-radius: 50%;
367
- display: flex;
368
- align-items: center;
369
- justify-content: center;
370
- }
371
-
372
- .favorite-btn:hover {
373
- color: var(--accent-color);
374
- transform: scale(1.1);
375
- background: rgba(255, 255, 255, 0.05);
376
- }
377
-
378
- .favorite-btn.active {
379
- color: #ff6b6b;
380
- animation: heartBeat 0.3s ease;
381
- }
382
-
383
- @keyframes heartBeat {
384
- 0% { transform: scale(1); }
385
- 50% { transform: scale(1.2); }
386
- 100% { transform: scale(1); }
387
- }
388
-
389
- /* Player Bar */
390
- .player-bar {
391
- position: fixed;
392
- bottom: 0;
393
- left: 0;
394
- right: 0;
395
- background: var(--glass-bg);
396
- backdrop-filter: blur(20px) saturate(180%);
397
- -webkit-backdrop-filter: blur(20px) saturate(180%);
398
- border-top: 1px solid var(--glass-border);
399
- padding: 1rem 2rem;
400
- transform: translateY(100%);
401
- transition: var(--transition);
402
- z-index: 200;
403
- box-shadow: var(--glass-shadow);
404
- }
405
-
406
- .player-bar.active {
407
- transform: translateY(0);
408
- }
409
-
410
- .player-content {
411
- max-width: 1400px;
412
- margin: 0 auto;
413
- display: grid;
414
- grid-template-columns: 1fr auto 1fr;
415
- gap: 2rem;
416
- align-items: center;
417
- }
418
-
419
- .now-playing {
420
- display: flex;
421
- align-items: center;
422
- gap: 1rem;
423
- min-width: 0;
424
- }
425
-
426
- .now-playing-logo {
427
- width: 50px;
428
- height: 50px;
429
- border-radius: 10px;
430
- background: var(--secondary-color);
431
- border: 1px solid var(--glass-border);
432
- flex-shrink: 0;
433
- }
434
-
435
- .now-playing-info {
436
- min-width: 0;
437
- }
438
-
439
- .now-playing-info h4 {
440
- font-size: 1rem;
441
- margin-bottom: 0.25rem;
442
- white-space: nowrap;
443
- overflow: hidden;
444
- text-overflow: ellipsis;
445
- }
446
-
447
- .now-playing-info p {
448
- font-size: 0.85rem;
449
- color: var(--text-secondary);
450
- white-space: nowrap;
451
- overflow: hidden;
452
- text-overflow: ellipsis;
453
- }
454
-
455
- .player-controls {
456
- display: flex;
457
- align-items: center;
458
- gap: 1rem;
459
- }
460
-
461
- .control-btn {
462
- background: var(--glass-bg);
463
- border: 1px solid var(--glass-border);
464
- color: var(--text-primary);
465
- width: 50px;
466
- height: 50px;
467
- border-radius: 50%;
468
- cursor: pointer;
469
- display: flex;
470
- align-items: center;
471
- justify-content: center;
472
- transition: var(--transition);
473
- font-size: 1.2rem;
474
- backdrop-filter: blur(10px);
475
- -webkit-backdrop-filter: blur(10px);
476
- user-select: none;
477
- }
478
-
479
- .control-btn:hover {
480
- background: var(--accent-color);
481
- color: var(--primary-color);
482
- transform: scale(1.1);
483
- border-color: var(--accent-color);
484
- }
485
-
486
- .control-btn:active {
487
- transform: scale(0.95);
488
- }
489
-
490
- .control-btn:disabled {
491
- opacity: 0.5;
492
- cursor: not-allowed;
493
- transform: none;
494
- }
495
-
496
- .volume-control {
497
- display: flex;
498
- align-items: center;
499
- gap: 1rem;
500
- justify-self: end;
501
- }
502
-
503
- .volume-slider {
504
- width: 120px;
505
- height: 4px;
506
- -webkit-appearance: none;
507
- appearance: none;
508
- background: var(--glass-bg);
509
- border-radius: 2px;
510
- outline: none;
511
- cursor: pointer;
512
- transition: var(--transition);
513
- }
514
-
515
- .volume-slider::-webkit-slider-thumb {
516
- -webkit-appearance: none;
517
- appearance: none;
518
- width: 16px;
519
- height: 16px;
520
- background: var(--accent-color);
521
- border-radius: 50%;
522
- cursor: pointer;
523
- transition: var(--transition);
524
- }
525
-
526
- .volume-slider::-webkit-slider-thumb:hover {
527
- transform: scale(1.2);
528
- box-shadow: 0 0 8px var(--accent-color);
529
- }
530
-
531
- .volume-slider::-moz-range-thumb {
532
- width: 16px;
533
- height: 16px;
534
- background: var(--accent-color);
535
- border-radius: 50%;
536
- cursor: pointer;
537
- border: none;
538
- transition: var(--transition);
539
- }
540
-
541
- .volume-slider::-moz-range-thumb:hover {
542
- transform: scale(1.2);
543
- }
544
-
545
- .volume-slider::-moz-range-track {
546
- background: var(--glass-bg);
547
- height: 4px;
548
- border-radius: 2px;
549
- }
550
-
551
- /* Loading Spinner */
552
- .loading-spinner {
553
- display: none;
554
- position: fixed;
555
- top: 50%;
556
- left: 50%;
557
- transform: translate(-50%, -50%);
558
- z-index: 300;
559
- }
560
-
561
- .loading-spinner.active {
562
- display: block;
563
- }
564
-
565
- .spinner {
566
- width: 60px;
567
- height: 60px;
568
- border: 3px solid var(--glass-bg);
569
- border-top: 3px solid var(--accent-color);
570
- border-radius: 50%;
571
- animation: spin 1s linear infinite;
572
- }
573
-
574
- @keyframes spin {
575
- 0% { transform: rotate(0deg); }
576
- 100% { transform: rotate(360deg); }
577
- }
578
-
579
- /* Toast Notifications */
580
- .toast {
581
- position: fixed;
582
- top: 2rem;
583
- right: 2rem;
584
- background: var(--glass-bg);
585
- border: 1px solid var(--glass-border);
586
- border-radius: 12px;
587
- padding: 1rem 1.5rem;
588
- color: var(--text-primary);
589
- transform: translateX(400px);
590
- transition: var(--transition);
591
- z-index: 400;
592
- backdrop-filter: blur(10px);
593
- -webkit-backdrop-filter: blur(10px);
594
- display: flex;
595
- align-items: center;
596
- gap: 0.75rem;
597
- max-width: 350px;
598
- box-shadow: var(--glass-shadow);
599
- }
600
-
601
- .toast.show {
602
- transform: translateX(0);
603
- }
604
-
605
- .toast.error {
606
- border-color: var(--error-color);
607
- }
608
-
609
- .toast.success {
610
- border-color: var(--success-color);
611
- }
612
-
613
- .toast.warning {
614
- border-color: var(--warning-color);
615
- }
616
-
617
- /* Empty State */
618
- .empty-state {
619
- text-align: center;
620
- padding: 4rem 2rem;
621
- color: var(--text-secondary);
622
- display: none;
623
- }
624
-
625
- .empty-state.show {
626
- display: block;
627
- }
628
-
629
- .empty-state i {
630
- font-size: 4rem;
631
- margin-bottom: 1rem;
632
- opacity: 0.5;
633
- }
634
-
635
- .empty-state h3 {
636
- margin-bottom: 0.5rem;
637
- }
638
-
639
- /* Error State for Stations */
640
- .station-error {
641
- position: absolute;
642
- top: 0.5rem;
643
- right: 0.5rem;
644
- background: var(--error-color);
645
- color: white;
646
- padding: 0.25rem 0.5rem;
647
- border-radius: 8px;
648
- font-size: 0.7rem;
649
- font-weight: 500;
650
- display: none;
651
- }
652
-
653
- .station-card.error .station-error {
654
- display: block;
655
- }
656
-
657
- /* Responsive Design */
658
- @media (max-width: 768px) {
659
- header {
660
- padding: 1rem;
661
- }
662
-
663
- header h1 {
664
- gap: 0.5rem;
665
- }
666
-
667
- main {
668
- padding: 1rem;
669
- padding-bottom: 100px;
670
- }
671
-
672
- .player-content {
673
- grid-template-columns: 1fr;
674
- gap: 1rem;
675
- text-align: center;
676
- }
677
-
678
- .now-playing {
679
- justify-content: center;
680
- }
681
-
682
- .now-playing-info {
683
- text-align: left;
684
- }
685
-
686
- .volume-control {
687
- justify-self: center;
688
- }
689
-
690
- .stations-grid {
691
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
692
- gap: 1rem;
693
- }
694
-
695
- .search-input {
696
- font-size: 0.9rem;
697
- padding: 0.75rem 1.25rem;
698
- }
699
-
700
- .player-bar {
701
- padding: 1rem;
702
- }
703
-
704
- .control-btn {
705
- width: 45px;
706
- height: 45px;
707
- font-size: 1.1rem;
708
- }
709
-
710
- .volume-slider {
711
- width: 100px;
712
- }
713
- }
714
-
715
- @media (max-width: 480px) {
716
- .stations-grid {
717
- grid-template-columns: 1fr;
718
- }
719
-
720
- .toast {
721
- left: 1rem;
722
- right: 1rem;
723
- max-width: none;
724
- }
725
- }
726
-
727
- /* Accessibility */
728
- @media (prefers-reduced-motion: reduce) {
729
- * {
730
- animation-duration: 0.01ms !important;
731
- animation-iteration-count: 1 !important;
732
- transition-duration: 0.01ms !important;
733
- }
734
- }
735
-
736
- /* Focus styles */
737
- button:focus-visible,
738
- input:focus-visible {
739
- outline: 2px solid var(--accent-color);
740
- outline-offset: 2px;
741
- }
742
-
743
- /* Scrollbar styling */
744
- ::-webkit-scrollbar {
745
- width: 8px;
746
- }
747
-
748
- ::-webkit-scrollbar-track {
749
- background: var(--primary-color);
750
- }
751
-
752
- ::-webkit-scrollbar-thumb {
753
- background: var(--accent-color);
754
- border-radius: 4px;
755
- }
756
-
757
- ::-webkit-scrollbar-thumb:hover {
758
- background: #00a8cc;
759
- }
760
-
761
- /* High contrast mode support */
762
- @media (prefers-contrast: high) {
763
- :root {
764
- --glass-bg: rgba(255, 255, 255, 0.2);
765
- --glass-border: rgba(255, 255, 255, 0.5);
766
- }
767
- }
768
-
769
- /* Print styles */
770
- @media print {
771
- header, .player-bar, .filter-buttons, .search-section {
772
- display: none;
773
- }
774
-
775
- main {
776
- padding: 0;
777
- }
778
-
779
- .stations-grid {
780
- display: block;
781
- }
782
-
783
- .station-card {
784
- break-inside: avoid;
785
- page-break-inside: avoid;
786
- margin-bottom: 1rem;
787
- }
788
- }
789
- </style>
790
- </head>
791
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  <body>
793
- <header role="banner">
794
- <h1><i class="fas fa-broadcast-tower" aria-hidden="true"></i> Deutsches Online Radio</h1>
795
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener" class="anycoder-link">
796
- Built with anycoder
797
- </a>
798
- </header>
799
-
800
- <main role="main">
801
- <section class="search-section" aria-label="Suche">
802
- <input type="text" class="search-input" id="searchInput"
803
- placeholder="🔍 Sender nach Name, Genre oder Region suchen..."
804
- aria-label="Radiosender suchen" autocomplete="off">
805
- </section>
806
-
807
- <section class="filter-buttons" id="filterButtons" role="group" aria-label="Filter nach Kategorie">
808
- <button class="filter-btn active" data-filter="all" aria-pressed="true">Alle</button>
809
- <button class="filter-btn" data-filter="national" aria-pressed="false">National</button>
810
- <button class="filter-btn" data-filter="regional" aria-pressed="false">Regional</button>
811
- <button class="filter-btn" data-filter="news" aria-pressed="false">News</button>
812
- <button class="filter-btn" data-filter="music" aria-pressed="false">Musik</button>
813
- <button class="filter-btn" data-filter="kultur" aria-pressed="false">Kultur</button>
814
- </section>
815
-
816
- <section class="stations-grid" id="stationsGrid" role="list" aria-live="polite"></section>
817
-
818
- <section class="empty-state" id="emptyState" role="status">
819
- <i class="fas fa-search" aria-hidden="true"></i>
820
- <h3>Keine Sender gefunden</h3>
821
- <p>Probiere es mit einem anderen Suchbegriff oder Filter.</p>
822
- </section>
823
- </main>
824
-
825
- <footer class="player-bar" id="playerBar" role="complementary" aria-label="Audioplayer">
826
- <div class="player-content">
827
- <div class="now-playing" id="nowPlaying">
828
- <img src="" alt="Sender Logo" class="now-playing-logo" id="nowPlayingLogo">
829
- <div class="now-playing-info">
830
- <h4 id="nowPlayingName">Kein Sender ausgewählt</h4>
831
- <p id="nowPlayingGenre">Wähle einen Sender zum Abspielen</p>
832
  </div>
833
- </div>
834
-
835
- <div class="player-controls">
836
- <button class="control-btn" id="prevBtn" title="Vorheriger Sender" aria-label="Vorheriger Sender">
837
- <i class="fas fa-step-backward" aria-hidden="true"></i>
838
- </button>
839
- <button class="control-btn" id="playPauseBtn" title="Play/Pause" aria-label="Play/Pause">
840
- <i class="fas fa-play" id="playPauseIcon" aria-hidden="true"></i>
841
- </button>
842
- <button class="control-btn" id="nextBtn" title="Nächster Sender" aria-label="Nächster Sender">
843
- <i class="fas fa-step-forward" aria-hidden="true"></i>
844
- </button>
845
- </div>
846
-
847
- <div class="volume-control">
848
- <button class="control-btn" id="muteBtn" title="Stummschalten" aria-label="Stummschalten">
849
- <i class="fas fa-volume-up" id="volumeIcon" aria-hidden="true"></i>
850
- </button>
851
- <input type="range" class="volume-slider" id="volumeSlider"
852
- min="0" max="100" value="70" aria-label="Lautstärke">
853
- </div>
854
- </div>
855
- </footer>
856
-
857
- <div class="loading-spinner" id="loadingSpinner" role="status" aria-live="assertive">
858
- <div class="spinner" aria-hidden="true"></div>
859
- <span class="sr-only">Lade Sender...</span>
860
- </div>
861
-
862
- <div class="toast" id="toast" role="alert" aria-live="assertive"></div>
863
-
864
- <audio id="audioPlayer" preload="none"></audio>
865
-
866
- <script>
867
- // Station Data - Official Public Broadcasters (Complete & Validated)
868
- const stations = [
869
- {
870
- id: 'dlf',
871
- name: 'Deutschlandfunk',
872
- description: 'Nachrichten, Politik und Wissenschaft',
873
- streamUrl: 'https://stream.deutschlandradio.de/dlf/04/dlfdia.cast',
874
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Deutschlandfunk_Logo_2017.svg/300px-Deutschlandfunk_Logo_2017.svg.png',
875
- category: 'national',
876
- genre: 'news',
877
- region: 'Deutschlandweit'
878
- },
879
- {
880
- id: 'dlfkultur',
881
- name: 'Deutschlandfunk Kultur',
882
- description: 'Kultur, Literatur und Gesellschaft',
883
- streamUrl: 'https://stream.deutschlandradio.de/dlfkultur/04/dlfkulturdia.cast',
884
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Deutschlandfunk_Kultur_Logo_2017.svg/300px-Deutschlandfunk_Kultur_Logo_2017.svg.png',
885
- category: 'national',
886
- genre: 'kultur',
887
- region: 'Deutschlandweit'
888
- },
889
- {
890
- id: 'dlfnova',
891
- name: 'Deutschlandfunk Nova',
892
- description: 'Jugendradio mit Musik und Talk',
893
- streamUrl: 'https://stream.deutschlandradio.de/dlfnova/04/dlfnovadia.cast',
894
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/Deutschlandfunk_Nova_Logo_2017.svg/300px-Deutschlandfunk_Nova_Logo_2017.svg.png',
895
- category: 'national',
896
- genre: 'music',
897
- region: 'Deutschlandweit'
898
- },
899
- {
900
- id: 'wdr2',
901
- name: 'WDR 2',
902
- description: 'Regionalradio für NRW mit Nachrichten',
903
- streamUrl: 'https://wdr-wdr2-koeln.icecastssl.wdr.de/wdr/wdr2/koeln/mp3/128/stream.mp3',
904
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/WDR_2_Logo_2016.svg/300px-WDR_2_Logo_2016.svg.png',
905
- category: 'regional',
906
- genre: 'news',
907
- region: 'Nordrhein-Westfalen'
908
- },
909
- {
910
- id: 'wdr5',
911
- name: 'WDR 5',
912
- description: 'Informationen und Hintergründe',
913
- streamUrl: 'https://wdr-wdr5-live.icecastssl.wdr.de/wdr/wdr5/live/mp3/128/stream.mp3',
914
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/WDR_5_Logo_2016.svg/300px-WDR_5_Logo_2016.svg.png',
915
- category: 'national',
916
- genre: 'news',
917
- region: 'Deutschlandweit'
918
- },
919
- {
920
- id: 'ndrinfo',
921
- name: 'NDR Info',
922
- description: 'Nachrichten aus Norddeutschland',
923
- streamUrl: 'https://ndr-ndrinfo-hh.sslcast.addradio.de/ndr/ndrinfo/hh/mp3/128/stream.mp3',
924
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/NDR_Info_logo.svg/300px-NDR_Info_logo.svg.png',
925
- category: 'regional',
926
- genre: 'news',
927
- region: 'Norddeutschland'
928
- },
929
- {
930
- id: 'ndr2',
931
- name: 'NDR 2',
932
- description: 'Die beste Musik für Norddeutschland',
933
- streamUrl: 'https://ndr-ndr2-hh.sslcast.addradio.de/ndr/ndr2/hh/mp3/128/stream.mp3',
934
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/NDR_2_logo.svg/300px-NDR_2_logo.svg.png',
935
- category: 'regional',
936
- genre: 'music',
937
- region: 'Norddeutschland'
938
- },
939
- {
940
- id: 'br24',
941
- name: 'BR24',
942
- description: 'Bayerisches Nachrichtenradio',
943
- streamUrl: 'https://br-br24-live.sslcast.addradio.de/br/br24/live/mp3/128/stream.mp3',
944
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/BR24_logo.svg/300px-BR24_logo.svg.png',
945
- category: 'regional',
946
- genre: 'news',
947
- region: 'Bayern'
948
- },
949
- {
950
- id: 'bayern1',
951
- name: 'Bayern 1',
952
- description: 'Die beste Musik für Bayern',
953
- streamUrl: 'https://br-bayern1-obb.sslcast.addradio.de/br/bayern1/obb/mp3/128/stream.mp3',
954
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Bayern_1_logo.svg/300px-Bayern_1_logo.svg.png',
955
- category: 'regional',
956
- genre: 'music',
957
- region: 'Bayern'
958
- },
959
- {
960
- id: 'bayern3',
961
- name: 'Bayern 3',
962
- description: 'Das junge Radio für Bayern',
963
- streamUrl: 'https://br-bayern3-live.sslcast.addradio.de/br/bayern3/live/mp3/128/stream.mp3',
964
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Bayern_3_logo.svg/300px-Bayern_3_logo.svg.png',
965
- category: 'regional',
966
- genre: 'music',
967
- region: 'Bayern'
968
- },
969
- {
970
- id: 'swr1bw',
971
- name: 'SWR1 Baden-Württemberg',
972
- description: 'Das Radio für Baden-Württemberg',
973
- streamUrl: 'https://swr-swr1-bw.cast.addradio.de/swr/swr1/bw/mp3/128/stream.mp3',
974
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/SWR_1_Logo_2015.svg/300px-SWR_1_Logo_2015.svg.png',
975
- category: 'regional',
976
- genre: 'music',
977
- region: 'Baden-Württemberg'
978
- },
979
- {
980
- id: 'swr3',
981
- name: 'SWR3',
982
- description: 'Das junge Radio für BW und RP',
983
- streamUrl: 'https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3',
984
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/SWR3_logo.svg/300px-SWR3_logo.svg.png',
985
- category: 'national',
986
- genre: 'music',
987
- region: 'Baden-Württemberg, Rheinland-Pfalz'
988
- },
989
- {
990
- id: 'hr1',
991
- name: 'hr1',
992
- description: 'Informationen aus Hessen',
993
- streamUrl: 'https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3',
994
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Hr1_logo.svg/300px-Hr1_logo.svg.png',
995
- category: 'regional',
996
- genre: 'news',
997
- region: 'Hessen'
998
- },
999
- {
1000
- id: 'hr3',
1001
- name: 'hr3',
1002
- description: 'Das Pop- und Eventradio für Hessen',
1003
- streamUrl: 'https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3',
1004
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Hr3_logo.svg/300px-Hr3_logo.svg.png',
1005
- category: 'regional',
1006
- genre: 'music',
1007
- region: 'Hessen'
1008
- },
1009
- {
1010
- id: 'mdrsachsen',
1011
- name: 'MDR Sachsen',
1012
- description: 'Das Radio für Sachsen',
1013
- streamUrl: 'https://mdr-mdrsachsen-live.cast.addradio.de/mdr/mdrsachsen/live/mp3/128/stream.mp3',
1014
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/MDR_Sachsen_Logo.svg/300px-MDR_Sachsen_Logo.svg.png',
1015
- category: 'regional',
1016
- genre: 'music',
1017
- region: 'Sachsen'
1018
- },
1019
- {
1020
- id: 'mdrjump',
1021
- name: 'MDR JUMP',
1022
- description: 'Der beste Mix für Sachsen, Sachsen-Anhalt, Thüringen',
1023
- streamUrl: 'https://mdr-mdrjump-live.cast.addradio.de/mdr/mdrjump/live/mp3/128/stream.mp3',
1024
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/MDR_Jump_Logo.svg/300px-MDR_Jump_Logo.svg.png',
1025
- category: 'regional',
1026
- genre: 'music',
1027
- region: 'Sachsen, Sachsen-Anhalt, Thüringen'
1028
- },
1029
- {
1030
- id: 'rbb',
1031
- name: 'rbbKultur',
1032
- description: 'Kultur- und Wortradio für Berlin und Brandenburg',
1033
- streamUrl: 'https://rbb-rbbkultur-live.sslcast.addradio.de/rbb/rbbkultur/live/mp3/128/stream.mp3',
1034
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Rbb_Kultur_logo.svg/300px-Rbb_Kultur_logo.svg.png',
1035
- category: 'regional',
1036
- genre: 'kultur',
1037
- region: 'Berlin, Brandenburg'
1038
- },
1039
- {
1040
- id: 'radioeins',
1041
- name: 'radioeins',
1042
- description: 'Berlins alternativer Sender',
1043
- streamUrl: 'https://rbb-radioeins-live.sslcast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3',
1044
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Radioeins_Logo_2017.svg/300px-Radioeins_Logo_2017.svg.png',
1045
- category: 'regional',
1046
- genre: 'music',
1047
- region: 'Berlin, Brandenburg'
1048
- },
1049
- {
1050
- id: 'sr1',
1051
- name: 'SR 1 Europawelle',
1052
- description: 'Das Radio für das Saarland',
1053
- streamUrl: 'https://sr-sr1-live.cast.addradio.de/sr/sr1/live/mp3/128/stream.mp3',
1054
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/SR_1_Europawelle_logo.svg/300px-SR_1_Europawelle_logo.svg.png',
1055
- category: 'regional',
1056
- genre: 'music',
1057
- region: 'Saarland'
1058
- }
1059
- ];
1060
-
1061
- // App State
1062
- let currentStation = null;
1063
- let isPlaying = false;
1064
- let currentVolume = 70;
1065
- let favorites = JSON.parse(localStorage.getItem('radioFavorites')) || [];
1066
- let currentFilter = 'all';
1067
- let searchTerm = '';
1068
- let stationHistory = [];
1069
- let currentHistoryIndex = -1;
1070
-
1071
- // DOM Elements
1072
- const audioPlayer = document.getElementById('audioPlayer');
1073
- const stationsGrid = document.getElementById('stationsGrid');
1074
- const searchInput = document.getElementById('searchInput');
1075
- const filterButtons = document.querySelectorAll('.filter-btn');
1076
- const playerBar = document.getElementById('playerBar');
1077
- const nowPlayingLogo = document.getElementById('nowPlayingLogo');
1078
- const nowPlayingName = document.getElementById('nowPlayingName');
1079
- const nowPlayingGenre = document.getElementById('nowPlayingGenre');
1080
- const playPauseBtn = document.getElementById('playPauseBtn');
1081
- const playPauseIcon = document.getElementById('playPauseIcon');
1082
- const prevBtn = document.getElementById('prevBtn');
1083
- const nextBtn = document.getElementById('nextBtn');
1084
- const muteBtn = document.getElementById('muteBtn');
1085
- const volumeIcon = document.getElementById('volumeIcon');
1086
- const volumeSlider = document.getElementById('volumeSlider');
1087
- const loadingSpinner = document.getElementById('loadingSpinner');
1088
- const toast = document.getElementById('toast');
1089
- const emptyState = document.getElementById('emptyState');
1090
-
1091
- // Initialize App
1092
- document.addEventListener('DOMContentLoaded', () => {
1093
- // Sort stations alphabetically
1094
- stations.sort((a, b) => a.name.localeCompare(b.name, 'de'));
1095
-
1096
- renderStations();
1097
- setupEventListeners();
1098
- audioPlayer.volume = currentVolume / 100;
1099
- volumeSlider.value = currentVolume;
1100
-
1101
- // Restore last played station
1102
- const lastStation = localStorage.getItem('lastStation');
1103
- if (lastStation) {
1104
- const station = stations.find(s => s.id === lastStation);
1105
- if (station) {
1106
- selectStation(station, false);
1107
  }
1108
- }
1109
-
1110
- // Check for browser support
1111
- if (!window.MediaSource && !audioPlayer.canPlayType('audio/mpeg')) {
1112
- showToast('Ihr Browser unterstützt kein Audio-Streaming. Bitte aktualisieren Sie Ihren Browser.', 'error');
1113
- }
1114
- });
1115
-
1116
- // Setup Event Listeners
1117
- function setupEventListeners() {
1118
- // Search functionality
1119
- searchInput.addEventListener('input', debounce((e) => {
1120
- searchTerm = e.target.value.toLowerCase().trim();
1121
- renderStations();
1122
- }, 300));
1123
-
1124
- // Filter buttons
1125
- filterButtons.forEach(btn => {
1126
- btn.addEventListener('click', () => {
1127
- filterButtons.forEach(b => {
1128
- b.classList.remove('active');
1129
- b.setAttribute('aria-pressed', 'false');
1130
- });
1131
- btn.classList.add('active');
1132
- btn.setAttribute('aria-pressed', 'true');
1133
- currentFilter = btn.dataset.filter;
1134
- renderStations();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
  });
1136
- });
1137
-
1138
- // Player controls
1139
- playPauseBtn.addEventListener('click', togglePlayPause);
1140
- prevBtn.addEventListener('click', playPreviousStation);
1141
- nextBtn.addEventListener('click', playNextStation);
1142
- muteBtn.addEventListener('click', toggleMute);
1143
- volumeSlider.addEventListener('input', (e) => {
1144
- currentVolume = parseInt(e.target.value);
1145
- audioPlayer.volume = currentVolume / 100;
1146
- updateVolumeIcon();
1147
- localStorage.setItem('volume', currentVolume);
1148
- });
1149
-
1150
- // Audio player events
1151
- audioPlayer.addEventListener('play', () => {
1152
- isPlaying = true;
1153
- updatePlayPauseIcon();
1154
- });
1155
-
1156
- audioPlayer.addEventListener('pause', () => {
1157
- isPlaying = false;
1158
- updatePlayPauseIcon();
1159
- });
1160
-
1161
- audioPlayer.addEventListener('error', (e) => {
1162
- handleAudioError(e);
1163
- });
1164
-
1165
- audioPlayer.addEventListener('loadeddata', () => {
1166
- hideLoading();
1167
- });
1168
-
1169
- // Keyboard shortcuts
1170
- document.addEventListener('keydown', (e) => {
1171
- if (e.target.tagName === 'INPUT') return;
1172
-
1173
- switch(e.key) {
1174
- case ' ':
1175
- e.preventDefault();
1176
- togglePlayPause();
1177
- break;
1178
- case 'ArrowLeft':
1179
- playPreviousStation();
1180
- break;
1181
- case 'ArrowRight':
1182
- playNextStation();
1183
- break;
1184
- case 'ArrowUp':
1185
- e.preventDefault();
1186
- changeVolume(5);
1187
- break;
1188
- case 'ArrowDown':
1189
- e.preventDefault();
1190
- changeVolume(-5);
1191
- break;
1192
- case 'm':
1193
- toggleMute();
1194
- break;
1195
- }
 
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>StreamFlow - Online Radio</title>
7
+ <!-- Importiere Icons (FontAwesome) -->
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+
10
+ <style>
11
+ :root {
12
+ --primary-color: #6366f1;
13
+ --primary-hover: #4f46e5;
14
+ --bg-dark: #0f172a;
15
+ --bg-card: #1e293b;
16
+ --text-main: #f8fafc;
17
+ --text-muted: #94a3b8;
18
+ --accent-glow: rgba(99, 102, 241, 0.5);
19
+ --glass-bg: rgba(30, 41, 59, 0.7);
20
+ --glass-border: rgba(255, 255, 255, 0.1);
21
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ margin: 0;
27
+ padding: 0;
28
+ outline: none;
29
+ }
30
+
31
+ body {
32
+ font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
33
+ background-color: var(--bg-dark);
34
+ color: var(--text-main);
35
+ min-height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow-x: hidden;
39
+ }
40
+
41
+ /* --- Header --- */
42
+ header {
43
+ background: var(--glass-bg);
44
+ backdrop-filter: blur(12px);
45
+ -webkit-backdrop-filter: blur(12px);
46
+ border-bottom: 1px solid var(--glass-border);
47
+ padding: 1rem 2rem;
48
+ position: sticky;
49
+ top: 0;
50
+ z-index: 100;
51
+ display: flex;
52
+ justify-content: space-between;
53
+ align-items: center;
54
+ flex-wrap: wrap;
55
+ gap: 1rem;
56
+ }
57
+
58
+ .logo {
59
+ font-size: 1.5rem;
60
+ font-weight: 700;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0.5rem;
64
+ color: var(--primary-color);
65
+ }
66
+
67
+ .logo i {
68
+ font-size: 1.8rem;
69
+ }
70
+
71
+ .search-container {
72
+ flex: 1;
73
+ max-width: 400px;
74
+ position: relative;
75
+ }
76
+
77
+ .search-container input {
78
+ width: 100%;
79
+ background: var(--bg-dark);
80
+ border: 1px solid var(--glass-border);
81
+ padding: 0.75rem 1rem 0.75rem 2.5rem;
82
+ border-radius: 99px;
83
+ color: var(--text-main);
84
+ font-size: 0.95rem;
85
+ transition: var(--transition);
86
+ }
87
+
88
+ .search-container input:focus {
89
+ border-color: var(--primary-color);
90
+ box-shadow: 0 0 0 2px var(--accent-glow);
91
+ }
92
+
93
+ .search-container i {
94
+ position: absolute;
95
+ left: 1rem;
96
+ top: 50%;
97
+ transform: translateY(-50%);
98
+ color: var(--text-muted);
99
+ }
100
+
101
+ .anycoder-link {
102
+ font-size: 0.85rem;
103
+ color: var(--text-muted);
104
+ text-decoration: none;
105
+ transition: var(--transition);
106
+ background: rgba(255,255,255,0.05);
107
+ padding: 0.4rem 0.8rem;
108
+ border-radius: 6px;
109
+ }
110
+
111
+ .anycoder-link:hover {
112
+ color: var(--text-main);
113
+ background: rgba(255,255,255,0.1);
114
+ }
115
+
116
+ /* --- Main Layout --- */
117
+ main {
118
+ flex: 1;
119
+ display: grid;
120
+ grid-template-columns: 350px 1fr;
121
+ gap: 2rem;
122
+ padding: 2rem;
123
+ max-width: 1400px;
124
+ margin: 0 auto;
125
+ width: 100%;
126
+ }
127
+
128
+ /* --- Player Section --- */
129
+ .player-card {
130
+ background: var(--bg-card);
131
+ border-radius: 24px;
132
+ padding: 2rem;
133
+ text-align: center;
134
+ border: 1px solid var(--glass-border);
135
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
136
+ display: flex;
137
+ flex-direction: column;
138
+ align-items: center;
139
+ justify-content: center;
140
+ height: fit-content;
141
+ position: sticky;
142
+ top: 6rem;
143
+ }
144
+
145
+ .album-art {
146
+ width: 200px;
147
+ height: 200px;
148
+ border-radius: 50%;
149
+ overflow: hidden;
150
+ margin-bottom: 1.5rem;
151
+ position: relative;
152
+ box-shadow: 0 0 20px var(--accent-glow);
153
+ transition: var(--transition);
154
+ }
155
+
156
+ .album-art img {
157
+ width: 100%;
158
+ height: 100%;
159
+ object-fit: cover;
160
+ transition: transform 10s linear;
161
+ }
162
+
163
+ .album-art.playing img {
164
+ transform: rotate(360deg);
165
+ animation: spin 10s linear infinite;
166
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
+ @keyframes spin {
169
+ from { transform: rotate(0deg); }
170
+ to { transform: rotate(360deg); }
171
+ }
172
+
173
+ .station-info h2 {
174
+ font-size: 1.5rem;
175
+ margin-bottom: 0.5rem;
176
+ color: var(--text-main);
177
+ }
178
+
179
+ .station-info p {
180
+ color: var(--text-muted);
181
+ font-size: 0.9rem;
182
+ margin-bottom: 2rem;
183
+ text-transform: uppercase;
184
+ letter-spacing: 1px;
185
+ }
186
+
187
+ .visualizer {
188
+ display: flex;
189
+ justify-content: center;
190
+ align-items: flex-end;
191
+ gap: 4px;
192
+ height: 30px;
193
+ margin-bottom: 2rem;
194
+ opacity: 0;
195
+ transition: opacity 0.3s;
196
+ }
197
+
198
+ .visualizer.active {
199
+ opacity: 1;
200
+ }
201
+
202
+ .bar {
203
+ width: 6px;
204
+ background: var(--primary-color);
205
+ border-radius: 3px;
206
+ animation: bounce 0s infinite ease-in-out;
207
+ }
208
+
209
+ .visualizer.active .bar {
210
+ animation-duration: 0.8s;
211
+ }
212
+
213
+ @keyframes bounce {
214
+ 0%, 100% { height: 5px; }
215
+ 50% { height: 25px; }
216
+ }
217
+
218
+ /* Stagger animations for bars */
219
+ .bar:nth-child(1) { animation-delay: 0.1s; }
220
+ .bar:nth-child(2) { animation-delay: 0.3s; }
221
+ .bar:nth-child(3) { animation-delay: 0.5s; }
222
+ .bar:nth-child(4) { animation-delay: 0.2s; }
223
+ .bar:nth-child(5) { animation-delay: 0.4s; }
224
+
225
+ .controls {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 1.5rem;
229
+ margin-bottom: 1.5rem;
230
+ }
231
+
232
+ .btn-control {
233
+ background: none;
234
+ border: none;
235
+ color: var(--text-main);
236
+ cursor: pointer;
237
+ transition: var(--transition);
238
+ }
239
+
240
+ .btn-control:hover {
241
+ color: var(--primary-color);
242
+ }
243
+
244
+ .btn-play {
245
+ width: 60px;
246
+ height: 60px;
247
+ border-radius: 50%;
248
+ background: var(--primary-color);
249
+ color: white;
250
+ font-size: 1.5rem;
251
+ display: flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ box-shadow: 0 4px 15px var(--accent-glow);
255
+ }
256
+
257
+ .btn-play:hover {
258
+ background: var(--primary-hover);
259
+ transform: scale(1.05);
260
+ color: white;
261
+ }
262
+
263
+ .volume-container {
264
+ width: 100%;
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 10px;
268
+ color: var(--text-muted);
269
+ }
270
+
271
+ .volume-slider {
272
+ flex: 1;
273
+ -webkit-appearance: none;
274
+ height: 4px;
275
+ background: rgba(255,255,255,0.1);
276
+ border-radius: 2px;
277
+ cursor: pointer;
278
+ }
279
+
280
+ .volume-slider::-webkit-slider-thumb {
281
+ -webkit-appearance: none;
282
+ width: 12px;
283
+ height: 12px;
284
+ background: var(--text-main);
285
+ border-radius: 50%;
286
+ cursor: pointer;
287
+ transition: var(--transition);
288
+ }
289
+
290
+ .volume-slider::-webkit-slider-thumb:hover {
291
+ background: var(--primary-color);
292
+ }
293
+
294
+ /* --- Station List --- */
295
+ .stations-section h3 {
296
+ margin-bottom: 1.5rem;
297
+ font-size: 1.2rem;
298
+ border-left: 4px solid var(--primary-color);
299
+ padding-left: 1rem;
300
+ }
301
+
302
+ .stations-grid {
303
+ display: grid;
304
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
305
+ gap: 1.5rem;
306
+ }
307
+
308
+ .station-card {
309
+ background: var(--glass-bg);
310
+ border: 1px solid var(--glass-border);
311
+ border-radius: 16px;
312
+ padding: 1rem;
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 1rem;
316
+ cursor: pointer;
317
+ transition: var(--transition);
318
+ position: relative;
319
+ overflow: hidden;
320
+ }
321
+
322
+ .station-card:hover {
323
+ transform: translateY(-3px);
324
+ background: rgba(255,255,255,0.08);
325
+ border-color: rgba(255,255,255,0.2);
326
+ }
327
+
328
+ .station-card.active {
329
+ border-color: var(--primary-color);
330
+ background: rgba(99, 102, 241, 0.1);
331
+ }
332
+
333
+ .station-card.active::before {
334
+ content: '';
335
+ position: absolute;
336
+ top: 0;
337
+ left: 0;
338
+ width: 4px;
339
+ height: 100%;
340
+ background: var(--primary-color);
341
+ }
342
+
343
+ .station-thumb {
344
+ width: 60px;
345
+ height: 60px;
346
+ border-radius: 12px;
347
+ object-fit: cover;
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .station-details {
352
+ flex: 1;
353
+ min-width: 0; /* Text truncation fix */
354
+ }
355
+
356
+ .station-name {
357
+ font-weight: 600;
358
+ margin-bottom: 0.25rem;
359
+ white-space: nowrap;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ }
363
+
364
+ .station-genre {
365
+ font-size: 0.8rem;
366
+ color: var(--text-muted);
367
+ display: flex;
368
+ align-items: center;
369
+ gap: 0.5rem;
370
+ }
371
+
372
+ .play-indicator {
373
+ width: 32px;
374
+ height: 32px;
375
+ border-radius: 50%;
376
+ background: rgba(255,255,255,0.1);
377
+ display: flex;
378
+ align-items: center;
379
+ justify-content: center;
380
+ transition: var(--transition);
381
+ color: var(--text-main);
382
+ }
383
+
384
+ .station-card:hover .play-indicator {
385
+ background: var(--primary-color);
386
+ color: white;
387
+ }
388
+
389
+ .station-card.active .play-indicator {
390
+ background: var(--primary-color);
391
+ color: white;
392
+ animation: pulse 2s infinite;
393
+ }
394
+
395
+ @keyframes pulse {
396
+ 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
397
+ 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
398
+ 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
399
+ }
400
+
401
+ /* --- Toast Notification --- */
402
+ #toast-container {
403
+ position: fixed;
404
+ bottom: 2rem;
405
+ right: 2rem;
406
+ z-index: 1000;
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: 1rem;
410
+ }
411
+
412
+ .toast {
413
+ background: var(--bg-card);
414
+ border: 1px solid var(--glass-border);
415
+ padding: 1rem 1.5rem;
416
+ border-radius: 12px;
417
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 0.75rem;
421
+ animation: slideIn 0.3s ease-out forwards;
422
+ min-width: 250px;
423
+ }
424
+
425
+ .toast.error { border-left: 4px solid #ef4444; }
426
+ .toast.success { border-left: 4px solid #22c55e; }
427
+ .toast.info { border-left: 4px solid var(--primary-color); }
428
+
429
+ @keyframes slideIn {
430
+ from { transform: translateX(100%); opacity: 0; }
431
+ to { transform: translateX(0); opacity: 1; }
432
+ }
433
+
434
+ @keyframes fadeOut {
435
+ to { transform: translateX(100%); opacity: 0; }
436
+ }
437
+
438
+ /* --- Responsive --- */
439
+ @media (max-width: 900px) {
440
+ main {
441
+ grid-template-columns: 1fr;
442
+ }
443
+
444
+ .player-card {
445
+ position: relative;
446
+ top: 0;
447
+ margin-bottom: 2rem;
448
+ }
449
+
450
+ .album-art {
451
+ width: 150px;
452
+ height: 150px;
453
+ }
454
+ }
455
+
456
+ @media (max-width: 600px) {
457
+ header {
458
+ flex-direction: column;
459
+ align-items: stretch;
460
+ padding: 1rem;
461
+ }
462
+
463
+ .search-container {
464
+ max-width: 100%;
465
+ order: 3;
466
+ }
467
+
468
+ .logo {
469
+ justify-content: center;
470
+ }
471
+
472
+ .anycoder-link {
473
+ align-self: flex-end;
474
+ }
475
+ }
476
+ </style>
477
+ </head>
478
  <body>
479
+
480
+ <header>
481
+ <div class="logo">
482
+ <i class="fa-solid fa-radio"></i>
483
+ StreamFlow
484
+ </div>
485
+ <div class="search-container">
486
+ <i class="fa-solid fa-search"></i>
487
+ <input type="text" id="searchInput" placeholder="Sender suchen...">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  </div>
489
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
490
+ Built with anycoder
491
+ </a>
492
+ </header>
493
+
494
+ <main>
495
+ <!-- Player Section -->
496
+ <section class="player-card">
497
+ <div class="album-art" id="albumArt">
498
+ <img src="https://picsum.photos/seed/music/300/300" alt="Album Art" id="currentImage">
499
+ </div>
500
+
501
+ <div class="station-info">
502
+ <h2 id="currentStationName">Wähle einen Sender</h2>
503
+ <p id="currentGenre">Bereit zum Abspielen</p>
504
+ </div>
505
+
506
+ <!-- CSS Visualizer -->
507
+ <div class="visualizer" id="visualizer">
508
+ <div class="bar"></div>
509
+ <div class="bar"></div>
510
+ <div class="bar"></div>
511
+ <div class="bar"></div>
512
+ <div class="bar"></div>
513
+ </div>
514
+
515
+ <div class="controls">
516
+ <!-- Prev Button (Visual only for this demo) -->
517
+ <button class="btn-control" title="Vorheriger">
518
+ <i class="fa-solid fa-backward-step fa-lg"></i>
519
+ </button>
520
+
521
+ <button class="btn-play" id="playPauseBtn" title="Play/Pause">
522
+ <i class="fa-solid fa-play" id="playIcon"></i>
523
+ </button>
524
+
525
+ <!-- Next Button (Visual only for this demo) -->
526
+ <button class="btn-control" title="Nächster">
527
+ <i class="fa-solid fa-forward-step fa-lg"></i>
528
+ </button>
529
+ </div>
530
+
531
+ <div class="volume-container">
532
+ <i class="fa-solid fa-volume-low"></i>
533
+ <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.05" value="0.8">
534
+ <i class="fa-solid fa-volume-high"></i>
535
+ </div>
536
+ </section>
537
+
538
+ <!-- Stations List Section -->
539
+ <section class="stations-section">
540
+ <h3>Verfügbare Sender</h3>
541
+ <div class="stations-grid" id="stationsGrid">
542
+ <!-- Stations will be injected here via JS -->
543
+ </div>
544
+ </section>
545
+ </main>
546
+
547
+ <div id="toast-container"></div>
548
+
549
+ <script>
550
+ // --- Data: Radio Stations ---
551
+ // Using reliable public MP3 streams.
552
+ const stations = [
553
+ {
554
+ name: "Antenne Bayern",
555
+ genre: "Pop",
556
+ url: "https://stream.antenne.de/antenne",
557
+ image: "https://picsum.photos/seed/antenne/200/200"
558
+ },
559
+ {
560
+ name: "NDR 1 Niedersachsen",
561
+ genre: "Information & Pop",
562
+ 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
563
+ // Fallback to direct MP3 for broader compatibility in this demo
564
+ url: "https://icecast.ndr.de/ndr/ndr1/niedersachsen/mp3/128/stream.mp3",
565
+ image: "https://picsum.photos/seed/ndr/200/200"
566
+ },
567
+ {
568
+ name: "1LIVE",
569
+ genre: "Rock & Pop",
570
+ url: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3",
571
+ image: "https://picsum.photos/seed/1live/200/200"
572
+ },
573
+ {
574
+ name: "SWR3",
575
+ genre: "Pop & Hits",
576
+ url: "https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3",
577
+ image: "https://picsum.photos/seed/swr3/200/200"
578
+ },
579
+ {
580
+ name: "Deutschlandfunk",
581
+ genre: "Nachrichten & Kultur",
582
+ url: "https://st01.dlf.de/dlf/01/128/mp3/stream.mp3",
583
+ image: "https://picsum.photos/seed/dlf/200/200"
584
+ },
585
+ {
586
+ name: "SRF 3",
587
+ genre: "Pop & Rock",
588
+ url: "https://stream.srg-ssr.ch/m/drs3/mp3_128",
589
+ image: "https://picsum.photos/seed/srf3/200/200"
590
+ },
591
+ {
592
+ name: "BBC World Service",
593
+ genre: "International News",
594
+ url: "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service",
595
+ image: "https://picsum.photos/seed/bbc/200/200"
596
+ },
597
+ {
598
+ name: "Ibiza Global Radio",
599
+ genre: "Electronic",
600
+ url: "http://ibizaglobalradio.streaming-pro.com:8024/;stream.mp3",
601
+ image: "https://picsum.photos/seed/ibiza/200/200"
602
+ },
603
+ {
604
+ name: "Classic FM",
605
+ genre: "Classical",
606
+ url: "http://media-the.musicradio.com/ClassicFMMP3",
607
+ image: "https://picsum.photos/seed/classic/200/200"
608
+ },
609
+ {
610
+ name: "Radio Paradise",
611
+ genre: "Eclectic Rock",
612
+ url: "http://stream.radioparadise.com/mp3-192",
613
+ image: "https://picsum.photos/seed/paradise/200/200"
614
+ }
615
+ ];
616
+
617
+ // --- DOM Elements ---
618
+ const audio = new Audio();
619
+ const stationsGrid = document.getElementById('stationsGrid');
620
+ const playPauseBtn = document.getElementById('playPauseBtn');
621
+ const playIcon = document.getElementById('playIcon');
622
+ const volumeSlider = document.getElementById('volumeSlider');
623
+ const searchInput = document.getElementById('searchInput');
624
+ const currentStationName = document.getElementById('currentStationName');
625
+ const currentGenre = document.getElementById('currentGenre');
626
+ const currentImage = document.getElementById('currentImage');
627
+ const albumArt = document.getElementById('albumArt');
628
+ const visualizer = document.getElementById('visualizer');
629
+ const toastContainer = document.getElementById('toast-container');
630
+
631
+ // --- State ---
632
+ let isPlaying = false;
633
+ let currentStation = null;
634
+
635
+ // --- Initialization ---
636
+ function init() {
637
+ renderStations(stations);
638
+ audio.volume = volumeSlider.value;
639
+ }
640
+
641
+ // --- Render Functions ---
642
+ function renderStations(list) {
643
+ stationsGrid.innerHTML = '';
644
+
645
+ if (list.length === 0) {
646
+ stationsGrid.innerHTML = '<p style="color:var(--text-muted); grid-column: 1/-1;">Keine Sender gefunden.</p>';
647
+ return;
648
+ }
649
+
650
+ list.forEach(station => {
651
+ const card = document.createElement('div');
652
+ card.className = `station-card ${currentStation && currentStation.name === station.name ? 'active' : ''}`;
653
+ card.onclick = () => loadStation(station);
654
+
655
+ card.innerHTML = `
656
+ <img src="${station.image}" alt="${station.name}" class="station-thumb">
657
+ <div class="station-details">
658
+ <div class="station-name">${station.name}</div>
659
+ <div class="station-genre"><i class="fa-solid fa-music"></i> ${station.genre}</div>
660
+ </div>
661
+ <div class="play-indicator">
662
+ <i class="fa-solid ${currentStation && currentStation.name === station.name && isPlaying ? 'fa-chart-simple' : 'fa-play'}"></i>
663
+ </div>
664
+ `;
665
+ stationsGrid.appendChild(card);
666
+ });
667
+ }
668
+
669
+ // --- Player Logic ---
670
+ function loadStation(station) {
671
+ if (currentStation && currentStation.name === station.name) {
672
+ togglePlay();
673
+ return;
674
+ }
675
+
676
+ currentStation = station;
677
+ audio.src = station.url;
678
+
679
+ // Update UI
680
+ currentStationName.textContent = station.name;
681
+ currentGenre.textContent = station.genre;
682
+ currentImage.src = station.image;
683
+
684
+ // Update List UI to show active state
685
+ renderStations(stations.filter(s => s.name.toLowerCase().includes(searchInput.value.toLowerCase())));
686
+
687
+ playAudio();
688
+ }
689
+
690
+ function togglePlay() {
691
+ if (!currentStation) {
692
+ showToast('Bitte wähle zuerst einen Sender aus.', 'info');
693
+ return;
694
+ }
695
+
696
+ if (isPlaying) {
697
+ pauseAudio();
698
+ } else {
699
+ playAudio();
700
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  }
702
+
703
+ function playAudio() {
704
+ const playPromise = audio.play();
705
+
706
+ if (playPromise !== undefined) {
707
+ playPromise.then(_ => {
708
+ isPlaying = true;
709
+ updatePlayerUI();
710
+ })
711
+ .catch(error => {
712
+ console.error('Playback failed:', error);
713
+ isPlaying = false;
714
+ updatePlayerUI();
715
+ showToast('Fehler beim Laden des Streams. Versuche einen anderen Sender.', 'error');
716
+ });
717
+ }
718
+ }
719
+
720
+ function pauseAudio() {
721
+ audio.pause();
722
+ isPlaying = false;
723
+ updatePlayerUI();
724
+ }
725
+
726
+ function updatePlayerUI() {
727
+ if (isPlaying) {
728
+ playIcon.classList.remove('fa-play');
729
+ playIcon.classList.add('fa-pause');
730
+ albumArt.classList.add('playing');
731
+ visualizer.classList.add('active');
732
+ } else {
733
+ playIcon.classList.remove('fa-pause');
734
+ playIcon.classList.add('fa-play');
735
+ albumArt.classList.remove('playing');
736
+ visualizer.classList.remove('active');
737
+ }
738
+ // Refresh list icons
739
+ renderStations(stations.filter(s => s.name.toLowerCase().includes(searchInput.value.toLowerCase())));
740
+ }
741
+
742
+ // --- Event Listeners ---
743
+ playPauseBtn.addEventListener('click', togglePlay);
744
+
745
+ volumeSlider.addEventListener('input', (e) => {
746
+ audio.volume = e.target.value;
747
  });
748
+
749
+ searchInput.addEventListener('input', (e) => {
750
+ const query = e.target.value.toLowerCase();
751
+ const filtered = stations.filter(station =>
752
+ station.name.toLowerCase().includes(query) ||
753
+ station.genre.toLowerCase().includes(query)
754
+ );
755
+ renderStations(filtered);
756
+ });
757
+
758
+ // Handle audio errors (e.g., stream offline)
759
+ audio.addEventListener('error', (e) => {
760
+ if(currentStation) {
761
+ showToast(`Stream "${currentStation.name}" ist nicht verfügbar.`, 'error');
762
+ isPlaying = false;
763
+ updatePlayerUI();
764
+ }
765
+ });
766
+
767
+ // --- Toast Notification System ---
768
+ function showToast(message, type = 'info') {
769
+ const toast = document.createElement('div');
770
+ toast.className = `toast ${type}`;
771
+
772
+ let iconClass = 'fa-info-circle';
773
+ if (type === 'error') iconClass = 'fa-exclamation-circle';
774
+ if (type === 'success') iconClass = 'fa-check-circle';
775
+
776
+ toast.innerHTML = `
777
+ <i class="fa-solid ${iconClass}"></i>
778
+ <span>${message}</span>
779
+ `;
780
+
781
+ toastContainer.appendChild(toast);
782
+
783
+ // Remove after 3 seconds
784
+ setTimeout(() => {
785
+ toast.style.animation = 'fadeOut 0.3s ease-out forwards';
786
+ toast.addEventListener('animationend', () => {
787
+ toast.remove();
788
+ });
789
+ }, 3000);
790
+ }
791
+
792
+ // Run init
793
+ init();
794
+
795
+ </script>
796
+ </body>
797
+ </html>