Multimedix commited on
Commit
4a4fe03
·
verified ·
1 Parent(s): 9fe76fb

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +570 -289
index.html CHANGED
@@ -3,6 +3,8 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
 
6
  <title>Deutsches Online Radio | Öffentlich-rechtliche Sender</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
  <style>
@@ -20,6 +22,7 @@
20
  --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21
  --error-color: #ff4757;
22
  --success-color: #2ed573;
 
23
  }
24
 
25
  * {
@@ -37,13 +40,14 @@
37
  flex-direction: column;
38
  line-height: 1.6;
39
  overflow-x: hidden;
 
40
  }
41
 
42
  /* Header */
43
  header {
44
  background: var(--glass-bg);
45
- backdrop-filter: blur(12px);
46
- -webkit-backdrop-filter: blur(12px);
47
  border-bottom: 1px solid var(--glass-border);
48
  padding: 1.5rem 2rem;
49
  display: flex;
@@ -63,23 +67,26 @@
63
  background-clip: text;
64
  display: flex;
65
  align-items: center;
66
- gap: 0.5rem;
 
67
  }
68
 
69
  .anycoder-link {
70
  color: var(--accent-color);
71
  text-decoration: none;
72
- font-size: 0.9rem;
73
  padding: 0.5rem 1rem;
74
  border-radius: 20px;
75
  background: var(--glass-bg);
76
  border: 1px solid var(--glass-border);
77
  transition: var(--transition);
 
78
  }
79
 
80
  .anycoder-link:hover {
81
- background: rgba(0, 212, 255, 0.1);
82
  transform: translateY(-2px);
 
83
  }
84
 
85
  /* Main Content */
@@ -89,6 +96,7 @@
89
  max-width: 1400px;
90
  margin: 0 auto;
91
  width: 100%;
 
92
  }
93
 
94
  /* Search Section */
@@ -110,10 +118,12 @@
110
  transition: var(--transition);
111
  backdrop-filter: blur(10px);
112
  -webkit-backdrop-filter: blur(10px);
 
113
  }
114
 
115
  .search-input::placeholder {
116
  color: var(--text-secondary);
 
117
  }
118
 
119
  .search-input:focus {
@@ -121,6 +131,10 @@
121
  box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2);
122
  }
123
 
 
 
 
 
124
  /* Filter Buttons */
125
  .filter-buttons {
126
  display: flex;
@@ -130,7 +144,7 @@
130
  }
131
 
132
  .filter-btn {
133
- padding: 0.5rem 1.5rem;
134
  background: var(--glass-bg);
135
  border: 1px solid var(--glass-border);
136
  border-radius: 25px;
@@ -138,24 +152,34 @@
138
  cursor: pointer;
139
  transition: var(--transition);
140
  font-size: 0.9rem;
 
141
  backdrop-filter: blur(10px);
 
 
142
  }
143
 
144
  .filter-btn:hover {
145
  background: rgba(255, 255, 255, 0.15);
146
  color: var(--text-primary);
 
147
  }
148
 
149
  .filter-btn.active {
150
  background: var(--accent-color);
151
  color: var(--primary-color);
152
  border-color: var(--accent-color);
 
 
 
 
 
 
153
  }
154
 
155
  /* Stations Grid */
156
  .stations-grid {
157
  display: grid;
158
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
159
  gap: 1.5rem;
160
  margin-bottom: 2rem;
161
  }
@@ -172,6 +196,8 @@
172
  overflow: hidden;
173
  backdrop-filter: blur(10px);
174
  -webkit-backdrop-filter: blur(10px);
 
 
175
  }
176
 
177
  .station-card::before {
@@ -183,7 +209,7 @@
183
  height: 4px;
184
  background: var(--gradient);
185
  transform: scaleX(0);
186
- transition: transform 0.3s ease;
187
  }
188
 
