@libs-ui/components-audio 0.2.190 → 0.2.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -57
- package/audio.component.d.ts +16 -5
- package/demo/audio-demo.component.d.ts +112 -0
- package/esm2022/audio.component.mjs +74 -15
- package/esm2022/demo/audio-demo.component.mjs +473 -0
- package/esm2022/index.mjs +3 -2
- package/esm2022/interfaces/function-control-event.interface.mjs +2 -0
- package/fesm2022/libs-ui-components-audio.mjs +528 -124
- package/fesm2022/libs-ui-components-audio.mjs.map +1 -1
- package/index.d.ts +2 -1
- package/interfaces/function-control-event.interface.d.ts +46 -0
- package/package.json +2 -2
- package/demo.component.d.ts +0 -20
- package/esm2022/demo.component.mjs +0 -128
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, input, viewChild, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { signal, input, viewChild, output, effect, ChangeDetectionStrategy, Component, computed, ViewChild } from '@angular/core';
|
|
3
3
|
import { LibsUiComponentsInputsRangeSliderComponent } from '@libs-ui/components-inputs-range-slider';
|
|
4
4
|
import { Subject, merge, tap, takeUntil, fromEvent } from 'rxjs';
|
|
5
|
-
import { CommonModule } from '@angular/common';
|
|
6
5
|
|
|
7
6
|
class LibsUiComponentsAudioComponent {
|
|
8
7
|
// #region PROPERTY
|
|
@@ -22,8 +21,48 @@ class LibsUiComponentsAudioComponent {
|
|
|
22
21
|
/* VIEW CHILD */
|
|
23
22
|
audioRef = viewChild.required('audioRef');
|
|
24
23
|
volumeControlRef = viewChild.required('volumeControlRef');
|
|
24
|
+
/* OUTPUTS */
|
|
25
|
+
outFunctionsControl = output();
|
|
26
|
+
outVolumeControl = output();
|
|
27
|
+
outTimeUpdate = output();
|
|
28
|
+
outEnded = output();
|
|
29
|
+
outMute = output();
|
|
30
|
+
outPlay = output();
|
|
31
|
+
constructor() {
|
|
32
|
+
// Watch for file audio changes
|
|
33
|
+
effect(() => {
|
|
34
|
+
if (this.fileAudio() && this.audioRef()) {
|
|
35
|
+
// Skip initial setup, only reload on changes
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
this.audioRef().nativeElement.load();
|
|
38
|
+
}, 0);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
effect(() => {
|
|
42
|
+
this.outVolumeControl.emit(this.volumeRatioValue());
|
|
43
|
+
});
|
|
44
|
+
effect(() => {
|
|
45
|
+
this.outTimeUpdate.emit({ currentTime: this.audioTimeCurrent(), duration: this.audioTimeDuration() });
|
|
46
|
+
});
|
|
47
|
+
effect(() => {
|
|
48
|
+
this.outMute.emit(this.isMute());
|
|
49
|
+
});
|
|
50
|
+
effect(() => {
|
|
51
|
+
this.outPlay.emit(this.isPlay());
|
|
52
|
+
});
|
|
53
|
+
}
|
|
25
54
|
ngAfterViewInit() {
|
|
26
55
|
merge(this.initObservable(this.volumeControlRef().nativeElement, 'mouseenter').pipe(tap(() => this.showFullControlVolume.set(true))), this.initObservable(this.volumeControlRef().nativeElement, 'mouseleave').pipe(tap(() => this.showFullControlVolume.set(false)))).pipe(takeUntil(this.onDestroy)).subscribe();
|
|
56
|
+
// Emit function control event after view is initialized
|
|
57
|
+
this.outFunctionsControl.emit({
|
|
58
|
+
playPause: (event) => this.handlerAudioPausePlay(event),
|
|
59
|
+
toggleMute: (event) => this.handlerAudioMuteMuted(event),
|
|
60
|
+
seekTo: this.handlerChangeAudio.bind(this),
|
|
61
|
+
setVolume: this.handlerChangeVolume.bind(this),
|
|
62
|
+
download: (event) => this.handlerDownload(event),
|
|
63
|
+
isPlaying: () => this.isPlay(),
|
|
64
|
+
isMuted: () => this.isMute()
|
|
65
|
+
});
|
|
27
66
|
}
|
|
28
67
|
/* FUNCTIONS */
|
|
29
68
|
initObservable(el, eventName) {
|
|
@@ -33,7 +72,9 @@ class LibsUiComponentsAudioComponent {
|
|
|
33
72
|
this.isSliderAudioPress.set(true);
|
|
34
73
|
}
|
|
35
74
|
async handlerAudioMuteMuted(event) {
|
|
36
|
-
event
|
|
75
|
+
if (event) {
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
}
|
|
37
78
|
if (this.audioRef().nativeElement.muted === true) {
|
|
38
79
|
this.audioRef().nativeElement.muted = false;
|
|
39
80
|
this.isMute.set(false);
|
|
@@ -45,7 +86,9 @@ class LibsUiComponentsAudioComponent {
|
|
|
45
86
|
this.audioRef().nativeElement.muted = true;
|
|
46
87
|
}
|
|
47
88
|
async handlerAudioPausePlay(event) {
|
|
48
|
-
event
|
|
89
|
+
if (event) {
|
|
90
|
+
event.stopPropagation();
|
|
91
|
+
}
|
|
49
92
|
const audioElement = this.audioRef().nativeElement;
|
|
50
93
|
if (!audioElement.paused) {
|
|
51
94
|
audioElement.pause();
|
|
@@ -61,31 +104,40 @@ class LibsUiComponentsAudioComponent {
|
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
async handlerLoadedData(event) {
|
|
64
|
-
event
|
|
107
|
+
if (event) {
|
|
108
|
+
event.stopPropagation();
|
|
109
|
+
}
|
|
65
110
|
if (this.audioRef().nativeElement) {
|
|
66
111
|
this.audioTimeDuration.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.duration)));
|
|
67
|
-
this.audioTimeCurrent.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.currentTime)));
|
|
112
|
+
this.audioTimeCurrent.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.currentTime || 0)));
|
|
68
113
|
this.isDisable.set(false);
|
|
114
|
+
this.isPlay.set(false);
|
|
115
|
+
this.audioRatioValue.set(0);
|
|
116
|
+
this.audioRef().nativeElement.pause();
|
|
69
117
|
}
|
|
70
118
|
}
|
|
71
119
|
async handlerTimeUpdate(event) {
|
|
72
|
-
event
|
|
120
|
+
if (event) {
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
}
|
|
123
|
+
this.isDisable.set(!(this.audioRef().nativeElement.duration || 0));
|
|
73
124
|
if (!this.audioRef().nativeElement) {
|
|
74
125
|
return;
|
|
75
126
|
}
|
|
76
127
|
this.audioTimeDuration.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.duration)));
|
|
77
|
-
this.audioTimeCurrent.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.currentTime)));
|
|
128
|
+
this.audioTimeCurrent.set(await this.toHHMMSS(Math.floor(this.audioRef().nativeElement.currentTime || 0)));
|
|
78
129
|
if (this.isSliderAudioPress()) {
|
|
79
|
-
this.audioRef().nativeElement.currentTime = this.audioRatioValue() * Math.floor(this.audioRef().nativeElement.duration) / 100;
|
|
130
|
+
this.audioRef().nativeElement.currentTime = this.audioRatioValue() * Math.floor(this.audioRef().nativeElement.duration || 0) / 100;
|
|
80
131
|
return;
|
|
81
132
|
}
|
|
82
|
-
this.audioRatioValue.set(Math.floor((this.audioRef().nativeElement.currentTime / this.audioRef().nativeElement.duration) * 100));
|
|
133
|
+
this.audioRatioValue.set(Math.floor(((this.audioRef().nativeElement.currentTime || 0) / (this.audioRef().nativeElement.duration || 1)) * 100));
|
|
83
134
|
}
|
|
84
135
|
async toHHMMSS(time) {
|
|
85
136
|
const hours = Math.floor(time / 3600);
|
|
86
137
|
const minutes = Math.floor((time - (hours * 3600)) / 60);
|
|
87
138
|
const seconds = time - (hours * 3600) - (minutes * 60);
|
|
88
139
|
const getLabel = ((val) => {
|
|
140
|
+
val = val || 0;
|
|
89
141
|
return `${val < 10 ? '0' : ''}${val}`;
|
|
90
142
|
});
|
|
91
143
|
return `${getLabel(hours)}:${getLabel(minutes)}:${getLabel(seconds)}`;
|
|
@@ -94,12 +146,13 @@ class LibsUiComponentsAudioComponent {
|
|
|
94
146
|
if (value === this.audioRatioValue()) {
|
|
95
147
|
return;
|
|
96
148
|
}
|
|
97
|
-
this.audioRef().nativeElement.currentTime = value * this.audioRef().nativeElement.duration / 100;
|
|
149
|
+
this.audioRef().nativeElement.currentTime = (value || 0) * (this.audioRef().nativeElement.duration || 0) / 100;
|
|
98
150
|
this.audioRatioValue.set(value);
|
|
99
151
|
this.isSliderAudioPress.set(false);
|
|
100
152
|
}
|
|
101
153
|
async handlerChangeVolume(value) {
|
|
102
154
|
this.audioRef().nativeElement.volume = value / 100;
|
|
155
|
+
this.volumeRatioValue.set(value);
|
|
103
156
|
if (this.audioRef().nativeElement.volume) {
|
|
104
157
|
this.audioRef().nativeElement.muted = false;
|
|
105
158
|
this.isMute.set(false);
|
|
@@ -109,14 +162,19 @@ class LibsUiComponentsAudioComponent {
|
|
|
109
162
|
this.isMute.set(true);
|
|
110
163
|
}
|
|
111
164
|
async handlerEnded(event) {
|
|
112
|
-
event
|
|
165
|
+
if (event) {
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
}
|
|
113
168
|
this.isPlay.set(false);
|
|
169
|
+
this.outEnded.emit();
|
|
114
170
|
}
|
|
115
171
|
async handlerDownload(e) {
|
|
116
172
|
if (!this.checkPermissionDownloadAudio() || !await this.checkPermissionDownloadAudio()()) {
|
|
117
173
|
return;
|
|
118
174
|
}
|
|
119
|
-
e
|
|
175
|
+
if (e) {
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
}
|
|
120
178
|
if (!this.fileAudio()) {
|
|
121
179
|
return;
|
|
122
180
|
}
|
|
@@ -127,138 +185,484 @@ class LibsUiComponentsAudioComponent {
|
|
|
127
185
|
this.onDestroy.complete();
|
|
128
186
|
}
|
|
129
187
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsAudioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
130
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.13", type: LibsUiComponentsAudioComponent, isStandalone: true, selector: "libs_ui-components-audio", inputs: { fileAudio: { classPropertyName: "fileAudio", publicName: "fileAudio", isSignal: true, isRequired: true, transformFunction: null }, checkPermissionDownloadAudio: { classPropertyName: "checkPermissionDownloadAudio", publicName: "checkPermissionDownloadAudio", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "audioRef", first: true, predicate: ["audioRef"], descendants: true, isSignal: true }, { propertyName: "volumeControlRef", first: true, predicate: ["volumeControlRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<audio controls\n #audioRef\n class=\"hidden\"\n (timeupdate)=\"handlerTimeUpdate($event)\"\n (loadeddata)=\"handlerLoadedData($event)\"\n (ended)=\"handlerEnded($event)\">\n <source [src]=\"fileAudio()\"\n type=\"audio/mpeg\">\n</audio>\n<div [class.libs-ui-disable]=\"isDisable()\"\n [class.pointer-events-none]=\"isDisable()\">\n <div class=\"flex justify-between items-center\">\n <div class=\"w-[70%] flex p-0 items-center \">\n <div class=\"flex mr-[16px] cursor-pointer\"\n (click)=\"handlerAudioPausePlay($event)\">\n <i class=\"text-[16px]\"\n [class.libs-ui-icon-play-solid]=\"!isPlay()\"\n [class.libs-ui-icon-pause-solid]=\"isPlay()\">\n </i>\n </div>\n <div class=\"libs-ui-font-h5r mr-[16px]\">{{ audioTimeCurrent() }} /{{ audioTimeDuration() }}</div>\n </div>\n <div class=\"w-[30%] flex p-0 items-center justify-end\">\n <div #volumeControlRef\n class=\"flex py-[3px] items-center rounded-[12px] h-[28px]\"\n [class.bg-[#e6e7ea]]='showFullControlVolume()'\n [class.px-[12px]]='showFullControlVolume()'>\n <i class=\"text-[16px] cursor-pointer\"\n [class.libs-ui-icon-speaker-on-solid]=\"!isMute()\"\n [class.libs-ui-icon-speaker-off-solid]=\"isMute()\"\n (click)=\"handlerAudioMuteMuted($event)\">\n </i>\n <libs_ui-components-inputs-range_slider [class.hidden]=\"!showFullControlVolume()\"\n [mode]=\"'audio'\"\n classInclude=\"flex items-center !w-[54px] cursor-pointer ml-[8px]\"\n [value]=\"volumeRatioValue()\"\n (outChange)=\"handlerChangeVolume($event)\" />\n </div>\n\n <i class=\"libs-ui-icon-download-solid ml-[16px] cursor-pointer\"\n (click)=\"handlerDownload($event)\">\n </i>\n </div>\n\n </div>\n <div class=\"h-[24px]\">\n <libs_ui-components-inputs-range_slider [mode]=\"'audio'\"\n [value]=\"audioRatioValue()\"\n [disable]='isDisable()'\n (outChange)=\"handlerChangeAudio($event)\" />\n </div>\n</div>\n", dependencies: [{ kind: "component", type: LibsUiComponentsInputsRangeSliderComponent, selector: "libs_ui-components-inputs-range_slider", inputs: ["mode", "min", "max", "value", "classInclude", "disable", "unit", "step", "hideProgressingValue", "formatNumber"], outputs: ["valueChange", "outChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
188
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.13", type: LibsUiComponentsAudioComponent, isStandalone: true, selector: "libs_ui-components-audio", inputs: { fileAudio: { classPropertyName: "fileAudio", publicName: "fileAudio", isSignal: true, isRequired: true, transformFunction: null }, checkPermissionDownloadAudio: { classPropertyName: "checkPermissionDownloadAudio", publicName: "checkPermissionDownloadAudio", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { outFunctionsControl: "outFunctionsControl", outVolumeControl: "outVolumeControl", outTimeUpdate: "outTimeUpdate", outEnded: "outEnded", outMute: "outMute", outPlay: "outPlay" }, viewQueries: [{ propertyName: "audioRef", first: true, predicate: ["audioRef"], descendants: true, isSignal: true }, { propertyName: "volumeControlRef", first: true, predicate: ["volumeControlRef"], descendants: true, isSignal: true }], ngImport: i0, template: "<audio controls\n #audioRef\n class=\"hidden\"\n (timeupdate)=\"handlerTimeUpdate($event)\"\n (loadeddata)=\"handlerLoadedData($event)\"\n (ended)=\"handlerEnded($event)\">\n <source [src]=\"fileAudio()\"\n type=\"audio/mpeg\">\n</audio>\n<div [class.libs-ui-disable]=\"isDisable()\"\n [class.pointer-events-none]=\"isDisable()\">\n <div class=\"flex justify-between items-center\">\n <div class=\"w-[70%] flex p-0 items-center \">\n <div class=\"flex mr-[16px] cursor-pointer\"\n (click)=\"handlerAudioPausePlay($event)\">\n <i class=\"text-[16px]\"\n [class.libs-ui-icon-play-solid]=\"!isPlay()\"\n [class.libs-ui-icon-pause-solid]=\"isPlay()\">\n </i>\n </div>\n <div class=\"libs-ui-font-h5r mr-[16px]\">{{ audioTimeCurrent() }} /{{ audioTimeDuration() }}</div>\n </div>\n <div class=\"w-[30%] flex p-0 items-center justify-end\">\n <div #volumeControlRef\n class=\"flex py-[3px] items-center rounded-[12px] h-[28px]\"\n [class.bg-[#e6e7ea]]='showFullControlVolume()'\n [class.px-[12px]]='showFullControlVolume()'>\n <i class=\"text-[16px] cursor-pointer\"\n [class.libs-ui-icon-speaker-on-solid]=\"!isMute()\"\n [class.libs-ui-icon-speaker-off-solid]=\"isMute()\"\n (click)=\"handlerAudioMuteMuted($event)\">\n </i>\n <libs_ui-components-inputs-range_slider [class.hidden]=\"!showFullControlVolume()\"\n [mode]=\"'audio'\"\n classInclude=\"flex items-center !w-[54px] cursor-pointer ml-[8px]\"\n [value]=\"volumeRatioValue()\"\n (outChange)=\"handlerChangeVolume($event)\" />\n </div>\n\n <i class=\"libs-ui-icon-download-solid ml-[16px] cursor-pointer\"\n (click)=\"handlerDownload($event)\">\n </i>\n </div>\n\n </div>\n <div class=\"h-[24px]\">\n <libs_ui-components-inputs-range_slider [mode]=\"'audio'\"\n [value]=\"audioRatioValue()\"\n [disable]='isDisable()'\n (outChange)=\"handlerChangeAudio($event)\" />\n </div>\n</div>\n", dependencies: [{ kind: "component", type: LibsUiComponentsInputsRangeSliderComponent, selector: "libs_ui-components-inputs-range_slider", inputs: ["mode", "min", "max", "value", "classInclude", "disable", "unit", "step", "hideProgressingValue", "formatNumber"], outputs: ["valueChange", "outChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
131
189
|
}
|
|
132
190
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsAudioComponent, decorators: [{
|
|
133
191
|
type: Component,
|
|
134
192
|
args: [{ selector: 'libs_ui-components-audio', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
135
193
|
LibsUiComponentsInputsRangeSliderComponent
|
|
136
194
|
], template: "<audio controls\n #audioRef\n class=\"hidden\"\n (timeupdate)=\"handlerTimeUpdate($event)\"\n (loadeddata)=\"handlerLoadedData($event)\"\n (ended)=\"handlerEnded($event)\">\n <source [src]=\"fileAudio()\"\n type=\"audio/mpeg\">\n</audio>\n<div [class.libs-ui-disable]=\"isDisable()\"\n [class.pointer-events-none]=\"isDisable()\">\n <div class=\"flex justify-between items-center\">\n <div class=\"w-[70%] flex p-0 items-center \">\n <div class=\"flex mr-[16px] cursor-pointer\"\n (click)=\"handlerAudioPausePlay($event)\">\n <i class=\"text-[16px]\"\n [class.libs-ui-icon-play-solid]=\"!isPlay()\"\n [class.libs-ui-icon-pause-solid]=\"isPlay()\">\n </i>\n </div>\n <div class=\"libs-ui-font-h5r mr-[16px]\">{{ audioTimeCurrent() }} /{{ audioTimeDuration() }}</div>\n </div>\n <div class=\"w-[30%] flex p-0 items-center justify-end\">\n <div #volumeControlRef\n class=\"flex py-[3px] items-center rounded-[12px] h-[28px]\"\n [class.bg-[#e6e7ea]]='showFullControlVolume()'\n [class.px-[12px]]='showFullControlVolume()'>\n <i class=\"text-[16px] cursor-pointer\"\n [class.libs-ui-icon-speaker-on-solid]=\"!isMute()\"\n [class.libs-ui-icon-speaker-off-solid]=\"isMute()\"\n (click)=\"handlerAudioMuteMuted($event)\">\n </i>\n <libs_ui-components-inputs-range_slider [class.hidden]=\"!showFullControlVolume()\"\n [mode]=\"'audio'\"\n classInclude=\"flex items-center !w-[54px] cursor-pointer ml-[8px]\"\n [value]=\"volumeRatioValue()\"\n (outChange)=\"handlerChangeVolume($event)\" />\n </div>\n\n <i class=\"libs-ui-icon-download-solid ml-[16px] cursor-pointer\"\n (click)=\"handlerDownload($event)\">\n </i>\n </div>\n\n </div>\n <div class=\"h-[24px]\">\n <libs_ui-components-inputs-range_slider [mode]=\"'audio'\"\n [value]=\"audioRatioValue()\"\n [disable]='isDisable()'\n (outChange)=\"handlerChangeAudio($event)\" />\n </div>\n</div>\n" }]
|
|
137
|
-
}] });
|
|
195
|
+
}], ctorParameters: () => [] });
|
|
138
196
|
|
|
139
|
-
/**
|
|
140
|
-
* Demo component hiển thị các ví dụ khác nhau của Audio component
|
|
141
|
-
*/
|
|
142
197
|
class LibsUiComponentsAudioDemoComponent {
|
|
198
|
+
audioPlayer;
|
|
199
|
+
// State using signals
|
|
200
|
+
isPlaying = signal(false);
|
|
201
|
+
isMuted = signal(false);
|
|
202
|
+
volume = signal(100);
|
|
203
|
+
currentTime = signal('00:00:00');
|
|
204
|
+
duration = signal('00:00:00');
|
|
205
|
+
progress = signal(0);
|
|
206
|
+
// Computed properties for template display
|
|
207
|
+
volumePercent = computed(() => Math.round(this.volume()));
|
|
208
|
+
// Selected audio sample
|
|
209
|
+
selectedAudio = signal(1);
|
|
210
|
+
currentAudioSource = signal('https://nhacchuong123.com/nhac-chuong/abcdefgh/nhac-chuong-nguoi-ay-dau-co-dang-akira-phan-nguyen-van-chung.mp3');
|
|
211
|
+
// Function Control variables
|
|
212
|
+
functionControls = null;
|
|
213
|
+
// Audio samples for demo
|
|
214
|
+
audioSamples = signal([
|
|
215
|
+
{
|
|
216
|
+
id: 1,
|
|
217
|
+
name: 'Bản nhạc mẫu 1',
|
|
218
|
+
src: 'https://nhacchuong123.com/nhac-chuong/abcdefgh/nhac-chuong-nguoi-ay-dau-co-dang-akira-phan-nguyen-van-chung.mp3',
|
|
219
|
+
duration: '01:30'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 2,
|
|
223
|
+
name: 'Bản nhạc mẫu 2',
|
|
224
|
+
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
|
225
|
+
duration: '02:15'
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 3,
|
|
229
|
+
name: 'Bản nhạc mẫu 3',
|
|
230
|
+
src: 'https://dl.dropboxusercontent.com/s/75jpngrgnavyu1f/The-Noisy-Freaks.mp3',
|
|
231
|
+
duration: '03:45'
|
|
232
|
+
}
|
|
233
|
+
]);
|
|
234
|
+
// API Documentation data
|
|
235
|
+
inputsDoc = signal([
|
|
236
|
+
{
|
|
237
|
+
name: 'fileAudio',
|
|
238
|
+
type: 'string',
|
|
239
|
+
default: 'Bắt buộc',
|
|
240
|
+
description: 'URL của file audio cần phát'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'checkPermissionDownloadAudio',
|
|
244
|
+
type: '() => Promise<boolean>',
|
|
245
|
+
default: 'Bắt buộc',
|
|
246
|
+
description: 'Function trả về promise với kết quả boolean cho biết nếu được phép download'
|
|
247
|
+
}
|
|
248
|
+
]);
|
|
249
|
+
// Output documentation
|
|
250
|
+
outputsDoc = signal([
|
|
251
|
+
{
|
|
252
|
+
name: 'outFunctionsControl',
|
|
253
|
+
type: 'IAudioFunctionControlEvent',
|
|
254
|
+
description: 'Emits các hàm điều khiển audio'
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'outVolumeControl',
|
|
258
|
+
type: 'number',
|
|
259
|
+
description: 'Emits giá trị âm lượng hiện tại (0-100)'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'outTimeUpdate',
|
|
263
|
+
type: '{ currentTime: string, duration: string }',
|
|
264
|
+
description: 'Emits thông tin thời gian hiện tại và tổng thời gian'
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'outEnded',
|
|
268
|
+
type: 'void',
|
|
269
|
+
description: 'Emits khi audio kết thúc phát'
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'outMute',
|
|
273
|
+
type: 'boolean',
|
|
274
|
+
description: 'Emits trạng thái tắt/bật tiếng'
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'outPlay',
|
|
278
|
+
type: 'boolean',
|
|
279
|
+
description: 'Emits trạng thái phát/tạm dừng'
|
|
280
|
+
}
|
|
281
|
+
]);
|
|
282
|
+
// Interface documentation
|
|
283
|
+
interfacesDoc = signal([
|
|
284
|
+
{
|
|
285
|
+
name: 'IAudioFunctionControlEvent',
|
|
286
|
+
description: 'Interface cho các chức năng điều khiển audio được cung cấp qua output event',
|
|
287
|
+
properties: [
|
|
288
|
+
{
|
|
289
|
+
name: 'playPause',
|
|
290
|
+
type: '(event?: Event) => void',
|
|
291
|
+
description: 'Bắt đầu hoặc tạm dừng phát audio'
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'toggleMute',
|
|
295
|
+
type: '(event?: Event) => void',
|
|
296
|
+
description: 'Bật hoặc tắt âm thanh'
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'setVolume',
|
|
300
|
+
type: '(value: number) => void',
|
|
301
|
+
description: 'Điều chỉnh âm lượng (giá trị từ 0 đến 100)'
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'seekTo',
|
|
305
|
+
type: '(value: number) => void',
|
|
306
|
+
description: 'Di chuyển đến vị trí cụ thể trong audio (giá trị từ 0 đến 100)'
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'download',
|
|
310
|
+
type: '(event?: Event) => void',
|
|
311
|
+
description: 'Tải xuống file audio'
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'isPlaying',
|
|
315
|
+
type: '() => boolean',
|
|
316
|
+
description: 'Kiểm tra trạng thái đang phát audio'
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'isMuted',
|
|
320
|
+
type: '() => boolean',
|
|
321
|
+
description: 'Kiểm tra trạng thái tắt tiếng'
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
]);
|
|
326
|
+
// Method documentation
|
|
327
|
+
methodsDoc = signal([
|
|
328
|
+
{
|
|
329
|
+
name: 'playPause',
|
|
330
|
+
params: 'event?: Event',
|
|
331
|
+
returnType: 'void',
|
|
332
|
+
description: 'Phát hoặc tạm dừng audio'
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'toggleMute',
|
|
336
|
+
params: 'event?: Event',
|
|
337
|
+
returnType: 'void',
|
|
338
|
+
description: 'Bật/tắt âm thanh'
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'seekTo',
|
|
342
|
+
params: 'value: number',
|
|
343
|
+
returnType: 'void',
|
|
344
|
+
description: 'Di chuyển đến vị trí cụ thể trong audio (giá trị từ 0-100)'
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'setVolume',
|
|
348
|
+
params: 'value: number',
|
|
349
|
+
returnType: 'void',
|
|
350
|
+
description: 'Điều chỉnh âm lượng (giá trị từ 0-100)'
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'download',
|
|
354
|
+
params: 'event?: Event',
|
|
355
|
+
returnType: 'void',
|
|
356
|
+
description: 'Tải xuống file audio'
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'isPlaying',
|
|
360
|
+
params: '',
|
|
361
|
+
returnType: 'boolean',
|
|
362
|
+
description: 'Kiểm tra nếu audio đang phát'
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'isMuted',
|
|
366
|
+
params: '',
|
|
367
|
+
returnType: 'boolean',
|
|
368
|
+
description: 'Kiểm tra nếu audio đang tắt tiếng'
|
|
369
|
+
}
|
|
370
|
+
]);
|
|
371
|
+
// Features data
|
|
372
|
+
features = signal([
|
|
373
|
+
{
|
|
374
|
+
id: 1,
|
|
375
|
+
icon: '▶️',
|
|
376
|
+
title: 'Điều khiển audio',
|
|
377
|
+
description: 'Điều khiển audio component qua các API'
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 2,
|
|
381
|
+
icon: '🔊',
|
|
382
|
+
title: 'Quản lý âm lượng',
|
|
383
|
+
description: 'Điều chỉnh âm lượng và tắt tiếng với thanh trượt trực quan'
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: 3,
|
|
387
|
+
icon: '⏱️',
|
|
388
|
+
title: 'Hiển thị thời gian',
|
|
389
|
+
description: 'Hiển thị thời gian hiện tại và tổng thời gian theo định dạng HH:MM:SS'
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 4,
|
|
393
|
+
icon: '📊',
|
|
394
|
+
title: 'Thanh tiến độ',
|
|
395
|
+
description: 'Thanh tiến độ có thể tương tác để tua nhanh hoặc tua lại'
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: 5,
|
|
399
|
+
icon: '📥',
|
|
400
|
+
title: 'Tải xuống',
|
|
401
|
+
description: 'Hỗ trợ tải xuống file âm thanh với kiểm soát quyền'
|
|
402
|
+
}
|
|
403
|
+
]);
|
|
404
|
+
// Code examples data
|
|
405
|
+
codeExamples = signal([
|
|
406
|
+
{
|
|
407
|
+
id: 1,
|
|
408
|
+
title: 'Cài đặt cơ bản',
|
|
409
|
+
code: `<libs_ui-components-audio
|
|
410
|
+
[fileAudio]="'path/to/audio.mp3'"
|
|
411
|
+
[checkPermissionDownloadAudio]="checkPermission">
|
|
412
|
+
</libs_ui-components-audio>`
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: 2,
|
|
416
|
+
title: 'Sử dụng Function Control',
|
|
417
|
+
code: `import { Component, ViewChild, signal } from '@angular/core';
|
|
418
|
+
import { LibsUiComponentsAudioComponent } from '@libs-ui/components-audio';
|
|
419
|
+
import { IAudioFunctionControlEvent } from '@libs-ui/components-audio';
|
|
420
|
+
|
|
421
|
+
@Component({
|
|
422
|
+
selector: 'app-my-component',
|
|
423
|
+
template: \`
|
|
424
|
+
<libs_ui-components-audio
|
|
425
|
+
#audioPlayer
|
|
426
|
+
[fileAudio]="audioSource()"
|
|
427
|
+
[checkPermissionDownloadAudio]="checkPermission"
|
|
428
|
+
(outFunctionsControl)="registerFunctions($event)">
|
|
429
|
+
</libs_ui-components-audio>
|
|
430
|
+
|
|
431
|
+
<button (click)="playAudio()">Phát/Tạm dừng</button>
|
|
432
|
+
\`
|
|
433
|
+
})
|
|
434
|
+
export class MyComponent {
|
|
435
|
+
@ViewChild('audioPlayer') audioPlayer!: LibsUiComponentsAudioComponent;
|
|
436
|
+
audioSource = signal<string>('path/to/audio.mp3');
|
|
437
|
+
functionControls: IAudioFunctionControlEvent | null = null;
|
|
438
|
+
|
|
439
|
+
registerFunctions(event: IAudioFunctionControlEvent) {
|
|
440
|
+
this.functionControls = event;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
playAudio() {
|
|
444
|
+
if (this.functionControls) {
|
|
445
|
+
this.functionControls.playPause();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}`
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 3,
|
|
452
|
+
title: 'Sử dụng Events để cập nhật UI',
|
|
453
|
+
code: `import { Component, signal } from '@angular/core';
|
|
454
|
+
import { LibsUiComponentsAudioComponent } from '@libs-ui/components-audio';
|
|
455
|
+
|
|
456
|
+
@Component({
|
|
457
|
+
selector: 'app-my-component',
|
|
458
|
+
template: \`
|
|
459
|
+
<libs_ui-components-audio
|
|
460
|
+
[fileAudio]="audioSource()"
|
|
461
|
+
[checkPermissionDownloadAudio]="checkPermission"
|
|
462
|
+
(outTimeUpdate)="handleTimeUpdate($event)"
|
|
463
|
+
(outVolumeControl)="handleVolumeChange($event)"
|
|
464
|
+
(outPlay)="handlePlayChange($event)"
|
|
465
|
+
(outMute)="handleMuteChange($event)"
|
|
466
|
+
(outEnded)="handleEnded()">
|
|
467
|
+
</libs_ui-components-audio>
|
|
468
|
+
|
|
469
|
+
<div class="audio-info">
|
|
470
|
+
<p>Trạng thái: {{ isPlaying() ? 'Đang phát' : 'Tạm dừng' }}</p>
|
|
471
|
+
<p>Thời gian hiện tại: {{ currentTime() }}</p>
|
|
472
|
+
<p>Tổng thời gian: {{ duration() }}</p>
|
|
473
|
+
<p>Âm lượng: {{ volumeLevel() }}%</p>
|
|
474
|
+
</div>
|
|
475
|
+
\`
|
|
476
|
+
})
|
|
477
|
+
export class MyComponent {
|
|
478
|
+
audioSource = signal<string>('path/to/audio.mp3');
|
|
479
|
+
isPlaying = signal<boolean>(false);
|
|
480
|
+
isMuted = signal<boolean>(false);
|
|
481
|
+
currentTime = signal<string>('00:00:00');
|
|
482
|
+
duration = signal<string>('00:00:00');
|
|
483
|
+
volumeLevel = signal<number>(100);
|
|
484
|
+
|
|
485
|
+
checkPermission = (): Promise<boolean> => {
|
|
486
|
+
return Promise.resolve(true);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
handleTimeUpdate(timeInfo: { currentTime: string, duration: string }) {
|
|
490
|
+
this.currentTime.set(timeInfo.currentTime);
|
|
491
|
+
this.duration.set(timeInfo.duration);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
handleVolumeChange(volume: number) {
|
|
495
|
+
this.volumeLevel.set(volume);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
handlePlayChange(isPlaying: boolean) {
|
|
499
|
+
this.isPlaying.set(isPlaying);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
handleMuteChange(isMuted: boolean) {
|
|
503
|
+
this.isMuted.set(isMuted);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
handleEnded() {
|
|
507
|
+
this.isPlaying.set(false);
|
|
508
|
+
}
|
|
509
|
+
}`
|
|
510
|
+
}
|
|
511
|
+
]);
|
|
512
|
+
// Control methods
|
|
513
|
+
playAudio() {
|
|
514
|
+
if (this.functionControls) {
|
|
515
|
+
this.functionControls.playPause();
|
|
516
|
+
this.isPlaying.set(this.functionControls.isPlaying());
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
toggleMute() {
|
|
520
|
+
if (this.functionControls) {
|
|
521
|
+
this.functionControls.toggleMute();
|
|
522
|
+
this.isMuted.set(this.functionControls.isMuted());
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
changeVolume(event) {
|
|
526
|
+
if (this.functionControls && event.target) {
|
|
527
|
+
const value = parseInt(event.target.value);
|
|
528
|
+
this.volume.set(value);
|
|
529
|
+
this.functionControls.setVolume(value);
|
|
530
|
+
this.isMuted.set(this.functionControls.isMuted());
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
downloadAudio() {
|
|
534
|
+
if (this.functionControls) {
|
|
535
|
+
this.functionControls.download();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
143
538
|
/**
|
|
144
|
-
*
|
|
539
|
+
* Format thời gian từ giây sang chuỗi HH:MM:SS
|
|
145
540
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
541
|
+
formatTime(timeInSeconds) {
|
|
542
|
+
if (isNaN(timeInSeconds))
|
|
543
|
+
return '00:00:00';
|
|
544
|
+
const hours = Math.floor(timeInSeconds / 3600);
|
|
545
|
+
const minutes = Math.floor((timeInSeconds - (hours * 3600)) / 60);
|
|
546
|
+
const seconds = Math.floor(timeInSeconds - (hours * 3600) - (minutes * 60));
|
|
547
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
148
548
|
}
|
|
149
549
|
/**
|
|
150
|
-
*
|
|
550
|
+
* Sao chép text vào clipboard
|
|
151
551
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
552
|
+
copyToClipboard(text) {
|
|
553
|
+
navigator.clipboard.writeText(text)
|
|
554
|
+
.then(() => {
|
|
555
|
+
alert('Đã sao chép vào clipboard!');
|
|
556
|
+
})
|
|
557
|
+
.catch(err => {
|
|
558
|
+
console.error('Failed to copy: ', err);
|
|
559
|
+
});
|
|
154
560
|
}
|
|
155
561
|
/**
|
|
156
|
-
*
|
|
562
|
+
* Đăng ký các function control từ component
|
|
157
563
|
*/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
564
|
+
registerFunctions(event) {
|
|
565
|
+
this.functionControls = event;
|
|
566
|
+
// Initialize state
|
|
567
|
+
if (this.functionControls) {
|
|
568
|
+
this.isPlaying.set(this.functionControls.isPlaying());
|
|
569
|
+
this.isMuted.set(this.functionControls.isMuted());
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Xử lý sự kiện thay đổi thời gian
|
|
574
|
+
*/
|
|
575
|
+
handleTimeUpdate(timeInfo) {
|
|
576
|
+
this.currentTime.set(timeInfo.currentTime);
|
|
577
|
+
this.duration.set(timeInfo.duration);
|
|
578
|
+
// Calculate progress based on currentTime and duration
|
|
579
|
+
const regex = /(\d+):(\d+):(\d+)/;
|
|
580
|
+
const currentMatches = timeInfo.currentTime.match(regex);
|
|
581
|
+
const durationMatches = timeInfo.duration.match(regex);
|
|
582
|
+
if (currentMatches && durationMatches) {
|
|
583
|
+
const currentSeconds = parseInt(currentMatches[1]) * 3600 +
|
|
584
|
+
parseInt(currentMatches[2]) * 60 +
|
|
585
|
+
parseInt(currentMatches[3]);
|
|
586
|
+
const totalSeconds = parseInt(durationMatches[1]) * 3600 +
|
|
587
|
+
parseInt(durationMatches[2]) * 60 +
|
|
588
|
+
parseInt(durationMatches[3]);
|
|
589
|
+
if (totalSeconds > 0) {
|
|
590
|
+
this.progress.set(Math.floor((currentSeconds / totalSeconds) * 100));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Xử lý sự kiện thay đổi âm lượng
|
|
596
|
+
*/
|
|
597
|
+
handleVolumeChange(volume) {
|
|
598
|
+
this.volume.set(volume);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Xử lý sự kiện thay đổi trạng thái phát
|
|
602
|
+
*/
|
|
603
|
+
handlePlayChange(isPlaying) {
|
|
604
|
+
this.isPlaying.set(isPlaying);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Xử lý sự kiện thay đổi trạng thái tắt tiếng
|
|
608
|
+
*/
|
|
609
|
+
handleMuteChange(isMuted) {
|
|
610
|
+
this.isMuted.set(isMuted);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Xử lý sự kiện kết thúc phát
|
|
614
|
+
*/
|
|
615
|
+
handleEnded() {
|
|
616
|
+
this.isPlaying.set(false);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Chọn mẫu âm thanh
|
|
620
|
+
*/
|
|
621
|
+
selectAudio(id) {
|
|
622
|
+
this.selectedAudio.set(id);
|
|
623
|
+
const sample = this.audioSamples().find(audio => audio.id === id);
|
|
624
|
+
if (sample) {
|
|
625
|
+
// Reset audio state
|
|
626
|
+
this.isPlaying.set(false);
|
|
627
|
+
this.progress.set(0);
|
|
628
|
+
this.currentTime.set('00:00:00');
|
|
629
|
+
this.duration.set('00:00:00');
|
|
630
|
+
// Simply update the source - the component will handle the reload
|
|
631
|
+
this.currentAudioSource.set(sample.src);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Kiểm tra quyền tải xuống
|
|
636
|
+
*/
|
|
637
|
+
checkDownloadPermission = () => {
|
|
638
|
+
return Promise.resolve(true);
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* Thay đổi vị trí phát bằng cách click trực tiếp vào thanh tiến độ
|
|
642
|
+
*/
|
|
643
|
+
seekAudioByClick(event) {
|
|
644
|
+
if (this.functionControls) {
|
|
645
|
+
const progressBar = event.currentTarget;
|
|
646
|
+
const rect = progressBar.getBoundingClientRect();
|
|
647
|
+
const offsetX = event.clientX - rect.left;
|
|
648
|
+
const percentX = (offsetX / rect.width) * 100;
|
|
649
|
+
// Đảm bảo giá trị nằm trong khoảng 0-100
|
|
650
|
+
const clampedPercent = Math.max(0, Math.min(100, percentX));
|
|
651
|
+
this.progress.set(Math.round(clampedPercent));
|
|
652
|
+
// Gọi method seekTo để cập nhật vị trí phát
|
|
653
|
+
this.functionControls.seekTo(this.progress());
|
|
654
|
+
}
|
|
164
655
|
}
|
|
165
656
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsAudioDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
166
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: LibsUiComponentsAudioDemoComponent, isStandalone: true, selector: "lib-audio-demo", ngImport: i0, template: `
|
|
167
|
-
<div class="p-4">
|
|
168
|
-
<h1 class="text-2xl font-bold mb-4">Audio Component Demo</h1>
|
|
169
|
-
|
|
170
|
-
<p class="mb-4">
|
|
171
|
-
Audio Component là một trình phát âm thanh tùy chỉnh với các controls đầy đủ như play/pause,
|
|
172
|
-
volume control, progress bar và download button.
|
|
173
|
-
</p>
|
|
174
|
-
|
|
175
|
-
<div class="demo-section">
|
|
176
|
-
<div class="demo-title">Basic Usage</div>
|
|
177
|
-
<p class="demo-description">Simple audio player với default settings</p>
|
|
178
|
-
<libs_ui-components-audio
|
|
179
|
-
fileAudio="https://nhacchuong123.com/nhac-chuong/abcdefgh/nhac-chuong-nguoi-ay-dau-co-dang-akira-phan-nguyen-van-chung.mp3"
|
|
180
|
-
[checkPermissionDownloadAudio]="basicPermissionCheck"
|
|
181
|
-
/>
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<div class="demo-section">
|
|
185
|
-
<div class="demo-title">Disabled Download</div>
|
|
186
|
-
<p class="demo-description">Audio player không cho phép download</p>
|
|
187
|
-
<libs_ui-components-audio
|
|
188
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
|
|
189
|
-
[checkPermissionDownloadAudio]="denyPermissionCheck"
|
|
190
|
-
/>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
<div class="demo-section">
|
|
194
|
-
<div class="demo-title">Long Audio File</div>
|
|
195
|
-
<p class="demo-description">Audio player với track có duration dài hơn</p>
|
|
196
|
-
<libs_ui-components-audio
|
|
197
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3"
|
|
198
|
-
[checkPermissionDownloadAudio]="basicPermissionCheck"
|
|
199
|
-
/>
|
|
200
|
-
</div>
|
|
201
|
-
|
|
202
|
-
<div class="demo-section">
|
|
203
|
-
<div class="demo-title">Delayed Permission Check</div>
|
|
204
|
-
<p class="demo-description">Mô phỏng permission check có delay, simulate API call</p>
|
|
205
|
-
<libs_ui-components-audio
|
|
206
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3"
|
|
207
|
-
[checkPermissionDownloadAudio]="delayedPermissionCheck"
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
`, isInline: true, styles: [".demo-section{margin-bottom:2rem;padding:1rem;border:1px solid #e5e7eb;border-radius:.5rem}.demo-title{font-size:1.25rem;font-weight:600;margin-bottom:1rem}.demo-description{margin-bottom:1rem;color:#4b5563}\n"], dependencies: [{ kind: "component", type: LibsUiComponentsAudioComponent, selector: "libs_ui-components-audio", inputs: ["fileAudio", "checkPermissionDownloadAudio"] }, { kind: "ngmodule", type: CommonModule }] });
|
|
657
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: LibsUiComponentsAudioDemoComponent, isStandalone: true, selector: "lib-audio-demo", viewQueries: [{ propertyName: "audioPlayer", first: true, predicate: ["audioPlayer"], descendants: true }], ngImport: i0, template: "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 font-sans text-gray-800\">\n <header class=\"text-center py-10 bg-white rounded-lg shadow mb-8\">\n <h1 class=\"text-4xl font-bold text-gray-800 mb-2\">Demo Tr\u00ECnh Ph\u00E1t \u00C2m Thanh</h1>\n <p class=\"text-xl text-gray-600\">Th\u01B0 vi\u1EC7n component cho Angular \u0111\u1EC3 ph\u00E1t v\u00E0 \u0111i\u1EC1u khi\u1EC3n \u00E2m thanh</p>\n </header>\n\n <main>\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">Gi\u1EDBi thi\u1EC7u</h2>\n <p class=\"text-lg\">\n <code class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">@libs-ui/components-audio</code> l\u00E0 m\u1ED9t\n component Angular m\u1EA1nh m\u1EBD cho ph\u00E9p ng\u01B0\u1EDDi d\u00F9ng ph\u00E1t, t\u1EA1m d\u1EEBng v\u00E0 \u0111i\u1EC1u khi\u1EC3n c\u00E1c t\u1EC7p \u00E2m thanh v\u1EDBi nhi\u1EC1u t\u00EDnh n\u0103ng.\n </p>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">C\u00E0i \u0111\u1EB7t</h2>\n <p>\u0110\u1EC3 c\u00E0i \u0111\u1EB7t th\u01B0 vi\u1EC7n, s\u1EED d\u1EE5ng npm ho\u1EB7c yarn:</p>\n\n <div class=\"relative my-4\">\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\"><code class=\"font-mono\">npm install @libs-ui/components-audio</code></pre>\n <button\n class=\"absolute top-2 right-2 bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 transition\"\n (click)=\"copyToClipboard('npm install @libs-ui/components-audio')\">\n Sao ch\u00E9p\n </button>\n </div>\n\n <p>Ho\u1EB7c v\u1EDBi yarn:</p>\n\n <div class=\"relative my-4\">\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\"><code class=\"font-mono\">yarn add @libs-ui/components-audio</code></pre>\n <button\n class=\"absolute top-2 right-2 bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 transition\"\n (click)=\"copyToClipboard('yarn add @libs-ui/components-audio')\">\n Sao ch\u00E9p\n </button>\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">Demo tr\u1EF1c ti\u1EBFp</h2>\n <div class=\"bg-gray-50 p-6 rounded-lg\">\n <p class=\"mb-4\">Th\u1EED nghi\u1EC7m tr\u00ECnh ph\u00E1t \u00E2m thanh v\u1EDBi c\u00E1c file m\u1EABu:</p>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Ch\u1ECDn file \u00E2m thanh:</h3>\n <div class=\"flex flex-col gap-2\">\n @for (audio of audioSamples(); track audio.id) {\n <div\n class=\"flex justify-between p-3 bg-white rounded border cursor-pointer transition hover:bg-blue-50 hover:border-blue-500\"\n [class.bg-blue-50]=\"selectedAudio() === audio.id\"\n [class.border-blue-500]=\"selectedAudio() === audio.id\"\n [class.font-medium]=\"selectedAudio() === audio.id\"\n (click)=\"selectAudio(audio.id)\">\n <span>{{ audio.name }}</span>\n <span>{{ audio.duration }}</span>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Tr\u00ECnh ph\u00E1t:</h3>\n <libs_ui-components-audio #audioPlayer\n [fileAudio]=\"currentAudioSource()\"\n [checkPermissionDownloadAudio]=\"checkDownloadPermission\"\n (outFunctionsControl)=\"registerFunctions($event)\"\n (outTimeUpdate)=\"handleTimeUpdate($event)\"\n (outVolumeControl)=\"handleVolumeChange($event)\"\n (outPlay)=\"handlePlayChange($event)\"\n (outMute)=\"handleMuteChange($event)\"\n (outEnded)=\"handleEnded()\">\n </libs_ui-components-audio>\n </div>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">\u0110i\u1EC1u khi\u1EC3n qua Function Control:</h3>\n <div class=\"flex flex-wrap gap-3 items-center bg-white p-4 rounded border\">\n <button class=\"px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600 transition\"\n (click)=\"playAudio()\">\n Ph\u00E1t/T\u1EA1m d\u1EEBng\n </button>\n <button class=\"px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600 transition\"\n (click)=\"toggleMute()\">\n {{ isMuted() ? 'B\u1EADt ti\u1EBFng' : 'T\u1EAFt ti\u1EBFng' }}\n </button>\n\n <div class=\"flex items-center gap-2 ml-2\">\n <span>\u00C2m l\u01B0\u1EE3ng:</span>\n <input type=\"range\"\n min=\"0\"\n max=\"100\"\n [value]=\"volumePercent()\"\n (input)=\"changeVolume($event)\"\n class=\"w-24 volume-slider\"\n [style.--volume-percent.%]=\"volumePercent()\">\n <span>{{ volumePercent() }}%</span>\n </div>\n\n <div class=\"w-full mt-2\">\n <div class=\"flex items-center gap-2\">\n <span>Ti\u1EBFn \u0111\u1ED9:</span>\n <div class=\"relative h-2 bg-gray-200 rounded-full flex-1 cursor-pointer\"\n (click)=\"seekAudioByClick($event)\">\n <div class=\"absolute top-0 left-0 h-full bg-blue-500 rounded-full\"\n [style.width.%]=\"progress()\"></div>\n </div>\n <span>{{ progress() }}%</span>\n </div>\n </div>\n\n <button class=\"px-4 py-2 bg-green-500 text-white font-medium rounded hover:bg-green-600 transition mt-4\"\n (click)=\"downloadAudio()\">\n T\u1EA3i xu\u1ED1ng\n </button>\n </div>\n\n <div class=\"mt-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Tr\u1EA1ng th\u00E1i:</h3>\n <div class=\"bg-white p-4 rounded border\">\n <div class=\"mb-2\">\n <strong>Tr\u1EA1ng th\u00E1i:</strong>\n <span [class.text-green-600]=\"isPlaying()\"\n [class.text-red-600]=\"!isPlaying()\">\n {{ isPlaying() ? '\u0110ang ph\u00E1t' : 'T\u1EA1m d\u1EEBng' }}\n </span>\n </div>\n <div class=\"mb-2\">\n <strong>Th\u1EDDi gian hi\u1EC7n t\u1EA1i:</strong>\n <span>{{ currentTime() }}</span>\n </div>\n <div class=\"mb-2\">\n <strong>T\u1ED5ng th\u1EDDi gian:</strong>\n <span>{{ duration() }}</span>\n </div>\n <div class=\"mb-2\">\n <strong>Ti\u1EBFn \u0111\u1ED9:</strong>\n <span>{{ progress() }}%</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">T\u00E0i li\u1EC7u API</h2>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Inputs</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u1EB7c \u0111\u1ECBnh</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (input of inputsDoc(); track input.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ input.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ input.type }}</code></td>\n <td class=\"py-3 px-4\">{{ input.default }}</td>\n <td class=\"py-3 px-4\">{{ input.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Outputs</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (output of outputsDoc(); track output.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ output.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ output.type }}</code></td>\n <td class=\"py-3 px-4\">{{ output.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Function Control Methods</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Tham s\u1ED1</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u tr\u1EA3 v\u1EC1</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (method of methodsDoc(); track method.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.params }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.returnType }}</code></td>\n <td class=\"py-3 px-4\">{{ method.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Interfaces</h3>\n <div class=\"overflow-x-auto mb-8\">\n @for (interfaceDoc of interfacesDoc(); track interfaceDoc.name) {\n <div class=\"mb-6 bg-white p-4 rounded border\">\n <h4 class=\"text-lg font-semibold text-gray-700 mb-2\">{{ interfaceDoc.name }}</h4>\n <p class=\"mb-4\">{{ interfaceDoc.description }}</p>\n\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (prop of interfaceDoc.properties; track prop.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ prop.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ prop.type }}</code></td>\n <td class=\"py-3 px-4\">{{ prop.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">T\u00EDnh n\u0103ng</h2>\n <ul class=\"space-y-6\">\n @for (feature of features(); track feature.id) {\n <li class=\"flex items-start\">\n <span class=\"text-3xl mr-4 min-w-10 text-center\">{{ feature.icon }}</span>\n <div>\n <h3 class=\"text-xl font-semibold text-gray-700\">{{ feature.title }}</h3>\n <p>{{ feature.description }}</p>\n </div>\n </li>\n }\n </ul>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">C\u00E1ch s\u1EED d\u1EE5ng</h2>\n <div class=\"flex flex-col gap-4\">\n @for (example of codeExamples(); track example.id) {\n <div class=\"border border-gray-200 rounded-lg p-4\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-2\">{{ example.title }}</h3>\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto\"><code [innerHTML]=\"example.code\" class=\"font-mono\"></code></pre>\n </div>\n }\n </div>\n </section>\n </main>\n</div>\n", styles: ["input[type=range]{cursor:pointer;-webkit-appearance:none;appearance:none;height:6px;border-radius:999px;background-color:#e5e7eb;outline:none;width:100%}input[type=range].volume-slider{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}input[type=range]::-webkit-slider-runnable-track{width:100%;height:6px;background-color:#e5e7eb;border-radius:999px}input[type=range]::-moz-range-track{width:100%;height:6px;background-color:#e5e7eb;border-radius:999px}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background-color:#3b82f6;cursor:pointer;transition:background-color .2s ease;margin-top:-5px}input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background-color:#3b82f6;cursor:pointer;transition:background-color .2s ease;border:none}input[type=range]::-webkit-slider-thumb:hover{background-color:#2563eb}input[type=range]::-moz-range-thumb:hover{background-color:#2563eb}input[type=range].volume-slider::-webkit-slider-runnable-track{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}input[type=range].volume-slider::-moz-range-track{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}.control-button:active{transform:scale(.95)}libs_ui-components-audio{display:block;width:100%}.transition{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}pre::-webkit-scrollbar{height:8px}pre::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}pre::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}pre::-webkit-scrollbar-thumb:hover{background:#a1a1a1}\n"], dependencies: [{ kind: "component", type: LibsUiComponentsAudioComponent, selector: "libs_ui-components-audio", inputs: ["fileAudio", "checkPermissionDownloadAudio"], outputs: ["outFunctionsControl", "outVolumeControl", "outTimeUpdate", "outEnded", "outMute", "outPlay"] }] });
|
|
212
658
|
}
|
|
213
659
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsAudioDemoComponent, decorators: [{
|
|
214
660
|
type: Component,
|
|
215
|
-
args: [{ selector: 'lib-audio-demo', standalone: true, imports: [LibsUiComponentsAudioComponent, CommonModule], template: `
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Audio Component là một trình phát âm thanh tùy chỉnh với các controls đầy đủ như play/pause,
|
|
221
|
-
volume control, progress bar và download button.
|
|
222
|
-
</p>
|
|
223
|
-
|
|
224
|
-
<div class="demo-section">
|
|
225
|
-
<div class="demo-title">Basic Usage</div>
|
|
226
|
-
<p class="demo-description">Simple audio player với default settings</p>
|
|
227
|
-
<libs_ui-components-audio
|
|
228
|
-
fileAudio="https://nhacchuong123.com/nhac-chuong/abcdefgh/nhac-chuong-nguoi-ay-dau-co-dang-akira-phan-nguyen-van-chung.mp3"
|
|
229
|
-
[checkPermissionDownloadAudio]="basicPermissionCheck"
|
|
230
|
-
/>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div class="demo-section">
|
|
234
|
-
<div class="demo-title">Disabled Download</div>
|
|
235
|
-
<p class="demo-description">Audio player không cho phép download</p>
|
|
236
|
-
<libs_ui-components-audio
|
|
237
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
|
|
238
|
-
[checkPermissionDownloadAudio]="denyPermissionCheck"
|
|
239
|
-
/>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<div class="demo-section">
|
|
243
|
-
<div class="demo-title">Long Audio File</div>
|
|
244
|
-
<p class="demo-description">Audio player với track có duration dài hơn</p>
|
|
245
|
-
<libs_ui-components-audio
|
|
246
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3"
|
|
247
|
-
[checkPermissionDownloadAudio]="basicPermissionCheck"
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<div class="demo-section">
|
|
252
|
-
<div class="demo-title">Delayed Permission Check</div>
|
|
253
|
-
<p class="demo-description">Mô phỏng permission check có delay, simulate API call</p>
|
|
254
|
-
<libs_ui-components-audio
|
|
255
|
-
fileAudio="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3"
|
|
256
|
-
[checkPermissionDownloadAudio]="delayedPermissionCheck"
|
|
257
|
-
/>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
`, styles: [".demo-section{margin-bottom:2rem;padding:1rem;border:1px solid #e5e7eb;border-radius:.5rem}.demo-title{font-size:1.25rem;font-weight:600;margin-bottom:1rem}.demo-description{margin-bottom:1rem;color:#4b5563}\n"] }]
|
|
261
|
-
}] });
|
|
661
|
+
args: [{ selector: 'lib-audio-demo', standalone: true, imports: [LibsUiComponentsAudioComponent], template: "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 font-sans text-gray-800\">\n <header class=\"text-center py-10 bg-white rounded-lg shadow mb-8\">\n <h1 class=\"text-4xl font-bold text-gray-800 mb-2\">Demo Tr\u00ECnh Ph\u00E1t \u00C2m Thanh</h1>\n <p class=\"text-xl text-gray-600\">Th\u01B0 vi\u1EC7n component cho Angular \u0111\u1EC3 ph\u00E1t v\u00E0 \u0111i\u1EC1u khi\u1EC3n \u00E2m thanh</p>\n </header>\n\n <main>\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">Gi\u1EDBi thi\u1EC7u</h2>\n <p class=\"text-lg\">\n <code class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">@libs-ui/components-audio</code> l\u00E0 m\u1ED9t\n component Angular m\u1EA1nh m\u1EBD cho ph\u00E9p ng\u01B0\u1EDDi d\u00F9ng ph\u00E1t, t\u1EA1m d\u1EEBng v\u00E0 \u0111i\u1EC1u khi\u1EC3n c\u00E1c t\u1EC7p \u00E2m thanh v\u1EDBi nhi\u1EC1u t\u00EDnh n\u0103ng.\n </p>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">C\u00E0i \u0111\u1EB7t</h2>\n <p>\u0110\u1EC3 c\u00E0i \u0111\u1EB7t th\u01B0 vi\u1EC7n, s\u1EED d\u1EE5ng npm ho\u1EB7c yarn:</p>\n\n <div class=\"relative my-4\">\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\"><code class=\"font-mono\">npm install @libs-ui/components-audio</code></pre>\n <button\n class=\"absolute top-2 right-2 bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 transition\"\n (click)=\"copyToClipboard('npm install @libs-ui/components-audio')\">\n Sao ch\u00E9p\n </button>\n </div>\n\n <p>Ho\u1EB7c v\u1EDBi yarn:</p>\n\n <div class=\"relative my-4\">\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto mb-4\"><code class=\"font-mono\">yarn add @libs-ui/components-audio</code></pre>\n <button\n class=\"absolute top-2 right-2 bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 transition\"\n (click)=\"copyToClipboard('yarn add @libs-ui/components-audio')\">\n Sao ch\u00E9p\n </button>\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">Demo tr\u1EF1c ti\u1EBFp</h2>\n <div class=\"bg-gray-50 p-6 rounded-lg\">\n <p class=\"mb-4\">Th\u1EED nghi\u1EC7m tr\u00ECnh ph\u00E1t \u00E2m thanh v\u1EDBi c\u00E1c file m\u1EABu:</p>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Ch\u1ECDn file \u00E2m thanh:</h3>\n <div class=\"flex flex-col gap-2\">\n @for (audio of audioSamples(); track audio.id) {\n <div\n class=\"flex justify-between p-3 bg-white rounded border cursor-pointer transition hover:bg-blue-50 hover:border-blue-500\"\n [class.bg-blue-50]=\"selectedAudio() === audio.id\"\n [class.border-blue-500]=\"selectedAudio() === audio.id\"\n [class.font-medium]=\"selectedAudio() === audio.id\"\n (click)=\"selectAudio(audio.id)\">\n <span>{{ audio.name }}</span>\n <span>{{ audio.duration }}</span>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Tr\u00ECnh ph\u00E1t:</h3>\n <libs_ui-components-audio #audioPlayer\n [fileAudio]=\"currentAudioSource()\"\n [checkPermissionDownloadAudio]=\"checkDownloadPermission\"\n (outFunctionsControl)=\"registerFunctions($event)\"\n (outTimeUpdate)=\"handleTimeUpdate($event)\"\n (outVolumeControl)=\"handleVolumeChange($event)\"\n (outPlay)=\"handlePlayChange($event)\"\n (outMute)=\"handleMuteChange($event)\"\n (outEnded)=\"handleEnded()\">\n </libs_ui-components-audio>\n </div>\n\n <div class=\"mb-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">\u0110i\u1EC1u khi\u1EC3n qua Function Control:</h3>\n <div class=\"flex flex-wrap gap-3 items-center bg-white p-4 rounded border\">\n <button class=\"px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600 transition\"\n (click)=\"playAudio()\">\n Ph\u00E1t/T\u1EA1m d\u1EEBng\n </button>\n <button class=\"px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600 transition\"\n (click)=\"toggleMute()\">\n {{ isMuted() ? 'B\u1EADt ti\u1EBFng' : 'T\u1EAFt ti\u1EBFng' }}\n </button>\n\n <div class=\"flex items-center gap-2 ml-2\">\n <span>\u00C2m l\u01B0\u1EE3ng:</span>\n <input type=\"range\"\n min=\"0\"\n max=\"100\"\n [value]=\"volumePercent()\"\n (input)=\"changeVolume($event)\"\n class=\"w-24 volume-slider\"\n [style.--volume-percent.%]=\"volumePercent()\">\n <span>{{ volumePercent() }}%</span>\n </div>\n\n <div class=\"w-full mt-2\">\n <div class=\"flex items-center gap-2\">\n <span>Ti\u1EBFn \u0111\u1ED9:</span>\n <div class=\"relative h-2 bg-gray-200 rounded-full flex-1 cursor-pointer\"\n (click)=\"seekAudioByClick($event)\">\n <div class=\"absolute top-0 left-0 h-full bg-blue-500 rounded-full\"\n [style.width.%]=\"progress()\"></div>\n </div>\n <span>{{ progress() }}%</span>\n </div>\n </div>\n\n <button class=\"px-4 py-2 bg-green-500 text-white font-medium rounded hover:bg-green-600 transition mt-4\"\n (click)=\"downloadAudio()\">\n T\u1EA3i xu\u1ED1ng\n </button>\n </div>\n\n <div class=\"mt-6\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Tr\u1EA1ng th\u00E1i:</h3>\n <div class=\"bg-white p-4 rounded border\">\n <div class=\"mb-2\">\n <strong>Tr\u1EA1ng th\u00E1i:</strong>\n <span [class.text-green-600]=\"isPlaying()\"\n [class.text-red-600]=\"!isPlaying()\">\n {{ isPlaying() ? '\u0110ang ph\u00E1t' : 'T\u1EA1m d\u1EEBng' }}\n </span>\n </div>\n <div class=\"mb-2\">\n <strong>Th\u1EDDi gian hi\u1EC7n t\u1EA1i:</strong>\n <span>{{ currentTime() }}</span>\n </div>\n <div class=\"mb-2\">\n <strong>T\u1ED5ng th\u1EDDi gian:</strong>\n <span>{{ duration() }}</span>\n </div>\n <div class=\"mb-2\">\n <strong>Ti\u1EBFn \u0111\u1ED9:</strong>\n <span>{{ progress() }}%</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">T\u00E0i li\u1EC7u API</h2>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Inputs</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u1EB7c \u0111\u1ECBnh</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (input of inputsDoc(); track input.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ input.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ input.type }}</code></td>\n <td class=\"py-3 px-4\">{{ input.default }}</td>\n <td class=\"py-3 px-4\">{{ input.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Outputs</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (output of outputsDoc(); track output.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ output.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ output.type }}</code></td>\n <td class=\"py-3 px-4\">{{ output.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Function Control Methods</h3>\n <div class=\"overflow-x-auto mb-8\">\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Tham s\u1ED1</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u tr\u1EA3 v\u1EC1</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (method of methodsDoc(); track method.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.params }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ method.returnType }}</code></td>\n <td class=\"py-3 px-4\">{{ method.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <h3 class=\"text-xl font-semibold text-gray-700 mb-3\">Interfaces</h3>\n <div class=\"overflow-x-auto mb-8\">\n @for (interfaceDoc of interfacesDoc(); track interfaceDoc.name) {\n <div class=\"mb-6 bg-white p-4 rounded border\">\n <h4 class=\"text-lg font-semibold text-gray-700 mb-2\">{{ interfaceDoc.name }}</h4>\n <p class=\"mb-4\">{{ interfaceDoc.description }}</p>\n\n <table class=\"min-w-full bg-white\">\n <thead class=\"bg-gray-100\">\n <tr>\n <th class=\"py-3 px-4 text-left\">T\u00EAn</th>\n <th class=\"py-3 px-4 text-left\">Ki\u1EC3u d\u1EEF li\u1EC7u</th>\n <th class=\"py-3 px-4 text-left\">M\u00F4 t\u1EA3</th>\n </tr>\n </thead>\n <tbody>\n @for (prop of interfaceDoc.properties; track prop.name) {\n <tr class=\"border-b hover:bg-gray-50\">\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ prop.name }}</code></td>\n <td class=\"py-3 px-4\"><code\n class=\"bg-gray-100 px-1 py-0.5 rounded text-sm font-mono\">{{ prop.type }}</code></td>\n <td class=\"py-3 px-4\">{{ prop.description }}</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">T\u00EDnh n\u0103ng</h2>\n <ul class=\"space-y-6\">\n @for (feature of features(); track feature.id) {\n <li class=\"flex items-start\">\n <span class=\"text-3xl mr-4 min-w-10 text-center\">{{ feature.icon }}</span>\n <div>\n <h3 class=\"text-xl font-semibold text-gray-700\">{{ feature.title }}</h3>\n <p>{{ feature.description }}</p>\n </div>\n </li>\n }\n </ul>\n </section>\n\n <section class=\"bg-white rounded-lg shadow p-8 mb-8\">\n <h2 class=\"text-2xl font-bold text-gray-800 mb-4 pb-2 border-b border-gray-200\">C\u00E1ch s\u1EED d\u1EE5ng</h2>\n <div class=\"flex flex-col gap-4\">\n @for (example of codeExamples(); track example.id) {\n <div class=\"border border-gray-200 rounded-lg p-4\">\n <h3 class=\"text-xl font-semibold text-gray-700 mb-2\">{{ example.title }}</h3>\n <pre\n class=\"bg-gray-100 p-4 rounded overflow-x-auto\"><code [innerHTML]=\"example.code\" class=\"font-mono\"></code></pre>\n </div>\n }\n </div>\n </section>\n </main>\n</div>\n", styles: ["input[type=range]{cursor:pointer;-webkit-appearance:none;appearance:none;height:6px;border-radius:999px;background-color:#e5e7eb;outline:none;width:100%}input[type=range].volume-slider{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}input[type=range]::-webkit-slider-runnable-track{width:100%;height:6px;background-color:#e5e7eb;border-radius:999px}input[type=range]::-moz-range-track{width:100%;height:6px;background-color:#e5e7eb;border-radius:999px}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background-color:#3b82f6;cursor:pointer;transition:background-color .2s ease;margin-top:-5px}input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background-color:#3b82f6;cursor:pointer;transition:background-color .2s ease;border:none}input[type=range]::-webkit-slider-thumb:hover{background-color:#2563eb}input[type=range]::-moz-range-thumb:hover{background-color:#2563eb}input[type=range].volume-slider::-webkit-slider-runnable-track{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}input[type=range].volume-slider::-moz-range-track{background:linear-gradient(to right,#3b82f6 var(--volume-percent, 50%),#e5e7eb var(--volume-percent, 50%))}.control-button:active{transform:scale(.95)}libs_ui-components-audio{display:block;width:100%}.transition{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}pre::-webkit-scrollbar{height:8px}pre::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}pre::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}pre::-webkit-scrollbar-thumb:hover{background:#a1a1a1}\n"] }]
|
|
662
|
+
}], propDecorators: { audioPlayer: [{
|
|
663
|
+
type: ViewChild,
|
|
664
|
+
args: ['audioPlayer']
|
|
665
|
+
}] } });
|
|
262
666
|
|
|
263
667
|
/**
|
|
264
668
|
* Generated bundle index. Do not edit.
|