Merge branch 'main' into feat/voice-geometry

Picks up the Radio Next/Previous fix (#1474) and the Codespaces docs
(#1515). The one conflict is the Radio cycle assertion in
gevActions.test.mjs: main's new fallback flag is kept in this branch's
layout.
This commit is contained in:
Manjunath committed 2026-11-03 16:40:39 +05:30
commit 05edae0606
31 files changed
+775 -170

No files matched your search

+3
View File
@@ -241,9 +241,12 @@ export function bindRadioControls() {
const pool = this._radioTunerPool.length
? this._radioTunerPool
: this._radioTunerStations;
// Silent unless a stream is active; an active stream follows the selection.
const { audioState } = this.radio.getUIState();
const cycled = this.radio.cycleStation(direction, {
rotate,
stationIds: pool.map((station) => station.id),
autoplay: ['loading', 'buffering', 'playing'].includes(audioState),
origin: 'user',
});
if (!cycled) {
+35 -1
View File
@@ -47,7 +47,9 @@ function fixture() {
publishTuner() {},
};
const calls = [];
const cycleOptions = [];
const subscribers = [];
const uiState = { audioState: 'stopped' };
const radio = {
subscribe(callback) {
subscribers.push(callback);
@@ -57,10 +59,12 @@ function fixture() {
endTuning() {
calls.push('endTuning');
},
cycleStation(direction) {
cycleStation(direction, options) {
calls.push(['cycle', direction]);
cycleOptions.push(options);
return true;
},
getUIState: () => uiState,
};
const actions = {
isRegistered: () => true,
@@ -90,6 +94,8 @@ function fixture() {
store,
actions,
calls,
cycleOptions,
uiState,
subscribers,
document,
cleanup() {
@@ -177,3 +183,31 @@ test('destruction releases an active tuner pointer after revoking listeners', ()
f.cleanup();
}
});
test('Next and Previous play only while a stream is active', () => {
const f = fixture();
try {
f.controls.connect();
for (const audioState of ['stopped', 'paused', 'error']) {
f.uiState.audioState = audioState;
f.store.act('cycleRadio', 1);
f.store.act('cycleRadio', -1);
}
assert.deepEqual(
f.cycleOptions.map((options) => options.autoplay),
[false, false, false, false, false, false],
);
f.cycleOptions.length = 0;
for (const audioState of ['loading', 'buffering', 'playing']) {
f.uiState.audioState = audioState;
f.store.act('cycleRadio', 1);
f.store.act('cycleRadio', -1);
}
assert.deepEqual(
f.cycleOptions.map((options) => options.autoplay),
[true, true, true, true, true, true],
);
} finally {
f.cleanup();
}
});
+3 -1
View File
@@ -25,7 +25,9 @@ export function renderRadioState(state) {
const interactive = enabled && !transitioning && !uncertain;
const selected = state.selected || null;
const hasStations = state.filteredCount > 0;
const activePlayback = ['playing', 'buffering'].includes(state.audioState);
const activePlayback = ['loading', 'buffering', 'playing'].includes(
state.audioState,
);
document
.getElementById('title-bar')
?.classList.toggle('radio-broadcasting', state.audioState === 'playing');
+1 -1
View File
@@ -67,7 +67,7 @@ const radioWords = computed(() => ({
<a id="radio-station-homepage" :href="radio ? radio.station.homepage || undefined : '#'" target="_blank" rel="noopener noreferrer" :hidden="radio ? !radio.station.homepage : true">{{ t('STATION SITE') }}</a>
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer">{{ t('DIRECTORY: RADIO BROWSER') }}</a>
</div>
<p class="radio-privacy">{{ t('Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.') }}</p>
<p class="radio-privacy">{{ t('Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.') }}</p>
<SdrCard />
</RailPanel>
</template>