189
  .station-card:hover {
@@ -198,7 +224,7 @@
198
 
199
  .station-card.playing {
200
  border-color: var(--accent-color);
201
- background: rgba(0, 212, 255, 0.05);
202
  }
203
 
204
  .station-card.playing::before {
@@ -220,20 +246,38 @@
220
  object-fit: cover;
221
  background: var(--secondary-color);
222
  border: 1px solid var(--glass-border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
 
225
  .station-info h3 {
226
  font-size: 1.1rem;
227
  margin-bottom: 0.25rem;
 
 
 
228
  }
229
 
230
  .station-genre {
231
- font-size: 0.85rem;
232
  color: var(--text-secondary);
233
  background: rgba(255, 255, 255, 0.05);
234
  padding: 0.25rem 0.75rem;
235
  border-radius: 12px;
236
  display: inline-block;
 
237
  }
238
 
239
  .station-description {
@@ -244,12 +288,14 @@
244
  -webkit-line-clamp: 2;
245
  -webkit-box-orient: vertical;
246
  overflow: hidden;
 
247
  }
248
 
249
  .station-actions {
250
  display: flex;
251
  justify-content: space-between;
252
  align-items: center;
 
253
  }
254
 
255
  .play-indicator {
@@ -258,6 +304,7 @@
258
  gap: 0.5rem;
259
  font-size: 0.8rem;
260
  color: var(--accent-color);
 
261
  }
262
 
263
  .station-card.playing .play-indicator {
@@ -276,6 +323,7 @@
276
  background: var(--accent-color);
277
  border-radius: 2px;
278
  animation: equalize 1s ease-in-out infinite alternate;
 
279
  }
280
 
281
  .equalizer-bar:nth-child(1) {
@@ -315,15 +363,27 @@
315
  cursor: pointer;
316
  transition: var(--transition);
317
  padding: 0.5rem;
 
 
 
 
318
  }
319
 
320
  .favorite-btn:hover {
321
  color: var(--accent-color);
322
  transform: scale(1.1);
 
323
  }
324
 
325
  .favorite-btn.active {
326
  color: #ff6b6b;
 
 
 
 
 
 
 
327
  }
328
 
329
  /* Player Bar */
@@ -333,8 +393,8 @@
333
  left: 0;
334
  right: 0;
335
  background: var(--glass-bg);
336
- backdrop-filter: blur(20px);
337
- -webkit-backdrop-filter: blur(20px);
338
  border-top: 1px solid var(--glass-border);
339
  padding: 1rem 2rem;
340
  transform: translateY(100%);
@@ -360,6 +420,7 @@
360
  display: flex;
361
  align-items: center;
362
  gap: 1rem;
 
363
  }
364
 
365
  .now-playing-logo {
@@ -368,16 +429,27 @@
368
  border-radius: 10px;
369
  background: var(--secondary-color);
370
  border: 1px solid var(--glass-border);
 
 
 
 
 
371
  }
372
 
373
  .now-playing-info h4 {
374
  font-size: 1rem;
375
  margin-bottom: 0.25rem;
 
 
 
376
  }
377
 
378
  .now-playing-info p {
379
  font-size: 0.85rem;
380
  color: var(--text-secondary);
 
 
 
381
  }
382
 
383
  .player-controls {
@@ -399,12 +471,26 @@
399
  justify-content: center;
400
  transition: var(--transition);
401
  font-size: 1.2rem;
 
 
 
402
  }
403
 
404
  .control-btn:hover {
405
  background: var(--accent-color);
406
  color: var(--primary-color);
407
  transform: scale(1.1);
 
 
 
 
 
 
 
 
 
 
 
408
  }
409
 
410
  .volume-control {
@@ -423,6 +509,7 @@
423
  border-radius: 2px;
424
  outline: none;
425
  cursor: pointer;
 
426
  }
427
 
428
  .volume-slider::-webkit-slider-thumb {
@@ -438,6 +525,7 @@
438
 
439
  .volume-slider::-webkit-slider-thumb:hover {
440
  transform: scale(1.2);
 
441
  }
442
 
443
  .volume-slider::-moz-range-thumb {
@@ -447,6 +535,17 @@
447
  border-radius: 50%;
448
  cursor: pointer;
449
  border: none;
 
 
 
 
 
 
 
 
 
 
 
450
  }
451
 
452
  /* Loading Spinner */
@@ -473,12 +572,8 @@
473
  }
474
 
475
  @keyframes spin {
476
- 0% {
477
- transform: rotate(0deg);
478
- }
479
- 100% {
480
- transform: rotate(360deg);
481
- }
482
  }
483
 
484
  /* Toast Notifications */
@@ -499,6 +594,8 @@
499
  display: flex;
500
  align-items: center;
501
  gap: 0.75rem;
 
 
502
  }
503
 
504
  .toast.show {
@@ -513,6 +610,10 @@
513
  border-color: var(--success-color);
514
  }
515
 
 
 
 
 
516
  /* Empty State */
517
  .empty-state {
518
  text-align: center;
@@ -531,14 +632,41 @@
531
  opacity: 0.5;
532
  }
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  /* Responsive Design */
535
  @media (max-width: 768px) {
536
  header {
537
  padding: 1rem;
538
  }
539
 
 
 
 
 
540
  main {
541
  padding: 1rem;
 
542
  }
543
 
544
  .player-content {
@@ -547,6 +675,14 @@
547
  text-align: center;
548
  }
549
 
 
 
 
 
 
 
 
 
550
  .volume-control {
551
  justify-self: center;
552
  }
@@ -560,6 +696,20 @@
560
  font-size: 0.9rem;
561
  padding: 0.75rem 1.25rem;
562
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  }
564
 
565
  @media (max-width: 480px) {
@@ -567,14 +717,10 @@
567
  grid-template-columns: 1fr;
568
  }
569
 
570
- .player-bar {
571
- padding: 1rem;
572
- }
573
-
574
- .control-btn {
575
- width: 40px;
576
- height: 40px;
577
- font-size: 1rem;
578
  }
579
  }
