const LUMA_MEDIA_VERSION = '0.2.0'; class LumaMediaCard extends HTMLElement { set hass(hass) { this._hass = hass; this._render(); } setConfig(config) { if (!config.entity) throw new Error('Entity is required'); this._config = { entity: config.entity, name: config.name || '', size: config.size || '1x2', accent_color: config.accent_color || '', icon: config.icon || '', tap_action: config.tap_action || { action: 'more-info' }, hold_action: config.hold_action || { action: 'more-info' }, ...config }; this._render(); } getCardSize() { return 4; } static getStubConfig() { return { entity: '', name: '', size: '1x2' }; } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); } _getState() { return this._hass?.states?.[this._config?.entity]; } _isActive() { const s = this._getState(); return s && (s.state === 'playing' || s.state === 'paused'); } _callService(service, data = {}) { if (!this._hass || !this._config) return; this._hass.callService('media_player', service, { entity_id: this._config.entity, ...data }); } _handleClick() { if (!this._hass || !this._config) return; const ev = new Event('hass-more-info', { composed: true }); ev.detail = { entityId: this._config.entity }; this.dispatchEvent(ev); } _formatTime(secs) { if (!secs || secs <= 0) return '0:00'; const m = Math.floor(secs / 60); const s = Math.floor(secs % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; } _render() { if (!this.shadowRoot || !this._config) return; const s = this._getState(); const isPlaying = s?.state === 'playing'; const isPaused = s?.state === 'paused'; const isActive = isPlaying || isPaused; const hasMedia = s?.attributes?.media_title ? true : false; const title = s?.attributes?.media_title || this._config.name || s?.attributes?.friendly_name || ''; const artist = s?.attributes?.media_artist || ''; const entityPicture = s?.attributes?.entity_picture || ''; const volume = Math.round((s?.attributes?.volume_level || 0) * 100); const duration = s?.attributes?.media_duration || 0; const position = s?.attributes?.media_position || 0; const progress = duration > 0 ? Math.min(position / duration, 1) : 0; const artBg = entityPicture ? `background-image: url('${entityPicture}'); background-size: cover; background-position: center;` : `background: rgba(36,40,48,0.9); display: flex; align-items: center; justify-content: center;`; const volFillH = volume > 0 ? `height: ${volume}%` : 'height: 0'; const volAlpha = Math.min(volume / 100 * 0.35, 0.30).toFixed(2); this.shadowRoot.innerHTML = `
${!entityPicture ? '' : ''}
${title}
${artist ? `
${artist}
` : ''}
${volume}%
${duration > 0 ? `
${this._formatTime(position)} -${this._formatTime(duration - position)}
` : ''}
`; this.shadowRoot.querySelector('.top').addEventListener('click', () => this._handleClick()); this.shadowRoot.querySelectorAll('.ctrl-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); switch (btn.dataset.action) { case 'play': this._callService(isPlaying ? 'media_pause' : 'media_play'); break; case 'prev': this._callService('media_previous_track'); break; case 'next': this._callService('media_next_track'); break; case 'seek-back': if (duration > 0) this._callService('media_seek', { seek_position: Math.max(0, position - 10) }); break; case 'seek-fwd': if (duration > 0) this._callService('media_seek', { seek_position: Math.min(duration, position + 10) }); break; default: this._handleClick(); } }); }); } } customElements.define('luma-media-card', LumaMediaCard); window.customCards = window.customCards || []; window.customCards.push({ type: 'luma-media-card', name: 'Luma Media', description: 'Luma-style media card with album art, progress bar, and 7-button transport.', preview: true }); console.info(`%c LUMA-MEDIA-CARD %c v${LUMA_MEDIA_VERSION} `, 'background:#9b7ddb;color:#111;font-weight:bold', 'background:#161a22;color:#9b7ddb');