import { LitElement, html, type TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { controlsStyles } from '../styles/controls-styles'; import { overlayStyles } from '../styles/overlay-styles'; import { formatTimestamp } from '../utils/date-formatter'; import type { RadarFrame, RadarResponse, ControlsDisplayConfig } from '../types'; @customElement('bom-controls-section') export class BomControlsSection extends LitElement { static override styles = [controlsStyles, overlayStyles]; @property({ attribute: false }) frames: RadarFrame[] = []; @property({ attribute: false }) currentFrameIndex: number = 0; @property({ attribute: false }) isPlaying: boolean = false; @property({ attribute: false }) isExtendedMode: boolean = false; @property({ attribute: false }) radarData?: RadarResponse; @property({ attribute: false }) config?: ControlsDisplayConfig | boolean; @property({ attribute: false }) position?: 'above' | 'below' | 'overlay'; @property({ attribute: false }) locale: string = 'en-AU'; @property({ attribute: false }) timeZone?: string; @property({ attribute: false }) overlayOpacity?: number; @property({ attribute: false }) overlayPosition?: string; @property({ attribute: false }) showFrameTimes?: boolean; // Event handlers @property({ attribute: false }) onFrameChange?: (index: number) => void; @property({ attribute: false }) onPrevious?: () => void; @property({ attribute: false }) onNext?: () => void; @property({ attribute: false }) onJumpFrame?: (offset: number) => void; @property({ attribute: false }) onToggleAnimation?: () => void; private _getDisplayConfig(): ControlsDisplayConfig | null { if (!this.config || (typeof this.config === 'boolean' && !this.config)) { return null; } return typeof this.config === 'object' ? this.config : {}; } protected override render(): TemplateResult { const displayConfig = this._getDisplayConfig(); if (!displayConfig) { return html``; } const configPosition = displayConfig.position ?? 'below'; // Only render if position matches (or if no position filter specified) if (this.position !== undefined && configPosition !== this.position) { return html``; } // If overlay, render differently if (configPosition === 'overlay') { return this._renderOverlay(displayConfig); } return html`
${displayConfig.show_slider !== false ? this._renderFrameSlider() : ''} ${displayConfig.show_nav_buttons !== false ? this._renderNavButtons() : ''} ${displayConfig.show_play_pause !== false ? this._renderPlayPause() : ''} ${displayConfig.show_prev_next !== false ? this._renderPrevNext() : ''} ${displayConfig.show_frame_info !== false ? this._renderFrameInfo() : ''}
`; } private _renderOverlay(displayConfig: ControlsDisplayConfig): TemplateResult { const opacity = this.overlayOpacity ?? 0.9; const position = this.overlayPosition ?? 'bottom'; return html`
${displayConfig.show_slider !== false ? this._renderFrameSlider() : ''} ${displayConfig.show_play_pause !== false ? this._renderPlayPause() : ''} ${displayConfig.show_prev_next !== false ? this._renderPrevNext() : ''} ${displayConfig.show_frame_info !== false ? this._renderFrameInfo() : ''}
`; } private _renderFrameSlider(): TemplateResult { if (this.frames.length === 0) return html``; return html`
`; } private _renderNavButtons(): TemplateResult { if (this.frames.length === 0) return html``; // Calculate dynamic skip amount based on total frames (1/3 of total, min 1, max 50) const skipAmount = this.isExtendedMode ? Math.max(1, Math.min(50, Math.round(this.frames.length / 3))) : 0; return html`
${this.isExtendedMode ? html` ` : html` `}
`; } private _renderPlayPause(): TemplateResult { return html`
`; } private _renderPrevNext(): TemplateResult { return html`
`; } private _renderFrameInfo(): TemplateResult { const currentFrame = this.frames[this.currentFrameIndex]; if (!currentFrame) return html``; const showFrameTimes = this.showFrameTimes !== false; // Format frame info let frameInfoText = ''; if (this.isExtendedMode && currentFrame.absoluteObservationTime) { frameInfoText = `Frame ${(currentFrame.sequentialIndex ?? this.currentFrameIndex) + 1} of ${this.frames.length}`; } else { frameInfoText = `Frame ${currentFrame.frameIndex + 1} of ${this.frames.length}`; } const progress = this.frames.length > 0 ? Math.round(((this.currentFrameIndex + 1) / this.frames.length) * 100) : 0; return html`
${frameInfoText} ${showFrameTimes && currentFrame.absoluteObservationTime ? html` ${formatTimestamp(currentFrame.absoluteObservationTime, this.locale, this.timeZone)} ` : showFrameTimes && currentFrame.minutesAgo !== undefined ? html` ${currentFrame.minutesAgo} min ago ` : ''} ${this.radarData?.observationTime && showFrameTimes ? html` Obs: ${formatTimestamp(this.radarData.observationTime, this.locale, this.timeZone)} ` : ''} ${progress}%
`; } } declare global { interface HTMLElementTagNameMap { 'bom-controls-section': BomControlsSection; } }