580
 
@@ -588,8 +734,8 @@
588
  }
589
 
590
  /* Focus styles */
591
- button:focus,
592
- input:focus {
593
  outline: 2px solid var(--accent-color);
594
  outline-offset: 2px;
595
  }
@@ -611,44 +757,75 @@
611
  ::-webkit-scrollbar-thumb:hover {
612
  background: #00a8cc;
613
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  </style>
615
  </head>
616
 
617
  <body>
618
- <header>
619
- <h1><i class="fas fa-broadcast-tower"></i> Deutsches Online Radio</h1>
620
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
621
  Built with anycoder
622
  </a>
623
  </header>
624
 
625
- <main>
626
- <section class="search-section">
627
- <input type="text" class="search-input" id="searchInput" placeholder="🔍 Sender nach Name, Genre oder Region suchen...">
 
 
628
  </section>
629
 
630
- <section class="filter-buttons" id="filterButtons">
631
- <button class="filter-btn active" data-filter="all">Alle</button>
632
- <button class="filter-btn" data-filter="national">National</button>
633
- <button class="filter-btn" data-filter="regional">Regional</button>
634
- <button class="filter-btn" data-filter="news">News</button>
635
- <button class="filter-btn" data-filter="music">Musik</button>
636
- <button class="filter-btn" data-filter="kultur">Kultur</button>
637
  </section>
638
 
639
- <section class="stations-grid" id="stationsGrid"></section>
640
 
641
- <section class="empty-state" id="emptyState">
642
- <i class="fas fa-search"></i>
643
  <h3>Keine Sender gefunden</h3>
644
  <p>Probiere es mit einem anderen Suchbegriff oder Filter.</p>
645
  </section>
646
  </main>
647
 
648
- <footer class="player-bar" id="playerBar">
649
  <div class="player-content">
650
  <div class="now-playing" id="nowPlaying">
651
- <img src="" alt="" class="now-playing-logo" id="nowPlayingLogo">
652
  <div class="now-playing-info">
653
  <h4 id="nowPlayingName">Kein Sender ausgewählt</h4>
654
  <p id="nowPlayingGenre">Wähle einen Sender zum Abspielen</p>
@@ -656,259 +833,363 @@
656
  </div>
657
 
658
  <div class="player-controls">
659
- <button class="control-btn" id="prevBtn" title="Vorheriger Sender">
660
- <i class="fas fa-step-backward"></i>
661
- </button>
662
- <button class="control-btn" id="playPauseBtn" title="Play/Pause">
663
- <i class="fas fa-play" id="playPauseIcon"></i>
664
- </button>
665
- <button class="control-btn" id="nextBtn" title="Nächster Sender">
666
- <i class="fas fa-step-forward"></i>
667
- </button>
668
  </div>
669
 
670
  <div class="volume-control">
671
- <button class="control-btn" id="muteBtn" title="Stummschalten">
672
- <i class="fas fa-volume-up" id="volumeIcon"></i>
673
- </button>
674
- <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="70">
 
675
  </div>
676
  </div>
677
  </footer>
678
 
679
- <div class="loading-spinner" id="loadingSpinner">
680
- <div class="spinner"></div>
 
681
  </div>
682
 
683
- <div class="toast" id="toast"></div>
684
 
685
- <audio id="audioPlayer"></audio>
686
 
687
  <script>
688
- // Station Data - Official Public Broadcasters
689
- const stations = [
690
- {
691
- id: 'dlf',
692
- name: 'Deutschlandfunk',
693
- description: 'Nachrichten, Politik und Wissenschaft',
694
- streamUrl: 'https://stream.deutschlandradio.de/dlf/04/dlfdia.cast',
695
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Deutschlandfunk_Logo_2017.svg/300px-Deutschlandfunk_Logo_2017.svg.png',
696
- category: 'national',
697
- genre: 'news'
698
- },
699
- {
700
- id: 'dlfkultur',
701
- name: 'Deutschlandfunk Kultur',
702
- description: 'Kultur, Literatur und Gesellschaft',
703
- streamUrl: 'https://stream.deutschlandradio.de/dlfkultur/04/dlfkulturdia.cast',
704
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Deutschlandfunk_Kultur_Logo_2017.svg/300px-Deutschlandfunk_Kultur_Logo_2017.svg.png',
705
- category: 'national',
706
- genre: 'kultur'
707
- },
708
- {
709
- id: 'dlfnova',
710
- name: 'Deutschlandfunk Nova',
711
- description: 'Jugendradio mit Musik und Talk',
712
- streamUrl: 'https://stream.deutschlandradio.de/dlfnova/04/dlfnovadia.cast',
713
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/Deutschlandfunk_Nova_Logo_2017.svg/300px-Deutschlandfunk_Nova_Logo_2017.svg.png',
714
- category: 'national',
715
- genre: 'music'
716
- },
717
- {
718
- id: 'wdr2',
719
- name: 'WDR 2',
720
- description: 'Regionalradio für NRW mit Nachrichten',
721
- streamUrl: 'https://wdr-wdr2-koeln.icecastssl.wdr.de/wdr/wdr2/koeln/mp3/128/stream.mp3',
722
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/WDR_2_Logo_2016.svg/300px-WDR_2_Logo_2016.svg.png',
723
- category: 'regional',
724
- genre: 'news'
725
- },
726
- {
727
- id: 'wdr5',
728
- name: 'WDR 5',
729
- description: 'Informationen und Hintergründe',
730
- streamUrl: 'https://wdr-wdr5-live.icecastssl.wdr.de/wdr/wdr5/live/mp3/128/stream.mp3',
731
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/WDR_5_Logo_2016.svg/300px-WDR_5_Logo_2016.svg.png',
732
- category: 'national',
733
- genre: 'news'
734
- },
735
- {
736
- id: 'ndrinfo',
737
- name: 'NDR Info',
738
- description: 'Nachrichten aus Norddeutschland',
739
- streamUrl: 'https://ndr-ndrinfo-hh.sslcast.addradio.de/ndr/ndrinfo/hh/mp3/128/stream.mp3',
740
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/NDR_Info_logo.svg/300px-NDR_Info_logo.svg.png',
741
- category: 'regional',
742
- genre: 'news'
743
- },
744
- {
745
- id: 'ndr2',
746
- name: 'NDR 2',
747
- description: 'Die beste Musik für Norddeutschland',
748
- streamUrl: 'https://ndr-ndr2-hh.sslcast.addradio.de/ndr/ndr2/hh/mp3/128/stream.mp3',
749
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/NDR_2_logo.svg/300px-NDR_2_logo.svg.png',
750
- category: 'regional',
751
- genre: 'music'
752
- },
753
- {
754
- id: 'br24',
755
- name: 'BR24',
756
- description: 'Bayerisches Nachrichtenradio',
757
- streamUrl: 'https://br-br24-live.sslcast.addradio.de/br/br24/live/mp3/128/stream.mp3',
758
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/BR24_logo.svg/300px-BR24_logo.svg.png',
759
- category: 'regional',
760
- genre: 'news'
761
- },
762
- {
763
- id: 'bayern1',
764
- name: 'Bayern 1',
765
- description: 'Die beste Musik für Bayern',
766
- streamUrl: 'https://br-bayern1-obb.sslcast.addradio.de/br/bayern1/obb/mp3/128/stream.mp3',
767
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Bayern_1_logo.svg/300px-Bayern_1_logo.svg.png',
768
- category: 'regional',
769
- genre: 'music'
770
- },
771
- {
772
- id: 'bayern3',
773
- name: 'Bayern 3',
774
- description: 'Das junge Radio für Bayern',
775
- streamUrl: 'https://br-bayern3-live.sslcast.addradio.de/br/bayern3/live/mp3/128/stream.mp3',
776
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Bayern_3_logo.svg/300px-Bayern_3_logo.svg.png',
777
- category: 'regional',
778
- genre: 'music'
779
- },
780
- {
781
- id: 'swr1bw',
782
- name: 'SWR1 Baden-Württemberg',
783
- description: 'Das Radio für Baden-Württemberg',
784
- streamUrl: 'https://swr-swr1-bw.cast.addradio.de/swr/swr1/bw/mp3/128/stream.mp3',
785
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/SWR_1_Logo_2015.svg/300px-SWR_1_Logo_2015.svg.png',
786
- category: 'regional',
787
- genre: 'music'
788
- },
789
- {
790
- id: 'swr3',
791
- name: 'SWR3',
792
- description: 'Das junge Radio für BW und RP',
793
- streamUrl: 'https://swr-swr3-live.cast.addradio.de/swr/swr3/live/mp3/128/stream.mp3',
794
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/SWR3_logo.svg/300px-SWR3_logo.svg.png',
795
- category: 'national',
796
- genre: 'music'
797
- },
798
- {
799
- id: 'hr1',
800
- name: 'hr1',
801
- description: 'Informationen aus Hessen',
802
- streamUrl: 'https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3',
803
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Hr1_logo.svg/300px-Hr1_logo.svg.png',
804
- category: 'regional',
805
- genre: 'news'
806
- },
807
- {
808
- id: 'hr3',
809
- name: 'hr3',
810
- description: 'Das Pop- und Eventradio für Hessen',
811
- streamUrl: 'https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3',
812
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Hr3_logo.svg/300px-Hr3_logo.svg.png',
813
- category: 'regional',
814
- genre: 'music'
815
- },
816
- {
817
- id: 'mdrsachsen',
818
- name: 'MDR Sachsen',
819
- description: 'Das Radio für Sachsen',
820
- streamUrl: 'https://mdr-mdrsachsen-live.cast.addradio.de/mdr/mdrsachsen/live/mp3/128/stream.mp3',
821
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/MDR_Sachsen_Logo.svg/300px-MDR_Sachsen_Logo.svg.png',
822
- category: 'regional',
823
- genre: 'music'
824
- },
825
- {
826
- id: 'mdrjump',
827
- name: 'MDR JUMP',
828
- description: 'Der beste Mix für Sachsen, Sachsen-Anhalt, Thüringen',
829
- streamUrl: 'https://mdr-mdrjump-live.cast.addradio.de/mdr/mdrjump/live/mp3/128/stream.mp3',
830
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/MDR_Jump_Logo.svg/300px-MDR_Jump_Logo.svg.png',
831
- category: 'regional',
832
- genre: 'music'
833
- },
834
- {
835
- id: 'rbb',
836
- name: 'rbbKultur',
837
- description: 'Kultur- und Wortradio für Berlin und Brandenburg',
838
- streamUrl: 'https://rbb-rbbkultur-live.sslcast.addradio.de/rbb/rbbkultur/live/mp3/128/stream.mp3',
839
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Rbb_Kultur_logo.svg/300px-Rbb_Kultur_logo.svg.png',
840
- category: 'regional',
841
- genre: 'kultur'
842
- },
843
- {
844
- id: 'radioeins',
845
- name: 'radioeins',
846
- description: 'Berlins alternativer Sender',
847
- streamUrl: 'https://rbb-radioeins-live.sslcast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3',
848
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Radioeins_Logo_2017.svg/300px-Radioeins_Logo_2017.svg.png',
849
- category: 'regional',
850
- genre: 'music'
851
- },
852
- {
853
- id: 'sr1',
854
- name: 'SR 1 Europawelle',
855
- description: 'Das Radio für das Saarland',
856
- streamUrl: 'https://sr-sr1-live.cast.addradio.de/sr/sr1/live/mp3/128/stream.mp3',
857
- logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/SR_1_Europawelle_logo.svg/300px-SR_1_Europawelle_logo.svg.png',
858
- category: 'regional',
859
- genre: 'music'
860
- }
861
- ];
862
-
863
- // App State
864
- let currentStation = null;
865
- let isPlaying = false;
866
- let currentVolume = 70;
867
- let favorites = JSON.parse(localStorage.getItem('radioFavorites')) || [];
868
- let currentFilter = 'all';
869
- let searchTerm = '';
870
-
871
- // DOM Elements
872
- const audioPlayer = document.getElementById('audioPlayer');
873
- const stationsGrid = document.getElementById('stationsGrid');
874
- const searchInput = document.getElementById('searchInput');
875
- const filterButtons = document.querySelectorAll('.filter-btn');
876
- const playerBar = document.getElementById('playerBar');
877
- const nowPlayingLogo = document.getElementById('nowPlayingLogo');
878
- const nowPlayingName = document.getElementById('nowPlayingName');
879
- const nowPlayingGenre = document.getElementById('nowPlayingGenre');
880
- const playPauseBtn = document.getElementById('playPauseBtn');
881
- const playPauseIcon = document.getElementById('playPauseIcon');
882
- const prevBtn = document.getElementById('prevBtn');
883
- const nextBtn = document.getElementById('nextBtn');
884
- const muteBtn = document.getElementById('muteBtn');
885
- const volumeIcon = document.getElementById('volumeIcon');
886
- const volumeSlider = document.getElementById('volumeSlider');
887
- const loadingSpinner = document.getElementById('loadingSpinner');
888
- const toast = document.getElementById('toast');
889
- const emptyState = document.getElementById('emptyState');
890
-
891
- // Initialize
892
- document.addEventListener('DOMContentLoaded', () => {
893
- renderStations();
894
- setupEventListeners();
895
- audioPlayer.volume = currentVolume / 100;
896
- volumeSlider.value = currentVolume;
897
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
 
899
- // Render Stations
900
- function renderStations() {
901
- const filteredStations = getFilteredStations();
902
-
903
- if (filteredStations.length === 0) {
904
- stationsGrid.style.display = 'none';
905
- emptyState.classList.add('show');
906
- } else {
907
- stationsGrid.style.display = 'grid';
908
- emptyState.classList.remove('show');
909
-
910
- stationsGrid.innerHTML = filteredStations.map(station => `
911
- <div class="station-card ${currentStation?.id === station.id ? 'playing' : ''}"
912
- data-id="${station.id}">
913
- <div class="station-header">
914
- <img src="${station.logo}" alt="${station.name}" class="station-logo" loading="lazy" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiByeD0iMTIiIGZpbGw9IiMxYTFhMmUiLz4KPHBhdGggZD0iTTMwIDIwQzI2LjY4NjMgMjAgMjQgMjIuNjg2MyAyNCAyNkMyNCAyOS4zMTM3IDI2LjY4NjMgMzIgMzAgMzJDMzMuMzEzNyAzMiAzNiAyOS4zMTM3IDM2IDI2QzM2IDIyLjY4NjMgMzMuMzEz
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>
 
22
  --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
  --error-color: #ff4757;
24
  --success-color: #2ed573;
25
+ --warning-color: #ffa502;
26
  }
27
 
28
  * {
 
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;
 
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 */
 
96
  max-width: 1400px;
97
  margin: 0 auto;
98
  width: 100%;
99
+ padding-bottom: 120px;
100
  }
101
 
102
  /* Search Section */
 
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 {
 
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;
 
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;
 
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
  }
 
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 {
 
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 {
 
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 {
 
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 {
 
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 {
 
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 {
 
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) {
 
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 */
 
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%);
 
420
  display: flex;
421
  align-items: center;
422
  gap: 1rem;
423
+ min-width: 0;
424
  }
425
 
426
  .now-playing-logo {
 
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 {
 
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 {
 
509
  border-radius: 2px;
510
  outline: none;
511
  cursor: pointer;
512
+ transition: var(--transition);
513
  }
514
 
515
  .volume-slider::-webkit-slider-thumb {
 
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 {
 
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 */
 
572
  }
573
 
574
  @keyframes spin {
575
+ 0% { transform: rotate(0deg); }
576
+ 100% { transform: rotate(360deg); }
 
 
 
 
577
  }
578
 
579
  /* Toast Notifications */
 
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 {
 
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;
 
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 {
 
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
  }
 
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) {
 
717
  grid-template-columns: 1fr;
718
  }
719
 
720
+ .toast {
721
+ left: 1rem;
722
+ right: 1rem;
723
+ max-width: none;
 
 
 
 
724
  }
725
  }
726
 
 
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
  }
 
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>
 
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
+ }