UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

544 lines (535 loc) 48.8 kB
import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, signal, computed, Injectable, input, output, ChangeDetectionStrategy, Component, viewChild, HostListener, model, effect } from '@angular/core'; import { matPlayArrow, matPause, matSkipPrevious, matSkipNext } from '@ng-icons/material-icons/baseline'; import { ButtonComponent } from '@sixbell-telco/sdk/components/button'; import { IconComponent } from '@sixbell-telco/sdk/components/icon'; import { heroMusicalNoteMini } from '@sixbell-telco/sdk/components/icon/heroicons/mini'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { TranslatePipe } from '@ngx-translate/core'; import { TypographyDirective } from '@sixbell-telco/sdk/directives/typography'; import { TimePipe } from '@sixbell-telco/sdk/utils/pipes/time'; import { matVolumeUpOutline, matVolumeDownOutline, matVolumeMuteOutline } from '@ng-icons/material-icons/outline'; // audio-player.service.ts class AudioPlayerService { audio = new Audio(); http = inject(HttpClient); objectUrls = new Map(); // State signals _tracks = signal([]); _currentTrackIndex = signal(-1); _isPlaying = signal(false); _duration = signal(0); _currentTime = signal(0); _volume = signal(1); _isMuted = signal(false); _lastVolume = signal(1); _isBuffering = signal(false); _isInitializing = signal(false); _autoPlayNext = signal(false); _error = signal(null); // Readonly signals tracks = this._tracks.asReadonly(); currentTrackIndex = this._currentTrackIndex.asReadonly(); isPlaying = this._isPlaying.asReadonly(); duration = this._duration.asReadonly(); currentTime = this._currentTime.asReadonly(); volume = this._volume.asReadonly(); isMuted = this._isMuted.asReadonly(); isBuffering = this._isBuffering.asReadonly(); isInitializing = this._isInitializing.asReadonly(); currentTrack = computed(() => this.tracks()[this.currentTrackIndex()] || undefined); autoPlayNext = this._autoPlayNext.asReadonly(); error = this._error.asReadonly(); constructor() { this.setupAudioListeners(); this.restoreVolumeSettings(); } setupAudioListeners() { this.audio.addEventListener('loadedmetadata', () => this.updateDuration()); this.audio.addEventListener('timeupdate', () => this.updateTime()); this.audio.addEventListener('playing', () => this.setPlaying(true)); this.audio.addEventListener('pause', () => this.setPlaying(false)); this.audio.addEventListener('ended', () => this.handleEnded()); this.audio.addEventListener('waiting', () => this._isBuffering.set(true)); this.audio.addEventListener('error', (e) => { this._error.set(e); console.error('Audio error:', e); }); } async getAuthenticatedAudioUrl(track) { if (!this.objectUrls.has(track.url)) { const blob = await firstValueFrom(this.http.get(track.url, { responseType: 'blob', headers: { Accept: 'audio/mpeg' }, })); this.objectUrls.set(track.url, URL.createObjectURL(blob)); } return this.objectUrls.get(track.url); } async playTrack(index) { if (index < 0 || index >= this.tracks().length) return; this._isInitializing.set(true); this._currentTrackIndex.set(index); const track = this.tracks()[index]; // Get authenticated URL const authenticatedUrl = await this.getAuthenticatedAudioUrl(track); // Set audio source this.audio.src = authenticatedUrl; this.audio.load(); try { // Wait for audio to be ready await new Promise((resolve, reject) => { this.audio.addEventListener('canplay', () => resolve(), { once: true }); this.audio.addEventListener('error', (e) => reject(e), { once: true }); }); await this.audio.play(); } catch (error) { console.error('Error playing track:', error); this._error.set(error); this._isPlaying.set(false); } finally { this._isInitializing.set(false); } } init() { this.audio = new Audio(); this.setupAudioListeners(); this.restoreVolumeSettings(); } cleanup() { this.audio.pause(); this.audio.src = ''; this.objectUrls.forEach((url) => URL.revokeObjectURL(url)); this.objectUrls.clear(); // reset signals this._tracks.set([]); this._currentTrackIndex.set(-1); this._isPlaying.set(false); this._duration.set(0); this._currentTime.set(0); this._volume.set(1); this._isMuted.set(false); this._lastVolume.set(1); this._isBuffering.set(false); this._isInitializing.set(false); this._autoPlayNext.set(false); this._error.set(null); } async togglePlay() { if (!this.checkIfTrackIsLoaded()) return; // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.audio.paused ? await this.audio.play() : this.audio.pause(); } async previous() { this.clearCurrentTime(); const totalTracks = this.tracks().length; if (totalTracks === 0) return; const newIndex = (this.currentTrackIndex() - 1 + totalTracks) % totalTracks; await this.playTrack(newIndex); } async next() { this.clearCurrentTime(); const totalTracks = this.tracks().length; if (totalTracks === 0) return; const newIndex = (this.currentTrackIndex() + 1) % totalTracks; await this.playTrack(newIndex); } setTracks(tracks) { this._tracks.set(tracks); } setCurrentTrackIndex(index) { this._currentTrackIndex.set(index); } setPlaying(playing) { this._isPlaying.set(playing); this._isBuffering.set(false); } setAutoPlayNext(autoPlayNext) { this._autoPlayNext.set(autoPlayNext); } setVolume(volume) { this._volume.set(volume); this.audio.volume = volume; if (volume > 0) this._lastVolume.set(volume); localStorage.setItem('volume', JSON.stringify(this.volume())); } setMuted(muted) { this._isMuted.set(muted); if (muted) { this._lastVolume.set(this.volume()); this.setVolume(0); } else { this.setVolume(this._lastVolume()); } localStorage.setItem('isMuted', JSON.stringify(this.isMuted())); } seekTo(time) { this.audio.currentTime = time; } isPlayingATrack() { return this.audio.src !== '' && !this.audio.paused; } // Private handlers updateDuration() { this._duration.set(this.audio.duration); this._isInitializing.set(false); } updateTime() { this._currentTime.set(this.audio.currentTime); } handleEnded() { this._isPlaying.set(false); if (this._autoPlayNext()) this.next(); } clearCurrentTime() { this.audio.currentTime = 0; this._currentTime.set(0); } restoreVolumeSettings() { const volume = localStorage.getItem('volume'); const isMuted = localStorage.getItem('isMuted'); if (volume) this.setVolume(JSON.parse(volume)); if (isMuted) this.setMuted(JSON.parse(isMuted)); } checkIfTrackIsLoaded() { return this.audio.src !== ''; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioPlayerService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioPlayerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class ButtonPlayComponent { audioService = inject(AudioPlayerService); variant = input(); ghost = input(false); outline = input(false); circle = input(false); square = input(false); glass = input(false); size = input(); shadow = input(false); asButton = input(false); isHovered = signal(false); trackIndex = input.required(); togglePlay = output(); iconMusicNote = heroMusicalNoteMini; iconPlay = matPlayArrow; iconPause = matPause; waveBars = [ { delay: '0.4s', class: 'h-1/5 animate-wave w-[2px] rounded-full bg-current' }, { delay: '0.3s', class: 'h-3/5 animate-wave w-[2px] rounded-full bg-current' }, { delay: '0.2s', class: 'h-full animate-wave w-[2px] rounded-full bg-current' }, { delay: '0.3s', class: 'h-3/5 animate-wave w-[2px] rounded-full bg-current' }, { delay: '0.4s', class: 'h-1/5 animate-wave w-[2px] rounded-full bg-current' }, ]; buttonClass = computed(() => { return cn('absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-200 inline-flex items-center justify-center align-top', { 'opacity-100': (this.audioService.isPlaying() && this.trackIndex() === this.audioService.currentTrackIndex()) || this.isHovered(), }); }); waveBarsClass = computed(() => { return cn('h-6 w-6 opacity-0 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transition-opacity duration-200', { 'opacity-100': this.audioService.isPlaying() && this.trackIndex() === this.audioService.currentTrackIndex() && !this.isHovered(), }); }); iconClass = computed(() => { return cn('opacity-0 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transition-opacity duration-200 inline-flex items-center justify-center align-top text-lg', { 'opacity-100': !this.audioService.isPlaying() || this.trackIndex() !== this.audioService.currentTrackIndex() || this.isHovered(), }); }); icon = computed(() => { return this.audioService.isPlaying() && this.trackIndex() === this.audioService.currentTrackIndex() ? this.iconPause : this.iconPlay; }); handleHover(isHovered) { this.isHovered.set(isHovered); } async handleTogglePlay() { if (this.trackIndex() !== undefined && this.trackIndex() !== this.audioService.currentTrackIndex()) { await this.audioService.playTrack(this.trackIndex()); } else { await this.audioService.togglePlay(); } this.togglePlay.emit(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ButtonPlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: ButtonPlayComponent, isStandalone: true, selector: "st-button-play", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, ghost: { classPropertyName: "ghost", publicName: "ghost", isSignal: true, isRequired: false, transformFunction: null }, outline: { classPropertyName: "outline", publicName: "outline", isSignal: true, isRequired: false, transformFunction: null }, circle: { classPropertyName: "circle", publicName: "circle", isSignal: true, isRequired: false, transformFunction: null }, square: { classPropertyName: "square", publicName: "square", isSignal: true, isRequired: false, transformFunction: null }, glass: { classPropertyName: "glass", publicName: "glass", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, asButton: { classPropertyName: "asButton", publicName: "asButton", isSignal: true, isRequired: false, transformFunction: null }, trackIndex: { classPropertyName: "trackIndex", publicName: "trackIndex", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { togglePlay: "togglePlay" }, ngImport: i0, template: "@let waveBarsVar = waveBarsClass();\n@let iconClassVar = iconClass();\n@let iconVar = icon();\n\n@if (!asButton()) {\n\t<div\n\t\tclass=\"from-primary to-primary-gradient relative grid h-10 min-h-10 w-10 min-w-10 place-content-center rounded-sm bg-linear-to-br text-lg text-white\"\n\t\t(mouseenter)=\"handleHover(true)\"\n\t\t(mouseleave)=\"handleHover(false)\"\n\t>\n\t\t<st-icon [icon]=\"iconMusicNote\"></st-icon>\n\n\t\t<div [class]=\"buttonClass()\">\n\t\t\t<st-button [circle]=\"true\" size=\"sm\" [shadow]=\"true\" variant=\"secondary\" (click)=\"handleTogglePlay()\">\n\t\t\t\t<div class=\"relative h-full w-full\">\n\t\t\t\t\t<div [class]=\"waveBarsVar\">\n\t\t\t\t\t\t<div class=\"flex h-full w-full items-center justify-between\">\n\t\t\t\t\t\t\t@for (bar of waveBars; track $index) {\n\t\t\t\t\t\t\t\t<div [class]=\"bar.class\" [style.--i]=\"bar.delay\"></div>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div [class]=\"iconClassVar\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconVar\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</st-button>\n\t\t</div>\n\t</div>\n} @else {\n\t<st-button\n\t\t[variant]=\"variant()\"\n\t\t[ghost]=\"ghost()\"\n\t\t[outline]=\"outline()\"\n\t\t[circle]=\"circle()\"\n\t\t[square]=\"square()\"\n\t\t[glass]=\"glass()\"\n\t\t[size]=\"size()\"\n\t\t[shadow]=\"shadow()\"\n\t\t(click)=\"handleTogglePlay()\"\n\t\t(mouseenter)=\"handleHover(true)\"\n\t\t(mouseleave)=\"handleHover(false)\"\n\t>\n\t\t<div class=\"relative h-full w-full\">\n\t\t\t<div [class]=\"waveBarsVar\">\n\t\t\t\t<div class=\"flex h-full w-full items-center justify-between\">\n\t\t\t\t\t@for (bar of waveBars; track $index) {\n\t\t\t\t\t\t<div [class]=\"bar.class\" [style.--i]=\"bar.delay\"></div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div [class]=\"iconClassVar\">\n\t\t\t\t<st-icon [icon]=\"iconVar\"></st-icon>\n\t\t\t</div>\n\t\t</div>\n\t</st-button>\n}\n", styles: [""], dependencies: [{ kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ButtonPlayComponent, decorators: [{ type: Component, args: [{ selector: 'st-button-play', imports: [IconComponent, CommonModule, ButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let waveBarsVar = waveBarsClass();\n@let iconClassVar = iconClass();\n@let iconVar = icon();\n\n@if (!asButton()) {\n\t<div\n\t\tclass=\"from-primary to-primary-gradient relative grid h-10 min-h-10 w-10 min-w-10 place-content-center rounded-sm bg-linear-to-br text-lg text-white\"\n\t\t(mouseenter)=\"handleHover(true)\"\n\t\t(mouseleave)=\"handleHover(false)\"\n\t>\n\t\t<st-icon [icon]=\"iconMusicNote\"></st-icon>\n\n\t\t<div [class]=\"buttonClass()\">\n\t\t\t<st-button [circle]=\"true\" size=\"sm\" [shadow]=\"true\" variant=\"secondary\" (click)=\"handleTogglePlay()\">\n\t\t\t\t<div class=\"relative h-full w-full\">\n\t\t\t\t\t<div [class]=\"waveBarsVar\">\n\t\t\t\t\t\t<div class=\"flex h-full w-full items-center justify-between\">\n\t\t\t\t\t\t\t@for (bar of waveBars; track $index) {\n\t\t\t\t\t\t\t\t<div [class]=\"bar.class\" [style.--i]=\"bar.delay\"></div>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div [class]=\"iconClassVar\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconVar\"></st-icon>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</st-button>\n\t\t</div>\n\t</div>\n} @else {\n\t<st-button\n\t\t[variant]=\"variant()\"\n\t\t[ghost]=\"ghost()\"\n\t\t[outline]=\"outline()\"\n\t\t[circle]=\"circle()\"\n\t\t[square]=\"square()\"\n\t\t[glass]=\"glass()\"\n\t\t[size]=\"size()\"\n\t\t[shadow]=\"shadow()\"\n\t\t(click)=\"handleTogglePlay()\"\n\t\t(mouseenter)=\"handleHover(true)\"\n\t\t(mouseleave)=\"handleHover(false)\"\n\t>\n\t\t<div class=\"relative h-full w-full\">\n\t\t\t<div [class]=\"waveBarsVar\">\n\t\t\t\t<div class=\"flex h-full w-full items-center justify-between\">\n\t\t\t\t\t@for (bar of waveBars; track $index) {\n\t\t\t\t\t\t<div [class]=\"bar.class\" [style.--i]=\"bar.delay\"></div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div [class]=\"iconClassVar\">\n\t\t\t\t<st-icon [icon]=\"iconVar\"></st-icon>\n\t\t\t</div>\n\t\t</div>\n\t</st-button>\n}\n" }] }] }); class AudioTimelineComponent { timeline = viewChild('timeline'); audioService = inject(AudioPlayerService); isDragging = signal(false); isHovered = signal(false); timelineClass = computed(() => cn('relative h-1 w-full cursor-pointer rounded-full bg-neutral ', { 'h-[6px]': this.isHovered() || this.isDragging() })); timelineCircleClass = computed(() => cn('transition-opacity duration-200 absolute top-1/2 h-3 w-3 -translate-y-1/2 transform cursor-grab rounded-full bg-tertiary shadow-main', this.isHovered() || this.isDragging() ? 'opacity-100' : 'opacity-0')); // Handle timeline hover handleTimelineHover(isHovered) { this.isHovered.set(isHovered); } // Start dragging startDrag(event) { event.preventDefault(); // Prevent default behavior event.stopPropagation(); // Stop event propagation this.isDragging.set(true); this.updateProgress(event); } // Handle dragging onDrag(event) { if (this.isDragging()) { this.updateProgress(event); } } // Stop dragging stopDrag() { this.isDragging.set(false); } // Update progress based on mouse/touch position updateProgress(event) { const timelineRect = this.timeline()?.nativeElement.getBoundingClientRect(); if (!timelineRect) return; const clientX = this.getClientX(event); const newProgress = Math.max(0, Math.min(1, (clientX - timelineRect.left) / timelineRect.width)); this.audioService.seekTo(newProgress * this.audioService.duration()); } getClientX(event) { return event instanceof MouseEvent ? event.clientX : event.touches[0].clientX; } seek(event) { if (!this.isDragging()) { this.updateProgress(event); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioTimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: AudioTimelineComponent, isStandalone: true, selector: "st-timeline", host: { listeners: { "document:mousemove": "onDrag($event)", "document:touchmove": "onDrag($event)", "document:mouseup": "stopDrag()", "document:touchend": "stopDrag()" } }, viewQueries: [{ propertyName: "timeline", first: true, predicate: ["timeline"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n\n<!-- Progress Bar -->\n<div class=\"grid w-full grid-cols-[1fr_5fr_1fr] items-center gap-4\">\n\t<!-- Current Time Display -->\n\t<div class=\"text-tertiary inline-flex min-h-5 items-center justify-end align-top select-none\">\n\t\t@if (!audioService.isInitializing()) {\n\t\t\t@if (audioService.isBuffering()) {\n\t\t\t\t<span class=\"loading loading-ring loading-xs\"></span>\n\t\t\t} @else {\n\t\t\t\t<span typography tyVariant=\"body-xxs\">{{ audioService.currentTime() | time }}</span>\n\t\t\t}\n\t\t}\n\t</div>\n\n\t<div [class]=\"timelineClass()\" (click)=\"seek($event)\" #timeline (mouseenter)=\"handleTimelineHover(true)\" (mouseleave)=\"handleTimelineHover(false)\">\n\t\t<!-- Progress Bar Background -->\n\t\t<div class=\"bg-primary absolute h-full rounded-full\" [style.width]=\"(audioService.currentTime() / audioService.duration()) * 100 + '%'\"></div>\n\n\t\t<!-- Draggable Circle -->\n\t\t<div\n\t\t\t[style.left]=\"\n\t\t\t\t'calc(' +\n\t\t\t\t(audioService.currentTime() / audioService.duration()) * 100 +\n\t\t\t\t'% - ' +\n\t\t\t\t(audioService.currentTime() / audioService.duration() === 1 ? '6px' : '3px') +\n\t\t\t\t')'\n\t\t\t\"\n\t\t\t[class]=\"timelineCircleClass()\"\n\t\t\t#circle\n\t\t\t(mousedown)=\"startDrag($event)\"\n\t\t\t(touchstart)=\"startDrag($event)\"\n\t\t></div>\n\t</div>\n\n\t<!-- Total Time Display -->\n\t<div class=\"text-tertiary inline-flex min-h-5 items-center justify-start align-top select-none\">\n\t\t@if (audioService.isInitializing()) {\n\t\t\t<span class=\"loading loading-ring loading-xs\"></span>\n\t\t} @else {\n\t\t\t<span typography tyVariant=\"body-xxs\">{{ audioService.duration() | time }}</span>\n\t\t}\n\t</div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: TimePipe, name: "time" }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioTimelineComponent, decorators: [{ type: Component, args: [{ selector: 'st-timeline', imports: [CommonModule, TimePipe, TypographyDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n\n<!-- Progress Bar -->\n<div class=\"grid w-full grid-cols-[1fr_5fr_1fr] items-center gap-4\">\n\t<!-- Current Time Display -->\n\t<div class=\"text-tertiary inline-flex min-h-5 items-center justify-end align-top select-none\">\n\t\t@if (!audioService.isInitializing()) {\n\t\t\t@if (audioService.isBuffering()) {\n\t\t\t\t<span class=\"loading loading-ring loading-xs\"></span>\n\t\t\t} @else {\n\t\t\t\t<span typography tyVariant=\"body-xxs\">{{ audioService.currentTime() | time }}</span>\n\t\t\t}\n\t\t}\n\t</div>\n\n\t<div [class]=\"timelineClass()\" (click)=\"seek($event)\" #timeline (mouseenter)=\"handleTimelineHover(true)\" (mouseleave)=\"handleTimelineHover(false)\">\n\t\t<!-- Progress Bar Background -->\n\t\t<div class=\"bg-primary absolute h-full rounded-full\" [style.width]=\"(audioService.currentTime() / audioService.duration()) * 100 + '%'\"></div>\n\n\t\t<!-- Draggable Circle -->\n\t\t<div\n\t\t\t[style.left]=\"\n\t\t\t\t'calc(' +\n\t\t\t\t(audioService.currentTime() / audioService.duration()) * 100 +\n\t\t\t\t'% - ' +\n\t\t\t\t(audioService.currentTime() / audioService.duration() === 1 ? '6px' : '3px') +\n\t\t\t\t')'\n\t\t\t\"\n\t\t\t[class]=\"timelineCircleClass()\"\n\t\t\t#circle\n\t\t\t(mousedown)=\"startDrag($event)\"\n\t\t\t(touchstart)=\"startDrag($event)\"\n\t\t></div>\n\t</div>\n\n\t<!-- Total Time Display -->\n\t<div class=\"text-tertiary inline-flex min-h-5 items-center justify-start align-top select-none\">\n\t\t@if (audioService.isInitializing()) {\n\t\t\t<span class=\"loading loading-ring loading-xs\"></span>\n\t\t} @else {\n\t\t\t<span typography tyVariant=\"body-xxs\">{{ audioService.duration() | time }}</span>\n\t\t}\n\t</div>\n</div>\n" }] }], propDecorators: { onDrag: [{ type: HostListener, args: ['document:mousemove', ['$event']] }, { type: HostListener, args: ['document:touchmove', ['$event']] }], stopDrag: [{ type: HostListener, args: ['document:mouseup'] }, { type: HostListener, args: ['document:touchend'] }] } }); class TrackInfoComponent { currentTrack = input(); iconMusicNote = heroMusicalNoteMini; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TrackInfoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.0", type: TrackInfoComponent, isStandalone: true, selector: "st-track-info", inputs: { currentTrack: { classPropertyName: "currentTrack", publicName: "currentTrack", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative flex max-w-80 items-center gap-4 overflow-x-hidden whitespace-nowrap\">\n\t<div class=\"flex items-center gap-3\">\n\t\t<!-- Album/Track Icon -->\n\t\t<div class=\"from-primary to-primary-gradient grid h-12 min-h-12 w-12 min-w-12 place-content-center rounded-sm bg-linear-to-br text-white\">\n\t\t\t<st-icon [icon]=\"iconMusicNote\" size=\"md\"></st-icon>\n\t\t</div>\n\n\t\t<!-- Track Info -->\n\t\t@let currentTrackVar = this.currentTrack();\n\t\t<div class=\"min-w-0 flex-1\">\n\t\t\t<p typography tyVariant=\"body\" tyFontWeight=\"medium\" tyColor=\"base\">\n\t\t\t\t{{ currentTrackVar?.title ?? 'sdk.audioPlayer.trackInfo.title' | translate }}\n\t\t\t</p>\n\t\t\t<p typography tyVariant=\"body-sm\" tyFontWeight=\"normal\" tyColor=\"base\">\n\t\t\t\t{{ currentTrackVar?.description ?? 'sdk.audioPlayer.trackInfo.description' | translate }}\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\t<div class=\"from-base-200 pointer-events-none absolute inset-y-0 right-0 w-12 bg-linear-to-l to-transparent\"></div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: TrackInfoComponent, decorators: [{ type: Component, args: [{ selector: 'st-track-info', imports: [IconComponent, CommonModule, TypographyDirective, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative flex max-w-80 items-center gap-4 overflow-x-hidden whitespace-nowrap\">\n\t<div class=\"flex items-center gap-3\">\n\t\t<!-- Album/Track Icon -->\n\t\t<div class=\"from-primary to-primary-gradient grid h-12 min-h-12 w-12 min-w-12 place-content-center rounded-sm bg-linear-to-br text-white\">\n\t\t\t<st-icon [icon]=\"iconMusicNote\" size=\"md\"></st-icon>\n\t\t</div>\n\n\t\t<!-- Track Info -->\n\t\t@let currentTrackVar = this.currentTrack();\n\t\t<div class=\"min-w-0 flex-1\">\n\t\t\t<p typography tyVariant=\"body\" tyFontWeight=\"medium\" tyColor=\"base\">\n\t\t\t\t{{ currentTrackVar?.title ?? 'sdk.audioPlayer.trackInfo.title' | translate }}\n\t\t\t</p>\n\t\t\t<p typography tyVariant=\"body-sm\" tyFontWeight=\"normal\" tyColor=\"base\">\n\t\t\t\t{{ currentTrackVar?.description ?? 'sdk.audioPlayer.trackInfo.description' | translate }}\n\t\t\t</p>\n\t\t</div>\n\t</div>\n\t<div class=\"from-base-200 pointer-events-none absolute inset-y-0 right-0 w-12 bg-linear-to-l to-transparent\"></div>\n</div>\n" }] }] }); class VolumeControlComponent { volumeTimeline = viewChild('volumeTimeline'); audioService = inject(AudioPlayerService); isVolumeDragging = signal(false); isVolumeHovered = signal(false); iconVolumeUp = matVolumeUpOutline; iconVolumeDown = matVolumeDownOutline; iconVolumeMute = matVolumeMuteOutline; volumeTimelineClass = computed(() => { return cn('relative h-1 w-20 cursor-pointer rounded-full bg-neutral', { 'h-[6px]': this.isVolumeHovered() || this.isVolumeDragging(), }); }); volumeTimelineCircleClass = computed(() => { return cn('absolute top-1/2 h-3 w-3 -translate-y-1/2 transform cursor-grab rounded-full bg-tertiary shadow-main transition-opacity duration-200', this.isVolumeHovered() || this.isVolumeDragging() ? 'opacity-100' : 'opacity-0'); }); // Handle volume hover handleVolumeHover(isHovered) { this.isVolumeHovered.set(isHovered); } // Start volume drag startVolumeDrag(event) { event.preventDefault(); // Prevent default behavior event.stopPropagation(); // Prevent event propagation this.isVolumeDragging.set(true); this.updateVolume(event); } // Handle volume drag onVolumeDrag(event) { if (this.isVolumeDragging()) { this.updateVolume(event); } } // Stop volume drag stopVolumeDrag() { this.isVolumeDragging.set(false); } getClientX(event) { return event instanceof MouseEvent ? event.clientX : event.touches[0].clientX; } updateVolume(event) { const volumeTimelineRect = this.volumeTimeline()?.nativeElement.getBoundingClientRect(); if (!volumeTimelineRect) return; const clientX = this.getClientX(event); const newVolume = Math.max(0, Math.min(1, (clientX - volumeTimelineRect.left) / volumeTimelineRect.width)); this.audioService.setVolume(newVolume); } // Seek volume on click seekVolume(event) { if (!this.isVolumeDragging()) { this.updateVolume(event); } } toggleMute() { this.audioService.setMuted(!this.audioService.isMuted()); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: VolumeControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: VolumeControlComponent, isStandalone: true, selector: "st-volume-control", host: { listeners: { "document:mousemove": "onVolumeDrag($event)", "document:touchmove": "onVolumeDrag($event)", "document:mouseup": "stopVolumeDrag()", "document:touchend": "stopVolumeDrag()" } }, viewQueries: [{ propertyName: "volumeTimeline", first: true, predicate: ["volumeTimeline"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n\n<!-- Volume Control -->\n<div class=\"flex items-center justify-end gap-2\">\n\t<!-- Mute/Unmute Button -->\n\t<st-button [ghost]=\"true\" size=\"sm\" [circle]=\"true\" (click)=\"toggleMute()\">\n\t\t@if (audioService.volume() === 0) {\n\t\t\t<st-icon [icon]=\"iconVolumeMute\" size=\"sm\"></st-icon>\n\t\t} @else if (audioService.volume() > 0.5) {\n\t\t\t<st-icon [icon]=\"iconVolumeUp\" size=\"sm\"></st-icon>\n\t\t} @else {\n\t\t\t<st-icon [icon]=\"iconVolumeDown\" size=\"sm\"></st-icon>\n\t\t}\n\t</st-button>\n\n\t<!-- Custom Volume Progress Bar -->\n\t<div\n\t\t[class]=\"volumeTimelineClass()\"\n\t\t(click)=\"seekVolume($event)\"\n\t\t#volumeTimeline\n\t\t(mouseenter)=\"handleVolumeHover(true)\"\n\t\t(mouseleave)=\"handleVolumeHover(false)\"\n\t>\n\t\t<!-- Volume Progress Bar Background -->\n\t\t<div class=\"bg-primary absolute h-full rounded-full\" [style.width]=\"audioService.volume() * 100 + '%'\"></div>\n\n\t\t<!-- Draggable Volume Circle -->\n\t\t<div\n\t\t\t[style.left]=\"'calc(' + audioService.volume() * 100 + '% - ' + (audioService.volume() === 1 ? '6px' : '3px') + ')'\"\n\t\t\t[class]=\"volumeTimelineCircleClass()\"\n\t\t\t#volumeCircle\n\t\t\t(mousedown)=\"startVolumeDrag($event)\"\n\t\t\t(touchstart)=\"startVolumeDrag($event)\"\n\t\t></div>\n\t</div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: VolumeControlComponent, decorators: [{ type: Component, args: [{ selector: 'st-volume-control', imports: [IconComponent, CommonModule, ButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n\n<!-- Volume Control -->\n<div class=\"flex items-center justify-end gap-2\">\n\t<!-- Mute/Unmute Button -->\n\t<st-button [ghost]=\"true\" size=\"sm\" [circle]=\"true\" (click)=\"toggleMute()\">\n\t\t@if (audioService.volume() === 0) {\n\t\t\t<st-icon [icon]=\"iconVolumeMute\" size=\"sm\"></st-icon>\n\t\t} @else if (audioService.volume() > 0.5) {\n\t\t\t<st-icon [icon]=\"iconVolumeUp\" size=\"sm\"></st-icon>\n\t\t} @else {\n\t\t\t<st-icon [icon]=\"iconVolumeDown\" size=\"sm\"></st-icon>\n\t\t}\n\t</st-button>\n\n\t<!-- Custom Volume Progress Bar -->\n\t<div\n\t\t[class]=\"volumeTimelineClass()\"\n\t\t(click)=\"seekVolume($event)\"\n\t\t#volumeTimeline\n\t\t(mouseenter)=\"handleVolumeHover(true)\"\n\t\t(mouseleave)=\"handleVolumeHover(false)\"\n\t>\n\t\t<!-- Volume Progress Bar Background -->\n\t\t<div class=\"bg-primary absolute h-full rounded-full\" [style.width]=\"audioService.volume() * 100 + '%'\"></div>\n\n\t\t<!-- Draggable Volume Circle -->\n\t\t<div\n\t\t\t[style.left]=\"'calc(' + audioService.volume() * 100 + '% - ' + (audioService.volume() === 1 ? '6px' : '3px') + ')'\"\n\t\t\t[class]=\"volumeTimelineCircleClass()\"\n\t\t\t#volumeCircle\n\t\t\t(mousedown)=\"startVolumeDrag($event)\"\n\t\t\t(touchstart)=\"startVolumeDrag($event)\"\n\t\t></div>\n\t</div>\n</div>\n" }] }], propDecorators: { onVolumeDrag: [{ type: HostListener, args: ['document:mousemove', ['$event']] }, { type: HostListener, args: ['document:touchmove', ['$event']] }], stopVolumeDrag: [{ type: HostListener, args: ['document:mouseup'] }, { type: HostListener, args: ['document:touchend'] }] } }); class AudioPlayerComponent { audioPlayerService = inject(AudioPlayerService); variant = input('default'); shadow = input(false); opened = model(false); tracks = input([]); showTrackList = input(false); currentTrackIndex = model(-1); currentTrack = computed(() => { const tracks = this.tracks(); return this.currentTrackIndex() >= 0 && tracks ? tracks[this.currentTrackIndex()] : undefined; }); iconSkipPrevious = matSkipPrevious; iconSkipNext = matSkipNext; iconPlay = matPlayArrow; iconPause = matPause; iconMusicNote = heroMusicalNoteMini; offScreen = signal(false); playerClass = computed(() => cn('grid w-full grid-cols-[minmax(0,1fr)_minmax(0,2fr)_minmax(0,1fr)] items-center gap-4 relative translate-y-0', { 'shadow-main': this.shadow(), }, { 'fixed bottom-0 left-0 bg-base-200 p-4 z-50 border-t border-neutral transform transition-transform duration-300': this.variant() === 'fixed', }, { 'animate-fade-in-up animate-steps-modern animate-duration-300': this.opened() && this.variant() === 'fixed', }, { 'animate-fade-out-down animate-steps-modern animate-duration-300': !this.opened() && this.variant() === 'fixed', }, { 'invisible pointer-events-none -z-10 fixed bottom-5': this.offScreen() && this.variant() === 'fixed', })); constructor() { // Sync tracks with service effect(() => { this.audioPlayerService.setTracks(this.tracks()); }); // One-way sync from service to component effect(() => { this.currentTrackIndex.set(this.audioPlayerService.currentTrackIndex()); }); } entering = signal(false); leaving = signal(false); handleAnimationEnd(event) { if (this.variant() !== 'fixed') return; if (event.animationName === 'fade-out-down' && !this.opened()) { this.offScreen.set(true); this.leaving.set(false); } if (event.animationName === 'fade-in-up' && this.opened()) { this.entering.set(false); } } handleAnimationStart(event) { if (this.variant() !== 'fixed') return; if (event.animationName === 'fade-in-up' && this.opened()) { this.offScreen.set(false); this.entering.set(true); } if (event.animationName === 'fade-out-down' && !this.opened()) { this.leaving.set(true); } } // Simplify methods to call service async playTrack(index) { await this.audioPlayerService.playTrack(index); } async togglePlay() { await this.audioPlayerService.togglePlay(); } async previous() { await this.audioPlayerService.previous(); } async next() { await this.audioPlayerService.next(); } handleOpen() { this.opened.set(true); } handleClose() { this.opened.set(false); } ngOnDestroy() { this.audioPlayerService.cleanup(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: AudioPlayerComponent, isStandalone: true, selector: "st-audio-player", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, opened: { classPropertyName: "opened", publicName: "opened", isSignal: true, isRequired: false, transformFunction: null }, tracks: { classPropertyName: "tracks", publicName: "tracks", isSignal: true, isRequired: false, transformFunction: null }, showTrackList: { classPropertyName: "showTrackList", publicName: "showTrackList", isSignal: true, isRequired: false, transformFunction: null }, currentTrackIndex: { classPropertyName: "currentTrackIndex", publicName: "currentTrackIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "openedChange", currentTrackIndex: "currentTrackIndexChange" }, ngImport: i0, template: "<div class=\"flex w-full flex-col gap-4\">\n\t<!-- Track List -->\n\t@if (showTrackList()) {\n\t\t<div class=\"rounded-box border-neutral shadow-main mb-4 border\">\n\t\t\t<div class=\"p-4\">\n\t\t\t\t<h4 typography tyVariant=\"h4\">{{ 'sdk.audioPlayer.trackListTitle' | translate }}</h4>\n\t\t\t\t<div class=\"max-h-[300px] space-y-2 overflow-y-auto\">\n\t\t\t\t\t@for (track of tracks(); track $index; let i = $index) {\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"rounded-field hover:bg-base-300 flex cursor-pointer items-center gap-2 p-2\"\n\t\t\t\t\t\t\t[class.bg-base-300]=\"audioPlayerService.currentTrackIndex() === i\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<st-button-play [trackIndex]=\"i\" (click)=\"handleOpen()\"> </st-button-play>\n\t\t\t\t\t\t\t<div class=\"flex-1\">\n\t\t\t\t\t\t\t\t<div typography tyVariant=\"body\">{{ track.title }}</div>\n\t\t\t\t\t\t\t\t<div typography tyVariant=\"body-sm\">{{ track.description }}</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t}\n\n\t<div [class]=\"playerClass()\" (animationend)=\"handleAnimationEnd($event)\" (animationstart)=\"handleAnimationStart($event)\">\n\t\t<!-- close button -->\n\t\t@if (variant() === 'fixed') {\n\t\t\t<div\n\t\t\t\tclass=\"group/close btn btn-ghost btn-square btn-primary btn-xs absolute top-1 right-1 grid place-content-center gap-0 hover:cursor-pointer\"\n\t\t\t\ttabindex=\"0\"\n\t\t\t\t(click)=\"handleClose()\"\n\t\t\t\t(keydown.enter)=\"handleClose()\"\n\t\t\t\taria-label=\"Close player\"\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"bg-primary group-hover/close:bg-primary-content group-active/close:bg-primary-content group-focus-within/close:bg-primary-content relative h-0.5 w-4 translate-y-0.5 rounded-full transition-transform duration-300 ease-in-out group-focus-within/close:rotate-45 group-hover/close:rotate-45 group-active/close:rotate-45\"\n\t\t\t\t></div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"bg-primary group-hover/close:bg-primary-content group-active/close:bg-primary-content group-focus-within/close:bg-primary-content h-0.5 w-4 rounded-full transition-transform duration-300 ease-in-out group-focus-within/close:-rotate-45 group-hover/close:-rotate-45 group-active/close:-rotate-45\"\n\t\t\t\t></div>\n\t\t\t</div>\n\t\t}\n\n\t\t<!-- track info -->\n\t\t<div class=\"min-w-0\">\n\t\t\t<!-- Added min-w-0 to prevent overflow -->\n\t\t\t<st-track-info [currentTrack]=\"currentTrack()\"></st-track-info>\n\t\t</div>\n\n\t\t<div class=\"min-w-0\">\n\t\t\t<!-- Center section -->\n\t\t\t<!-- Your controls content -->\n\t\t\t<div class=\"grid items-center\">\n\t\t\t\t<!-- Controls -->\n\t\t\t\t<div class=\"flex items-center justify-center gap-2\">\n\t\t\t\t\t<st-button (click)=\"previous()\" [ghost]=\"true\" [circle]=\"true\" size=\"sm\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconSkipPrevious\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\n\t\t\t\t\t<st-button (click)=\"togglePlay()\" [circle]=\"true\" size=\"sm\" [shadow]=\"true\">\n\t\t\t\t\t\t<st-icon [icon]=\"audioPlayerService.isPlaying() ? iconPause : iconPlay\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\n\t\t\t\t\t<st-button (click)=\"next()\" [ghost]=\"true\" [circle]=\"true\" size=\"sm\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconSkipNext\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Progress Bar -->\n\t\t\t\t<st-timeline></st-timeline>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Volume Control -->\n\t\t<div class=\"min-w-0 justify-self-end\">\n\t\t\t<!-- Right-aligned -->\n\t\t\t<st-volume-control></st-volume-control>\n\t\t</div>\n\t</div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "component", type: VolumeControlComponent, selector: "st-volume-control" }, { kind: "component", type: TrackInfoComponent, selector: "st-track-info", inputs: ["currentTrack"] }, { kind: "component", type: AudioTimelineComponent, selector: "st-timeline" }, { kind: "component", type: ButtonPlayComponent, selector: "st-button-play", inputs: ["variant", "ghost", "outline", "circle", "square", "glass", "size", "shadow", "asButton", "trackIndex"], outputs: ["togglePlay"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: AudioPlayerComponent, decorators: [{ type: Component, args: [{ selector: 'st-audio-player', imports: [ CommonModule, ButtonComponent, TypographyDirective, VolumeControlComponent, TrackInfoComponent, AudioTimelineComponent, ButtonPlayComponent, TranslatePipe, IconComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex w-full flex-col gap-4\">\n\t<!-- Track List -->\n\t@if (showTrackList()) {\n\t\t<div class=\"rounded-box border-neutral shadow-main mb-4 border\">\n\t\t\t<div class=\"p-4\">\n\t\t\t\t<h4 typography tyVariant=\"h4\">{{ 'sdk.audioPlayer.trackListTitle' | translate }}</h4>\n\t\t\t\t<div class=\"max-h-[300px] space-y-2 overflow-y-auto\">\n\t\t\t\t\t@for (track of tracks(); track $index; let i = $index) {\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"rounded-field hover:bg-base-300 flex cursor-pointer items-center gap-2 p-2\"\n\t\t\t\t\t\t\t[class.bg-base-300]=\"audioPlayerService.currentTrackIndex() === i\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<st-button-play [trackIndex]=\"i\" (click)=\"handleOpen()\"> </st-button-play>\n\t\t\t\t\t\t\t<div class=\"flex-1\">\n\t\t\t\t\t\t\t\t<div typography tyVariant=\"body\">{{ track.title }}</div>\n\t\t\t\t\t\t\t\t<div typography tyVariant=\"body-sm\">{{ track.description }}</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t}\n\n\t<div [class]=\"playerClass()\" (animationend)=\"handleAnimationEnd($event)\" (animationstart)=\"handleAnimationStart($event)\">\n\t\t<!-- close button -->\n\t\t@if (variant() === 'fixed') {\n\t\t\t<div\n\t\t\t\tclass=\"group/close btn btn-ghost btn-square btn-primary btn-xs absolute top-1 right-1 grid place-content-center gap-0 hover:cursor-pointer\"\n\t\t\t\ttabindex=\"0\"\n\t\t\t\t(click)=\"handleClose()\"\n\t\t\t\t(keydown.enter)=\"handleClose()\"\n\t\t\t\taria-label=\"Close player\"\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"bg-primary group-hover/close:bg-primary-content group-active/close:bg-primary-content group-focus-within/close:bg-primary-content relative h-0.5 w-4 translate-y-0.5 rounded-full transition-transform duration-300 ease-in-out group-focus-within/close:rotate-45 group-hover/close:rotate-45 group-active/close:rotate-45\"\n\t\t\t\t></div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"bg-primary group-hover/close:bg-primary-content group-active/close:bg-primary-content group-focus-within/close:bg-primary-content h-0.5 w-4 rounded-full transition-transform duration-300 ease-in-out group-focus-within/close:-rotate-45 group-hover/close:-rotate-45 group-active/close:-rotate-45\"\n\t\t\t\t></div>\n\t\t\t</div>\n\t\t}\n\n\t\t<!-- track info -->\n\t\t<div class=\"min-w-0\">\n\t\t\t<!-- Added min-w-0 to prevent overflow -->\n\t\t\t<st-track-info [currentTrack]=\"currentTrack()\"></st-track-info>\n\t\t</div>\n\n\t\t<div class=\"min-w-0\">\n\t\t\t<!-- Center section -->\n\t\t\t<!-- Your controls content -->\n\t\t\t<div class=\"grid items-center\">\n\t\t\t\t<!-- Controls -->\n\t\t\t\t<div class=\"flex items-center justify-center gap-2\">\n\t\t\t\t\t<st-button (click)=\"previous()\" [ghost]=\"true\" [circle]=\"true\" size=\"sm\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconSkipPrevious\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\n\t\t\t\t\t<st-button (click)=\"togglePlay()\" [circle]=\"true\" size=\"sm\" [shadow]=\"true\">\n\t\t\t\t\t\t<st-icon [icon]=\"audioPlayerService.isPlaying() ? iconPause : iconPlay\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\n\t\t\t\t\t<st-button (click)=\"next()\" [ghost]=\"true\" [circle]=\"true\" size=\"sm\">\n\t\t\t\t\t\t<st-icon [icon]=\"iconSkipNext\" size=\"sm\"></st-icon>\n\t\t\t\t\t</st-button>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Progress Bar -->\n\t\t\t\t<st-timeline></st-timeline>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Volume Control -->\n\t\t<div class=\"min-w-0 justify-self-end\">\n\t\t\t<!-- Right-aligned -->\n\t\t\t<st-volume-control></st-volume-control>\n\t\t</div>\n\t</div>\n</div>\n" }] }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { AudioPlayerComponent, AudioPlayerService, ButtonPlayComponent }; //# sourceMappingURL=sixbell-telco-sdk-components-audio-player.mjs.map