+
{'\u{1F30D}'}
+
World Radio
- {/* ── Theme Selector ── */}
-
- {THEMES.map(t => (
-
setTheme(t.id)}
- />
- ))}
-
-
- {/* ── Search ── */}
-
-
- {'\u{1F50D}'}
- handleSearch(e.target.value)}
- onFocus={() => { if (searchResults.length) setSearchOpen(true); }}
- />
- {searchQuery && (
-
- )}
-
- {searchOpen && searchResults.length > 0 && (
-
- {searchResults.slice(0, 12).map(hit => (
-
- ))}
-
- )}
-
-
- {/* ── Favorites toggle ── */}
-
-
- {/* ── Side Panel: Favorites ── */}
- {showFavorites && (
-
-
-
{'\u2B50'} Favoriten
-
-
-
- {favorites.length === 0 ? (
-
Noch keine Favoriten
- ) : (
- favorites.map(fav => (
-
-
- {fav.stationName}
- {fav.placeName}, {fav.country}
-
-
-
-
-
-
- ))
- )}
-
-
- )}
-
- {/* ── Side Panel: Stations at place ── */}
- {selectedPlace && !showFavorites && (
-
-
-
-
{selectedPlace.title}
- {selectedPlace.country}
-
-
-
-
- {stationsLoading ? (
-
-
- Sender werden geladen...
-
- ) : stations.length === 0 ? (
-
Keine Sender gefunden
- ) : (
- stations.map(s => (
-
-
- {s.title}
- {currentPlaying?.stationId === s.id && (
-
-
- Live
-
- )}
-
-
- {currentPlaying?.stationId === s.id ? (
-
- ) : (
-
- )}
-
-
-
- ))
- )}
-
-
- )}
-
- {/* ── Bottom Bar ── */}
-
-
{guilds.length > 1 && (
{currentPlaying && (
-
+
{currentPlaying.stationName}
{currentPlaying.placeName}{currentPlaying.country ? `, ${currentPlaying.country}` : ''}
-
- {volume === 0 ? '\u{1F507}' : volume < 0.4 ? '\u{1F509}' : '\u{1F50A}'}
- handleVolume(Number(e.target.value))}
- />
- {Math.round(volume * 100)}%
-
-
{'\u{1F50A}'} {currentPlaying.channelName}
-
setShowConnModal(true)} title="Verbindungsdetails">
-
- Verbunden
- {voiceStats?.voicePing != null && (
- {voiceStats.voicePing}ms
- )}
-
-
)}
-
- {/* ── Places counter ── */}
-
- {'\u{1F4FB}'} {places.length.toLocaleString('de-DE')} Sender weltweit
+
+ {currentPlaying && (
+ <>
+
+ {volume === 0 ? '\u{1F507}' : volume < 0.4 ? '\u{1F509}' : '\u{1F50A}'}
+ handleVolume(Number(e.target.value))}
+ />
+ {Math.round(volume * 100)}%
+
+
setShowConnModal(true)} title="Verbindungsdetails">
+
+ Verbunden
+ {voiceStats?.voicePing != null && (
+ {voiceStats.voicePing}ms
+ )}
+
+
+ >
+ )}
+
+ {THEMES.map(t => (
+
setTheme(t.id)}
+ />
+ ))}
+
+
+
+
+ {/* ═══ GLOBE AREA ═══ */}
+
+
+
+ {/* ── Search ── */}
+
+
+ {'\u{1F50D}'}
+ handleSearch(e.target.value)}
+ onFocus={() => { if (searchResults.length) setSearchOpen(true); }}
+ />
+ {searchQuery && (
+
+ )}
+
+ {searchOpen && searchResults.length > 0 && (
+
+ {searchResults.slice(0, 12).map(hit => (
+
+ ))}
+
+ )}
+
+
+ {/* ── Favorites toggle ── */}
+
+
+ {/* ── Side Panel: Favorites ── */}
+ {showFavorites && (
+
+
+
{'\u2B50'} Favoriten
+
+
+
+ {favorites.length === 0 ? (
+
Noch keine Favoriten
+ ) : (
+ favorites.map(fav => (
+
+
+ {fav.stationName}
+ {fav.placeName}, {fav.country}
+
+
+
+
+
+
+ ))
+ )}
+
+
+ )}
+
+ {/* ── Side Panel: Stations at place ── */}
+ {selectedPlace && !showFavorites && (
+
+
+
+
{selectedPlace.title}
+ {selectedPlace.country}
+
+
+
+
+ {stationsLoading ? (
+
+
+ Sender werden geladen...
+
+ ) : stations.length === 0 ? (
+
Keine Sender gefunden
+ ) : (
+ stations.map(s => (
+
+
+ {s.title}
+ {currentPlaying?.stationId === s.id && (
+
+
+ Live
+
+ )}
+
+
+ {currentPlaying?.stationId === s.id ? (
+
+ ) : (
+
+ )}
+
+
+
+ ))
+ )}
+
+
+ )}
+
+ {/* ── Places counter ── */}
+
+ {'\u{1F4FB}'} {places.length.toLocaleString('de-DE')} Sender weltweit
+
{/* ── Connection Details Modal ── */}
- {showConnModal && voiceStats && (() => {
- const uptimeSec = voiceStats.connectedSince
+ {showConnModal && (() => {
+ const uptimeSec = voiceStats?.connectedSince
? Math.floor((Date.now() - new Date(voiceStats.connectedSince).getTime()) / 1000)
: 0;
const h = Math.floor(uptimeSec / 3600);
@@ -657,30 +666,30 @@ export default function RadioTab({ data }: { data: any }) {
Voice Ping
-
- {voiceStats.voicePing != null ? `${voiceStats.voicePing} ms` : '---'}
+
+ {voiceStats?.voicePing != null ? `${voiceStats.voicePing} ms` : '---'}
Gateway Ping
-
- {voiceStats.gatewayPing >= 0 ? `${voiceStats.gatewayPing} ms` : '---'}
+
+ {voiceStats && voiceStats.gatewayPing >= 0 ? `${voiceStats.gatewayPing} ms` : '---'}
Status
-
- {voiceStats.status === 'ready' ? 'Verbunden' : voiceStats.status}
+
+ {voiceStats?.status === 'ready' ? 'Verbunden' : voiceStats?.status ?? 'Warte auf Verbindung'}
Kanal
- {voiceStats.channelName || '---'}
+ {voiceStats?.channelName || '---'}
Verbunden seit
- {uptimeStr}
+ {uptimeStr || '---'}
diff --git a/web/src/plugins/soundboard/SoundboardTab.tsx b/web/src/plugins/soundboard/SoundboardTab.tsx
index 504da5b..5ab4f24 100644
--- a/web/src/plugins/soundboard/SoundboardTab.tsx
+++ b/web/src/plugins/soundboard/SoundboardTab.tsx
@@ -1238,8 +1238,8 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
)}
{/* ═══ CONNECTION MODAL ═══ */}
- {showConnModal && voiceStats && (() => {
- const uptimeSec = voiceStats.connectedSince
+ {showConnModal && (() => {
+ const uptimeSec = voiceStats?.connectedSince
? Math.floor((Date.now() - new Date(voiceStats.connectedSince).getTime()) / 1000)
: 0;
const h = Math.floor(uptimeSec / 3600);
@@ -1266,30 +1266,30 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
Voice Ping
-
- {voiceStats.voicePing != null ? `${voiceStats.voicePing} ms` : '---'}
+
+ {voiceStats?.voicePing != null ? `${voiceStats.voicePing} ms` : '---'}
Gateway Ping
-
- {voiceStats.gatewayPing >= 0 ? `${voiceStats.gatewayPing} ms` : '---'}
+
+ {voiceStats && voiceStats.gatewayPing >= 0 ? `${voiceStats.gatewayPing} ms` : '---'}
Status
-
- {voiceStats.status === 'ready' ? 'Verbunden' : voiceStats.status}
+
+ {voiceStats?.status === 'ready' ? 'Verbunden' : voiceStats?.status ?? 'Warte auf Verbindung'}
Kanal
- {voiceStats.channelName || '---'}
+ {voiceStats?.channelName || '---'}
Verbunden seit
- {uptimeStr}
+ {uptimeStr || '---'}
diff --git a/web/src/styles.css b/web/src/styles.css
index 163e95e..98d2fb5 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -354,7 +354,8 @@ html, body {
══════════════════════════════════════════════ */
.radio-container {
- position: relative;
+ display: flex;
+ flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
@@ -416,6 +417,88 @@ html, body {
}
/* ── Globe ── */
+/* ── Radio Topbar ── */
+.radio-topbar {
+ display: flex;
+ align-items: center;
+ padding: 0 16px;
+ height: 52px;
+ background: var(--bg-secondary, #2b2d31);
+ border-bottom: 1px solid rgba(0, 0, 0, .24);
+ z-index: 10;
+ flex-shrink: 0;
+ gap: 16px;
+}
+
+.radio-topbar-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+}
+
+.radio-topbar-logo {
+ font-size: 20px;
+}
+
+.radio-topbar-title {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--text-normal);
+ letter-spacing: -.02em;
+}
+
+.radio-topbar-np {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ justify-content: center;
+}
+
+.radio-topbar-right {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.radio-topbar-stop {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--danger);
+ color: #fff;
+ border: none;
+ border-radius: var(--radius);
+ padding: 6px 14px;
+ font-size: 13px;
+ font-family: var(--font);
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition);
+ flex-shrink: 0;
+}
+
+.radio-topbar-stop:hover {
+ background: #c63639;
+}
+
+.radio-theme-inline {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: 4px;
+}
+
+/* ── Globe Wrapper ── */
+.radio-globe-wrap {
+ position: relative;
+ flex: 1;
+ overflow: hidden;
+}
+
.radio-globe {
width: 100%;
height: 100%;
@@ -819,29 +902,6 @@ html, body {
50% { transform: scaleY(1); }
}
-/* ── Bottom Bar ── */
-.radio-bar {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 20;
- background: rgba(30, 31, 34, 0.95);
- backdrop-filter: blur(16px);
- border-top: 1px solid var(--border);
- padding: 10px 16px;
- display: flex;
- align-items: center;
- gap: 16px;
- box-shadow: 0 -4px 24px rgba(0,0,0,0.3);
-}
-
-.radio-bar-channel {
- display: flex;
- gap: 8px;
- flex-shrink: 0;
-}
-
.radio-sel {
background: var(--bg-secondary);
border: 1px solid var(--border);
@@ -859,15 +919,6 @@ html, body {
border-color: var(--accent);
}
-/* ── Now Playing ── */
-.radio-np {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
- min-width: 0;
-}
-
.radio-eq-np {
flex-shrink: 0;
}
@@ -897,21 +948,6 @@ html, body {
text-overflow: ellipsis;
}
-.radio-np-ch {
- font-size: 12px;
- color: var(--text-faint);
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.radio-bar .radio-btn-stop {
- width: auto;
- border-radius: var(--radius);
- padding: 6px 14px;
- font-size: 13px;
- gap: 4px;
- flex-shrink: 0;
-}
/* ── Volume Slider ── */
.radio-volume {
@@ -968,22 +1004,6 @@ html, body {
text-align: right;
}
-/* ── Theme Selector ── */
-.radio-theme {
- position: absolute;
- top: 16px;
- right: 72px;
- z-index: 25;
- display: flex;
- align-items: center;
- gap: 5px;
- padding: 5px 10px;
- border-radius: 20px;
- background: rgba(30, 31, 34, 0.85);
- backdrop-filter: blur(12px);
- border: 1px solid var(--border);
-}
-
.radio-theme-dot {
width: 16px;
height: 16px;
@@ -1005,7 +1025,7 @@ html, body {
/* ── Station count ── */
.radio-counter {
position: absolute;
- bottom: 70px;
+ bottom: 16px;
left: 16px;
z-index: 10;
font-size: 12px;
@@ -1049,30 +1069,28 @@ html, body {
left: calc(50% - 24px);
}
- .radio-bar {
- flex-wrap: wrap;
- padding: 8px 12px;
+ .radio-topbar {
+ padding: 0 12px;
gap: 8px;
}
+ .radio-topbar-title {
+ display: none;
+ }
+
.radio-sel {
max-width: 140px;
font-size: 12px;
}
-
- .radio-counter {
- bottom: 62px;
- left: 12px;
- }
}
@media (max-width: 480px) {
- .radio-np-ch {
+ .radio-topbar-np {
display: none;
}
- .radio-bar-channel {
- flex-wrap: wrap;
+ .radio-volume {
+ display: none;
}
.radio-sel {