mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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.
160 lines
5.1 KiB
JavaScript
160 lines
5.1 KiB
JavaScript
import { bindRadioControls } from './radioBindings.js';
|
|
import {
|
|
createRadioTunerState,
|
|
renderRadioState,
|
|
showRadioTuner,
|
|
} from './radioPresentation.js';
|
|
import { useRadioStore } from './stores/radio.js';
|
|
import { applicationPinia } from './vueRuntime.js';
|
|
|
|
/**
|
|
* Own Radio interaction, the tuner and the subscription; receive playback and
|
|
* application actions. The panel view renders the rest of the Radio panel
|
|
* from what this publishes (`gev-radio` store).
|
|
*/
|
|
export class RadioControls {
|
|
constructor({
|
|
elements,
|
|
radio,
|
|
actions,
|
|
canvas,
|
|
store = useRadioStore(applicationPinia()),
|
|
}) {
|
|
Object.assign(this, elements);
|
|
this.store = store;
|
|
this.radio = radio;
|
|
this.actions = actions;
|
|
this.canvas = canvas;
|
|
this.destroyed = false;
|
|
this.listeners = new AbortController();
|
|
this._radioUnsubscribe = null;
|
|
this._radioState = null;
|
|
this._radioView = null;
|
|
this._radioCategorySignature = '';
|
|
this._radioFilterOptions = null;
|
|
this._radioVolumeShown = null;
|
|
this._cockpitDisclosure = { display: false, radio: false };
|
|
this._radioTunerView = createRadioTunerState();
|
|
this._radioTunerStations = [];
|
|
this._radioTunerPool = [];
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerDragStartSlot = 0;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerLastSlot = 0;
|
|
this._radioTunerDragDirection = 0;
|
|
this._radioTunerCoordinate = 0;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerBandSignature = '';
|
|
this._radioTunerSelectedId = null;
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
this._refreshRadioTunerBand = null;
|
|
bindRadioControls.call(this);
|
|
}
|
|
|
|
listen(target, type, handler, options = {}) {
|
|
target?.addEventListener(type, handler, {
|
|
...options,
|
|
signal: this.listeners.signal,
|
|
});
|
|
}
|
|
|
|
connect() {
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
if (this.destroyed) return;
|
|
this._radioUnsubscribe = this.radio.subscribe?.((state) =>
|
|
this._renderRadioState(state),
|
|
);
|
|
}
|
|
|
|
_renderRadioState(state) {
|
|
renderRadioState.call(this, state);
|
|
}
|
|
|
|
/**
|
|
* Reveal the newly enabled directory and transport inside Context without
|
|
* moving focus, the page, or the globe. Only the expanded Enable path calls
|
|
* this helper.
|
|
* @param {HTMLElement} trigger Initiating Radio Enable button.
|
|
* @returns {Promise<boolean>} Whether the internal scroller moved.
|
|
*/
|
|
async _revealRadioControlsAfterExplicitEnable(trigger) {
|
|
const contextPanel = document.getElementById('global-context-panel');
|
|
const scroller =
|
|
(document.documentElement?.dataset.uiTheme === 'cyber' &&
|
|
contextPanel?.querySelector('.cyber-panel-body')) ||
|
|
contextPanel?.querySelector('.global-context-panel-inner');
|
|
const directory = this._radioPanel?.querySelector('.radio-directory-row');
|
|
const transport = this._radioPanel?.querySelector('.radio-transport');
|
|
if (
|
|
!contextPanel ||
|
|
contextPanel.classList.contains('collapsed') ||
|
|
!scroller ||
|
|
!directory ||
|
|
!transport ||
|
|
!this._radioState?.enabled
|
|
)
|
|
return false;
|
|
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
if (this.destroyed || !this._radioState?.enabled || !trigger?.isConnected)
|
|
return false;
|
|
|
|
const viewport = scroller.getBoundingClientRect();
|
|
const directoryRect = directory.getBoundingClientRect();
|
|
const transportRect = transport.getBoundingClientRect();
|
|
const margin = 10;
|
|
const minimum =
|
|
scroller.scrollTop + transportRect.bottom - (viewport.bottom - margin);
|
|
const maximum =
|
|
scroller.scrollTop + directoryRect.top - (viewport.top + margin);
|
|
const desired =
|
|
minimum <= maximum
|
|
? Math.min(Math.max(scroller.scrollTop, minimum), maximum)
|
|
: minimum;
|
|
const next = Math.min(
|
|
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
|
Math.max(0, desired),
|
|
);
|
|
if (Math.abs(next - scroller.scrollTop) < 1) return false;
|
|
const reducedMotion = window.matchMedia?.(
|
|
'(prefers-reduced-motion: reduce)',
|
|
)?.matches;
|
|
scroller.scrollTo({
|
|
top: next,
|
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
|
});
|
|
return true;
|
|
}
|
|
|
|
destroy() {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
this.listeners.abort();
|
|
this._radioUnsubscribe?.();
|
|
this._radioUnsubscribe = null;
|
|
const pointer = this._radioTunerPointerId;
|
|
this._radioTunerPointerId = null;
|
|
this._radioTunerDragging = false;
|
|
this._radioTunerKeyboardKey = null;
|
|
this._radioTunerDragSnapshot = null;
|
|
this._radioTunerPool = [];
|
|
this._radioTunerStations = [];
|
|
this._radioTunerBandPinnedForNavigation = false;
|
|
try {
|
|
if (pointer !== null)
|
|
this._radioTunerSlider?.releasePointerCapture(pointer);
|
|
} catch {
|
|
/* capture already released */
|
|
}
|
|
showRadioTuner.call(this, { dragging: false, static: false });
|
|
document
|
|
.getElementById('title-bar')
|
|
?.classList.remove('radio-broadcasting');
|
|
this.radio.endTuning();
|
|
}
|
|
}
|