| class PlaybackControls extends HTMLElement { |
| constructor() { |
| super(); |
| this.attachShadow({ mode: 'open' }); |
| this.isPlaying = false; |
| } |
|
|
| connectedCallback() { |
| this.render(); |
| this.setupEventListeners(); |
| } |
|
|
| static get observedAttributes() { |
| return ['duration', 'current-tick']; |
| } |
|
|
| attributeChangedCallback(name, oldValue, newValue) { |
| if (oldValue !== newValue) { |
| this.render(); |
| } |
| } |
|
|
| render() { |
| const duration = parseInt(this.getAttribute('duration') || '0'); |
| const currentTick = parseInt(this.getAttribute('current-tick') || '0'); |
| const progress = duration > 0 ? (currentTick / duration) * 100 : 0; |
| |
| this.shadowRoot.innerHTML = ` |
| <style> |
| :host { |
| display: block; |
| } |
| |
| .controls { |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| } |
| |
| .play-pause { |
| background-color: #3b82f6; |
| color: white; |
| border: none; |
| border-radius: 50%; |
| width: 40px; |
| height: 40px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: background-color 0.2s; |
| } |
| |
| .play-pause:hover { |
| background-color: #2563eb; |
| } |
| |
| .progress-container { |
| flex-grow: 1; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .progress-bar { |
| flex-grow: 1; |
| height: 6px; |
| background-color: #374151; |
| border-radius: 3px; |
| cursor: pointer; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .progress-fill { |
| position: absolute; |
| top: 0; |
| left: 0; |
| height: 100%; |
| background-color: #3b82f6; |
| width: ${progress}%; |
| } |
| |
| .time { |
| font-size: 0.875rem; |
| color: #9ca3af; |
| min-width: 80px; |
| text-align: center; |
| } |
| |
| .speed-control { |
| background-color: #374151; |
| color: white; |
| border: none; |
| border-radius: 4px; |
| padding: 0.25rem 0.5rem; |
| font-size: 0.875rem; |
| cursor: pointer; |
| } |
| </style> |
| |
| <div class="controls"> |
| <button class="play-pause"> |
| <i data-feather="${this.isPlaying ? 'pause' : 'play'}"></i> |
| </button> |
| |
| <div class="progress-container"> |
| <div class="time"> |
| ${this.formatTime(currentTick)} / ${this.formatTime(duration)} |
| </div> |
| |
| <div class="progress-bar"> |
| <div class="progress-fill"></div> |
| </div> |
| </div> |
| |
| <button class="speed-control">1x</button> |
| </div> |
| `; |
| |
| |
| if (window.feather) { |
| window.feather.replace(); |
| } |
| } |
|
|
| setupEventListeners() { |
| this.shadowRoot.querySelector('.play-pause').addEventListener('click', () => { |
| this.togglePlayback(); |
| }); |
| |
| this.shadowRoot.querySelector('.progress-bar').addEventListener('click', (e) => { |
| const rect = e.target.getBoundingClientRect(); |
| const percent = (e.clientX - rect.left) / rect.width; |
| const duration = parseInt(this.getAttribute('duration') || '0'); |
| const seekTick = Math.floor(percent * duration); |
| |
| this.setAttribute('current-tick', seekTick.toString()); |
| window.playbackControls.seek(seekTick); |
| }); |
| |
| this.shadowRoot.querySelector('.speed-control').addEventListener('click', () => { |
| |
| }); |
| } |
|
|
| togglePlayback() { |
| this.isPlaying = !this.isPlaying; |
| this.render(); |
| |
| if (this.isPlaying) { |
| window.playbackControls.play(); |
| } else { |
| window.playbackControls.pause(); |
| } |
| } |
|
|
| formatTime(ticks) { |
| if (!ticks) return '0:00'; |
| const seconds = Math.floor(ticks / 30); |
| const minutes = Math.floor(seconds / 60); |
| const remainingSeconds = seconds % 60; |
| return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; |
| } |
| } |
|
|
| customElements.define('custom-playback-controls', PlaybackControls); |