@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.
@@ -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.stopPropagation();
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.stopPropagation();
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.stopPropagation();
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.stopPropagation();
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.stopPropagation();
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.stopPropagation();
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: `&lt;libs_ui-components-audio
410
+ [fileAudio]="'path/to/audio.mp3'"
411
+ [checkPermissionDownloadAudio]="checkPermission"&gt;
412
+ &lt;/libs_ui-components-audio&gt;`
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
+ &lt;libs_ui-components-audio
425
+ #audioPlayer
426
+ [fileAudio]="audioSource()"
427
+ [checkPermissionDownloadAudio]="checkPermission"
428
+ (outFunctionsControl)="registerFunctions($event)"&gt;
429
+ &lt;/libs_ui-components-audio&gt;
430
+
431
+ &lt;button (click)="playAudio()"&gt;Phát/Tạm dừng&lt;/button&gt;
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
+ &lt;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()"&gt;
467
+ &lt;/libs_ui-components-audio&gt;
468
+
469
+ &lt;div class="audio-info"&gt;
470
+ &lt;p&gt;Trạng thái: {{ isPlaying() ? 'Đang phát' : 'Tạm dừng' }}&lt;/p&gt;
471
+ &lt;p&gt;Thời gian hiện tại: {{ currentTime() }}&lt;/p&gt;
472
+ &lt;p&gt;Tổng thời gian: {{ duration() }}&lt;/p&gt;
473
+ &lt;p&gt;Âm lượng: {{ volumeLevel() }}%&lt;/p&gt;
474
+ &lt;/div&gt;
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
- * Permission function luôn allow download
539
+ * Format thời gian từ giây sang chuỗi HH:MM:SS
145
540
  */
146
- basicPermissionCheck() {
147
- return Promise.resolve(true);
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
- * Permission function không cho phép download
550
+ * Sao chép text vào clipboard
151
551
  */
152
- denyPermissionCheck() {
153
- return Promise.resolve(false);
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
- * Permission function delay để phỏng API call
562
+ * Đăng các function control từ component
157
563
  */
158
- delayedPermissionCheck() {
159
- return new Promise(resolve => {
160
- setTimeout(() => {
161
- resolve(true);
162
- }, 1500);
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\">&#64;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 &#64;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 &#64;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
- <div class="p-4">
217
- <h1 class="text-2xl font-bold mb-4">Audio Component Demo</h1>
218
-
219
- <p class="mb-4">
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\">&#64;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 &#64;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 &#64;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.