mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
feat(ui): render the Radio directory tuner from a store
RadioTuner renders the band label, readouts, slider and needle from the Radio store. RadioControls keeps the drag and its slot and publishes what the tuner shows; the component lays out the tick tape against the dial's measured width, drawing no ticks before it has one, and lays out the snapped needle before it drops the drag class so the needle does not animate from where the drag let go.
This commit is contained in:
1 parent
b6d565a83f
commit
1072df325e
17 files changed
+595
-159
No files matched your search
+59
-100
@@ -1,10 +1,13 @@
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { showRadioEnableBusy, showRadioVolume } from './radioPresentation.js';
|
||||
import {
|
||||
showRadioEnableBusy,
|
||||
showRadioTuner,
|
||||
showRadioVolume,
|
||||
} from './radioPresentation.js';
|
||||
import {
|
||||
radioTunerSlot,
|
||||
radioTunerCommitSlot,
|
||||
radioTunerPointerPosition,
|
||||
buildRadioTunerTicks,
|
||||
} from './radioTunerModel.js';
|
||||
|
||||
/** Bind Radio input using a RadioControls receiver and supplied actions. */
|
||||
@@ -43,39 +46,13 @@ export function bindRadioControls() {
|
||||
/** @param {'display'|'radio'} kind @returns {boolean} Whether it is open. */
|
||||
this._cockpitDisclosureOpen = (kind) =>
|
||||
Boolean(this._cockpitDisclosure?.[kind]);
|
||||
const syncTunerTape = (coordinate) => {
|
||||
const scale = this._radioTuner?.querySelector('.radio-tuner-scale');
|
||||
const dial = this._radioTuner?.querySelector('.radio-tuner-dial');
|
||||
if (!scale || !dial) return null;
|
||||
const model = buildRadioTunerTicks(
|
||||
coordinate,
|
||||
this._radioTunerStations.length,
|
||||
dial.getBoundingClientRect().width,
|
||||
);
|
||||
while (scale.children.length < model.ticks.length) {
|
||||
const tick = document.createElement('span');
|
||||
tick.className = 'radio-tuner-tick';
|
||||
scale.append(tick);
|
||||
}
|
||||
while (scale.children.length > model.ticks.length)
|
||||
scale.lastElementChild?.remove();
|
||||
model.ticks.forEach((entry, index) => {
|
||||
const tick = scale.children[index];
|
||||
tick.style.left = `${entry.xPx}px`;
|
||||
tick.textContent = entry.label;
|
||||
tick.dataset.stationIndex = String(entry.stationIndex);
|
||||
tick.classList.toggle('is-current', entry.current);
|
||||
});
|
||||
scale.style.setProperty('--radio-tuner-tick-pitch', `${model.pitchPx}px`);
|
||||
return model;
|
||||
};
|
||||
const tunerPreview = ({
|
||||
coordinate = this._radioTunerCoordinate,
|
||||
syncStatic = true,
|
||||
rotate = true,
|
||||
} = {}) => {
|
||||
const slot = radioTunerSlot(
|
||||
this._radioTunerSlider?.value,
|
||||
this._radioTunerView.slot,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
const station = slot.locked
|
||||
@@ -93,35 +70,26 @@ export function bindRadioControls() {
|
||||
? 0.5
|
||||
: resolvedCoordinate / Math.max(1, this._radioTunerStations.length - 1);
|
||||
this._radioTunerCoordinate = resolvedCoordinate;
|
||||
this._radioTuner?.style.setProperty('--radio-tuner-ratio', String(ratio));
|
||||
this._radioTuner?.classList.toggle(
|
||||
'is-static',
|
||||
syncStatic ? false : Boolean(this._radioState?.tuningStatic),
|
||||
);
|
||||
syncTunerTape(resolvedCoordinate);
|
||||
if (this._radioTunerValue) {
|
||||
this._radioTunerValue.textContent = station
|
||||
showRadioTuner.call(this, {
|
||||
ratio,
|
||||
static: syncStatic ? false : Boolean(this._radioState?.tuningStatic),
|
||||
coordinate: resolvedCoordinate,
|
||||
count: this._radioTunerStations.length,
|
||||
value: station
|
||||
? t('CH {index} / {total}', {
|
||||
index: String(slot.stationIndex + 1).padStart(2, '0'),
|
||||
total: String(this._radioTunerStations.length).padStart(2, '0'),
|
||||
})
|
||||
: t('NO STATIONS');
|
||||
}
|
||||
if (this._radioTunerStation)
|
||||
this._radioTunerStation.textContent =
|
||||
station?.name || t('NO STATION AVAILABLE');
|
||||
if (this._radioTunerSlider) {
|
||||
this._radioTunerSlider.setAttribute(
|
||||
'aria-valuetext',
|
||||
station
|
||||
? t('{name}, station {index} of {total}', {
|
||||
name: station.name,
|
||||
index: slot.stationIndex + 1,
|
||||
total: this._radioTunerStations.length,
|
||||
})
|
||||
: t('No station available'),
|
||||
);
|
||||
}
|
||||
: t('NO STATIONS'),
|
||||
station: station?.name || t('NO STATION AVAILABLE'),
|
||||
valuetext: station
|
||||
? t('{name}, station {index} of {total}', {
|
||||
name: station.name,
|
||||
index: slot.stationIndex + 1,
|
||||
total: this._radioTunerStations.length,
|
||||
})
|
||||
: t('No station available'),
|
||||
});
|
||||
if (syncStatic)
|
||||
this.radio.previewTuningStation(station?.id || null, { rotate });
|
||||
return station;
|
||||
@@ -132,20 +100,12 @@ export function bindRadioControls() {
|
||||
this._radioTunerBandSignature = this._radioTunerStations
|
||||
.map((station) => station.id)
|
||||
.join('|');
|
||||
if (this._radioTunerSlider) {
|
||||
this._radioTunerSlider.min = '0';
|
||||
this._radioTunerSlider.max = String(
|
||||
Math.max(0, this._radioTunerStations.length - 1),
|
||||
);
|
||||
this._radioTunerSlider.step = '1';
|
||||
}
|
||||
showRadioTuner.call(this, {
|
||||
max: Math.max(0, this._radioTunerStations.length - 1),
|
||||
});
|
||||
};
|
||||
const refreshTunerBand = ({ force = false } = {}) => {
|
||||
if (
|
||||
this._radioTunerDragging ||
|
||||
this._radioTuner?.hidden ||
|
||||
this._radioTunerSlider?.disabled
|
||||
)
|
||||
if (this._radioTunerDragging || !this._radioTunerView.available)
|
||||
return false;
|
||||
const selectedId = this._radioState?.selected?.id || null;
|
||||
const pool = this.radio.getTunerStations(750);
|
||||
@@ -168,14 +128,14 @@ export function bindRadioControls() {
|
||||
selectedPoolIndex >= 0 ? selectedPoolIndex : 0,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
this._radioTunerSlider.value = String(slot.slot);
|
||||
showRadioTuner.call(this, { slot: slot.slot });
|
||||
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
||||
tunerPreview({ coordinate: this._radioTunerCoordinate, syncStatic: false });
|
||||
return true;
|
||||
};
|
||||
this._refreshRadioTunerBand = refreshTunerBand;
|
||||
const beginTuner = () => {
|
||||
if (this._radioTunerDragging || this._radioTunerSlider?.disabled)
|
||||
if (this._radioTunerDragging || !this._radioTunerView.available)
|
||||
return false;
|
||||
refreshTunerBand();
|
||||
if (!this._radioTunerStations.length || !this.radio.beginTuning())
|
||||
@@ -184,7 +144,7 @@ export function bindRadioControls() {
|
||||
// Only an explicit globe pointer/wheel gesture releases camera pinning.
|
||||
this._radioTunerBandPinnedForNavigation = true;
|
||||
this._radioTunerDragging = true;
|
||||
this._radioTuner?.classList.add('is-dragging');
|
||||
showRadioTuner.call(this, { dragging: true });
|
||||
const selectedIndex = this._radioTunerStations.findIndex(
|
||||
(station) => station.id === this._radioState?.selected?.id,
|
||||
);
|
||||
@@ -192,7 +152,7 @@ export function bindRadioControls() {
|
||||
selectedIndex >= 0 ? selectedIndex : 0,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
this._radioTunerSlider.value = String(slot.slot);
|
||||
showRadioTuner.call(this, { slot: slot.slot });
|
||||
this._radioTunerCoordinate = slot.stationIndex >= 0 ? slot.stationIndex : 0;
|
||||
this._radioTunerDragStartSlot = slot.slot;
|
||||
this._radioTunerDragSnapshot = {
|
||||
@@ -215,31 +175,28 @@ export function bindRadioControls() {
|
||||
this._radioTunerPool = [...dragSnapshot.stations];
|
||||
this._radioTunerBandSignature = dragSnapshot.bandSignature;
|
||||
this._radioTunerSelectedId = dragSnapshot.selectedId;
|
||||
if (this._radioTunerSlider) {
|
||||
const startSlot = radioTunerSlot(
|
||||
dragSnapshot.slot,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
this._radioTunerSlider.max = String(startSlot.max);
|
||||
this._radioTunerSlider.value = String(startSlot.slot);
|
||||
}
|
||||
const startSlot = radioTunerSlot(
|
||||
dragSnapshot.slot,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
showRadioTuner.call(this, { max: startSlot.max, slot: startSlot.slot });
|
||||
this._radioTunerCoordinate = Number.isFinite(dragSnapshot.coordinate)
|
||||
? dragSnapshot.coordinate
|
||||
: dragSnapshot.slot;
|
||||
} else if (!commit && this._radioTunerSlider) {
|
||||
this._radioTunerSlider.value = String(this._radioTunerDragStartSlot);
|
||||
} else if (!commit) {
|
||||
showRadioTuner.call(this, { slot: this._radioTunerDragStartSlot });
|
||||
this._radioTunerCoordinate = this._radioTunerDragStartSlot;
|
||||
}
|
||||
let station = tunerPreview({
|
||||
coordinate: this._radioTunerCoordinate,
|
||||
rotate: commit,
|
||||
});
|
||||
if (commit && !station && this._radioTunerSlider) {
|
||||
if (commit && !station) {
|
||||
const snapped = radioTunerCommitSlot(
|
||||
this._radioTunerSlider.value,
|
||||
this._radioTunerView.slot,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
this._radioTunerSlider.value = String(snapped.slot);
|
||||
showRadioTuner.call(this, { slot: snapped.slot });
|
||||
this._radioTunerCoordinate = snapped.stationIndex;
|
||||
station = tunerPreview({ coordinate: this._radioTunerCoordinate });
|
||||
}
|
||||
@@ -262,21 +219,18 @@ export function bindRadioControls() {
|
||||
this._radioTunerKeyboardKey = null;
|
||||
if (commit && (!result || result.ok)) refreshTunerBand();
|
||||
this._radioTunerDragSnapshot = null;
|
||||
// Flush the snapped position while transitions are still disabled so
|
||||
// removing the drag class cannot interpolate from the released gap.
|
||||
void this._radioTunerNeedle?.offsetLeft;
|
||||
this._radioTuner?.classList.remove('is-dragging');
|
||||
// The tuner lays out the snapped position while transitions are still
|
||||
// disabled before it drops the drag class, so the needle cannot
|
||||
// interpolate from the released gap.
|
||||
showRadioTuner.call(this, { dragging: false });
|
||||
if (result && !result.ok) {
|
||||
this._radioTunerBandPinnedForNavigation = false;
|
||||
if (result.reason === 'station-unavailable') {
|
||||
if (this._radioTunerValue)
|
||||
this._radioTunerValue.textContent = t('OFF AIR');
|
||||
if (this._radioTunerStation)
|
||||
this._radioTunerStation.textContent = t('STATION UNAVAILABLE');
|
||||
this._radioTunerSlider?.setAttribute(
|
||||
'aria-valuetext',
|
||||
t('Station unavailable after directory refresh'),
|
||||
);
|
||||
showRadioTuner.call(this, {
|
||||
value: t('OFF AIR'),
|
||||
station: t('STATION UNAVAILABLE'),
|
||||
valuetext: t('Station unavailable after directory refresh'),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -512,7 +466,7 @@ export function bindRadioControls() {
|
||||
this._radioTunerDragDirection = -1;
|
||||
this._radioTunerLastSlot = position.stationIndex;
|
||||
this._radioTunerCoordinate = position.coordinate;
|
||||
this._radioTunerSlider.value = String(position.stationIndex);
|
||||
showRadioTuner.call(this, { slot: position.stationIndex });
|
||||
tunerPreview({ coordinate: position.coordinate });
|
||||
return true;
|
||||
};
|
||||
@@ -553,9 +507,14 @@ export function bindRadioControls() {
|
||||
() => {
|
||||
if (this._radioTunerPointerId !== null || this._radioTunerKeyboardKey)
|
||||
return;
|
||||
if (!this._radioTunerDragging && !beginTuner()) return;
|
||||
// A drag the input starts begins from the selected station's slot.
|
||||
if (this._radioTunerDragging)
|
||||
showRadioTuner.call(this, {
|
||||
slot: Number(this._radioTunerSlider.value),
|
||||
});
|
||||
else if (!beginTuner()) return;
|
||||
const inputSlot = radioTunerSlot(
|
||||
this._radioTunerSlider.value,
|
||||
this._radioTunerView.slot,
|
||||
this._radioTunerStations.length,
|
||||
);
|
||||
if (inputSlot.slot > this._radioTunerLastSlot)
|
||||
@@ -648,7 +607,7 @@ export function bindRadioControls() {
|
||||
this._radioTunerKeyboardKey = event.key;
|
||||
const max = Math.max(0, this._radioTunerStations.length - 1);
|
||||
const current = radioTunerSlot(
|
||||
this._radioTunerSlider.value,
|
||||
this._radioTunerView.slot,
|
||||
this._radioTunerStations.length,
|
||||
).slot;
|
||||
const page = Math.max(1, Math.round(max / 10));
|
||||
@@ -669,7 +628,7 @@ export function bindRadioControls() {
|
||||
this._radioTunerDragDirection = -1;
|
||||
this._radioTunerLastSlot = slot.slot;
|
||||
this._radioTunerCoordinate = slot.stationIndex;
|
||||
this._radioTunerSlider.value = String(slot.slot);
|
||||
showRadioTuner.call(this, { slot: slot.slot });
|
||||
tunerPreview({ coordinate: this._radioTunerCoordinate });
|
||||
},
|
||||
tunerListenerOptions,
|
||||
|
||||
Reference in new issue
Block a user