mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
feat(radio): add audio player for radio playback
This commit is contained in:
1 parent
7791686093
commit
5e9e53a02a
3 files changed
+248
No files matched your search
@@ -0,0 +1,112 @@
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#audio-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-logo {
|
||||
border-radius: 14px;
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-panel {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
height: 100px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.playing {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: .3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding: .5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.playing__bar {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
animation: up-and-down 1.3s ease infinite alternate;
|
||||
}
|
||||
|
||||
.playing__bar__stopped {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.playing__bar1 {
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.playing__bar2 {
|
||||
height: 30%;
|
||||
animation-delay: -2.2s;
|
||||
}
|
||||
|
||||
.playing__bar3 {
|
||||
height: 75%;
|
||||
animation-delay: -3.7s;
|
||||
}
|
||||
|
||||
@keyframes up-and-down {
|
||||
10% {
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
30% {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
60% {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
80% {
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button-large {
|
||||
transform: scale(2);
|
||||
margin: 0 20px;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { setAdjacentChannelAsActive } from '../../../state/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-audio-player',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div id="audio-player">
|
||||
<div class="radio-logo">
|
||||
<img [src]="icon" alt="radio icon" />
|
||||
</div>
|
||||
<audio preload="metadata" autoplay #audio>
|
||||
<source [src]="url" />
|
||||
</audio>
|
||||
<div class="controls">
|
||||
<button mat-icon-button (click)="switchChannel('previous')">
|
||||
<mat-icon>skip_previous</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="play()"
|
||||
*ngIf="playState === 'paused'"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button-large"
|
||||
mat-icon-button
|
||||
(click)="stop()"
|
||||
*ngIf="playState === 'play'"
|
||||
>
|
||||
<mat-icon>pause</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="switchChannel('next')">
|
||||
<mat-icon>skip_next</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="volume-panel">
|
||||
<div class="playing" *ngIf="playState === 'play'">
|
||||
<span class="playing__bar playing__bar1"></span>
|
||||
<span class="playing__bar playing__bar2"></span>
|
||||
<span class="playing__bar playing__bar3"></span>
|
||||
</div>
|
||||
<div class="playing" *ngIf="playState === 'paused'">
|
||||
<span class="playing__bar__stopped playing__bar1"></span>
|
||||
<span class="playing__bar__stopped playing__bar2"></span>
|
||||
<span class="playing__bar__stopped playing__bar3"></span>
|
||||
</div>
|
||||
<mat-slider min="0" max="1" step="0.1" color="accent">
|
||||
<input matSliderThumb [(ngModel)]="audio.volume" />
|
||||
</mat-slider>
|
||||
<button mat-icon-button (click)="mute()">
|
||||
<mat-icon *ngIf="audio.volume > 0 && !audio.muted"
|
||||
>volume_up</mat-icon
|
||||
>
|
||||
<mat-icon *ngIf="audio.volume === 0 || audio.muted"
|
||||
>volume_off</mat-icon
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./audio-player.component.scss'],
|
||||
imports: [
|
||||
MatSliderModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
NgClass,
|
||||
FormsModule,
|
||||
NgOptimizedImage,
|
||||
],
|
||||
})
|
||||
export class AudioPlayerComponent implements OnChanges {
|
||||
@Input() icon: string;
|
||||
@Input({ required: true }) url: string;
|
||||
|
||||
playState: 'play' | 'paused' = 'paused';
|
||||
|
||||
fallbackVolume: number;
|
||||
|
||||
@ViewChild('audio', { static: true }) audio!: ElementRef<HTMLAudioElement>;
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.audio.nativeElement.src = changes.url.currentValue;
|
||||
this.audio.nativeElement.load();
|
||||
this.play();
|
||||
}
|
||||
|
||||
play() {
|
||||
const playPromise = this.audio.nativeElement.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
this.playState = 'play';
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.audio.nativeElement.pause();
|
||||
this.playState = 'paused';
|
||||
}
|
||||
|
||||
mute() {
|
||||
this.audio.nativeElement.muted = !this.audio.nativeElement.muted;
|
||||
if (this.audio.nativeElement.muted) {
|
||||
this.fallbackVolume = this.audio.nativeElement.volume;
|
||||
this.audio.nativeElement.volume = 0;
|
||||
} else this.audio.nativeElement.volume = this.fallbackVolume;
|
||||
}
|
||||
|
||||
switchChannel(direction: 'next' | 'previous') {
|
||||
console.log(direction);
|
||||
this.store.dispatch(setAdjacentChannelAsActive({ direction }));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AudioPlayerComponent } from './components/audio-player/audio-player.component';
|
||||
import { ChannelListContainerComponent } from './components/channel-list-container/channel-list-container.component';
|
||||
import { ChannelListItemComponent } from './components/channel-list-container/channel-list-item/channel-list-item.component';
|
||||
import { EpgItemDescriptionComponent } from './components/epg-list/epg-item-description/epg-item-description.component';
|
||||
@@ -20,6 +21,7 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AudioPlayerComponent,
|
||||
CommonModule,
|
||||
HtmlVideoPlayerComponent,
|
||||
OverlayModule,
|
||||
|
||||
Reference in new issue
Block a user