mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
refactor: isolate Radio controls and tuner presentation
This commit is contained in:
1 parent
79a0f09087
commit
7b9aca8597
21 files changed
+1507
-1134
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import { bindRadioControls } from './radioBindings.js';
|
||||
import { renderRadioState } from './radioPresentation.js';
|
||||
|
||||
/** Own Radio DOM, tuner interaction and subscription; receive playback and application actions. */
|
||||
export class RadioControls {
|
||||
constructor({ elements, radio, actions, canvas }) {
|
||||
Object.assign(this, elements);
|
||||
this.radio = radio;
|
||||
this.actions = actions;
|
||||
this.canvas = canvas;
|
||||
this.destroyed = false;
|
||||
this.listeners = new AbortController();
|
||||
this._radioUnsubscribe = null;
|
||||
this._radioState = null;
|
||||
this._radioCategorySignature = '';
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the embedded Radio section into the expanded Context scroller.
|
||||
* This never changes Radio power, playback, selection, or Context mode.
|
||||
* @param {{focusTarget?: HTMLElement|null}} [options]
|
||||
* @returns {Promise<boolean>} Whether the internal scroller moved.
|
||||
*/
|
||||
async _revealRadioPanelInsideContext({ focusTarget = null } = {}) {
|
||||
const contextPanel = document.getElementById('global-context-panel');
|
||||
const scroller = contextPanel?.querySelector('.global-context-panel-inner');
|
||||
if (!contextPanel || contextPanel.classList.contains('collapsed')
|
||||
|| !scroller || !this._radioPanel || this._radioPanel.classList.contains('collapsed')) return false;
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
if (this.destroyed || contextPanel.classList.contains('collapsed') || this._radioPanel.classList.contains('collapsed')) return false;
|
||||
|
||||
const viewport = scroller.getBoundingClientRect();
|
||||
const radioRect = this._radioPanel.getBoundingClientRect();
|
||||
const desired = scroller.scrollTop + radioRect.top - viewport.top - 10;
|
||||
const next = Math.min(
|
||||
Math.max(0, scroller.scrollHeight - scroller.clientHeight),
|
||||
Math.max(0, desired),
|
||||
);
|
||||
const moved = Math.abs(next - scroller.scrollTop) >= 1;
|
||||
if (moved) {
|
||||
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
|
||||
scroller.scrollTo({ top: next, behavior: reducedMotion ? 'auto' : 'smooth' });
|
||||
}
|
||||
focusTarget?.focus?.({ preventScroll: true });
|
||||
return moved;
|
||||
}
|
||||
|
||||
/** Keep the Context header Radio shortcut truthful for its current route. */
|
||||
_syncContextRadioLauncherState() {
|
||||
if (!this._contextRadioToggleBtn) return;
|
||||
const contextPanel = document.getElementById('global-context-panel');
|
||||
const contextExpanded = Boolean(contextPanel && !contextPanel.classList.contains('collapsed'));
|
||||
if (contextExpanded) {
|
||||
const radioExpanded = Boolean(this._radioPanel && !this._radioPanel.classList.contains('collapsed'));
|
||||
this._contextRadioToggleBtn.setAttribute('aria-controls', 'radio-panel');
|
||||
this._contextRadioToggleBtn.setAttribute('aria-expanded', String(radioExpanded));
|
||||
const label = radioExpanded ? 'Go to expanded Radio section' : 'Expand Radio section in Context';
|
||||
this._contextRadioToggleBtn.setAttribute('aria-label', label);
|
||||
this._contextRadioToggleBtn.title = label;
|
||||
return;
|
||||
}
|
||||
const compactOpen = Boolean(this._contextRadioDock?.classList.contains('disclosure-open'));
|
||||
this._contextRadioToggleBtn.setAttribute('aria-controls', 'context-radio-mini');
|
||||
this._contextRadioToggleBtn.setAttribute('aria-expanded', String(compactOpen));
|
||||
const action = compactOpen ? 'Close' : 'Open';
|
||||
this._contextRadioToggleBtn.setAttribute('aria-label', `${action} compact Radio controls`);
|
||||
this._contextRadioToggleBtn.title = `${action} compact Radio controls`;
|
||||
}
|
||||
|
||||
|
||||
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 */ }
|
||||
this._radioTuner?.classList.remove('is-dragging', 'is-static');
|
||||
document.getElementById('title-bar')?.classList.remove('radio-broadcasting');
|
||||
this.radio.endTuning();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user