@ngxsplayer/ngx-smart-player 0.0.1-next.1 → 0.1.0

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,15 +1,15 @@
1
- import { PlayerFacade, createSsaiManifestRewritePlugin } from '@ngxsp/core';
1
+ import { PlayerFacade, createSsaiManifestRewritePlugin, describeSubtitleFile, subtitleTextToVtt, decodeSubtitleBytes } from '@ngxsp/core';
2
2
  export * from '@ngxsp/core';
3
3
  import * as i0 from '@angular/core';
4
- import { Input, ChangeDetectionStrategy, Component, input, output, computed, EventEmitter, inject, ChangeDetectorRef, HostListener, ViewChild, Output, InjectionToken, makeEnvironmentProviders } from '@angular/core';
4
+ import { Input, ChangeDetectionStrategy, Component, EventEmitter, Output, HostListener, ViewChild, input, output, computed, inject, ChangeDetectorRef, ElementRef, InjectionToken, makeEnvironmentProviders } from '@angular/core';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import { Subscription } from 'rxjs';
7
- import { NgxspPlayButton, NgxspTimeSlider } from '@ngxsplayer/ngx-smart-player-ui-core';
7
+ import { NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl, NgxspLabeledControl, NgxspLiveBadge } from '@ngxsplayer/ngx-smart-player-ui-core';
8
8
 
9
9
  class SmartPlayerIconComponent {
10
10
  name = 'play';
11
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: SmartPlayerIconComponent, isStandalone: true, selector: "ngxsp-smart-icon", inputs: { name: "name" }, ngImport: i0, template: `
11
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartPlayerIconComponent, isStandalone: true, selector: "ngxsp-smart-icon", inputs: { name: "name" }, ngImport: i0, template: `
13
13
  @switch (name) {
14
14
  @case ('play') {
15
15
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
@@ -18,16 +18,16 @@ class SmartPlayerIconComponent {
18
18
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
19
19
  }
20
20
  @case ('prev') {
21
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg>
21
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M19 6.55v10.9a1.25 1.25 0 0 1-1.93 1.05l-8.2-5.45a1.25 1.25 0 0 1 0-2.1l8.2-5.45A1.25 1.25 0 0 1 19 6.55Z"/></svg>
22
22
  }
23
23
  @case ('next') {
24
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg>
24
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M5 6.55v10.9a1.25 1.25 0 0 0 1.93 1.05l8.2-5.45a1.25 1.25 0 0 0 0-2.1l-8.2-5.45A1.25 1.25 0 0 0 5 6.55Z"/></svg>
25
25
  }
26
26
  @case ('seek-back') {
27
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
27
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 13a6.5 6.5 0 1 0 6.5-6.5H8M10 4.5 8 6.5l2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
28
28
  }
29
29
  @case ('seek-forward') {
30
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
30
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18.5 13a6.5 6.5 0 1 1-6.5-6.5H16M14 4.5 16 6.5l-2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
31
31
  }
32
32
  @case ('volume-mute') {
33
33
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
@@ -42,10 +42,10 @@ class SmartPlayerIconComponent {
42
42
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
43
43
  }
44
44
  @case ('repeat') {
45
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
45
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/></svg>
46
46
  }
47
47
  @case ('repeat-one') {
48
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/><text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text></svg>
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/><path d="M11 10.9 12.4 10v4.1"/></svg>
49
49
  }
50
50
  @case ('tracks') {
51
51
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
@@ -60,10 +60,16 @@ class SmartPlayerIconComponent {
60
60
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
61
61
  }
62
62
  @case ('pip') {
63
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
63
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
64
64
  }
65
65
  @case ('pip-active') {
66
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
66
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5" fill="currentColor" fill-opacity=".22"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
67
+ }
68
+ @case ('airplay') {
69
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m12 14 4.5 6h-9L12 14Z"/></svg>
70
+ }
71
+ @case ('airplay-active') {
72
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path fill="currentColor" stroke="none" d="m12 13.4 5.2 6.9H6.8L12 13.4Z"/></svg>
67
73
  }
68
74
  @case ('cast') {
69
75
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/></svg>
@@ -71,10 +77,160 @@ class SmartPlayerIconComponent {
71
77
  @case ('cast-active') {
72
78
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/><circle cx="7" cy="19" r="1.5" fill="currentColor" stroke="none"/></svg>
73
79
  }
80
+ @case ('devices') {
81
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.4"/><path d="M18.75 11.25h.01"/></svg>
82
+ }
83
+ @case ('devices-active') {
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.6" fill="currentColor" stroke="none"/><path d="M18.75 11.25h.01"/></svg>
85
+ }
86
+ @case ('radio') {
87
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 8 16.5 3.75"/><rect x="3" y="8" width="18" height="12.5" rx="2.5"/><circle cx="15.25" cy="14.25" r="2.75"/><path d="M6.75 12.75h3.5M6.75 15.75h3.5"/></svg>
88
+ }
89
+ @case ('server') {
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="3.25" width="16" height="11" rx="2"/><path d="M7.5 6.75h.01M7.5 10.75h.01M10.5 6.75h6M10.5 10.75h6"/><path d="M12 14.25v4.5M3.5 20.5h6.75M13.75 20.5h6.75"/><circle cx="12" cy="20.5" r="1.6"/></svg>
91
+ }
92
+ @case ('window-mini') {
93
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 12.5 15 9.5M11 9.5h4v4"/><rect x="5.5" y="13" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
94
+ }
95
+ @case ('window-bar') {
96
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="5" y="15" width="14" height="2.8" rx="1" fill="currentColor" stroke="none"/></svg>
97
+ }
98
+ @case ('cinema') {
99
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
100
+ }
101
+ @case ('cinema-active') {
102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2" fill="currentColor" fill-opacity=".22"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
103
+ }
104
+ @case ('info') {
105
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg>
106
+ }
107
+ @case ('lyrics') {
108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.5h16v10H10l-4.5 4v-4H4z"/><path d="M8 9.5h8M8 12.5h5"/></svg>
109
+ }
110
+ @case ('window-restore') {
111
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 14.5 15 9.5M11 9.5h4v4"/></svg>
112
+ }
113
+ @case ('bell') {
114
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.75 10.4a5.25 5.25 0 0 1 10.5 0c0 4.65 1.75 5.6 1.75 5.6H5s1.75-.95 1.75-5.6Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 19a2.15 2.15 0 0 0 4 0"/></svg>
115
+ }
116
+ @case ('thumb-up') {
117
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 10.5v9.25H4.7a1.7 1.7 0 0 1-1.7-1.7V12.2a1.7 1.7 0 0 1 1.7-1.7h2.55Zm0 0 4.85-7.05a1.75 1.75 0 0 1 3.18 1.16l-.44 3.2h3.88a2.1 2.1 0 0 1 2.05 2.55l-1.35 6.45a3.7 3.7 0 0 1-3.62 2.94H7.25"/></svg>
118
+ }
119
+ @case ('thumb-down') {
120
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 13.5V4.25H4.7A1.7 1.7 0 0 0 3 5.95v5.85a1.7 1.7 0 0 0 1.7 1.7h2.55Zm0 0 4.85 7.05a1.75 1.75 0 0 0 3.18-1.16l-.44-3.2h3.88a2.1 2.1 0 0 0 2.05-2.55L19.42 7.2a3.7 3.7 0 0 0-3.62-2.94H7.25"/></svg>
121
+ }
122
+ @case ('share') {
123
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M18.25 8.25 21 5.5l-2.75-2.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.4 5.5h-5.2A8.2 8.2 0 0 0 7 13.7v.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 11.5v6.75A1.75 1.75 0 0 0 5.75 20h12.5A1.75 1.75 0 0 0 20 18.25V14"/></svg>
124
+ }
125
+ @case ('playlist-plus') {
126
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 6.5h11M4 11.5h11M4 16.5h8"/><path stroke-linecap="round" stroke-width="1.8" d="M18 13.5v6m-3-3h6"/></svg>
127
+ }
128
+ @case ('clock') {
129
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 7.75v4.65l3.45 2.05"/></svg>
130
+ }
131
+ @case ('home') {
132
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 11.5 8-6.75 8 6.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.5 10.5v8.25h11v-8.25"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 18.75v-5h4v5"/></svg>
133
+ }
134
+ @case ('layers') {
135
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M12 3.75 3.75 8 12 12.25 20.25 8 12 3.75Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12 7 3.6 7-3.6M5 16l7 3.6 7-3.6"/></svg>
136
+ }
137
+ @case ('video') {
138
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 5v14M16 5v14M4 9h4M4 15h4M16 9h4M16 15h4"/></svg>
139
+ }
140
+ @case ('music') {
141
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 18.25a2.75 2.75 0 1 1-1.6-2.5V6.5l10-2v11.25a2.75 2.75 0 1 1-1.6-2.5V8.2l-8.4 1.68"/></svg>
142
+ }
143
+ @case ('broadcast') {
144
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="2" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8.7 8.7a4.65 4.65 0 0 0 0 6.6m6.6 0a4.65 4.65 0 0 0 0-6.6M6.1 6.1a8.35 8.35 0 0 0 0 11.8m11.8 0a8.35 8.35 0 0 0 0-11.8"/></svg>
145
+ }
146
+ @case ('heart') {
147
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.2 6.2c-1.8-1.9-4.8-1.8-6.5.2L12 8.3l-1.7-1.9c-1.7-2-4.7-2.1-6.5-.2-1.9 2-1.7 5.1.4 6.9L12 20l7.8-6.9c2.1-1.8 2.3-4.9.4-6.9Z"/></svg>
148
+ }
149
+ @case ('download') {
150
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 4v10m0 0 4-4m-4 4-4-4M5 19h14"/></svg>
151
+ }
152
+ @case ('trash') {
153
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 6.75h15M9.75 6.75V5.25A1.5 1.5 0 0 1 11.25 3.75h1.5a1.5 1.5 0 0 1 1.5 1.5v1.5M6.75 6.75l.7 12a1.5 1.5 0 0 0 1.5 1.5h6.1a1.5 1.5 0 0 0 1.5-1.5l.7-12M10.5 10.5v6M13.5 10.5v6"/></svg>
154
+ }
155
+ @case ('folder') {
156
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3.75 7.5A2.25 2.25 0 0 1 6 5.25h4.15l2.1 2.25H18a2.25 2.25 0 0 1 2.25 2.25v6.75A2.25 2.25 0 0 1 18 18.75H6a2.25 2.25 0 0 1-2.25-2.25v-9Z"/></svg>
157
+ }
158
+ @case ('settings') {
159
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 15.25A3.25 3.25 0 1 0 12 8.75a3.25 3.25 0 0 0 0 6.5Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M19.4 13.5a7.8 7.8 0 0 0 .05-3l2-1.55-2-3.45-2.42.98a8 8 0 0 0-2.58-1.5L14.1 2.4h-4.2l-.35 2.58a8 8 0 0 0-2.58 1.5L4.55 5.5l-2 3.45 2 1.55a7.8 7.8 0 0 0 .05 3l-2.05 1.55 2 3.45 2.46-.98a8 8 0 0 0 2.54 1.47l.35 2.61h4.2l.35-2.6a8 8 0 0 0 2.54-1.48l2.46.98 2-3.45-2.05-1.55Z"/></svg>
160
+ }
161
+ @case ('search') {
162
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="10.75" cy="10.75" r="6.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="m15.4 15.4 4.85 4.85"/></svg>
163
+ }
164
+ @case ('grid') {
165
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="4" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/></svg>
166
+ }
167
+ @case ('list') {
168
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.8" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="18" r="1.2" fill="currentColor" stroke="none"/></svg>
169
+ }
170
+ @case ('more') {
171
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>
172
+ }
173
+ @case ('waveform') {
174
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 13v-2m4 5V8m4 11V5m4 10V9m4 4v-2"/></svg>
175
+ }
176
+ @case ('sliders') {
177
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h5m6 0h5M4 17h9m6 0h1"/><circle cx="12" cy="7" r="2.25" stroke-width="1.8"/><circle cx="16" cy="17" r="2.25" stroke-width="1.8"/></svg>
178
+ }
179
+ @case ('network') {
180
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2.3" stroke-width="1.8"/><circle cx="6" cy="18" r="2.3" stroke-width="1.8"/><circle cx="18" cy="18" r="2.3" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M11 7.1 7.1 15.9M13 7.1l3.9 8.8M8.3 18h7.4"/></svg>
181
+ }
182
+ @case ('cloud') {
183
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 18.5h10.2a4.05 4.05 0 0 0 .5-8.07 6.25 6.25 0 0 0-12-1.75A4.95 4.95 0 0 0 7.25 18.5Z"/></svg>
184
+ }
185
+ @case ('spark') {
186
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 3.5 14.3 9l5.7 2.2-5.7 2.3L12 20l-2.3-6.5L4 11.2 9.7 9 12 3.5Z"/></svg>
187
+ }
188
+ @case ('disc') {
189
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><circle cx="12" cy="12" r="2.25" stroke-width="1.8"/></svg>
190
+ }
191
+ @case ('user') {
192
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="8.5" r="3.75" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 19.75a7.25 7.25 0 0 1 14.5 0"/></svg>
193
+ }
194
+ @case ('tag') {
195
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M3.75 5.75v5.1c0 .6.24 1.17.66 1.6l7.4 7.4a2 2 0 0 0 2.83 0l5.1-5.1a2 2 0 0 0 0-2.83l-7.4-7.4a2.25 2.25 0 0 0-1.6-.66h-5a2 2 0 0 0-2 1.89Z"/><circle cx="8.25" cy="8.25" r="1.25" fill="currentColor" stroke="none"/></svg>
196
+ }
197
+ @case ('close') {
198
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="m6.5 6.5 11 11m0-11-11 11"/></svg>
199
+ }
200
+ @case ('plus') {
201
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M12 5v14M5 12h14"/></svg>
202
+ }
203
+ @case ('check') {
204
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12.5 4.5 4.5L19 7.5"/></svg>
205
+ }
206
+ @case ('menu') {
207
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h16M4 12h16M4 17h16"/></svg>
208
+ }
209
+ @case ('panel-right') {
210
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" stroke-width="1.8"/><path stroke-width="1.8" d="M14.5 4.5v15"/></svg>
211
+ }
212
+ @case ('chevron-left') {
213
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m14.5 6-6 6 6 6"/></svg>
214
+ }
215
+ @case ('chevron-right') {
216
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m9.5 6 6 6-6 6"/></svg>
217
+ }
218
+ @case ('chevron-up') {
219
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 14.5 6-6 6 6"/></svg>
220
+ }
221
+ @case ('chevron-down') {
222
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9.5 6 6 6-6"/></svg>
223
+ }
224
+ @case ('fullscreen') {
225
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 9V5.5a1 1 0 0 1 1-1H9m6 0h3.5a1 1 0 0 1 1 1V9m0 6v3.5a1 1 0 0 1-1 1H15m-6 0H5.5a1 1 0 0 1-1-1V15"/></svg>
226
+ }
227
+ @case ('fullscreen-exit') {
228
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 4.5V8a1 1 0 0 1-1 1H4.5m15 0H16a1 1 0 0 1-1-1V4.5m0 15V16a1 1 0 0 1 1-1h3.5m-15 0H8a1 1 0 0 1 1 1v3.5"/></svg>
229
+ }
74
230
  }
75
231
  `, isInline: true, styles: [":host{display:block;width:1em;height:1em;line-height:0;flex:0 0 auto}svg{display:block;width:100%;height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
76
232
  }
77
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerIconComponent, decorators: [{
233
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerIconComponent, decorators: [{
78
234
  type: Component,
79
235
  args: [{ selector: 'ngxsp-smart-icon', standalone: true, template: `
80
236
  @switch (name) {
@@ -85,16 +241,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
85
241
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
86
242
  }
87
243
  @case ('prev') {
88
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg>
244
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M19 6.55v10.9a1.25 1.25 0 0 1-1.93 1.05l-8.2-5.45a1.25 1.25 0 0 1 0-2.1l8.2-5.45A1.25 1.25 0 0 1 19 6.55Z"/></svg>
89
245
  }
90
246
  @case ('next') {
91
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg>
247
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M5 6.55v10.9a1.25 1.25 0 0 0 1.93 1.05l8.2-5.45a1.25 1.25 0 0 0 0-2.1l-8.2-5.45A1.25 1.25 0 0 0 5 6.55Z"/></svg>
92
248
  }
93
249
  @case ('seek-back') {
94
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
250
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 13a6.5 6.5 0 1 0 6.5-6.5H8M10 4.5 8 6.5l2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
95
251
  }
96
252
  @case ('seek-forward') {
97
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
253
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18.5 13a6.5 6.5 0 1 1-6.5-6.5H16M14 4.5 16 6.5l-2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
98
254
  }
99
255
  @case ('volume-mute') {
100
256
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
@@ -109,10 +265,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
109
265
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
110
266
  }
111
267
  @case ('repeat') {
112
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
268
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/></svg>
113
269
  }
114
270
  @case ('repeat-one') {
115
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/><text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text></svg>
271
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/><path d="M11 10.9 12.4 10v4.1"/></svg>
116
272
  }
117
273
  @case ('tracks') {
118
274
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
@@ -127,10 +283,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
127
283
  <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
128
284
  }
129
285
  @case ('pip') {
130
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
286
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
131
287
  }
132
288
  @case ('pip-active') {
133
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
289
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5" fill="currentColor" fill-opacity=".22"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
290
+ }
291
+ @case ('airplay') {
292
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m12 14 4.5 6h-9L12 14Z"/></svg>
293
+ }
294
+ @case ('airplay-active') {
295
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path fill="currentColor" stroke="none" d="m12 13.4 5.2 6.9H6.8L12 13.4Z"/></svg>
134
296
  }
135
297
  @case ('cast') {
136
298
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/></svg>
@@ -138,6 +300,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
138
300
  @case ('cast-active') {
139
301
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/><circle cx="7" cy="19" r="1.5" fill="currentColor" stroke="none"/></svg>
140
302
  }
303
+ @case ('devices') {
304
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.4"/><path d="M18.75 11.25h.01"/></svg>
305
+ }
306
+ @case ('devices-active') {
307
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.6" fill="currentColor" stroke="none"/><path d="M18.75 11.25h.01"/></svg>
308
+ }
309
+ @case ('radio') {
310
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 8 16.5 3.75"/><rect x="3" y="8" width="18" height="12.5" rx="2.5"/><circle cx="15.25" cy="14.25" r="2.75"/><path d="M6.75 12.75h3.5M6.75 15.75h3.5"/></svg>
311
+ }
312
+ @case ('server') {
313
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="3.25" width="16" height="11" rx="2"/><path d="M7.5 6.75h.01M7.5 10.75h.01M10.5 6.75h6M10.5 10.75h6"/><path d="M12 14.25v4.5M3.5 20.5h6.75M13.75 20.5h6.75"/><circle cx="12" cy="20.5" r="1.6"/></svg>
314
+ }
315
+ @case ('window-mini') {
316
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 12.5 15 9.5M11 9.5h4v4"/><rect x="5.5" y="13" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
317
+ }
318
+ @case ('window-bar') {
319
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="5" y="15" width="14" height="2.8" rx="1" fill="currentColor" stroke="none"/></svg>
320
+ }
321
+ @case ('cinema') {
322
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
323
+ }
324
+ @case ('cinema-active') {
325
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2" fill="currentColor" fill-opacity=".22"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
326
+ }
327
+ @case ('info') {
328
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg>
329
+ }
330
+ @case ('lyrics') {
331
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.5h16v10H10l-4.5 4v-4H4z"/><path d="M8 9.5h8M8 12.5h5"/></svg>
332
+ }
333
+ @case ('window-restore') {
334
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 14.5 15 9.5M11 9.5h4v4"/></svg>
335
+ }
336
+ @case ('bell') {
337
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.75 10.4a5.25 5.25 0 0 1 10.5 0c0 4.65 1.75 5.6 1.75 5.6H5s1.75-.95 1.75-5.6Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 19a2.15 2.15 0 0 0 4 0"/></svg>
338
+ }
339
+ @case ('thumb-up') {
340
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 10.5v9.25H4.7a1.7 1.7 0 0 1-1.7-1.7V12.2a1.7 1.7 0 0 1 1.7-1.7h2.55Zm0 0 4.85-7.05a1.75 1.75 0 0 1 3.18 1.16l-.44 3.2h3.88a2.1 2.1 0 0 1 2.05 2.55l-1.35 6.45a3.7 3.7 0 0 1-3.62 2.94H7.25"/></svg>
341
+ }
342
+ @case ('thumb-down') {
343
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 13.5V4.25H4.7A1.7 1.7 0 0 0 3 5.95v5.85a1.7 1.7 0 0 0 1.7 1.7h2.55Zm0 0 4.85 7.05a1.75 1.75 0 0 0 3.18-1.16l-.44-3.2h3.88a2.1 2.1 0 0 0 2.05-2.55L19.42 7.2a3.7 3.7 0 0 0-3.62-2.94H7.25"/></svg>
344
+ }
345
+ @case ('share') {
346
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M18.25 8.25 21 5.5l-2.75-2.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.4 5.5h-5.2A8.2 8.2 0 0 0 7 13.7v.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 11.5v6.75A1.75 1.75 0 0 0 5.75 20h12.5A1.75 1.75 0 0 0 20 18.25V14"/></svg>
347
+ }
348
+ @case ('playlist-plus') {
349
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 6.5h11M4 11.5h11M4 16.5h8"/><path stroke-linecap="round" stroke-width="1.8" d="M18 13.5v6m-3-3h6"/></svg>
350
+ }
351
+ @case ('clock') {
352
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 7.75v4.65l3.45 2.05"/></svg>
353
+ }
354
+ @case ('home') {
355
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 11.5 8-6.75 8 6.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.5 10.5v8.25h11v-8.25"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 18.75v-5h4v5"/></svg>
356
+ }
357
+ @case ('layers') {
358
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M12 3.75 3.75 8 12 12.25 20.25 8 12 3.75Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12 7 3.6 7-3.6M5 16l7 3.6 7-3.6"/></svg>
359
+ }
360
+ @case ('video') {
361
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 5v14M16 5v14M4 9h4M4 15h4M16 9h4M16 15h4"/></svg>
362
+ }
363
+ @case ('music') {
364
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 18.25a2.75 2.75 0 1 1-1.6-2.5V6.5l10-2v11.25a2.75 2.75 0 1 1-1.6-2.5V8.2l-8.4 1.68"/></svg>
365
+ }
366
+ @case ('broadcast') {
367
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="2" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8.7 8.7a4.65 4.65 0 0 0 0 6.6m6.6 0a4.65 4.65 0 0 0 0-6.6M6.1 6.1a8.35 8.35 0 0 0 0 11.8m11.8 0a8.35 8.35 0 0 0 0-11.8"/></svg>
368
+ }
369
+ @case ('heart') {
370
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.2 6.2c-1.8-1.9-4.8-1.8-6.5.2L12 8.3l-1.7-1.9c-1.7-2-4.7-2.1-6.5-.2-1.9 2-1.7 5.1.4 6.9L12 20l7.8-6.9c2.1-1.8 2.3-4.9.4-6.9Z"/></svg>
371
+ }
372
+ @case ('download') {
373
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 4v10m0 0 4-4m-4 4-4-4M5 19h14"/></svg>
374
+ }
375
+ @case ('trash') {
376
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 6.75h15M9.75 6.75V5.25A1.5 1.5 0 0 1 11.25 3.75h1.5a1.5 1.5 0 0 1 1.5 1.5v1.5M6.75 6.75l.7 12a1.5 1.5 0 0 0 1.5 1.5h6.1a1.5 1.5 0 0 0 1.5-1.5l.7-12M10.5 10.5v6M13.5 10.5v6"/></svg>
377
+ }
378
+ @case ('folder') {
379
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3.75 7.5A2.25 2.25 0 0 1 6 5.25h4.15l2.1 2.25H18a2.25 2.25 0 0 1 2.25 2.25v6.75A2.25 2.25 0 0 1 18 18.75H6a2.25 2.25 0 0 1-2.25-2.25v-9Z"/></svg>
380
+ }
381
+ @case ('settings') {
382
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 15.25A3.25 3.25 0 1 0 12 8.75a3.25 3.25 0 0 0 0 6.5Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M19.4 13.5a7.8 7.8 0 0 0 .05-3l2-1.55-2-3.45-2.42.98a8 8 0 0 0-2.58-1.5L14.1 2.4h-4.2l-.35 2.58a8 8 0 0 0-2.58 1.5L4.55 5.5l-2 3.45 2 1.55a7.8 7.8 0 0 0 .05 3l-2.05 1.55 2 3.45 2.46-.98a8 8 0 0 0 2.54 1.47l.35 2.61h4.2l.35-2.6a8 8 0 0 0 2.54-1.48l2.46.98 2-3.45-2.05-1.55Z"/></svg>
383
+ }
384
+ @case ('search') {
385
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="10.75" cy="10.75" r="6.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="m15.4 15.4 4.85 4.85"/></svg>
386
+ }
387
+ @case ('grid') {
388
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="4" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/></svg>
389
+ }
390
+ @case ('list') {
391
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.8" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="18" r="1.2" fill="currentColor" stroke="none"/></svg>
392
+ }
393
+ @case ('more') {
394
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>
395
+ }
396
+ @case ('waveform') {
397
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 13v-2m4 5V8m4 11V5m4 10V9m4 4v-2"/></svg>
398
+ }
399
+ @case ('sliders') {
400
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h5m6 0h5M4 17h9m6 0h1"/><circle cx="12" cy="7" r="2.25" stroke-width="1.8"/><circle cx="16" cy="17" r="2.25" stroke-width="1.8"/></svg>
401
+ }
402
+ @case ('network') {
403
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2.3" stroke-width="1.8"/><circle cx="6" cy="18" r="2.3" stroke-width="1.8"/><circle cx="18" cy="18" r="2.3" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M11 7.1 7.1 15.9M13 7.1l3.9 8.8M8.3 18h7.4"/></svg>
404
+ }
405
+ @case ('cloud') {
406
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 18.5h10.2a4.05 4.05 0 0 0 .5-8.07 6.25 6.25 0 0 0-12-1.75A4.95 4.95 0 0 0 7.25 18.5Z"/></svg>
407
+ }
408
+ @case ('spark') {
409
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 3.5 14.3 9l5.7 2.2-5.7 2.3L12 20l-2.3-6.5L4 11.2 9.7 9 12 3.5Z"/></svg>
410
+ }
411
+ @case ('disc') {
412
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><circle cx="12" cy="12" r="2.25" stroke-width="1.8"/></svg>
413
+ }
414
+ @case ('user') {
415
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="8.5" r="3.75" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 19.75a7.25 7.25 0 0 1 14.5 0"/></svg>
416
+ }
417
+ @case ('tag') {
418
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M3.75 5.75v5.1c0 .6.24 1.17.66 1.6l7.4 7.4a2 2 0 0 0 2.83 0l5.1-5.1a2 2 0 0 0 0-2.83l-7.4-7.4a2.25 2.25 0 0 0-1.6-.66h-5a2 2 0 0 0-2 1.89Z"/><circle cx="8.25" cy="8.25" r="1.25" fill="currentColor" stroke="none"/></svg>
419
+ }
420
+ @case ('close') {
421
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="m6.5 6.5 11 11m0-11-11 11"/></svg>
422
+ }
423
+ @case ('plus') {
424
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M12 5v14M5 12h14"/></svg>
425
+ }
426
+ @case ('check') {
427
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12.5 4.5 4.5L19 7.5"/></svg>
428
+ }
429
+ @case ('menu') {
430
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h16M4 12h16M4 17h16"/></svg>
431
+ }
432
+ @case ('panel-right') {
433
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" stroke-width="1.8"/><path stroke-width="1.8" d="M14.5 4.5v15"/></svg>
434
+ }
435
+ @case ('chevron-left') {
436
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m14.5 6-6 6 6 6"/></svg>
437
+ }
438
+ @case ('chevron-right') {
439
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m9.5 6 6 6-6 6"/></svg>
440
+ }
441
+ @case ('chevron-up') {
442
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 14.5 6-6 6 6"/></svg>
443
+ }
444
+ @case ('chevron-down') {
445
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9.5 6 6 6-6"/></svg>
446
+ }
447
+ @case ('fullscreen') {
448
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 9V5.5a1 1 0 0 1 1-1H9m6 0h3.5a1 1 0 0 1 1 1V9m0 6v3.5a1 1 0 0 1-1 1H15m-6 0H5.5a1 1 0 0 1-1-1V15"/></svg>
449
+ }
450
+ @case ('fullscreen-exit') {
451
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 4.5V8a1 1 0 0 1-1 1H4.5m15 0H16a1 1 0 0 1-1-1V4.5m0 15V16a1 1 0 0 1 1-1h3.5m-15 0H8a1 1 0 0 1 1 1v3.5"/></svg>
452
+ }
141
453
  }
142
454
  `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:1em;height:1em;line-height:0;flex:0 0 auto}svg{display:block;width:100%;height:100%}\n"] }]
143
455
  }], propDecorators: { name: [{
@@ -145,6 +457,286 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
145
457
  args: [{ required: true }]
146
458
  }] } });
147
459
 
460
+ /**
461
+ * The settings of a player, grouped in sections. Colours come from the host through CSS variables
462
+ * (`--ngxsp-settings-surface`, `-text`, `-muted`, `-line`, `-control`, `-accent`, `-accent-text`, `-radius`).
463
+ */
464
+ class SmartSettingsComponent {
465
+ sections = [];
466
+ settingChange = new EventEmitter();
467
+ settingAction = new EventEmitter();
468
+ /**
469
+ * Sends what was typed and empties the fields at once. Nothing secret stays in the component:
470
+ * the host is the only place it goes, and it does not come back through an input.
471
+ */
472
+ submitSecret(event, id) {
473
+ event.preventDefault();
474
+ const form = event.target;
475
+ const inputs = Array.from(form.querySelectorAll('input'));
476
+ const value = inputs.at(-1)?.value ?? '';
477
+ const previous = inputs.length > 1 ? inputs[0].value : undefined;
478
+ inputs.forEach((input) => { input.value = ''; });
479
+ if (!value)
480
+ return;
481
+ this.settingChange.emit({ id, value, ...(previous === undefined ? {} : { previous }) });
482
+ }
483
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
484
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartSettingsComponent, isStandalone: true, selector: "ngxsp-settings", inputs: { sections: "sections" }, outputs: { settingChange: "settingChange", settingAction: "settingAction" }, ngImport: i0, template: `
485
+ @for (section of sections; track section.id) {
486
+ <section class="ngxsp-settings-section" [attr.aria-labelledby]="'ngxsp-settings-' + section.id">
487
+ <h2 [id]="'ngxsp-settings-' + section.id">{{ section.title }}</h2>
488
+ <ul class="ngxsp-settings-items">
489
+ @for (item of section.items; track item.id) {
490
+ <li class="ngxsp-settings-item" [class.stacked]="item.kind === 'list'" [class.wraps]="item.kind === 'choice' || item.kind === 'action'">
491
+ <span class="ngxsp-settings-copy">
492
+ <strong [id]="'ngxsp-setting-' + item.id">{{ item.label }}</strong>
493
+ @if (item.description) { <small [id]="'ngxsp-setting-help-' + item.id">{{ item.description }}</small> }
494
+ </span>
495
+ @switch (item.kind) {
496
+ @case ('switch') {
497
+ <button class="ngxsp-settings-switch" type="button" role="switch" [attr.aria-checked]="item.value" [disabled]="item.disabled" [attr.aria-labelledby]="'ngxsp-setting-' + item.id" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingChange.emit({ id: item.id, value: !item.value })">
498
+ <span class="ngxsp-settings-knob" aria-hidden="true"></span>
499
+ </button>
500
+ }
501
+ @case ('choice') {
502
+ <span class="ngxsp-settings-choice" role="group" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
503
+ @for (option of item.options; track option.id) {
504
+ <button type="button" [class.active]="option.id === item.value" [attr.aria-pressed]="option.id === item.value" (click)="option.id !== item.value && settingChange.emit({ id: item.id, value: option.id })">{{ option.label }}</button>
505
+ }
506
+ </span>
507
+ }
508
+ @case ('secret') {
509
+ <form class="ngxsp-settings-secret" (submit)="submitSecret($event, item.id)">
510
+ @if (item.secondPlaceholder) {
511
+ <input
512
+ #firstSecret
513
+ type="password"
514
+ autocomplete="off"
515
+ [attr.inputmode]="item.digits ? 'numeric' : null"
516
+ [attr.placeholder]="item.secondPlaceholder"
517
+ [attr.aria-label]="item.secondPlaceholder"
518
+ [disabled]="item.disabled || item.busy"
519
+ />
520
+ }
521
+ <input
522
+ #secondSecret
523
+ type="password"
524
+ autocomplete="off"
525
+ [attr.inputmode]="item.digits ? 'numeric' : null"
526
+ [attr.placeholder]="item.placeholder ?? null"
527
+ [attr.aria-label]="item.placeholder ?? item.label"
528
+ [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null"
529
+ [disabled]="item.disabled || item.busy"
530
+ />
531
+ <button class="ngxsp-settings-button" type="submit" [disabled]="item.disabled || item.busy">{{ item.actionLabel }}</button>
532
+ </form>
533
+ }
534
+ @case ('action') {
535
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.disabled || item.busy" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
536
+ }
537
+ @case ('list') {
538
+ <ul class="ngxsp-settings-entries" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
539
+ @for (entry of item.entries; track entry.id) {
540
+ <li>
541
+ <span class="ngxsp-settings-entry"><span>{{ entry.label }}</span>@if (entry.detail) { <small>{{ entry.detail }}</small> }</span>
542
+ @if (!entry.fixed) {
543
+ <button class="ngxsp-settings-remove" type="button" [title]="item.removeLabel" [attr.aria-label]="item.removeLabel + ' - ' + entry.label" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id, entryId: entry.id })">
544
+ <ngxsp-smart-icon name="close" />
545
+ </button>
546
+ }
547
+ </li>
548
+ } @empty {
549
+ @if (item.emptyLabel) { <li class="empty">{{ item.emptyLabel }}</li> }
550
+ }
551
+ </ul>
552
+ @if (item.actionLabel) {
553
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
554
+ }
555
+ }
556
+ }
557
+ </li>
558
+ }
559
+ </ul>
560
+ </section>
561
+ }
562
+ `, isInline: true, styles: [":host{--_surface:var(--ngxsp-settings-surface, rgb(255 255 255 / .04));--_text:var(--ngxsp-settings-text, currentColor);--_muted:var(--ngxsp-settings-muted, rgb(127 127 127));--_line:var(--ngxsp-settings-line, rgb(127 127 127 / .3));--_control:var(--ngxsp-settings-control, rgb(127 127 127 / .2));--_accent:var(--ngxsp-settings-accent, #5b5bd6);--_accent-text:var(--ngxsp-settings-accent-text, #fff);--_radius:var(--ngxsp-settings-radius, 10px);display:grid;gap:28px;max-width:760px;color:var(--_text)}.ngxsp-settings-section{display:grid;gap:10px}h2{margin:0;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--_muted);text-transform:uppercase}.ngxsp-settings-items{margin:0;padding:0;list-style:none;display:grid;gap:2px;border-radius:var(--_radius);overflow:hidden}.ngxsp-settings-item{display:flex;align-items:center;justify-content:space-between;gap:12px 24px;padding:14px 16px;background:var(--_surface)}.ngxsp-settings-item.stacked{display:grid;justify-content:stretch}.ngxsp-settings-item.wraps{flex-wrap:wrap}.ngxsp-settings-item.wraps .ngxsp-settings-copy{flex:1 1 220px}.ngxsp-settings-copy{min-width:0;display:grid;gap:4px}.ngxsp-settings-copy strong{font-size:14px;font-weight:600}.ngxsp-settings-copy small{font-size:12px;line-height:1.45;color:var(--_muted)}.ngxsp-settings-switch{position:relative;flex:0 0 auto;width:42px;height:24px;padding:0;border:1px solid var(--_line);border-radius:999px;background:var(--_control);cursor:pointer;transition:background-color .16s ease,border-color .16s ease}.ngxsp-settings-knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #0000004d;transition:transform .16s ease}.ngxsp-settings-switch[aria-checked=true]{border-color:var(--_accent);background:var(--_accent)}.ngxsp-settings-switch[aria-checked=true] .ngxsp-settings-knob{transform:translate(18px)}.ngxsp-settings-choice{flex:0 0 auto;display:inline-flex;padding:2px;gap:2px;border:1px solid var(--_line);border-radius:999px;background:var(--_control)}.ngxsp-settings-choice button{height:28px;padding:0 12px;border:0;border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-choice button.active{background:var(--_accent);color:var(--_accent-text)}.ngxsp-settings-button{justify-self:start;flex:0 0 auto;height:32px;padding:0 14px;border:1px solid var(--_line);border-radius:999px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-button:disabled,.ngxsp-settings-switch:disabled,.ngxsp-settings-remove:disabled{opacity:.5;cursor:default}.ngxsp-settings-secret{display:flex;align-items:center;gap:8px;min-width:0}.ngxsp-settings-secret input{flex:1 1 96px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--_line);border-radius:8px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;letter-spacing:.14em}.ngxsp-settings-secret input::placeholder{color:var(--_muted);letter-spacing:0}.ngxsp-settings-entries{margin:0;padding:0;list-style:none;display:grid;gap:4px}.ngxsp-settings-entries li{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 8px 8px 12px;border:1px solid var(--_line);border-radius:8px}.ngxsp-settings-entries li.empty{display:block;color:var(--_muted);font-size:12px}.ngxsp-settings-entry{min-width:0;display:grid;gap:2px;font-size:13px}.ngxsp-settings-entry small{overflow:hidden;color:var(--_muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-settings-remove{flex:0 0 auto;width:30px;height:30px;display:grid;place-items:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--_muted);cursor:pointer}.ngxsp-settings-remove:hover{background:var(--_control);color:var(--_text)}.ngxsp-settings-remove ngxsp-smart-icon{width:16px;height:16px}button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}@media(max-width:560px){.ngxsp-settings-secret,.ngxsp-settings-choice{flex-wrap:wrap}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
563
+ }
564
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartSettingsComponent, decorators: [{
565
+ type: Component,
566
+ args: [{ selector: 'ngxsp-settings', standalone: true, imports: [SmartPlayerIconComponent], template: `
567
+ @for (section of sections; track section.id) {
568
+ <section class="ngxsp-settings-section" [attr.aria-labelledby]="'ngxsp-settings-' + section.id">
569
+ <h2 [id]="'ngxsp-settings-' + section.id">{{ section.title }}</h2>
570
+ <ul class="ngxsp-settings-items">
571
+ @for (item of section.items; track item.id) {
572
+ <li class="ngxsp-settings-item" [class.stacked]="item.kind === 'list'" [class.wraps]="item.kind === 'choice' || item.kind === 'action'">
573
+ <span class="ngxsp-settings-copy">
574
+ <strong [id]="'ngxsp-setting-' + item.id">{{ item.label }}</strong>
575
+ @if (item.description) { <small [id]="'ngxsp-setting-help-' + item.id">{{ item.description }}</small> }
576
+ </span>
577
+ @switch (item.kind) {
578
+ @case ('switch') {
579
+ <button class="ngxsp-settings-switch" type="button" role="switch" [attr.aria-checked]="item.value" [disabled]="item.disabled" [attr.aria-labelledby]="'ngxsp-setting-' + item.id" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingChange.emit({ id: item.id, value: !item.value })">
580
+ <span class="ngxsp-settings-knob" aria-hidden="true"></span>
581
+ </button>
582
+ }
583
+ @case ('choice') {
584
+ <span class="ngxsp-settings-choice" role="group" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
585
+ @for (option of item.options; track option.id) {
586
+ <button type="button" [class.active]="option.id === item.value" [attr.aria-pressed]="option.id === item.value" (click)="option.id !== item.value && settingChange.emit({ id: item.id, value: option.id })">{{ option.label }}</button>
587
+ }
588
+ </span>
589
+ }
590
+ @case ('secret') {
591
+ <form class="ngxsp-settings-secret" (submit)="submitSecret($event, item.id)">
592
+ @if (item.secondPlaceholder) {
593
+ <input
594
+ #firstSecret
595
+ type="password"
596
+ autocomplete="off"
597
+ [attr.inputmode]="item.digits ? 'numeric' : null"
598
+ [attr.placeholder]="item.secondPlaceholder"
599
+ [attr.aria-label]="item.secondPlaceholder"
600
+ [disabled]="item.disabled || item.busy"
601
+ />
602
+ }
603
+ <input
604
+ #secondSecret
605
+ type="password"
606
+ autocomplete="off"
607
+ [attr.inputmode]="item.digits ? 'numeric' : null"
608
+ [attr.placeholder]="item.placeholder ?? null"
609
+ [attr.aria-label]="item.placeholder ?? item.label"
610
+ [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null"
611
+ [disabled]="item.disabled || item.busy"
612
+ />
613
+ <button class="ngxsp-settings-button" type="submit" [disabled]="item.disabled || item.busy">{{ item.actionLabel }}</button>
614
+ </form>
615
+ }
616
+ @case ('action') {
617
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.disabled || item.busy" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
618
+ }
619
+ @case ('list') {
620
+ <ul class="ngxsp-settings-entries" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
621
+ @for (entry of item.entries; track entry.id) {
622
+ <li>
623
+ <span class="ngxsp-settings-entry"><span>{{ entry.label }}</span>@if (entry.detail) { <small>{{ entry.detail }}</small> }</span>
624
+ @if (!entry.fixed) {
625
+ <button class="ngxsp-settings-remove" type="button" [title]="item.removeLabel" [attr.aria-label]="item.removeLabel + ' - ' + entry.label" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id, entryId: entry.id })">
626
+ <ngxsp-smart-icon name="close" />
627
+ </button>
628
+ }
629
+ </li>
630
+ } @empty {
631
+ @if (item.emptyLabel) { <li class="empty">{{ item.emptyLabel }}</li> }
632
+ }
633
+ </ul>
634
+ @if (item.actionLabel) {
635
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
636
+ }
637
+ }
638
+ }
639
+ </li>
640
+ }
641
+ </ul>
642
+ </section>
643
+ }
644
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--_surface:var(--ngxsp-settings-surface, rgb(255 255 255 / .04));--_text:var(--ngxsp-settings-text, currentColor);--_muted:var(--ngxsp-settings-muted, rgb(127 127 127));--_line:var(--ngxsp-settings-line, rgb(127 127 127 / .3));--_control:var(--ngxsp-settings-control, rgb(127 127 127 / .2));--_accent:var(--ngxsp-settings-accent, #5b5bd6);--_accent-text:var(--ngxsp-settings-accent-text, #fff);--_radius:var(--ngxsp-settings-radius, 10px);display:grid;gap:28px;max-width:760px;color:var(--_text)}.ngxsp-settings-section{display:grid;gap:10px}h2{margin:0;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--_muted);text-transform:uppercase}.ngxsp-settings-items{margin:0;padding:0;list-style:none;display:grid;gap:2px;border-radius:var(--_radius);overflow:hidden}.ngxsp-settings-item{display:flex;align-items:center;justify-content:space-between;gap:12px 24px;padding:14px 16px;background:var(--_surface)}.ngxsp-settings-item.stacked{display:grid;justify-content:stretch}.ngxsp-settings-item.wraps{flex-wrap:wrap}.ngxsp-settings-item.wraps .ngxsp-settings-copy{flex:1 1 220px}.ngxsp-settings-copy{min-width:0;display:grid;gap:4px}.ngxsp-settings-copy strong{font-size:14px;font-weight:600}.ngxsp-settings-copy small{font-size:12px;line-height:1.45;color:var(--_muted)}.ngxsp-settings-switch{position:relative;flex:0 0 auto;width:42px;height:24px;padding:0;border:1px solid var(--_line);border-radius:999px;background:var(--_control);cursor:pointer;transition:background-color .16s ease,border-color .16s ease}.ngxsp-settings-knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #0000004d;transition:transform .16s ease}.ngxsp-settings-switch[aria-checked=true]{border-color:var(--_accent);background:var(--_accent)}.ngxsp-settings-switch[aria-checked=true] .ngxsp-settings-knob{transform:translate(18px)}.ngxsp-settings-choice{flex:0 0 auto;display:inline-flex;padding:2px;gap:2px;border:1px solid var(--_line);border-radius:999px;background:var(--_control)}.ngxsp-settings-choice button{height:28px;padding:0 12px;border:0;border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-choice button.active{background:var(--_accent);color:var(--_accent-text)}.ngxsp-settings-button{justify-self:start;flex:0 0 auto;height:32px;padding:0 14px;border:1px solid var(--_line);border-radius:999px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-button:disabled,.ngxsp-settings-switch:disabled,.ngxsp-settings-remove:disabled{opacity:.5;cursor:default}.ngxsp-settings-secret{display:flex;align-items:center;gap:8px;min-width:0}.ngxsp-settings-secret input{flex:1 1 96px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--_line);border-radius:8px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;letter-spacing:.14em}.ngxsp-settings-secret input::placeholder{color:var(--_muted);letter-spacing:0}.ngxsp-settings-entries{margin:0;padding:0;list-style:none;display:grid;gap:4px}.ngxsp-settings-entries li{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 8px 8px 12px;border:1px solid var(--_line);border-radius:8px}.ngxsp-settings-entries li.empty{display:block;color:var(--_muted);font-size:12px}.ngxsp-settings-entry{min-width:0;display:grid;gap:2px;font-size:13px}.ngxsp-settings-entry small{overflow:hidden;color:var(--_muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-settings-remove{flex:0 0 auto;width:30px;height:30px;display:grid;place-items:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--_muted);cursor:pointer}.ngxsp-settings-remove:hover{background:var(--_control);color:var(--_text)}.ngxsp-settings-remove ngxsp-smart-icon{width:16px;height:16px}button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}@media(max-width:560px){.ngxsp-settings-secret,.ngxsp-settings-choice{flex-wrap:wrap}}\n"] }]
645
+ }], propDecorators: { sections: [{
646
+ type: Input
647
+ }], settingChange: [{
648
+ type: Output
649
+ }], settingAction: [{
650
+ type: Output
651
+ }] } });
652
+
653
+ let nextDialogId = 0;
654
+ /**
655
+ * A question the user answers before anything happens (another device asking to send its playback here). The
656
+ * cautious answer has the focus and Escape; the dialog stays until one of the two buttons is pressed. Colours come
657
+ * from the host through CSS variables (`--ngxsp-dialog-surface`, `-text`, `-muted`, `-line`, `-accent`,
658
+ * `-accent-text`, `-radius`).
659
+ */
660
+ class SmartConfirmDialogComponent {
661
+ title = '';
662
+ message = '';
663
+ confirmLabel = 'OK';
664
+ cancelLabel = 'Cancel';
665
+ confirmed = new EventEmitter();
666
+ cancelled = new EventEmitter();
667
+ cancelButton;
668
+ id = `ngxsp-dialog-${++nextDialogId}`;
669
+ ngAfterViewInit() {
670
+ this.cancelButton?.nativeElement.focus({ preventScroll: true });
671
+ }
672
+ onEscape(event) {
673
+ event.preventDefault();
674
+ this.cancelled.emit();
675
+ }
676
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
677
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.33", type: SmartConfirmDialogComponent, isStandalone: true, selector: "ngxsp-confirm-dialog", inputs: { title: "title", message: "message", confirmLabel: "confirmLabel", cancelLabel: "cancelLabel" }, outputs: { confirmed: "confirmed", cancelled: "cancelled" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" } }, viewQueries: [{ propertyName: "cancelButton", first: true, predicate: ["cancelButton"], descendants: true }], ngImport: i0, template: `
678
+ <div class="ngxsp-dialog-backdrop" role="presentation">
679
+ <section class="ngxsp-dialog" role="alertdialog" aria-modal="true" [attr.aria-labelledby]="id + '-title'" [attr.aria-describedby]="id + '-message'">
680
+ <h2 [id]="id + '-title'">{{ title }}</h2>
681
+ <p [id]="id + '-message'">{{ message }}</p>
682
+ <div class="ngxsp-dialog-actions">
683
+ <button #cancelButton class="ngxsp-dialog-button" type="button" (click)="cancelled.emit()">{{ cancelLabel }}</button>
684
+ <button class="ngxsp-dialog-button primary" type="button" (click)="confirmed.emit()">{{ confirmLabel }}</button>
685
+ </div>
686
+ </section>
687
+ </div>
688
+ `, isInline: true, styles: [":host{--_surface:var(--ngxsp-dialog-surface, #17181d);--_text:var(--ngxsp-dialog-text, #ededf0);--_muted:var(--ngxsp-dialog-muted, #a3a5ae);--_line:var(--ngxsp-dialog-line, rgb(255 255 255 / .13));--_accent:var(--ngxsp-dialog-accent, #5b5bd6);--_accent-text:var(--ngxsp-dialog-accent-text, #fff);--_radius:var(--ngxsp-dialog-radius, 14px)}.ngxsp-dialog-backdrop{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:16px;background:#05060899;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.ngxsp-dialog{width:min(420px,100%);display:grid;gap:12px;padding:20px;border:1px solid var(--_line);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);box-shadow:0 24px 64px #00000073}h2{margin:0;font-size:17px;font-weight:650;line-height:1.3}p{margin:0;color:var(--_muted);font-size:14px;line-height:1.5}.ngxsp-dialog-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-dialog-button{min-height:36px;padding:0 16px;border:1px solid var(--_line);border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:13px;font-weight:600;cursor:pointer}.ngxsp-dialog-button.primary{border-color:var(--_accent);background:var(--_accent);color:var(--_accent-text)}.ngxsp-dialog-button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
689
+ }
690
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartConfirmDialogComponent, decorators: [{
691
+ type: Component,
692
+ args: [{ selector: 'ngxsp-confirm-dialog', standalone: true, template: `
693
+ <div class="ngxsp-dialog-backdrop" role="presentation">
694
+ <section class="ngxsp-dialog" role="alertdialog" aria-modal="true" [attr.aria-labelledby]="id + '-title'" [attr.aria-describedby]="id + '-message'">
695
+ <h2 [id]="id + '-title'">{{ title }}</h2>
696
+ <p [id]="id + '-message'">{{ message }}</p>
697
+ <div class="ngxsp-dialog-actions">
698
+ <button #cancelButton class="ngxsp-dialog-button" type="button" (click)="cancelled.emit()">{{ cancelLabel }}</button>
699
+ <button class="ngxsp-dialog-button primary" type="button" (click)="confirmed.emit()">{{ confirmLabel }}</button>
700
+ </div>
701
+ </section>
702
+ </div>
703
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--_surface:var(--ngxsp-dialog-surface, #17181d);--_text:var(--ngxsp-dialog-text, #ededf0);--_muted:var(--ngxsp-dialog-muted, #a3a5ae);--_line:var(--ngxsp-dialog-line, rgb(255 255 255 / .13));--_accent:var(--ngxsp-dialog-accent, #5b5bd6);--_accent-text:var(--ngxsp-dialog-accent-text, #fff);--_radius:var(--ngxsp-dialog-radius, 14px)}.ngxsp-dialog-backdrop{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:16px;background:#05060899;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.ngxsp-dialog{width:min(420px,100%);display:grid;gap:12px;padding:20px;border:1px solid var(--_line);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);box-shadow:0 24px 64px #00000073}h2{margin:0;font-size:17px;font-weight:650;line-height:1.3}p{margin:0;color:var(--_muted);font-size:14px;line-height:1.5}.ngxsp-dialog-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-dialog-button{min-height:36px;padding:0 16px;border:1px solid var(--_line);border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:13px;font-weight:600;cursor:pointer}.ngxsp-dialog-button.primary{border-color:var(--_accent);background:var(--_accent);color:var(--_accent-text)}.ngxsp-dialog-button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}\n"] }]
704
+ }], propDecorators: { title: [{
705
+ type: Input
706
+ }], message: [{
707
+ type: Input
708
+ }], confirmLabel: [{
709
+ type: Input
710
+ }], cancelLabel: [{
711
+ type: Input
712
+ }], confirmed: [{
713
+ type: Output
714
+ }], cancelled: [{
715
+ type: Output
716
+ }], cancelButton: [{
717
+ type: ViewChild,
718
+ args: ['cancelButton']
719
+ }], onEscape: [{
720
+ type: HostListener,
721
+ args: ['document:keydown.escape', ['$event']]
722
+ }] } });
723
+
724
+ const DEFAULT_PLAYER_MENU_LABELS = {
725
+ captions: 'Captions',
726
+ off: 'Off',
727
+ audio: 'Audio',
728
+ singleAudio: 'Single audio track',
729
+ quality: 'Quality',
730
+ auto: 'Auto',
731
+ singleQuality: 'Single quality',
732
+ speed: 'Playback speed',
733
+ subtitles: 'Subtitles',
734
+ current: 'Showing',
735
+ noSubtitles: 'No subtitles in this video yet.',
736
+ loadSubtitles: 'Load a file (SRT, VTT)…',
737
+ tracks: 'Tracks',
738
+ media: 'Media',
739
+ };
148
740
  /**
149
741
  * Menu de pistes (sous-titres / audio / qualité) et de vitesse de lecture.
150
742
  * Extrait du SmartPlayerComponent (décomposition, cf. docs/AUDIT.md).
@@ -161,12 +753,28 @@ class PlayerMenuComponent {
161
753
  selectedVideoTrackId = input(null, ...(ngDevMode ? [{ debugName: "selectedVideoTrackId" }] : []));
162
754
  playbackRates = input([], ...(ngDevMode ? [{ debugName: "playbackRates" }] : []));
163
755
  playbackRate = input(1, ...(ngDevMode ? [{ debugName: "playbackRate" }] : []));
756
+ labels = input(DEFAULT_PLAYER_MENU_LABELS, ...(ngDevMode ? [{ debugName: "labels" }] : []));
164
757
  selectText = output();
165
758
  selectAudio = output();
166
759
  selectVideo = output();
167
760
  selectRate = output();
761
+ /** "Load a file" in the subtitles menu: the host opens its file picker. */
762
+ loadSubtitles = output();
763
+ currentTextLabel() {
764
+ const track = this.textTracks().find((entry) => entry.id === this.selectedTextTrackId());
765
+ return track ? this.trackLabel(track) : this.labels().off;
766
+ }
767
+ /** "French" for "fr", in the labels' locale. */
768
+ languageName(code) {
769
+ try {
770
+ return new Intl.DisplayNames(this.labels().locale ? [this.labels().locale] : undefined, { type: 'language' }).of(code) ?? code;
771
+ }
772
+ catch {
773
+ return code;
774
+ }
775
+ }
168
776
  trackLabel(track) {
169
- return track.label || track.language || track.id;
777
+ return track.label || (track.language ? this.languageName(track.language) : track.id);
170
778
  }
171
779
  qualityLabel(track) {
172
780
  if (track.label)
@@ -178,13 +786,13 @@ class PlayerMenuComponent {
178
786
  parts.push(`${(track.bandwidth / 1_000_000).toFixed(1)} Mbps`);
179
787
  return parts.join(' • ') || track.id;
180
788
  }
181
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PlayerMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
182
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PlayerMenuComponent, isStandalone: true, selector: "ngxsp-player-menu", inputs: { openMenu: { classPropertyName: "openMenu", publicName: "openMenu", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, textTracks: { classPropertyName: "textTracks", publicName: "textTracks", isSignal: true, isRequired: false, transformFunction: null }, audioTracks: { classPropertyName: "audioTracks", publicName: "audioTracks", isSignal: true, isRequired: false, transformFunction: null }, videoTracks: { classPropertyName: "videoTracks", publicName: "videoTracks", isSignal: true, isRequired: false, transformFunction: null }, selectedTextTrackId: { classPropertyName: "selectedTextTrackId", publicName: "selectedTextTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedAudioTrackId: { classPropertyName: "selectedAudioTrackId", publicName: "selectedAudioTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedVideoTrackId: { classPropertyName: "selectedVideoTrackId", publicName: "selectedVideoTrackId", isSignal: true, isRequired: false, transformFunction: null }, playbackRates: { classPropertyName: "playbackRates", publicName: "playbackRates", isSignal: true, isRequired: false, transformFunction: null }, playbackRate: { classPropertyName: "playbackRate", publicName: "playbackRate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectText: "selectText", selectAudio: "selectAudio", selectVideo: "selectVideo", selectRate: "selectRate" }, ngImport: i0, template: `
789
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
790
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerMenuComponent, isStandalone: true, selector: "ngxsp-player-menu", inputs: { openMenu: { classPropertyName: "openMenu", publicName: "openMenu", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, textTracks: { classPropertyName: "textTracks", publicName: "textTracks", isSignal: true, isRequired: false, transformFunction: null }, audioTracks: { classPropertyName: "audioTracks", publicName: "audioTracks", isSignal: true, isRequired: false, transformFunction: null }, videoTracks: { classPropertyName: "videoTracks", publicName: "videoTracks", isSignal: true, isRequired: false, transformFunction: null }, selectedTextTrackId: { classPropertyName: "selectedTextTrackId", publicName: "selectedTextTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedAudioTrackId: { classPropertyName: "selectedAudioTrackId", publicName: "selectedAudioTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedVideoTrackId: { classPropertyName: "selectedVideoTrackId", publicName: "selectedVideoTrackId", isSignal: true, isRequired: false, transformFunction: null }, playbackRates: { classPropertyName: "playbackRates", publicName: "playbackRates", isSignal: true, isRequired: false, transformFunction: null }, playbackRate: { classPropertyName: "playbackRate", publicName: "playbackRate", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectText: "selectText", selectAudio: "selectAudio", selectVideo: "selectVideo", selectRate: "selectRate", loadSubtitles: "loadSubtitles" }, ngImport: i0, template: `
183
791
  @if (openMenu() === 'tracks') {
184
792
  <div class="ngxsp-menu">
185
793
  <div class="ngxsp-menuSection">
186
- <div class="ngxsp-menuTitle">Captions</div>
187
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">Off</button>
794
+ <div class="ngxsp-menuTitle">{{ labels().captions }}</div>
795
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
188
796
  @for (track of textTracks(); track track.id) {
189
797
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
190
798
  {{ trackLabel(track) }}
@@ -193,7 +801,7 @@ class PlayerMenuComponent {
193
801
  </div>
194
802
 
195
803
  <div class="ngxsp-menuSection">
196
- <div class="ngxsp-menuTitle">Audio</div>
804
+ <div class="ngxsp-menuTitle">{{ labels().audio }}</div>
197
805
  @if (audioTracks().length) {
198
806
  @for (track of audioTracks(); track track.id) {
199
807
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId() === track.id" (click)="selectAudio.emit(track.id)">
@@ -201,14 +809,14 @@ class PlayerMenuComponent {
201
809
  </button>
202
810
  }
203
811
  } @else {
204
- <div class="ngxsp-menuHint">Single audio track</div>
812
+ <div class="ngxsp-menuHint">{{ labels().singleAudio }}</div>
205
813
  }
206
814
  </div>
207
815
 
208
816
  @if (!isAudioMode()) {
209
817
  <div class="ngxsp-menuSection">
210
- <div class="ngxsp-menuTitle">Quality</div>
211
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">Auto</button>
818
+ <div class="ngxsp-menuTitle">{{ labels().quality }}</div>
819
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">{{ labels().auto }}</button>
212
820
  @if (videoTracks().length) {
213
821
  @for (track of videoTracks(); track track.id) {
214
822
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === track.id" (click)="selectVideo.emit(track.id)">
@@ -216,7 +824,7 @@ class PlayerMenuComponent {
216
824
  </button>
217
825
  }
218
826
  } @else {
219
- <div class="ngxsp-menuHint">Single quality</div>
827
+ <div class="ngxsp-menuHint">{{ labels().singleQuality }}</div>
220
828
  }
221
829
  </div>
222
830
  }
@@ -226,7 +834,7 @@ class PlayerMenuComponent {
226
834
  @if (openMenu() === 'speed') {
227
835
  <div class="ngxsp-menu ngxsp-menuCompact">
228
836
  <div class="ngxsp-menuSection">
229
- <div class="ngxsp-menuTitle">Playback speed</div>
837
+ <div class="ngxsp-menuTitle">{{ labels().speed }}</div>
230
838
  @for (rate of playbackRates(); track rate) {
231
839
  <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate() === rate" (click)="selectRate.emit(rate)">
232
840
  {{ rate }}x
@@ -235,16 +843,36 @@ class PlayerMenuComponent {
235
843
  </div>
236
844
  </div>
237
845
  }
238
- `, isInline: true, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;padding-top:10px}.ngxsp-menuCompact{grid-template-columns:minmax(180px,240px)}.ngxsp-menuSection{display:flex;flex-direction:column;gap:6px}.ngxsp-menuTitle{font-size:11px;text-transform:uppercase;letter-spacing:.16em;color:#bfdbfe8f}.ngxsp-menuItem{background:linear-gradient(180deg,#0f1c2ce6,#0a1421e6);border:1px solid rgba(148,163,184,.14);color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:9px 11px;text-align:left;cursor:pointer;font-size:12px}.ngxsp-menuItem.active{border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));background:linear-gradient(180deg,#14304af5,#0d2135f5);color:#d8f3ff}.ngxsp-menuItem:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-menuHint{font-size:12px;color:#bfdbfe8f;padding:8px 2px}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
846
+
847
+ @if (openMenu() === 'captions') {
848
+ <div class="ngxsp-menu ngxsp-menuCompact ngxsp-menuCaptions">
849
+ <div class="ngxsp-menuSection">
850
+ <div class="ngxsp-menuTitle">{{ labels().subtitles }}</div>
851
+ <div class="ngxsp-menuCurrent">{{ labels().current }} <strong>{{ currentTextLabel() }}</strong></div>
852
+ <div class="ngxsp-menuScroll" role="group" [attr.aria-label]="labels().subtitles">
853
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" [attr.aria-pressed]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
854
+ @for (track of textTracks(); track track.id) {
855
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" [attr.aria-pressed]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
856
+ <span class="ngxsp-menuItemLabel">{{ trackLabel(track) }}</span>
857
+ @if (track.language && track.label) { <small>{{ languageName(track.language) }}</small> }
858
+ </button>
859
+ }
860
+ </div>
861
+ @if (!textTracks().length) { <div class="ngxsp-menuHint">{{ labels().noSubtitles }}</div> }
862
+ <button type="button" class="ngxsp-menuItem ngxsp-menuAction" (click)="loadSubtitles.emit()">{{ labels().loadSubtitles }}</button>
863
+ </div>
864
+ </div>
865
+ }
866
+ `, isInline: true, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:6px 0 4px;padding:12px;border-radius:10px;background:#121317eb;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.ngxsp-menuCompact{grid-template-columns:minmax(160px,220px);justify-self:end}.ngxsp-menuSection{display:flex;flex-direction:column;gap:2px}.ngxsp-menuTitle{padding:0 8px 6px;font-size:12px;font-weight:600;color:#fff9}.ngxsp-menuItem{height:32px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:#ffffffe6;text-align:left;cursor:pointer;font:inherit;font-size:13px}.ngxsp-menuItem:hover{background:#ffffff1a}.ngxsp-menuItem.active{background:#8b90ff2e;color:var(--nsp-media-accent, #8b90ff);font-weight:500}.ngxsp-menuItem:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:-2px}.ngxsp-menuHint{font-size:12px;color:#fff9;padding:6px 8px}.ngxsp-menuCaptions{grid-template-columns:minmax(200px,280px)}.ngxsp-menuCurrent{padding:0 8px 8px;font-size:12px;color:#fff9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuCurrent strong{color:#ffffffeb;font-weight:600}.ngxsp-menuScroll{display:flex;flex-direction:column;gap:2px;max-height:min(36vh,232px);overflow:auto;overscroll-behavior:contain}.ngxsp-menuItem{display:flex;align-items:center;gap:8px;flex:none}.ngxsp-menuItemLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuItem small{flex:none;font-size:11px;color:#ffffff8c}.ngxsp-menuAction{margin-top:6px;border-top:1px solid rgb(255 255 255 / .1);border-radius:0 0 6px 6px;color:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
239
867
  }
240
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PlayerMenuComponent, decorators: [{
868
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerMenuComponent, decorators: [{
241
869
  type: Component,
242
870
  args: [{ selector: 'ngxsp-player-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
243
871
  @if (openMenu() === 'tracks') {
244
872
  <div class="ngxsp-menu">
245
873
  <div class="ngxsp-menuSection">
246
- <div class="ngxsp-menuTitle">Captions</div>
247
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">Off</button>
874
+ <div class="ngxsp-menuTitle">{{ labels().captions }}</div>
875
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
248
876
  @for (track of textTracks(); track track.id) {
249
877
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
250
878
  {{ trackLabel(track) }}
@@ -253,7 +881,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
253
881
  </div>
254
882
 
255
883
  <div class="ngxsp-menuSection">
256
- <div class="ngxsp-menuTitle">Audio</div>
884
+ <div class="ngxsp-menuTitle">{{ labels().audio }}</div>
257
885
  @if (audioTracks().length) {
258
886
  @for (track of audioTracks(); track track.id) {
259
887
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId() === track.id" (click)="selectAudio.emit(track.id)">
@@ -261,14 +889,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
261
889
  </button>
262
890
  }
263
891
  } @else {
264
- <div class="ngxsp-menuHint">Single audio track</div>
892
+ <div class="ngxsp-menuHint">{{ labels().singleAudio }}</div>
265
893
  }
266
894
  </div>
267
895
 
268
896
  @if (!isAudioMode()) {
269
897
  <div class="ngxsp-menuSection">
270
- <div class="ngxsp-menuTitle">Quality</div>
271
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">Auto</button>
898
+ <div class="ngxsp-menuTitle">{{ labels().quality }}</div>
899
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">{{ labels().auto }}</button>
272
900
  @if (videoTracks().length) {
273
901
  @for (track of videoTracks(); track track.id) {
274
902
  <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === track.id" (click)="selectVideo.emit(track.id)">
@@ -276,7 +904,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
276
904
  </button>
277
905
  }
278
906
  } @else {
279
- <div class="ngxsp-menuHint">Single quality</div>
907
+ <div class="ngxsp-menuHint">{{ labels().singleQuality }}</div>
280
908
  }
281
909
  </div>
282
910
  }
@@ -286,7 +914,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
286
914
  @if (openMenu() === 'speed') {
287
915
  <div class="ngxsp-menu ngxsp-menuCompact">
288
916
  <div class="ngxsp-menuSection">
289
- <div class="ngxsp-menuTitle">Playback speed</div>
917
+ <div class="ngxsp-menuTitle">{{ labels().speed }}</div>
290
918
  @for (rate of playbackRates(); track rate) {
291
919
  <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate() === rate" (click)="selectRate.emit(rate)">
292
920
  {{ rate }}x
@@ -295,8 +923,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
295
923
  </div>
296
924
  </div>
297
925
  }
298
- `, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;padding-top:10px}.ngxsp-menuCompact{grid-template-columns:minmax(180px,240px)}.ngxsp-menuSection{display:flex;flex-direction:column;gap:6px}.ngxsp-menuTitle{font-size:11px;text-transform:uppercase;letter-spacing:.16em;color:#bfdbfe8f}.ngxsp-menuItem{background:linear-gradient(180deg,#0f1c2ce6,#0a1421e6);border:1px solid rgba(148,163,184,.14);color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:9px 11px;text-align:left;cursor:pointer;font-size:12px}.ngxsp-menuItem.active{border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));background:linear-gradient(180deg,#14304af5,#0d2135f5);color:#d8f3ff}.ngxsp-menuItem:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-menuHint{font-size:12px;color:#bfdbfe8f;padding:8px 2px}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"] }]
299
- }], propDecorators: { openMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "openMenu", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], textTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "textTracks", required: false }] }], audioTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioTracks", required: false }] }], videoTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoTracks", required: false }] }], selectedTextTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedTextTrackId", required: false }] }], selectedAudioTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedAudioTrackId", required: false }] }], selectedVideoTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedVideoTrackId", required: false }] }], playbackRates: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRates", required: false }] }], playbackRate: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRate", required: false }] }], selectText: [{ type: i0.Output, args: ["selectText"] }], selectAudio: [{ type: i0.Output, args: ["selectAudio"] }], selectVideo: [{ type: i0.Output, args: ["selectVideo"] }], selectRate: [{ type: i0.Output, args: ["selectRate"] }] } });
926
+
927
+ @if (openMenu() === 'captions') {
928
+ <div class="ngxsp-menu ngxsp-menuCompact ngxsp-menuCaptions">
929
+ <div class="ngxsp-menuSection">
930
+ <div class="ngxsp-menuTitle">{{ labels().subtitles }}</div>
931
+ <div class="ngxsp-menuCurrent">{{ labels().current }} <strong>{{ currentTextLabel() }}</strong></div>
932
+ <div class="ngxsp-menuScroll" role="group" [attr.aria-label]="labels().subtitles">
933
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" [attr.aria-pressed]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
934
+ @for (track of textTracks(); track track.id) {
935
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" [attr.aria-pressed]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
936
+ <span class="ngxsp-menuItemLabel">{{ trackLabel(track) }}</span>
937
+ @if (track.language && track.label) { <small>{{ languageName(track.language) }}</small> }
938
+ </button>
939
+ }
940
+ </div>
941
+ @if (!textTracks().length) { <div class="ngxsp-menuHint">{{ labels().noSubtitles }}</div> }
942
+ <button type="button" class="ngxsp-menuItem ngxsp-menuAction" (click)="loadSubtitles.emit()">{{ labels().loadSubtitles }}</button>
943
+ </div>
944
+ </div>
945
+ }
946
+ `, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:6px 0 4px;padding:12px;border-radius:10px;background:#121317eb;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.ngxsp-menuCompact{grid-template-columns:minmax(160px,220px);justify-self:end}.ngxsp-menuSection{display:flex;flex-direction:column;gap:2px}.ngxsp-menuTitle{padding:0 8px 6px;font-size:12px;font-weight:600;color:#fff9}.ngxsp-menuItem{height:32px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:#ffffffe6;text-align:left;cursor:pointer;font:inherit;font-size:13px}.ngxsp-menuItem:hover{background:#ffffff1a}.ngxsp-menuItem.active{background:#8b90ff2e;color:var(--nsp-media-accent, #8b90ff);font-weight:500}.ngxsp-menuItem:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:-2px}.ngxsp-menuHint{font-size:12px;color:#fff9;padding:6px 8px}.ngxsp-menuCaptions{grid-template-columns:minmax(200px,280px)}.ngxsp-menuCurrent{padding:0 8px 8px;font-size:12px;color:#fff9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuCurrent strong{color:#ffffffeb;font-weight:600}.ngxsp-menuScroll{display:flex;flex-direction:column;gap:2px;max-height:min(36vh,232px);overflow:auto;overscroll-behavior:contain}.ngxsp-menuItem{display:flex;align-items:center;gap:8px;flex:none}.ngxsp-menuItemLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuItem small{flex:none;font-size:11px;color:#ffffff8c}.ngxsp-menuAction{margin-top:6px;border-top:1px solid rgb(255 255 255 / .1);border-radius:0 0 6px 6px;color:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"] }]
947
+ }], propDecorators: { openMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "openMenu", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], textTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "textTracks", required: false }] }], audioTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioTracks", required: false }] }], videoTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoTracks", required: false }] }], selectedTextTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedTextTrackId", required: false }] }], selectedAudioTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedAudioTrackId", required: false }] }], selectedVideoTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedVideoTrackId", required: false }] }], playbackRates: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRates", required: false }] }], playbackRate: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRate", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], selectText: [{ type: i0.Output, args: ["selectText"] }], selectAudio: [{ type: i0.Output, args: ["selectAudio"] }], selectVideo: [{ type: i0.Output, args: ["selectVideo"] }], selectRate: [{ type: i0.Output, args: ["selectRate"] }], loadSubtitles: [{ type: i0.Output, args: ["loadSubtitles"] }] } });
300
948
 
301
949
  /**
302
950
  * Surcouches du lecteur : carte d'erreur de lecture et panneau de diagnostic.
@@ -341,21 +989,19 @@ class PlayerOverlaysComponent {
341
989
  return this.error()?.message ? String(this.error().message) : 'Unknown error';
342
990
  }
343
991
  }, ...(ngDevMode ? [{ debugName: "hint" }] : []));
344
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PlayerOverlaysComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
345
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PlayerOverlaysComponent, isStandalone: true, selector: "ngxsp-player-overlays", inputs: { error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, debugJson: { classPropertyName: "debugJson", publicName: "debugJson", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry", requestPlay: "requestPlay", copyJson: "copyJson", downloadJson: "downloadJson" }, ngImport: i0, template: `
992
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerOverlaysComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
993
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerOverlaysComponent, isStandalone: true, selector: "ngxsp-player-overlays", inputs: { error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, debugJson: { classPropertyName: "debugJson", publicName: "debugJson", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry", requestPlay: "requestPlay", copyJson: "copyJson", downloadJson: "downloadJson" }, ngImport: i0, template: `
346
994
  @if (error()) {
347
995
  <div class="ngxsp-overlay" role="alert">
348
996
  <div class="ngxsp-overlay-card">
349
- <div class="ngxsp-overlay-title">Playback error</div>
350
- <div class="ngxsp-overlay-msg">
351
- <div style="font-weight:600; margin-bottom:6px;">{{ title() }}</div>
352
- <div style="opacity:.85; font-size: 13px; line-height: 1.3;">{{ hint() }}</div>
353
- <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
354
- <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
355
- @if (autoplayBlocked()) {
356
- <button type="button" class="ngxsp-btn" (click)="requestPlay.emit()">Play</button>
357
- }
358
- </div>
997
+ <span class="ngxsp-overlay-icon" aria-hidden="true">!</span>
998
+ <div class="ngxsp-overlay-title">{{ title() }}</div>
999
+ <div class="ngxsp-overlay-msg">{{ hint() }}</div>
1000
+ <div class="ngxsp-overlay-actions">
1001
+ <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
1002
+ @if (autoplayBlocked()) {
1003
+ <button type="button" class="ngxsp-btn primary" (click)="requestPlay.emit()">Play</button>
1004
+ }
359
1005
  </div>
360
1006
  </div>
361
1007
  </div>
@@ -364,31 +1010,29 @@ class PlayerOverlaysComponent {
364
1010
  @if (debug()) {
365
1011
  <details class="ngxsp-debug">
366
1012
  <summary>Debug</summary>
367
- <div style="display:flex; gap:8px; margin:8px 0; flex-wrap:wrap;">
1013
+ <div class="ngxsp-debug-actions">
368
1014
  <button type="button" class="ngxsp-btn" (click)="copyJson.emit()">Copy JSON</button>
369
1015
  <button type="button" class="ngxsp-btn" (click)="downloadJson.emit()">Download</button>
370
1016
  </div>
371
1017
  <pre>{{ debugJson() }}</pre>
372
1018
  </details>
373
1019
  }
374
- `, isInline: true, styles: [":host{display:contents}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:#03080fb8;padding:20px;text-align:center}.ngxsp-overlay-title{font-size:18px;font-weight:700;letter-spacing:.01em;color:var(--ngxsp-text, #e6eef8)}.ngxsp-overlay-msg{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));max-width:720px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:var(--ngxsp-text, #e6eef8)}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;background:#08121eeb;border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));border-radius:999px;padding:8px 12px;width:max-content;margin-left:auto;color:var(--ngxsp-text-dim, rgba(230,238,248,.72))}.ngxsp-debug[open]{background:#08121ef5;border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));border-radius:14px;padding:12px;max-height:calc(100% - 24px);overflow:auto;box-shadow:0 24px 60px #02061759}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-size:12px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1020
+ `, isInline: true, styles: [":host{display:contents}.ngxsp-btn{height:32px;padding:0 14px;border:1px solid rgb(255 255 255 / .16);border-radius:6px;background:#ffffff0f;color:#fff;font:inherit;font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:hover{background:#ffffff1f}.ngxsp-btn.primary{border-color:transparent;background:var(--nsp-accent-strong, #5d63ee)}.ngxsp-btn:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:2px}.ngxsp-overlay{position:absolute;inset:0;z-index:5;display:grid;place-items:center;padding:24px;background:#000000c7;text-align:center}.ngxsp-overlay-card{display:grid;justify-items:center;gap:8px;max-width:440px}.ngxsp-overlay-icon{width:40px;height:40px;display:grid;place-items:center;margin-bottom:4px;border-radius:999px;background:#f25f5c29;color:var(--nsp-danger, #f25f5c);font-size:18px;font-weight:700}.ngxsp-overlay-title{font-size:16px;font-weight:600;color:#fff}.ngxsp-overlay-msg{color:#ffffffb3;font-size:13px;line-height:1.5}.ngxsp-overlay-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:#fff}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;width:max-content;margin-left:auto;padding:4px 10px;border-radius:999px;background:#0009;color:#fffc;font-size:12px}.ngxsp-debug[open]{padding:12px;max-height:calc(100% - 24px);overflow:auto;border-radius:10px;background:#121317f5;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14}.ngxsp-debug-actions{display:flex;gap:8px;margin:8px 0;flex-wrap:wrap}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
375
1021
  }
376
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PlayerOverlaysComponent, decorators: [{
1022
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerOverlaysComponent, decorators: [{
377
1023
  type: Component,
378
1024
  args: [{ selector: 'ngxsp-player-overlays', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
379
1025
  @if (error()) {
380
1026
  <div class="ngxsp-overlay" role="alert">
381
1027
  <div class="ngxsp-overlay-card">
382
- <div class="ngxsp-overlay-title">Playback error</div>
383
- <div class="ngxsp-overlay-msg">
384
- <div style="font-weight:600; margin-bottom:6px;">{{ title() }}</div>
385
- <div style="opacity:.85; font-size: 13px; line-height: 1.3;">{{ hint() }}</div>
386
- <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
387
- <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
388
- @if (autoplayBlocked()) {
389
- <button type="button" class="ngxsp-btn" (click)="requestPlay.emit()">Play</button>
390
- }
391
- </div>
1028
+ <span class="ngxsp-overlay-icon" aria-hidden="true">!</span>
1029
+ <div class="ngxsp-overlay-title">{{ title() }}</div>
1030
+ <div class="ngxsp-overlay-msg">{{ hint() }}</div>
1031
+ <div class="ngxsp-overlay-actions">
1032
+ <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
1033
+ @if (autoplayBlocked()) {
1034
+ <button type="button" class="ngxsp-btn primary" (click)="requestPlay.emit()">Play</button>
1035
+ }
392
1036
  </div>
393
1037
  </div>
394
1038
  </div>
@@ -397,16 +1041,822 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
397
1041
  @if (debug()) {
398
1042
  <details class="ngxsp-debug">
399
1043
  <summary>Debug</summary>
400
- <div style="display:flex; gap:8px; margin:8px 0; flex-wrap:wrap;">
1044
+ <div class="ngxsp-debug-actions">
401
1045
  <button type="button" class="ngxsp-btn" (click)="copyJson.emit()">Copy JSON</button>
402
1046
  <button type="button" class="ngxsp-btn" (click)="downloadJson.emit()">Download</button>
403
1047
  </div>
404
1048
  <pre>{{ debugJson() }}</pre>
405
1049
  </details>
406
1050
  }
407
- `, styles: [":host{display:contents}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:#03080fb8;padding:20px;text-align:center}.ngxsp-overlay-title{font-size:18px;font-weight:700;letter-spacing:.01em;color:var(--ngxsp-text, #e6eef8)}.ngxsp-overlay-msg{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));max-width:720px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:var(--ngxsp-text, #e6eef8)}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;background:#08121eeb;border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));border-radius:999px;padding:8px 12px;width:max-content;margin-left:auto;color:var(--ngxsp-text-dim, rgba(230,238,248,.72))}.ngxsp-debug[open]{background:#08121ef5;border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));border-radius:14px;padding:12px;max-height:calc(100% - 24px);overflow:auto;box-shadow:0 24px 60px #02061759}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-size:12px}\n"] }]
1051
+ `, styles: [":host{display:contents}.ngxsp-btn{height:32px;padding:0 14px;border:1px solid rgb(255 255 255 / .16);border-radius:6px;background:#ffffff0f;color:#fff;font:inherit;font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:hover{background:#ffffff1f}.ngxsp-btn.primary{border-color:transparent;background:var(--nsp-accent-strong, #5d63ee)}.ngxsp-btn:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:2px}.ngxsp-overlay{position:absolute;inset:0;z-index:5;display:grid;place-items:center;padding:24px;background:#000000c7;text-align:center}.ngxsp-overlay-card{display:grid;justify-items:center;gap:8px;max-width:440px}.ngxsp-overlay-icon{width:40px;height:40px;display:grid;place-items:center;margin-bottom:4px;border-radius:999px;background:#f25f5c29;color:var(--nsp-danger, #f25f5c);font-size:18px;font-weight:700}.ngxsp-overlay-title{font-size:16px;font-weight:600;color:#fff}.ngxsp-overlay-msg{color:#ffffffb3;font-size:13px;line-height:1.5}.ngxsp-overlay-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:#fff}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;width:max-content;margin-left:auto;padding:4px 10px;border-radius:999px;background:#0009;color:#fffc;font-size:12px}.ngxsp-debug[open]{padding:12px;max-height:calc(100% - 24px);overflow:auto;border-radius:10px;background:#121317f5;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14}.ngxsp-debug-actions{display:flex;gap:8px;margin:8px 0;flex-wrap:wrap}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px}\n"] }]
408
1052
  }], propDecorators: { error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], debugJson: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugJson", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }], requestPlay: [{ type: i0.Output, args: ["requestPlay"] }], copyJson: [{ type: i0.Output, args: ["copyJson"] }], downloadJson: [{ type: i0.Output, args: ["downloadJson"] }] } });
409
1053
 
1054
+ /**
1055
+ * Barre de contrôle du skin classique, extraite du SmartPlayerComponent
1056
+ * (décomposition, cf. docs/AUDIT.md). Sans accès au moteur ni au <video> :
1057
+ * reçoit l'état + les capacités (cast/PiP) en entrée et émet les commandes ;
1058
+ * l'hôte applique l'action (et possède l'élément vidéo pour PiP/cast).
1059
+ */
1060
+ class PlayerControlsComponent {
1061
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1062
+ isAudioMode = input(false, ...(ngDevMode ? [{ debugName: "isAudioMode" }] : []));
1063
+ hasPlaylistControls = input(false, ...(ngDevMode ? [{ debugName: "hasPlaylistControls" }] : []));
1064
+ canPrev = input(false, ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
1065
+ canNext = input(false, ...(ngDevMode ? [{ debugName: "canNext" }] : []));
1066
+ shuffleActive = input(false, ...(ngDevMode ? [{ debugName: "shuffleActive" }] : []));
1067
+ shuffleLabel = input('Shuffle: off', ...(ngDevMode ? [{ debugName: "shuffleLabel" }] : []));
1068
+ repeatMode = input('off', ...(ngDevMode ? [{ debugName: "repeatMode" }] : []));
1069
+ repeatLabel = input('Repeat: off', ...(ngDevMode ? [{ debugName: "repeatLabel" }] : []));
1070
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1071
+ tracksLabel = input('Tracks', ...(ngDevMode ? [{ debugName: "tracksLabel" }] : []));
1072
+ captionsLabel = input('Load subtitles', ...(ngDevMode ? [{ debugName: "captionsLabel" }] : []));
1073
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1074
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1075
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1076
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1077
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1078
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1079
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1080
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1081
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1082
+ castLabel = input('Play on device', ...(ngDevMode ? [{ debugName: "castLabel" }] : []));
1083
+ castHint = input('', ...(ngDevMode ? [{ debugName: "castHint" }] : []));
1084
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1085
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1086
+ pipLabel = input('PiP', ...(ngDevMode ? [{ debugName: "pipLabel" }] : []));
1087
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1088
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1089
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1090
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1091
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1092
+ playToggle = output();
1093
+ seekRelative = output();
1094
+ prev = output();
1095
+ next = output();
1096
+ toggleShuffle = output();
1097
+ cycleRepeat = output();
1098
+ openMenu = output();
1099
+ captions = output();
1100
+ airPlay = output();
1101
+ cast = output();
1102
+ pip = output();
1103
+ mute = output();
1104
+ scrub = output();
1105
+ volumeChange = output();
1106
+ fmtTime(sec) {
1107
+ const s = Math.max(0, Math.floor(sec || 0));
1108
+ const h = Math.floor(s / 3600);
1109
+ const m = Math.floor((s % 3600) / 60);
1110
+ const r = s % 60;
1111
+ if (h > 0)
1112
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1113
+ return `${m}:${String(r).padStart(2, '0')}`;
1114
+ }
1115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsComponent, isStandalone: true, selector: "ngxsp-player-controls", inputs: { playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, hasPlaylistControls: { classPropertyName: "hasPlaylistControls", publicName: "hasPlaylistControls", isSignal: true, isRequired: false, transformFunction: null }, canPrev: { classPropertyName: "canPrev", publicName: "canPrev", isSignal: true, isRequired: false, transformFunction: null }, canNext: { classPropertyName: "canNext", publicName: "canNext", isSignal: true, isRequired: false, transformFunction: null }, shuffleActive: { classPropertyName: "shuffleActive", publicName: "shuffleActive", isSignal: true, isRequired: false, transformFunction: null }, shuffleLabel: { classPropertyName: "shuffleLabel", publicName: "shuffleLabel", isSignal: true, isRequired: false, transformFunction: null }, repeatMode: { classPropertyName: "repeatMode", publicName: "repeatMode", isSignal: true, isRequired: false, transformFunction: null }, repeatLabel: { classPropertyName: "repeatLabel", publicName: "repeatLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, tracksLabel: { classPropertyName: "tracksLabel", publicName: "tracksLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsLabel: { classPropertyName: "captionsLabel", publicName: "captionsLabel", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, castLabel: { classPropertyName: "castLabel", publicName: "castLabel", isSignal: true, isRequired: false, transformFunction: null }, castHint: { classPropertyName: "castHint", publicName: "castHint", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, pipLabel: { classPropertyName: "pipLabel", publicName: "pipLabel", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null }, profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", mute: "mute", scrub: "scrub", volumeChange: "volumeChange" }, ngImport: i0, template: `
1117
+ <div class="ngxsp-controls">
1118
+ <ngxsp-play-button class="ngxsp-btn" [playing]="playing()" (playToggle)="playToggle.emit()">
1119
+ @if (playing()) {
1120
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1121
+ } @else {
1122
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1123
+ }
1124
+ </ngxsp-play-button>
1125
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1126
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1127
+ </button>
1128
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1129
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1130
+ </button>
1131
+ @if (hasPlaylistControls()) {
1132
+ <button type="button" class="ngxsp-btn" (click)="prev.emit()" [disabled]="!canPrev()" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1133
+ <button type="button" class="ngxsp-btn" (click)="next.emit()" [disabled]="!canNext()" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1134
+ <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1135
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1136
+ </button>
1137
+ <button type="button" class="ngxsp-btn" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1138
+ @if (repeatMode() === 'one') {
1139
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1140
+ } @else {
1141
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1142
+ }
1143
+ </button>
1144
+ }
1145
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1146
+ <button type="button" class="ngxsp-btn" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1147
+ @if (captionsActive()) {
1148
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1149
+ } @else {
1150
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1151
+ }
1152
+ </button>
1153
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1154
+ @if (showAirPlay()) {
1155
+ <button type="button" class="ngxsp-btn" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1156
+ @if (airPlayActive()) {
1157
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay-active" />
1158
+ } @else {
1159
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay" />
1160
+ }
1161
+ </button>
1162
+ }
1163
+ @if (showCast()) {
1164
+ <button type="button" class="ngxsp-btn" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1165
+ @if (castActive()) {
1166
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1167
+ } @else {
1168
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1169
+ }
1170
+ </button>
1171
+ }
1172
+ @if (!isAudioMode() && pipSupported()) {
1173
+ <button type="button" class="ngxsp-btn" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1174
+ @if (pipActive()) {
1175
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1176
+ } @else {
1177
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1178
+ }
1179
+ </button>
1180
+ }
1181
+ <ngxsp-volume-control
1182
+ class="ngxsp-volumeControl"
1183
+ [volume]="volume()"
1184
+ [volumeLabel]="volumeLabel()"
1185
+ [muteLabel]="muteLabel()"
1186
+ [unmuteLabel]="unmuteLabel()"
1187
+ (muteToggle)="mute.emit()"
1188
+ (volumeChange)="volumeChange.emit($event)"
1189
+ />
1190
+
1191
+ <ngxsp-time-slider
1192
+ style="flex:1 1 160px; min-width:160px"
1193
+ [currentTime]="currentTime()"
1194
+ [duration]="duration()"
1195
+ [buffered]="buffered()"
1196
+ [profile]="profile()"
1197
+ (seek)="scrub.emit($event)"
1198
+ />
1199
+
1200
+ <div class="ngxsp-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1201
+
1202
+ </div>
1203
+ `, isInline: true, styles: [":host{display:block}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));color:#d8f3ff}.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-btn:disabled{opacity:.38;cursor:not-allowed}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-time{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));font-variant-numeric:tabular-nums}.ngxsp-volumeControl{--ngxsp-control-size: 38px;--ngxsp-volume-range-max: 140px;--ngxsp-volume-button-bg: linear-gradient(180deg, rgba(19,34,54,.92), rgba(11,21,35,.92));--ngxsp-volume-button-border: 1px solid var(--ngxsp-border, rgba(148,163,184,.18));--ngxsp-volume-accent: var(--nsp-accent, #7dd3fc);--ngxsp-volume-thumb-bg: var(--nsp-text, #f8fbff);color:var(--ngxsp-text, #e6eef8)}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1204
+ }
1205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsComponent, decorators: [{
1206
+ type: Component,
1207
+ args: [{ selector: 'ngxsp-player-controls', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl], template: `
1208
+ <div class="ngxsp-controls">
1209
+ <ngxsp-play-button class="ngxsp-btn" [playing]="playing()" (playToggle)="playToggle.emit()">
1210
+ @if (playing()) {
1211
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1212
+ } @else {
1213
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1214
+ }
1215
+ </ngxsp-play-button>
1216
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1217
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1218
+ </button>
1219
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1220
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1221
+ </button>
1222
+ @if (hasPlaylistControls()) {
1223
+ <button type="button" class="ngxsp-btn" (click)="prev.emit()" [disabled]="!canPrev()" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1224
+ <button type="button" class="ngxsp-btn" (click)="next.emit()" [disabled]="!canNext()" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1225
+ <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1226
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1227
+ </button>
1228
+ <button type="button" class="ngxsp-btn" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1229
+ @if (repeatMode() === 'one') {
1230
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1231
+ } @else {
1232
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1233
+ }
1234
+ </button>
1235
+ }
1236
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1237
+ <button type="button" class="ngxsp-btn" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1238
+ @if (captionsActive()) {
1239
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1240
+ } @else {
1241
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1242
+ }
1243
+ </button>
1244
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1245
+ @if (showAirPlay()) {
1246
+ <button type="button" class="ngxsp-btn" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1247
+ @if (airPlayActive()) {
1248
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay-active" />
1249
+ } @else {
1250
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay" />
1251
+ }
1252
+ </button>
1253
+ }
1254
+ @if (showCast()) {
1255
+ <button type="button" class="ngxsp-btn" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1256
+ @if (castActive()) {
1257
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1258
+ } @else {
1259
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1260
+ }
1261
+ </button>
1262
+ }
1263
+ @if (!isAudioMode() && pipSupported()) {
1264
+ <button type="button" class="ngxsp-btn" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1265
+ @if (pipActive()) {
1266
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1267
+ } @else {
1268
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1269
+ }
1270
+ </button>
1271
+ }
1272
+ <ngxsp-volume-control
1273
+ class="ngxsp-volumeControl"
1274
+ [volume]="volume()"
1275
+ [volumeLabel]="volumeLabel()"
1276
+ [muteLabel]="muteLabel()"
1277
+ [unmuteLabel]="unmuteLabel()"
1278
+ (muteToggle)="mute.emit()"
1279
+ (volumeChange)="volumeChange.emit($event)"
1280
+ />
1281
+
1282
+ <ngxsp-time-slider
1283
+ style="flex:1 1 160px; min-width:160px"
1284
+ [currentTime]="currentTime()"
1285
+ [duration]="duration()"
1286
+ [buffered]="buffered()"
1287
+ [profile]="profile()"
1288
+ (seek)="scrub.emit($event)"
1289
+ />
1290
+
1291
+ <div class="ngxsp-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1292
+
1293
+ </div>
1294
+ `, styles: [":host{display:block}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));color:#d8f3ff}.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-btn:disabled{opacity:.38;cursor:not-allowed}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-time{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));font-variant-numeric:tabular-nums}.ngxsp-volumeControl{--ngxsp-control-size: 38px;--ngxsp-volume-range-max: 140px;--ngxsp-volume-button-bg: linear-gradient(180deg, rgba(19,34,54,.92), rgba(11,21,35,.92));--ngxsp-volume-button-border: 1px solid var(--ngxsp-border, rgba(148,163,184,.18));--ngxsp-volume-accent: var(--nsp-accent, #7dd3fc);--ngxsp-volume-thumb-bg: var(--nsp-text, #f8fbff);color:var(--ngxsp-text, #e6eef8)}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}}\n"] }]
1295
+ }], propDecorators: { playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], hasPlaylistControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPlaylistControls", required: false }] }], canPrev: [{ type: i0.Input, args: [{ isSignal: true, alias: "canPrev", required: false }] }], canNext: [{ type: i0.Input, args: [{ isSignal: true, alias: "canNext", required: false }] }], shuffleActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleActive", required: false }] }], shuffleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleLabel", required: false }] }], repeatMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatMode", required: false }] }], repeatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], tracksLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracksLabel", required: false }] }], captionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsLabel", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], castLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "castLabel", required: false }] }], castHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "castHint", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], pipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipLabel", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], prev: [{ type: i0.Output, args: ["prev"] }], next: [{ type: i0.Output, args: ["next"] }], toggleShuffle: [{ type: i0.Output, args: ["toggleShuffle"] }], cycleRepeat: [{ type: i0.Output, args: ["cycleRepeat"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }] } });
1296
+
1297
+ /**
1298
+ * Barre de contrôle du skin vod (top bar + center dock + bottom bar),
1299
+ * extraite du SmartPlayerComponent (décomposition, cf. docs/AUDIT.md).
1300
+ * Le menu pistes/vitesse est projeté par l'hôte via <ng-content>.
1301
+ * PiP/cast restent au parent (propriétaire du <video>) : capacités en entrée,
1302
+ * commandes en sortie.
1303
+ */
1304
+ class PlayerControlsVodComponent {
1305
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1306
+ isAudioMode = input(false, ...(ngDevMode ? [{ debugName: "isAudioMode" }] : []));
1307
+ displayTitle = input('Player', ...(ngDevMode ? [{ debugName: "displayTitle" }] : []));
1308
+ nowPlayingLabel = input('Now playing', ...(ngDevMode ? [{ debugName: "nowPlayingLabel" }] : []));
1309
+ nowListeningLabel = input('Now listening', ...(ngDevMode ? [{ debugName: "nowListeningLabel" }] : []));
1310
+ fullscreenHintLabel = input('Double-click for fullscreen', ...(ngDevMode ? [{ debugName: "fullscreenHintLabel" }] : []));
1311
+ statusLabel = input('Ready', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
1312
+ keyboardHint = input('', ...(ngDevMode ? [{ debugName: "keyboardHint" }] : []));
1313
+ hasPlaylistControls = input(false, ...(ngDevMode ? [{ debugName: "hasPlaylistControls" }] : []));
1314
+ canPrev = input(false, ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
1315
+ canNext = input(false, ...(ngDevMode ? [{ debugName: "canNext" }] : []));
1316
+ previousLabel = input('Previous item', ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
1317
+ nextLabel = input('Next item', ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
1318
+ playLabel = input('Play', ...(ngDevMode ? [{ debugName: "playLabel" }] : []));
1319
+ pauseLabel = input('Pause', ...(ngDevMode ? [{ debugName: "pauseLabel" }] : []));
1320
+ shuffleActive = input(false, ...(ngDevMode ? [{ debugName: "shuffleActive" }] : []));
1321
+ shuffleLabel = input('Shuffle: off', ...(ngDevMode ? [{ debugName: "shuffleLabel" }] : []));
1322
+ repeatMode = input('off', ...(ngDevMode ? [{ debugName: "repeatMode" }] : []));
1323
+ repeatLabel = input('Repeat: off', ...(ngDevMode ? [{ debugName: "repeatLabel" }] : []));
1324
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1325
+ tracksLabel = input('Tracks', ...(ngDevMode ? [{ debugName: "tracksLabel" }] : []));
1326
+ captionsLabel = input('Load subtitles', ...(ngDevMode ? [{ debugName: "captionsLabel" }] : []));
1327
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1328
+ /** "1×", "1.5×": the rate on the speed pill. */
1329
+ speedBadge = computed(() => this.speedLabel().replace(/x$/i, '×'), ...(ngDevMode ? [{ debugName: "speedBadge" }] : []));
1330
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1331
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1332
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1333
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1334
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1335
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1336
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1337
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1338
+ castLabel = input('Play on device', ...(ngDevMode ? [{ debugName: "castLabel" }] : []));
1339
+ castHint = input('', ...(ngDevMode ? [{ debugName: "castHint" }] : []));
1340
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1341
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1342
+ pipLabel = input('PiP', ...(ngDevMode ? [{ debugName: "pipLabel" }] : []));
1343
+ fullscreenLabel = input('Fullscreen', ...(ngDevMode ? [{ debugName: "fullscreenLabel" }] : []));
1344
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1345
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1346
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1347
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1348
+ bufferPercent = input(0, ...(ngDevMode ? [{ debugName: "bufferPercent" }] : []));
1349
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1350
+ timelineDurationSec = input(0, ...(ngDevMode ? [{ debugName: "timelineDurationSec" }] : []));
1351
+ timelineMarkers = input([], ...(ngDevMode ? [{ debugName: "timelineMarkers" }] : []));
1352
+ playToggle = output();
1353
+ seekRelative = output();
1354
+ prev = output();
1355
+ next = output();
1356
+ toggleShuffle = output();
1357
+ cycleRepeat = output();
1358
+ openMenu = output();
1359
+ captions = output();
1360
+ airPlay = output();
1361
+ cast = output();
1362
+ pip = output();
1363
+ fullscreen = output();
1364
+ mute = output();
1365
+ scrub = output();
1366
+ volumeChange = output();
1367
+ fmtTime(sec) {
1368
+ const s = Math.max(0, Math.floor(sec || 0));
1369
+ const h = Math.floor(s / 3600);
1370
+ const m = Math.floor((s % 3600) / 60);
1371
+ const r = s % 60;
1372
+ if (h > 0)
1373
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1374
+ return `${m}:${String(r).padStart(2, '0')}`;
1375
+ }
1376
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsVodComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1377
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsVodComponent, isStandalone: true, selector: "ngxsp-player-controls-vod", inputs: { playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, nowPlayingLabel: { classPropertyName: "nowPlayingLabel", publicName: "nowPlayingLabel", isSignal: true, isRequired: false, transformFunction: null }, nowListeningLabel: { classPropertyName: "nowListeningLabel", publicName: "nowListeningLabel", isSignal: true, isRequired: false, transformFunction: null }, fullscreenHintLabel: { classPropertyName: "fullscreenHintLabel", publicName: "fullscreenHintLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, keyboardHint: { classPropertyName: "keyboardHint", publicName: "keyboardHint", isSignal: true, isRequired: false, transformFunction: null }, hasPlaylistControls: { classPropertyName: "hasPlaylistControls", publicName: "hasPlaylistControls", isSignal: true, isRequired: false, transformFunction: null }, canPrev: { classPropertyName: "canPrev", publicName: "canPrev", isSignal: true, isRequired: false, transformFunction: null }, canNext: { classPropertyName: "canNext", publicName: "canNext", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, playLabel: { classPropertyName: "playLabel", publicName: "playLabel", isSignal: true, isRequired: false, transformFunction: null }, pauseLabel: { classPropertyName: "pauseLabel", publicName: "pauseLabel", isSignal: true, isRequired: false, transformFunction: null }, shuffleActive: { classPropertyName: "shuffleActive", publicName: "shuffleActive", isSignal: true, isRequired: false, transformFunction: null }, shuffleLabel: { classPropertyName: "shuffleLabel", publicName: "shuffleLabel", isSignal: true, isRequired: false, transformFunction: null }, repeatMode: { classPropertyName: "repeatMode", publicName: "repeatMode", isSignal: true, isRequired: false, transformFunction: null }, repeatLabel: { classPropertyName: "repeatLabel", publicName: "repeatLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, tracksLabel: { classPropertyName: "tracksLabel", publicName: "tracksLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsLabel: { classPropertyName: "captionsLabel", publicName: "captionsLabel", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, castLabel: { classPropertyName: "castLabel", publicName: "castLabel", isSignal: true, isRequired: false, transformFunction: null }, castHint: { classPropertyName: "castHint", publicName: "castHint", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, pipLabel: { classPropertyName: "pipLabel", publicName: "pipLabel", isSignal: true, isRequired: false, transformFunction: null }, fullscreenLabel: { classPropertyName: "fullscreenLabel", publicName: "fullscreenLabel", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null }, bufferPercent: { classPropertyName: "bufferPercent", publicName: "bufferPercent", isSignal: true, isRequired: false, transformFunction: null }, profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null }, timelineDurationSec: { classPropertyName: "timelineDurationSec", publicName: "timelineDurationSec", isSignal: true, isRequired: false, transformFunction: null }, timelineMarkers: { classPropertyName: "timelineMarkers", publicName: "timelineMarkers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", fullscreen: "fullscreen", mute: "mute", scrub: "scrub", volumeChange: "volumeChange" }, ngImport: i0, template: `
1378
+ <div class="ngxsp-vod-top">
1379
+ <div class="ngxsp-vod-titleWrap">
1380
+ <div class="ngxsp-vod-eyebrow">{{ isAudioMode() ? nowListeningLabel() : nowPlayingLabel() }}</div>
1381
+ <div class="ngxsp-vod-title">{{ displayTitle() }}</div>
1382
+ </div>
1383
+ <div class="ngxsp-vod-spacer"></div>
1384
+ <div class="ngxsp-vod-status" [attr.data-state]="playing() ? 'playing' : 'idle'">{{ statusLabel() }}</div>
1385
+ @if (!isAudioMode()) {
1386
+ <div class="ngxsp-vod-topHint">{{ fullscreenHintLabel() }}</div>
1387
+ }
1388
+ </div>
1389
+
1390
+ <div class="ngxsp-vod-centerDock">
1391
+ @if (hasPlaylistControls()) {
1392
+ <button type="button" class="ngxsp-vod-side" (click)="prev.emit()" [disabled]="!canPrev()" [attr.aria-label]="previousLabel()">
1393
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1394
+ </button>
1395
+ }
1396
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds">
1397
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1398
+ </button>
1399
+ <ngxsp-play-button class="ngxsp-vod-center" [playing]="playing()" [playLabel]="playLabel()" [pauseLabel]="pauseLabel()" (playToggle)="playToggle.emit()">
1400
+ @if (playing()) {
1401
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1402
+ } @else {
1403
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1404
+ }
1405
+ </ngxsp-play-button>
1406
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds">
1407
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1408
+ </button>
1409
+ @if (hasPlaylistControls()) {
1410
+ <button type="button" class="ngxsp-vod-side" (click)="next.emit()" [disabled]="!canNext()" [attr.aria-label]="nextLabel()">
1411
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1412
+ </button>
1413
+ }
1414
+ </div>
1415
+
1416
+ <div class="ngxsp-controls ngxsp-vod-bottom">
1417
+ <div class="ngxsp-vod-metaRow">
1418
+ @if (hasPlaylistControls()) {
1419
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-prev" (click)="prev.emit()" [disabled]="!canPrev()" [attr.title]="previousLabel()" [attr.aria-label]="previousLabel()">
1420
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1421
+ </button>
1422
+ }
1423
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-play" (click)="playToggle.emit()" [attr.title]="playing() ? pauseLabel() : playLabel()" [attr.aria-label]="playing() ? pauseLabel() : playLabel()">
1424
+ @if (playing()) {
1425
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1426
+ } @else {
1427
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1428
+ }
1429
+ </button>
1430
+ @if (hasPlaylistControls()) {
1431
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-next" (click)="next.emit()" [disabled]="!canNext()" [attr.title]="nextLabel()" [attr.aria-label]="nextLabel()">
1432
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1433
+ </button>
1434
+ }
1435
+ <div class="ngxsp-vod-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1436
+ <div class="ngxsp-vod-volumeGroup">
1437
+ <ngxsp-volume-control
1438
+ class="ngxsp-vod-volumeControl"
1439
+ [compact]="true"
1440
+ [volume]="volume()"
1441
+ [volumeLabel]="volumeLabel()"
1442
+ [muteLabel]="muteLabel()"
1443
+ [unmuteLabel]="unmuteLabel()"
1444
+ (muteToggle)="mute.emit()"
1445
+ (volumeChange)="volumeChange.emit($event)"
1446
+ />
1447
+ </div>
1448
+ <ng-content select="[ngxsp-player-inline-controls]" />
1449
+ <div class="ngxsp-vod-spacer"></div>
1450
+ @if (hasPlaylistControls()) {
1451
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1452
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1453
+ </button>
1454
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1455
+ @if (repeatMode() === 'one') {
1456
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1457
+ } @else {
1458
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1459
+ }
1460
+ </button>
1461
+ }
1462
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-tracks" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()">
1463
+ <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1464
+ </button>
1465
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-captions" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1466
+ @if (captionsActive()) {
1467
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1468
+ } @else {
1469
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1470
+ }
1471
+ </button>
1472
+ <!-- The rate itself, as a pill: what it is shows at a glance. -->
1473
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-speed" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()">
1474
+ <span class="ngxsp-vod-speedBadge">{{ speedBadge() }}</span>
1475
+ </button>
1476
+ @if (showAirPlay()) {
1477
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-airplay" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1478
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" />
1479
+ </button>
1480
+ }
1481
+ @if (showCast()) {
1482
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-cast" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1483
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="castActive() ? 'devices-active' : 'devices'" />
1484
+ </button>
1485
+ }
1486
+ @if (!isAudioMode() && pipSupported()) {
1487
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-pip" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1488
+ @if (pipActive()) {
1489
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1490
+ } @else {
1491
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1492
+ }
1493
+ </button>
1494
+ }
1495
+ @if (!isAudioMode()) {
1496
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-fullscreen" (click)="fullscreen.emit()" [attr.title]="fullscreenLabel()" [attr.aria-label]="fullscreenLabel()">
1497
+ <ngxsp-smart-icon class="ngxsp-icon" name="fullscreen" />
1498
+ </button>
1499
+ }
1500
+ </div>
1501
+
1502
+ <ngxsp-time-slider
1503
+ class="ngxsp-vod-range"
1504
+ [currentTime]="currentTime()"
1505
+ [duration]="duration()"
1506
+ [buffered]="buffered()"
1507
+ [profile]="profile()"
1508
+ [durationHintSec]="timelineDurationSec()"
1509
+ [markers]="timelineMarkers()"
1510
+ (seek)="scrub.emit($event)"
1511
+ />
1512
+
1513
+ <ng-content select="ngxsp-player-menu" />
1514
+ </div>
1515
+ `, isInline: true, styles: [":host{display:contents}.ngxsp-icon{width:20px;height:20px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:28px;height:28px}.ngxsp-vod-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:16px 18px 40px;background:linear-gradient(to bottom,#0000009e,#0000);pointer-events:none;opacity:0;transition:opacity .2s ease;z-index:3}:host-context(.ngxsp-ui-visible) .ngxsp-vod-top{opacity:1}.ngxsp-vod-titleWrap{min-width:0;display:flex;flex-direction:column;gap:2px}.ngxsp-vod-eyebrow{font-size:12px;font-weight:600;color:#ffffffb8}.ngxsp-vod-title{font-weight:600;font-size:16px;color:#fff;text-shadow:0 1px 2px rgb(0 0 0 / .5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-vod-topHint{display:none}.ngxsp-vod-status{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:999px;background:#00000073;color:#ffffffdb;font-size:11px;font-weight:500;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-vod-status:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#ffffff80}.ngxsp-vod-status[data-state=playing]:before{background:var(--nsp-success, #3ecf8e)}.ngxsp-vod-spacer{flex:1}.ngxsp-vod-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:20px;z-index:4;opacity:0;visibility:hidden;transform:scale(.96);transition:opacity .2s ease,transform .2s ease,visibility 0s linear .2s;pointer-events:none}:host-context(.ngxsp-ui-visible) .ngxsp-vod-centerDock{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition:opacity .2s ease,transform .2s ease,visibility 0s}:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-centerDock,:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-top{display:none}.ngxsp-vod-center{height:68px;width:68px;border-radius:999px;border:0;background:#fffffff0;color:#0d0e11;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 12px 40px #00000073;transition:transform .15s ease,background-color .15s ease}.ngxsp-vod-center:hover{transform:scale(1.05);background:#fff}.ngxsp-vod-side{position:relative;height:48px;min-width:48px;width:48px;border-radius:999px;border:0;background:#00000073;color:#fff;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transition:background-color .15s ease}.ngxsp-vod-side-wide{min-width:48px}.ngxsp-vod-side-wide .ngxsp-icon{width:24px;height:24px}.ngxsp-vod-side:hover{background:#000000a6}.ngxsp-vod-bottom{position:absolute;left:0;right:0;bottom:0;padding:40px 14px 8px;background:linear-gradient(180deg,#0000,#0000008c 45%,#000000d1);display:flex;flex-direction:column;align-items:stretch;gap:0;z-index:4;opacity:0;transition:opacity .2s ease}:host-context(.ngxsp-ui-visible) .ngxsp-vod-bottom{opacity:1}.ngxsp-vod-metaRow{display:flex;align-items:center;gap:4px;width:100%;min-height:40px}.ngxsp-vod-speedBadge{box-sizing:border-box;min-width:30px;padding:2px 5px;border:1.5px solid currentColor;border-radius:999px;font-size:11px;font-weight:700;line-height:1.2;font-variant-numeric:tabular-nums;white-space:nowrap}.ngxsp-vod-time{padding:0 8px;color:#ffffffeb;font-variant-numeric:tabular-nums;font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-vod-range{order:-1;min-width:100%;min-height:32px;--ngxsp-track-size:4px;--ngxsp-track-bg:rgb(255 255 255 / .22);--ngxsp-buffered-bg:rgb(255 255 255 / .34);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-thumb-size:13px;--ngxsp-thumb-bg:#fff;--ngxsp-marker-width:2px;--ngxsp-marker-height:8px;--ngxsp-marker-bg:rgb(0 0 0 / .55);--ngxsp-marker-active-bg:#fff;--ngxsp-marker-active-shadow:none;--ngxsp-preview-bg:rgb(13 14 17 / .92);--ngxsp-preview-border:rgb(255 255 255 / .12);--ngxsp-preview-muted:rgb(255 255 255 / .64)}.ngxsp-vod-range:hover{--ngxsp-track-size:6px}.ngxsp-vod-volumeGroup{display:inline-flex;align-items:center}.ngxsp-vod-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-compact-min:72px;--ngxsp-volume-compact-max:104px;--ngxsp-volume-button-bg:transparent;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);color:#fff}.ngxsp-vod-label{font-size:12px;color:#ffffffa3}.ngxsp-vod-chip{position:relative;width:36px;height:36px;min-width:36px;min-height:36px;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background-color .15s ease,color .15s ease}.ngxsp-vod-chip:hover:not(:disabled){background:#ffffff1f;color:#fff}.ngxsp-vod-chip:disabled{opacity:.4;cursor:default}.ngxsp-vod-chip.accent{color:var(--nsp-media-accent, #8b90ff)}.ngxsp-vod-chip-quiet{background:transparent}.ngxsp-vod-center:disabled,.ngxsp-vod-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-vod-center:focus-visible,.ngxsp-vod-side:focus-visible,.ngxsp-vod-chip:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:1px}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff1f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-vod-top{padding:12px 14px 28px}.ngxsp-vod-centerDock{gap:14px}.ngxsp-vod-center{height:58px;width:58px}.ngxsp-vod-side{height:42px;width:42px;min-width:42px}.ngxsp-vod-bottom{padding:36px 8px 6px}.ngxsp-vod-chip-speed,.ngxsp-vod-chip-tracks{display:none}}@media(max-width:640px){.ngxsp-icon{width:18px;height:18px}.ngxsp-icon-lg{width:24px;height:24px}.ngxsp-vod-title{font-size:14px}.ngxsp-vod-status{display:none}.ngxsp-vod-time{font-size:11px;padding:0 4px}.ngxsp-vod-center{height:52px;width:52px}.ngxsp-vod-side{height:38px;width:38px;min-width:38px}.ngxsp-vod-metaRow{min-height:34px;gap:2px}.ngxsp-vod-chip{width:32px;height:32px;min-width:32px;min-height:32px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-shell{grid-template-columns:32px 64px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-range{opacity:1;visibility:visible;transform:none;pointer-events:auto}.ngxsp-vod-chip-playlist,.ngxsp-vod-chip-captions,.ngxsp-vod-chip-pip,.ngxsp-vod-top,.ngxsp-vod-side{display:none}}@media(prefers-reduced-motion:reduce){.ngxsp-vod-top,.ngxsp-vod-centerDock,.ngxsp-vod-bottom,.ngxsp-vod-center,.ngxsp-vod-side,.ngxsp-vod-chip{transition:none}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1516
+ }
1517
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsVodComponent, decorators: [{
1518
+ type: Component,
1519
+ args: [{ selector: 'ngxsp-player-controls-vod', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl], template: `
1520
+ <div class="ngxsp-vod-top">
1521
+ <div class="ngxsp-vod-titleWrap">
1522
+ <div class="ngxsp-vod-eyebrow">{{ isAudioMode() ? nowListeningLabel() : nowPlayingLabel() }}</div>
1523
+ <div class="ngxsp-vod-title">{{ displayTitle() }}</div>
1524
+ </div>
1525
+ <div class="ngxsp-vod-spacer"></div>
1526
+ <div class="ngxsp-vod-status" [attr.data-state]="playing() ? 'playing' : 'idle'">{{ statusLabel() }}</div>
1527
+ @if (!isAudioMode()) {
1528
+ <div class="ngxsp-vod-topHint">{{ fullscreenHintLabel() }}</div>
1529
+ }
1530
+ </div>
1531
+
1532
+ <div class="ngxsp-vod-centerDock">
1533
+ @if (hasPlaylistControls()) {
1534
+ <button type="button" class="ngxsp-vod-side" (click)="prev.emit()" [disabled]="!canPrev()" [attr.aria-label]="previousLabel()">
1535
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1536
+ </button>
1537
+ }
1538
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds">
1539
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1540
+ </button>
1541
+ <ngxsp-play-button class="ngxsp-vod-center" [playing]="playing()" [playLabel]="playLabel()" [pauseLabel]="pauseLabel()" (playToggle)="playToggle.emit()">
1542
+ @if (playing()) {
1543
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1544
+ } @else {
1545
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1546
+ }
1547
+ </ngxsp-play-button>
1548
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds">
1549
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1550
+ </button>
1551
+ @if (hasPlaylistControls()) {
1552
+ <button type="button" class="ngxsp-vod-side" (click)="next.emit()" [disabled]="!canNext()" [attr.aria-label]="nextLabel()">
1553
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1554
+ </button>
1555
+ }
1556
+ </div>
1557
+
1558
+ <div class="ngxsp-controls ngxsp-vod-bottom">
1559
+ <div class="ngxsp-vod-metaRow">
1560
+ @if (hasPlaylistControls()) {
1561
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-prev" (click)="prev.emit()" [disabled]="!canPrev()" [attr.title]="previousLabel()" [attr.aria-label]="previousLabel()">
1562
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1563
+ </button>
1564
+ }
1565
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-play" (click)="playToggle.emit()" [attr.title]="playing() ? pauseLabel() : playLabel()" [attr.aria-label]="playing() ? pauseLabel() : playLabel()">
1566
+ @if (playing()) {
1567
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1568
+ } @else {
1569
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1570
+ }
1571
+ </button>
1572
+ @if (hasPlaylistControls()) {
1573
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-next" (click)="next.emit()" [disabled]="!canNext()" [attr.title]="nextLabel()" [attr.aria-label]="nextLabel()">
1574
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1575
+ </button>
1576
+ }
1577
+ <div class="ngxsp-vod-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1578
+ <div class="ngxsp-vod-volumeGroup">
1579
+ <ngxsp-volume-control
1580
+ class="ngxsp-vod-volumeControl"
1581
+ [compact]="true"
1582
+ [volume]="volume()"
1583
+ [volumeLabel]="volumeLabel()"
1584
+ [muteLabel]="muteLabel()"
1585
+ [unmuteLabel]="unmuteLabel()"
1586
+ (muteToggle)="mute.emit()"
1587
+ (volumeChange)="volumeChange.emit($event)"
1588
+ />
1589
+ </div>
1590
+ <ng-content select="[ngxsp-player-inline-controls]" />
1591
+ <div class="ngxsp-vod-spacer"></div>
1592
+ @if (hasPlaylistControls()) {
1593
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1594
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1595
+ </button>
1596
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1597
+ @if (repeatMode() === 'one') {
1598
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1599
+ } @else {
1600
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1601
+ }
1602
+ </button>
1603
+ }
1604
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-tracks" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()">
1605
+ <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1606
+ </button>
1607
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-captions" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1608
+ @if (captionsActive()) {
1609
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1610
+ } @else {
1611
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1612
+ }
1613
+ </button>
1614
+ <!-- The rate itself, as a pill: what it is shows at a glance. -->
1615
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-speed" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()">
1616
+ <span class="ngxsp-vod-speedBadge">{{ speedBadge() }}</span>
1617
+ </button>
1618
+ @if (showAirPlay()) {
1619
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-airplay" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1620
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" />
1621
+ </button>
1622
+ }
1623
+ @if (showCast()) {
1624
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-cast" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1625
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="castActive() ? 'devices-active' : 'devices'" />
1626
+ </button>
1627
+ }
1628
+ @if (!isAudioMode() && pipSupported()) {
1629
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-pip" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1630
+ @if (pipActive()) {
1631
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1632
+ } @else {
1633
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1634
+ }
1635
+ </button>
1636
+ }
1637
+ @if (!isAudioMode()) {
1638
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-fullscreen" (click)="fullscreen.emit()" [attr.title]="fullscreenLabel()" [attr.aria-label]="fullscreenLabel()">
1639
+ <ngxsp-smart-icon class="ngxsp-icon" name="fullscreen" />
1640
+ </button>
1641
+ }
1642
+ </div>
1643
+
1644
+ <ngxsp-time-slider
1645
+ class="ngxsp-vod-range"
1646
+ [currentTime]="currentTime()"
1647
+ [duration]="duration()"
1648
+ [buffered]="buffered()"
1649
+ [profile]="profile()"
1650
+ [durationHintSec]="timelineDurationSec()"
1651
+ [markers]="timelineMarkers()"
1652
+ (seek)="scrub.emit($event)"
1653
+ />
1654
+
1655
+ <ng-content select="ngxsp-player-menu" />
1656
+ </div>
1657
+ `, styles: [":host{display:contents}.ngxsp-icon{width:20px;height:20px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:28px;height:28px}.ngxsp-vod-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:16px 18px 40px;background:linear-gradient(to bottom,#0000009e,#0000);pointer-events:none;opacity:0;transition:opacity .2s ease;z-index:3}:host-context(.ngxsp-ui-visible) .ngxsp-vod-top{opacity:1}.ngxsp-vod-titleWrap{min-width:0;display:flex;flex-direction:column;gap:2px}.ngxsp-vod-eyebrow{font-size:12px;font-weight:600;color:#ffffffb8}.ngxsp-vod-title{font-weight:600;font-size:16px;color:#fff;text-shadow:0 1px 2px rgb(0 0 0 / .5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-vod-topHint{display:none}.ngxsp-vod-status{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:999px;background:#00000073;color:#ffffffdb;font-size:11px;font-weight:500;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-vod-status:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#ffffff80}.ngxsp-vod-status[data-state=playing]:before{background:var(--nsp-success, #3ecf8e)}.ngxsp-vod-spacer{flex:1}.ngxsp-vod-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:20px;z-index:4;opacity:0;visibility:hidden;transform:scale(.96);transition:opacity .2s ease,transform .2s ease,visibility 0s linear .2s;pointer-events:none}:host-context(.ngxsp-ui-visible) .ngxsp-vod-centerDock{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition:opacity .2s ease,transform .2s ease,visibility 0s}:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-centerDock,:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-top{display:none}.ngxsp-vod-center{height:68px;width:68px;border-radius:999px;border:0;background:#fffffff0;color:#0d0e11;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 12px 40px #00000073;transition:transform .15s ease,background-color .15s ease}.ngxsp-vod-center:hover{transform:scale(1.05);background:#fff}.ngxsp-vod-side{position:relative;height:48px;min-width:48px;width:48px;border-radius:999px;border:0;background:#00000073;color:#fff;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transition:background-color .15s ease}.ngxsp-vod-side-wide{min-width:48px}.ngxsp-vod-side-wide .ngxsp-icon{width:24px;height:24px}.ngxsp-vod-side:hover{background:#000000a6}.ngxsp-vod-bottom{position:absolute;left:0;right:0;bottom:0;padding:40px 14px 8px;background:linear-gradient(180deg,#0000,#0000008c 45%,#000000d1);display:flex;flex-direction:column;align-items:stretch;gap:0;z-index:4;opacity:0;transition:opacity .2s ease}:host-context(.ngxsp-ui-visible) .ngxsp-vod-bottom{opacity:1}.ngxsp-vod-metaRow{display:flex;align-items:center;gap:4px;width:100%;min-height:40px}.ngxsp-vod-speedBadge{box-sizing:border-box;min-width:30px;padding:2px 5px;border:1.5px solid currentColor;border-radius:999px;font-size:11px;font-weight:700;line-height:1.2;font-variant-numeric:tabular-nums;white-space:nowrap}.ngxsp-vod-time{padding:0 8px;color:#ffffffeb;font-variant-numeric:tabular-nums;font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-vod-range{order:-1;min-width:100%;min-height:32px;--ngxsp-track-size:4px;--ngxsp-track-bg:rgb(255 255 255 / .22);--ngxsp-buffered-bg:rgb(255 255 255 / .34);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-thumb-size:13px;--ngxsp-thumb-bg:#fff;--ngxsp-marker-width:2px;--ngxsp-marker-height:8px;--ngxsp-marker-bg:rgb(0 0 0 / .55);--ngxsp-marker-active-bg:#fff;--ngxsp-marker-active-shadow:none;--ngxsp-preview-bg:rgb(13 14 17 / .92);--ngxsp-preview-border:rgb(255 255 255 / .12);--ngxsp-preview-muted:rgb(255 255 255 / .64)}.ngxsp-vod-range:hover{--ngxsp-track-size:6px}.ngxsp-vod-volumeGroup{display:inline-flex;align-items:center}.ngxsp-vod-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-compact-min:72px;--ngxsp-volume-compact-max:104px;--ngxsp-volume-button-bg:transparent;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);color:#fff}.ngxsp-vod-label{font-size:12px;color:#ffffffa3}.ngxsp-vod-chip{position:relative;width:36px;height:36px;min-width:36px;min-height:36px;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background-color .15s ease,color .15s ease}.ngxsp-vod-chip:hover:not(:disabled){background:#ffffff1f;color:#fff}.ngxsp-vod-chip:disabled{opacity:.4;cursor:default}.ngxsp-vod-chip.accent{color:var(--nsp-media-accent, #8b90ff)}.ngxsp-vod-chip-quiet{background:transparent}.ngxsp-vod-center:disabled,.ngxsp-vod-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-vod-center:focus-visible,.ngxsp-vod-side:focus-visible,.ngxsp-vod-chip:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:1px}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff1f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-vod-top{padding:12px 14px 28px}.ngxsp-vod-centerDock{gap:14px}.ngxsp-vod-center{height:58px;width:58px}.ngxsp-vod-side{height:42px;width:42px;min-width:42px}.ngxsp-vod-bottom{padding:36px 8px 6px}.ngxsp-vod-chip-speed,.ngxsp-vod-chip-tracks{display:none}}@media(max-width:640px){.ngxsp-icon{width:18px;height:18px}.ngxsp-icon-lg{width:24px;height:24px}.ngxsp-vod-title{font-size:14px}.ngxsp-vod-status{display:none}.ngxsp-vod-time{font-size:11px;padding:0 4px}.ngxsp-vod-center{height:52px;width:52px}.ngxsp-vod-side{height:38px;width:38px;min-width:38px}.ngxsp-vod-metaRow{min-height:34px;gap:2px}.ngxsp-vod-chip{width:32px;height:32px;min-width:32px;min-height:32px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-shell{grid-template-columns:32px 64px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-range{opacity:1;visibility:visible;transform:none;pointer-events:auto}.ngxsp-vod-chip-playlist,.ngxsp-vod-chip-captions,.ngxsp-vod-chip-pip,.ngxsp-vod-top,.ngxsp-vod-side{display:none}}@media(prefers-reduced-motion:reduce){.ngxsp-vod-top,.ngxsp-vod-centerDock,.ngxsp-vod-bottom,.ngxsp-vod-center,.ngxsp-vod-side,.ngxsp-vod-chip{transition:none}}\n"] }]
1658
+ }], propDecorators: { playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], nowPlayingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nowPlayingLabel", required: false }] }], nowListeningLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nowListeningLabel", required: false }] }], fullscreenHintLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullscreenHintLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], keyboardHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardHint", required: false }] }], hasPlaylistControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPlaylistControls", required: false }] }], canPrev: [{ type: i0.Input, args: [{ isSignal: true, alias: "canPrev", required: false }] }], canNext: [{ type: i0.Input, args: [{ isSignal: true, alias: "canNext", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], playLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "playLabel", required: false }] }], pauseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseLabel", required: false }] }], shuffleActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleActive", required: false }] }], shuffleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleLabel", required: false }] }], repeatMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatMode", required: false }] }], repeatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], tracksLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracksLabel", required: false }] }], captionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsLabel", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], castLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "castLabel", required: false }] }], castHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "castHint", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], pipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipLabel", required: false }] }], fullscreenLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullscreenLabel", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], bufferPercent: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferPercent", required: false }] }], profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], timelineDurationSec: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineDurationSec", required: false }] }], timelineMarkers: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMarkers", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], prev: [{ type: i0.Output, args: ["prev"] }], next: [{ type: i0.Output, args: ["next"] }], toggleShuffle: [{ type: i0.Output, args: ["toggleShuffle"] }], cycleRepeat: [{ type: i0.Output, args: ["cycleRepeat"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], fullscreen: [{ type: i0.Output, args: ["fullscreen"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }] } });
1659
+
1660
+ /**
1661
+ * Skin « édition » du lecteur : registre instrument (cf. docs/UX_VISION.md).
1662
+ * Titre display, coins francs, libellés en capitales, accent champagne, et le
1663
+ * traitement direct (NgxspLiveBadge + « Au direct ») en profil `live`.
1664
+ * Compose les primitives de @ngxsp/ui-core. Le menu est projeté via <ng-content>.
1665
+ */
1666
+ class PlayerControlsEditionComponent {
1667
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1668
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1669
+ displayTitle = input('Player', ...(ngDevMode ? [{ debugName: "displayTitle" }] : []));
1670
+ metaLabel = input('', ...(ngDevMode ? [{ debugName: "metaLabel" }] : []));
1671
+ statusLabel = input('', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
1672
+ behindSec = input(0, ...(ngDevMode ? [{ debugName: "behindSec" }] : []));
1673
+ liveLabel = input('Direct', ...(ngDevMode ? [{ debugName: "liveLabel" }] : []));
1674
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1675
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1676
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1677
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1678
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1679
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1680
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1681
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1682
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1683
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1684
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1685
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1686
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1687
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1688
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1689
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1690
+ playToggle = output();
1691
+ seekRelative = output();
1692
+ openMenu = output();
1693
+ captions = output();
1694
+ airPlay = output();
1695
+ cast = output();
1696
+ pip = output();
1697
+ mute = output();
1698
+ scrub = output();
1699
+ volumeChange = output();
1700
+ goLive = output();
1701
+ fmtTime(sec) {
1702
+ const s = Math.max(0, Math.floor(sec || 0));
1703
+ const h = Math.floor(s / 3600);
1704
+ const m = Math.floor((s % 3600) / 60);
1705
+ const r = s % 60;
1706
+ if (h > 0)
1707
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1708
+ return `${m}:${String(r).padStart(2, '0')}`;
1709
+ }
1710
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsEditionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1711
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsEditionComponent, isStandalone: true, selector: "ngxsp-player-controls-edition", inputs: { profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null }, playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, metaLabel: { classPropertyName: "metaLabel", publicName: "metaLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, behindSec: { classPropertyName: "behindSec", publicName: "behindSec", isSignal: true, isRequired: false, transformFunction: null }, liveLabel: { classPropertyName: "liveLabel", publicName: "liveLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", mute: "mute", scrub: "scrub", volumeChange: "volumeChange", goLive: "goLive" }, ngImport: i0, template: `
1712
+ <div class="ed-top">
1713
+ @if (profile() === 'live') {
1714
+ <ngxsp-live-badge [behindSec]="behindSec()" />
1715
+ }
1716
+ <div class="ed-titleWrap">
1717
+ <div class="ed-title">{{ displayTitle() }}</div>
1718
+ <div class="ed-meta">{{ metaLabel() }}</div>
1719
+ </div>
1720
+ <span class="ed-spacer"></span>
1721
+ <div class="ed-status">{{ statusLabel() }}</div>
1722
+ </div>
1723
+
1724
+ <div class="ed-bottom">
1725
+ <div class="ed-scrubRow">
1726
+ <span class="ed-time">{{ fmtTime(currentTime()) }}</span>
1727
+ <ngxsp-time-slider
1728
+ style="flex:1"
1729
+ [currentTime]="currentTime()"
1730
+ [duration]="duration()"
1731
+ [buffered]="buffered()"
1732
+ [profile]="profile()"
1733
+ (seek)="scrub.emit($event)"
1734
+ />
1735
+ <span class="ed-time ed-time-dim">{{ profile() === 'live' ? liveLabel() : fmtTime(duration()) }}</span>
1736
+ </div>
1737
+
1738
+ <div class="ed-controlsRow">
1739
+ @if (profile() === 'live') {
1740
+ <button type="button" class="ed-live" (click)="goLive.emit()">
1741
+ <ngxsp-smart-icon class="ed-icon" name="next" /> Au direct
1742
+ </button>
1743
+ <ngxsp-play-button class="ed-transport" [playing]="playing()" (playToggle)="playToggle.emit()">
1744
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1745
+ </ngxsp-play-button>
1746
+ } @else {
1747
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(-10)" aria-label="Reculer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-back" /></button>
1748
+ <ngxsp-play-button class="ed-transport ed-transport-primary" [playing]="playing()" (playToggle)="playToggle.emit()">
1749
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1750
+ </ngxsp-play-button>
1751
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(10)" aria-label="Avancer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-forward" /></button>
1752
+ }
1753
+
1754
+ <span class="ed-spacer"></span>
1755
+
1756
+ <ngxsp-labeled-control label="Sous-titres" [pressed]="captionsActive()" (activate)="captions.emit()"><ngxsp-smart-icon class="ed-icon" [name]="captionsActive() ? 'captions' : 'captions-off'" /></ngxsp-labeled-control>
1757
+ <ngxsp-labeled-control label="Pistes" (activate)="openMenu.emit('tracks')"><ngxsp-smart-icon class="ed-icon" name="tracks" /></ngxsp-labeled-control>
1758
+ <ngxsp-labeled-control [label]="speedLabel()" (activate)="openMenu.emit('speed')"><ngxsp-smart-icon class="ed-icon" name="speed" /></ngxsp-labeled-control>
1759
+ @if (showAirPlay()) {
1760
+ <ngxsp-labeled-control [label]="airPlayLabel()" [active]="airPlayActive()" (activate)="airPlay.emit()"><ngxsp-smart-icon class="ed-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" /></ngxsp-labeled-control>
1761
+ }
1762
+ @if (showCast()) {
1763
+ <ngxsp-labeled-control label="Diffuser" [active]="castActive()" (activate)="cast.emit()"><ngxsp-smart-icon class="ed-icon" [name]="castActive() ? 'cast-active' : 'cast'" /></ngxsp-labeled-control>
1764
+ }
1765
+ @if (pipSupported()) {
1766
+ <ngxsp-labeled-control label="PiP" [active]="pipActive()" (activate)="pip.emit()"><ngxsp-smart-icon class="ed-icon" [name]="pipActive() ? 'pip-active' : 'pip'" /></ngxsp-labeled-control>
1767
+ }
1768
+ <ngxsp-volume-control
1769
+ class="ed-volumeControl"
1770
+ [compact]="true"
1771
+ [volume]="volume()"
1772
+ [volumeLabel]="volumeLabel()"
1773
+ [muteLabel]="muteLabel()"
1774
+ [unmuteLabel]="unmuteLabel()"
1775
+ (muteToggle)="mute.emit()"
1776
+ (volumeChange)="volumeChange.emit($event)"
1777
+ />
1778
+ </div>
1779
+
1780
+ <ng-content />
1781
+ </div>
1782
+ `, isInline: true, styles: [":host{display:contents}.ed-icon{width:20px;height:20px;display:block}.ed-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:14px;padding:18px 22px 14px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 55%,transparent);border-bottom:1px solid var(--nsp-line, rgb(255 255 255 / .1));opacity:0;transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-top{opacity:1}.ed-titleWrap{display:flex;flex-direction:column;gap:6px}.ed-title{font-size:24px;font-weight:500;letter-spacing:-.025em;line-height:1;color:var(--nsp-text, #f4f2ec)}.ed-meta{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--nsp-text-2, #b4afa6)}.ed-status{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--nsp-text-3, #7c776f)}.ed-spacer{flex:1}.ed-bottom{position:absolute;left:0;right:0;bottom:0;z-index:4;display:flex;flex-direction:column;gap:14px;padding:16px 22px 18px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 82%,transparent);border-top:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));opacity:0;transform:translateY(8px);transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease),transform var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-bottom{opacity:1;transform:translateY(0)}.ed-scrubRow{display:flex;align-items:center;gap:14px}.ed-time{font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;color:var(--nsp-text, #f4f2ec)}.ed-time-dim{color:var(--nsp-text-2, #b4afa6)}.ed-controlsRow{display:flex;align-items:center;gap:6px}.ed-transport{all:unset;box-sizing:border-box;width:46px;height:46px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));border-radius:var(--nsp-radius-sharp, 3px);color:var(--nsp-text, #f4f2ec);cursor:pointer}.ed-transport-primary{background:var(--nsp-accent, #e3b26a);color:var(--nsp-on-accent, #14161b);border:none}.ed-transport:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-live{all:unset;box-sizing:border-box;height:46px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;background:var(--nsp-live, #e5402a);color:#fff;cursor:pointer;border-radius:var(--nsp-radius-sharp, 3px);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase}.ed-live:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-volumeControl{--ngxsp-control-size: 46px;--ngxsp-volume-button-radius: var(--nsp-radius-sharp, 3px);--ngxsp-volume-button-bg: transparent;--ngxsp-volume-button-border: 1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));--ngxsp-volume-accent: var(--nsp-accent, #e3b26a);--ngxsp-volume-compact-min: 96px;--ngxsp-volume-compact-max: 150px;--ngxsp-volume-color: var(--nsp-text, #f4f2ec);--ngxsp-focus-ring: var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }, { kind: "component", type: NgxspLabeledControl, selector: "ngxsp-labeled-control", inputs: ["label", "active", "pressed", "disabled"], outputs: ["activate"] }, { kind: "component", type: NgxspLiveBadge, selector: "ngxsp-live-badge", inputs: ["liveLabel", "behindSec"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1783
+ }
1784
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsEditionComponent, decorators: [{
1785
+ type: Component,
1786
+ args: [{ selector: 'ngxsp-player-controls-edition', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl, NgxspLabeledControl, NgxspLiveBadge], template: `
1787
+ <div class="ed-top">
1788
+ @if (profile() === 'live') {
1789
+ <ngxsp-live-badge [behindSec]="behindSec()" />
1790
+ }
1791
+ <div class="ed-titleWrap">
1792
+ <div class="ed-title">{{ displayTitle() }}</div>
1793
+ <div class="ed-meta">{{ metaLabel() }}</div>
1794
+ </div>
1795
+ <span class="ed-spacer"></span>
1796
+ <div class="ed-status">{{ statusLabel() }}</div>
1797
+ </div>
1798
+
1799
+ <div class="ed-bottom">
1800
+ <div class="ed-scrubRow">
1801
+ <span class="ed-time">{{ fmtTime(currentTime()) }}</span>
1802
+ <ngxsp-time-slider
1803
+ style="flex:1"
1804
+ [currentTime]="currentTime()"
1805
+ [duration]="duration()"
1806
+ [buffered]="buffered()"
1807
+ [profile]="profile()"
1808
+ (seek)="scrub.emit($event)"
1809
+ />
1810
+ <span class="ed-time ed-time-dim">{{ profile() === 'live' ? liveLabel() : fmtTime(duration()) }}</span>
1811
+ </div>
1812
+
1813
+ <div class="ed-controlsRow">
1814
+ @if (profile() === 'live') {
1815
+ <button type="button" class="ed-live" (click)="goLive.emit()">
1816
+ <ngxsp-smart-icon class="ed-icon" name="next" /> Au direct
1817
+ </button>
1818
+ <ngxsp-play-button class="ed-transport" [playing]="playing()" (playToggle)="playToggle.emit()">
1819
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1820
+ </ngxsp-play-button>
1821
+ } @else {
1822
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(-10)" aria-label="Reculer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-back" /></button>
1823
+ <ngxsp-play-button class="ed-transport ed-transport-primary" [playing]="playing()" (playToggle)="playToggle.emit()">
1824
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1825
+ </ngxsp-play-button>
1826
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(10)" aria-label="Avancer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-forward" /></button>
1827
+ }
1828
+
1829
+ <span class="ed-spacer"></span>
1830
+
1831
+ <ngxsp-labeled-control label="Sous-titres" [pressed]="captionsActive()" (activate)="captions.emit()"><ngxsp-smart-icon class="ed-icon" [name]="captionsActive() ? 'captions' : 'captions-off'" /></ngxsp-labeled-control>
1832
+ <ngxsp-labeled-control label="Pistes" (activate)="openMenu.emit('tracks')"><ngxsp-smart-icon class="ed-icon" name="tracks" /></ngxsp-labeled-control>
1833
+ <ngxsp-labeled-control [label]="speedLabel()" (activate)="openMenu.emit('speed')"><ngxsp-smart-icon class="ed-icon" name="speed" /></ngxsp-labeled-control>
1834
+ @if (showAirPlay()) {
1835
+ <ngxsp-labeled-control [label]="airPlayLabel()" [active]="airPlayActive()" (activate)="airPlay.emit()"><ngxsp-smart-icon class="ed-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" /></ngxsp-labeled-control>
1836
+ }
1837
+ @if (showCast()) {
1838
+ <ngxsp-labeled-control label="Diffuser" [active]="castActive()" (activate)="cast.emit()"><ngxsp-smart-icon class="ed-icon" [name]="castActive() ? 'cast-active' : 'cast'" /></ngxsp-labeled-control>
1839
+ }
1840
+ @if (pipSupported()) {
1841
+ <ngxsp-labeled-control label="PiP" [active]="pipActive()" (activate)="pip.emit()"><ngxsp-smart-icon class="ed-icon" [name]="pipActive() ? 'pip-active' : 'pip'" /></ngxsp-labeled-control>
1842
+ }
1843
+ <ngxsp-volume-control
1844
+ class="ed-volumeControl"
1845
+ [compact]="true"
1846
+ [volume]="volume()"
1847
+ [volumeLabel]="volumeLabel()"
1848
+ [muteLabel]="muteLabel()"
1849
+ [unmuteLabel]="unmuteLabel()"
1850
+ (muteToggle)="mute.emit()"
1851
+ (volumeChange)="volumeChange.emit($event)"
1852
+ />
1853
+ </div>
1854
+
1855
+ <ng-content />
1856
+ </div>
1857
+ `, styles: [":host{display:contents}.ed-icon{width:20px;height:20px;display:block}.ed-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:14px;padding:18px 22px 14px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 55%,transparent);border-bottom:1px solid var(--nsp-line, rgb(255 255 255 / .1));opacity:0;transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-top{opacity:1}.ed-titleWrap{display:flex;flex-direction:column;gap:6px}.ed-title{font-size:24px;font-weight:500;letter-spacing:-.025em;line-height:1;color:var(--nsp-text, #f4f2ec)}.ed-meta{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--nsp-text-2, #b4afa6)}.ed-status{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--nsp-text-3, #7c776f)}.ed-spacer{flex:1}.ed-bottom{position:absolute;left:0;right:0;bottom:0;z-index:4;display:flex;flex-direction:column;gap:14px;padding:16px 22px 18px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 82%,transparent);border-top:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));opacity:0;transform:translateY(8px);transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease),transform var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-bottom{opacity:1;transform:translateY(0)}.ed-scrubRow{display:flex;align-items:center;gap:14px}.ed-time{font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;color:var(--nsp-text, #f4f2ec)}.ed-time-dim{color:var(--nsp-text-2, #b4afa6)}.ed-controlsRow{display:flex;align-items:center;gap:6px}.ed-transport{all:unset;box-sizing:border-box;width:46px;height:46px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));border-radius:var(--nsp-radius-sharp, 3px);color:var(--nsp-text, #f4f2ec);cursor:pointer}.ed-transport-primary{background:var(--nsp-accent, #e3b26a);color:var(--nsp-on-accent, #14161b);border:none}.ed-transport:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-live{all:unset;box-sizing:border-box;height:46px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;background:var(--nsp-live, #e5402a);color:#fff;cursor:pointer;border-radius:var(--nsp-radius-sharp, 3px);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase}.ed-live:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-volumeControl{--ngxsp-control-size: 46px;--ngxsp-volume-button-radius: var(--nsp-radius-sharp, 3px);--ngxsp-volume-button-bg: transparent;--ngxsp-volume-button-border: 1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));--ngxsp-volume-accent: var(--nsp-accent, #e3b26a);--ngxsp-volume-compact-min: 96px;--ngxsp-volume-compact-max: 150px;--ngxsp-volume-color: var(--nsp-text, #f4f2ec);--ngxsp-focus-ring: var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}\n"] }]
1858
+ }], propDecorators: { profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], metaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "metaLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], behindSec: [{ type: i0.Input, args: [{ isSignal: true, alias: "behindSec", required: false }] }], liveLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "liveLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }], goLive: [{ type: i0.Output, args: ["goLive"] }] } });
1859
+
410
1860
  class SmartPlayerComponent {
411
1861
  config;
412
1862
  plugins = [];
@@ -415,8 +1865,16 @@ class SmartPlayerComponent {
415
1865
  debug = false;
416
1866
  /** Optional start time (seconds) used for "resume playback". */
417
1867
  startTimeSec = null;
1868
+ /** Starts the first source as soon as it is loaded. WebViews and desktop shells do not block it like browsers do. */
1869
+ autoplayOnInit = true;
1870
+ /** Increment when the user picks something to play: a new source starts, the current one resumes. */
1871
+ playRequest = 0;
418
1872
  canPrev = false;
419
1873
  canNext = false;
1874
+ previousLabel = 'Previous item';
1875
+ nextLabel = 'Next item';
1876
+ playLabel = 'Play';
1877
+ pauseLabel = 'Pause';
420
1878
  shuffleActive = false;
421
1879
  shuffleLabel = 'Shuffle: off';
422
1880
  repeatMode = 'off';
@@ -424,20 +1882,56 @@ class SmartPlayerComponent {
424
1882
  hasPlaylistControls = false;
425
1883
  mediaMode = 'video';
426
1884
  castEnabled = false;
1885
+ /** The media plays on a device the app drives (castEnabled): the devices button shows it. */
1886
+ castConnected = false;
1887
+ airPlayAvailable = false;
1888
+ airPlayActive = false;
1889
+ documentShortcuts = true;
1890
+ nowPlayingLabel = 'Now playing';
1891
+ nowListeningLabel = 'Now listening';
1892
+ fullscreenHintLabel = 'Double-click for fullscreen';
1893
+ fullscreenLabel = 'Fullscreen';
1894
+ readyStatusLabel = 'Ready';
1895
+ playingStatusLabel = 'Playing';
1896
+ pausedStatusLabel = 'Paused';
1897
+ bufferingStatusLabel = 'Buffering';
1898
+ loadingStatusLabel = 'Loading';
1899
+ endedStatusLabel = 'Ended';
1900
+ errorStatusLabel = 'Error';
1901
+ outputVolume = null;
1902
+ outputVolumeLabel = '';
1903
+ outputMuteLabel = 'Mute';
1904
+ outputUnmuteLabel = 'Restore sound';
1905
+ timelineDurationSec = null;
1906
+ timelineMarkers = [];
427
1907
  // Noms volontairement alignés sur les événements média natifs (API publique).
428
1908
  // eslint-disable-next-line @angular-eslint/no-output-native
429
1909
  ended = new EventEmitter();
430
1910
  videoSize = new EventEmitter();
431
- /** Emits playback position updates. */
1911
+ /**
1912
+ * Emits playback position updates, tagged with the media they belong to: a host switching tracks can still receive
1913
+ * a position of the previous one, and must not file it under the new one.
1914
+ */
432
1915
  // eslint-disable-next-line @angular-eslint/no-output-native
433
1916
  progress = new EventEmitter();
434
1917
  prev = new EventEmitter();
435
1918
  next = new EventEmitter();
436
1919
  toggleShuffle = new EventEmitter();
437
1920
  cycleRepeat = new EventEmitter();
1921
+ /** CC pressed; the vod and edition skins also open their subtitles menu. */
438
1922
  subtitles = new EventEmitter();
1923
+ /** A subtitle file was read (ok) or could not be (error). */
1924
+ subtitlesLoaded = new EventEmitter();
1925
+ /** The words of the player's menus and of its subtitles button. */
1926
+ menuLabels = {};
439
1927
  cast = new EventEmitter();
1928
+ stateChange = new EventEmitter();
1929
+ playbackToggle = new EventEmitter();
1930
+ outputVolumeChange = new EventEmitter();
440
1931
  videoEl;
1932
+ captionsLayer;
1933
+ /** Absent in audio mode, which has no subtitles. */
1934
+ subtitleFileInput;
441
1935
  state = null;
442
1936
  duration = 0;
443
1937
  currentTime = 0;
@@ -448,15 +1942,33 @@ class SmartPlayerComponent {
448
1942
  tracks = [];
449
1943
  openMenu = null;
450
1944
  controlsVisible = true;
1945
+ surfaceGesture = null;
451
1946
  castState = 'disconnected';
452
1947
  playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
453
1948
  facade = new PlayerFacade();
454
1949
  cdr = inject(ChangeDetectorRef);
455
1950
  sub = new Subscription();
456
1951
  hideControlsTimer = null;
1952
+ surfaceClickTimer = null;
1953
+ gestureTimer = null;
457
1954
  remotePlaybackCleanup = null;
1955
+ initialized = false;
1956
+ loadedSourceSignature = '';
1957
+ loadSequence = 0;
1958
+ loading = false;
1959
+ pendingPlay = false;
1960
+ /** Whether the load in progress was asked to start playback. */
1961
+ loadingAutoplay = false;
1962
+ /** Load whose end was already reported; reset when playback starts again (repeat one). */
1963
+ endedLoadSequence = -1;
1964
+ pendingSeekSec = null;
458
1965
  get uiSkin() {
459
- return (this.config?.ui?.skin ?? 'classic') === 'netflix' ? 'netflix' : 'classic';
1966
+ const skin = this.config?.ui?.skin;
1967
+ return skin === 'vod' || skin === 'edition' ? skin : 'classic';
1968
+ }
1969
+ /** Libellé méta du skin édition (genre · format). */
1970
+ get editionMeta() {
1971
+ return this.isAudioMode ? 'Audio' : 'Vidéo';
460
1972
  }
461
1973
  get uiVisible() {
462
1974
  const kind = this.state?.kind;
@@ -465,11 +1977,7 @@ class SmartPlayerComponent {
465
1977
  get displayTitle() {
466
1978
  const ui = this.config?.ui;
467
1979
  const src = this.config?.source;
468
- return (ui?.title ??
469
- ui?.displayTitle ??
470
- src?.title ??
471
- src?.contentId ??
472
- 'Player');
1980
+ return ui?.title ?? ui?.displayTitle ?? src?.title ?? src?.contentId ?? 'Player';
473
1981
  }
474
1982
  get displaySubtitle() {
475
1983
  const ui = this.config?.ui;
@@ -487,6 +1995,8 @@ class SmartPlayerComponent {
487
1995
  }
488
1996
  /** Profil de source transmis aux primitives UI (cf. @ngxsp/ui-core). */
489
1997
  get sourceProfile() {
1998
+ if (this.config?.source?.live)
1999
+ return 'live';
490
2000
  return this.isAudioMode ? 'audio' : 'vod';
491
2001
  }
492
2002
  get audioTracks() {
@@ -507,24 +2017,87 @@ class SmartPlayerComponent {
507
2017
  get selectedVideoTrackId() {
508
2018
  return this.videoTracks.find((track) => track.active)?.id ?? null;
509
2019
  }
2020
+ ngOnChanges(changes) {
2021
+ if (changes['outputVolume'])
2022
+ this.syncOutputVolumeInput();
2023
+ if (!this.initialized)
2024
+ return;
2025
+ const playRequested = !!changes['playRequest'] && !changes['playRequest'].firstChange;
2026
+ if (changes['engines'] && !changes['engines'].firstChange) {
2027
+ this.facade.setEngines(this.engines);
2028
+ // Playback moves to a device (or comes back): the media is loaded again from the same position. It keeps playing
2029
+ // when it moves to a device, and waits on this one when it comes back, unless the host asks for playback.
2030
+ // A different media arriving in the same change is not a move: it loads below from its own start, or it would
2031
+ // inherit the position of the one it replaces.
2032
+ if (this.sourceSignature(this.config) === this.loadedSourceSignature) {
2033
+ const kind = this.state?.kind;
2034
+ const wasPlaying = kind === 'playing' || kind === 'buffering' || (this.loading && (this.loadingAutoplay || this.pendingPlay));
2035
+ // Hosts keep their own engines (adaptive streaming) in the list: only the remote engine means a device.
2036
+ const toDevice = (this.engines ?? []).some((engine) => engine.id === 'remote');
2037
+ if (this.pendingSeekSec === null && this.currentTime > 0.5)
2038
+ this.pendingSeekSec = this.currentTime;
2039
+ void this.loadConfig(playRequested || (toDevice && wasPlaying));
2040
+ return;
2041
+ }
2042
+ }
2043
+ if (changes['config'] && this.sourceSignature(this.config) !== this.loadedSourceSignature) {
2044
+ // Keeps playing when the source is swapped while playing or stalled, right after the previous media ended (queue
2045
+ // chaining) or while a load meant to start is still in progress (a local copy replacing the network source),
2046
+ // and starts when the host asked for playback; nothing starts on its own otherwise.
2047
+ const kind = this.state?.kind;
2048
+ const autoplay = playRequested || kind === 'playing' || kind === 'buffering' || kind === 'ended'
2049
+ || (this.loading && (this.loadingAutoplay || this.pendingPlay));
2050
+ // A position still waiting for the previous media belongs to it: a session restored on a
2051
+ // paused track must not send the next file there (past its end, it would "finish" at once).
2052
+ // The new media starts where `startTimeSec` says.
2053
+ this.pendingSeekSec = null;
2054
+ void this.loadConfig(autoplay);
2055
+ return;
2056
+ }
2057
+ if (playRequested) {
2058
+ // Same source picked again (a paused track the listener clicks): play it, once any load has finished.
2059
+ if (this.loading)
2060
+ this.pendingPlay = true;
2061
+ else
2062
+ void this.facade.play().catch(() => { });
2063
+ }
2064
+ }
510
2065
  ngOnInit() {
511
2066
  const plugins = [...(this.plugins ?? [])];
512
2067
  // v1.11: SSAI (server-side stitched streams) – rewrite manifest URL if configured.
513
2068
  if (this.config?.ads?.enabled && this.config?.ads?.mode === 'ssai') {
514
2069
  plugins.push(createSsaiManifestRewritePlugin());
515
2070
  }
516
- this.facade.attach(this.videoEl.nativeElement, plugins, this.config, this.ports, this.engines);
2071
+ const media = this.mediaElementForMode();
2072
+ this.watchAirPlay(media);
2073
+ this.facade.attach(media, plugins, this.config, this.ports, this.engines);
517
2074
  // markForCheck() requis : la stratégie OnPush ne détecte pas les mutations
518
2075
  // poussées depuis des souscriptions RxJS (cf. docs/AUDIT.md, manquement #7).
519
2076
  this.sub.add(this.facade.state$.subscribe((s) => {
520
2077
  this.state = s;
2078
+ // Engines without a media element (remote devices) only report the end through their state.
2079
+ if (s.kind === 'ended')
2080
+ this.onEnded();
2081
+ else if (s.kind === 'playing')
2082
+ this.endedLoadSequence = -1;
2083
+ this.stateChange.emit(s);
521
2084
  this.syncControlVisibility();
522
2085
  this.cdr.markForCheck();
523
2086
  }));
524
2087
  this.sub.add(this.facade.time$.subscribe((t) => {
2088
+ // While a new source loads, the time still reported is the previous media's. Passed on, a
2089
+ // host would file it under the new track and resume that track there — past its end for a
2090
+ // short file, which then "finishes" at once.
2091
+ if (this.loading)
2092
+ return;
525
2093
  this.currentTime = t.currentTime;
526
2094
  this.duration = t.duration;
527
- this.progress.emit({ currentTime: this.currentTime, duration: this.duration });
2095
+ this.progress.emit({
2096
+ currentTime: this.currentTime,
2097
+ duration: this.duration,
2098
+ contentId: this.config?.source?.contentId,
2099
+ sourceUrl: this.config?.source?.url,
2100
+ });
528
2101
  this.cdr.markForCheck();
529
2102
  }));
530
2103
  this.sub.add(this.facade.bufferAheadSec$.subscribe((value) => {
@@ -535,24 +2108,17 @@ class SmartPlayerComponent {
535
2108
  this.tracks = tracks;
536
2109
  this.cdr.markForCheck();
537
2110
  }));
2111
+ this.sub.add(this.facade.captionCues$.subscribe((cues) => this.drawCaptions(cues)));
538
2112
  this.bindRemotePlaybackState();
539
- this.facade.setVolume(this.volume);
540
- this.facade.load(this.config)
541
- .then(async () => {
542
- const start = Number(this.startTimeSec ?? 0);
543
- if (Number.isFinite(start) && start > 0.2) {
544
- try {
545
- const safe = Math.max(0, Math.min(start, (this.duration || start + 1) - 0.3));
546
- await this.facade.seekTo(safe);
547
- }
548
- catch { /* ignore */ }
549
- }
550
- await this.facade.play().catch(() => { });
551
- })
552
- .catch(() => { });
2113
+ this.applyVolume();
2114
+ this.initialized = true;
2115
+ void this.loadConfig(this.autoplayOnInit);
553
2116
  }
554
2117
  ngOnDestroy() {
2118
+ this.controlsObserver?.disconnect();
555
2119
  this.clearHideControlsTimer();
2120
+ this.clearSurfaceClickTimer();
2121
+ this.clearGestureTimer();
556
2122
  this.remotePlaybackCleanup?.();
557
2123
  this.sub.unsubscribe();
558
2124
  this.facade.destroy();
@@ -562,7 +2128,7 @@ class SmartPlayerComponent {
562
2128
  }
563
2129
  get debugJson() {
564
2130
  try {
565
- const d = this.facade.diagnostics?.() ?? {};
2131
+ const d = this.facade.diagnostics();
566
2132
  const tel = this.ports?.telemetry?.export?.();
567
2133
  return JSON.stringify({ ...d, telemetry: tel }, null, 2);
568
2134
  }
@@ -601,20 +2167,36 @@ class SmartPlayerComponent {
601
2167
  else
602
2168
  this.facade.play();
603
2169
  }
2170
+ requestPlaybackToggle() {
2171
+ const intent = this.playbackToggleIntent();
2172
+ this.togglePlay();
2173
+ this.playbackToggle.emit(intent);
2174
+ this.showSurfaceGesture(intent);
2175
+ }
604
2176
  seekRel(deltaSec) {
605
- this.facade.seekTo(Math.max(0, this.currentTime + deltaSec));
2177
+ this.seekTo(this.currentTime + deltaSec);
2178
+ }
2179
+ seekTo(timeSec) {
2180
+ const value = Number(timeSec);
2181
+ if (!Number.isFinite(value))
2182
+ return;
2183
+ const target = Math.max(0, value);
2184
+ this.pendingSeekSec = target;
2185
+ // A load in progress applies the pending position once the media is ready: seeking now would hit no engine.
2186
+ if (this.loading)
2187
+ return;
2188
+ void this.applySeek(target).then((applied) => {
2189
+ if (applied && this.pendingSeekSec === target)
2190
+ this.pendingSeekSec = null;
2191
+ });
606
2192
  }
607
2193
  onScrub(value) {
608
2194
  const v = Number(value);
609
2195
  if (Number.isFinite(v))
610
- this.facade.seekTo(v);
2196
+ this.seekTo(v);
611
2197
  }
612
2198
  onVolume(value) {
613
- const v = Math.max(0, Math.min(1, Number(value)));
614
- this.volume = Number.isFinite(v) ? v : this.volume;
615
- if (this.volume > 0)
616
- this.lastVolumeBeforeMute = this.volume;
617
- this.facade.setVolume(this.volume);
2199
+ this.setVolume(value, true);
618
2200
  }
619
2201
  toggleMute() {
620
2202
  if (this.volume <= 0.001) {
@@ -624,7 +2206,8 @@ class SmartPlayerComponent {
624
2206
  this.lastVolumeBeforeMute = this.volume;
625
2207
  this.volume = 0;
626
2208
  }
627
- this.facade.setVolume(this.volume);
2209
+ this.applyVolume();
2210
+ this.outputVolumeChange.emit(this.volume);
628
2211
  this.onInteraction();
629
2212
  }
630
2213
  async setPlaybackRate(rate) {
@@ -640,14 +2223,30 @@ class SmartPlayerComponent {
640
2223
  const v = this.videoEl?.nativeElement;
641
2224
  if (!v)
642
2225
  return;
2226
+ // A position requested while the media was loading and not applied yet is applied now that it can be.
2227
+ if (this.pendingSeekSec !== null && !this.loading) {
2228
+ const target = this.pendingSeekSec;
2229
+ void this.applySeek(target).then((applied) => {
2230
+ if (applied && this.pendingSeekSec === target)
2231
+ this.pendingSeekSec = null;
2232
+ });
2233
+ }
643
2234
  const width = v.videoWidth || 0;
644
2235
  const height = v.videoHeight || 0;
645
2236
  if (width > 0 && height > 0) {
646
2237
  this.videoSize.emit({ width, height });
647
2238
  }
648
- }
649
- onEnded() {
650
- // facade already emits 'ended' based on engine events; this output lets host apps chain playlists.
2239
+ }
2240
+ onEnded() {
2241
+ // The media element's event and the engine state both announce the same end: handle it once per playback.
2242
+ if (this.endedLoadSequence === this.loadSequence)
2243
+ return;
2244
+ this.endedLoadSequence = this.loadSequence;
2245
+ // Repeat one replays the same media; otherwise hosts are told so they can chain their queue.
2246
+ if (this.repeatMode === 'one') {
2247
+ void this.facade.seekTo(0).then(() => this.facade.play()).catch(() => { });
2248
+ return;
2249
+ }
651
2250
  this.ended.emit();
652
2251
  }
653
2252
  toggleFullscreen() {
@@ -669,6 +2268,72 @@ class SmartPlayerComponent {
669
2268
  this.openMenu = this.openMenu === menu ? null : menu;
670
2269
  this.onInteraction(this.openMenu !== null);
671
2270
  }
2271
+ get playerMenuLabels() {
2272
+ return { ...DEFAULT_PLAYER_MENU_LABELS, ...this.menuLabels };
2273
+ }
2274
+ onCaptionsClick() {
2275
+ this.toggleMenu('captions');
2276
+ this.subtitles.emit();
2277
+ }
2278
+ openSubtitlePicker() {
2279
+ this.openMenu = null;
2280
+ this.onInteraction();
2281
+ this.subtitleFileInput?.nativeElement.click();
2282
+ }
2283
+ async onSubtitleFileChosen(event) {
2284
+ const input = event.target;
2285
+ const file = input.files?.[0];
2286
+ input.value = '';
2287
+ if (file)
2288
+ await this.loadSubtitleFile(file);
2289
+ }
2290
+ /** Reads a SubRip or WebVTT file and shows it over the video. */
2291
+ async loadSubtitleFile(file) {
2292
+ const { label, language } = describeSubtitleFile(file.name);
2293
+ try {
2294
+ const vtt = subtitleTextToVtt(decodeSubtitleBytes(new Uint8Array(await file.arrayBuffer())));
2295
+ if (!/-->/.test(vtt))
2296
+ throw new Error('No subtitle in this file');
2297
+ await this.facade.addExternalTextTrack({ label, ...(language ? { language } : {}), vtt });
2298
+ this.subtitlesLoaded.emit({ label, ok: true });
2299
+ }
2300
+ catch (error) {
2301
+ this.subtitlesLoaded.emit({ label, ok: false, error: error instanceof Error ? error.message : String(error) });
2302
+ }
2303
+ }
2304
+ controlsObserver;
2305
+ /** While the controls show, the subtitles sit just above them, however tall they are (a menu makes them taller). */
2306
+ followControlsHeight() {
2307
+ if (this.controlsObserver || typeof ResizeObserver !== 'function')
2308
+ return;
2309
+ const bar = this.videoEl.nativeElement.parentElement?.querySelector('.ngxsp-vod-bottom');
2310
+ const layer = this.captionsLayer?.nativeElement;
2311
+ if (!bar || !layer)
2312
+ return;
2313
+ // The top of the bar is a fade over the picture: the subtitles may overlap it.
2314
+ const lift = () => layer.style.setProperty('--ngxsp-captions-lift', `${Math.max(0, bar.offsetHeight - 24)}px`);
2315
+ this.controlsObserver = new ResizeObserver(lift);
2316
+ this.controlsObserver.observe(bar);
2317
+ lift();
2318
+ }
2319
+ /** Draws the cues of the loaded subtitles (their markup parsed by the browser: italics, bold, no script). */
2320
+ drawCaptions(cues) {
2321
+ const layer = this.captionsLayer?.nativeElement;
2322
+ if (!layer)
2323
+ return;
2324
+ if (cues.length)
2325
+ this.followControlsHeight();
2326
+ layer.replaceChildren(...cues.map((cue) => {
2327
+ const line = document.createElement('span');
2328
+ line.className = 'ngxsp-cue';
2329
+ const html = cue.getCueAsHTML?.();
2330
+ if (html)
2331
+ line.append(html);
2332
+ else
2333
+ line.textContent = cue.text ?? '';
2334
+ return line;
2335
+ }));
2336
+ }
672
2337
  async selectTextTrack(trackId) {
673
2338
  this.openMenu = null;
674
2339
  this.onInteraction();
@@ -739,11 +2404,42 @@ class SmartPlayerComponent {
739
2404
  return !!video && (!!video.remote?.prompt || typeof video.webkitShowPlaybackTargetPicker === 'function');
740
2405
  }
741
2406
  isCastActive() {
742
- return this.castEnabled || this.castState === 'connected';
2407
+ return this.castConnected || this.castState === 'connected';
743
2408
  }
744
2409
  showCastButton() {
745
2410
  return this.castEnabled || this.isCastSupported();
746
2411
  }
2412
+ /**
2413
+ * AirPlay, as WebKit offers it: the system decides what is reachable and draws the picker
2414
+ * itself. Nothing is discovered here, and nothing is sent — the route belongs to the device.
2415
+ * Everywhere else these stay false and the button never appears.
2416
+ */
2417
+ showAirPlayButton() {
2418
+ return this.airPlayAvailable;
2419
+ }
2420
+ isAirPlayActive() {
2421
+ return this.airPlayActive;
2422
+ }
2423
+ airPlayLabel() {
2424
+ return this.airPlayActive ? 'Stop AirPlay' : 'AirPlay';
2425
+ }
2426
+ openAirPlayPicker() {
2427
+ const media = this.videoEl?.nativeElement;
2428
+ media?.webkitShowPlaybackTargetPicker?.();
2429
+ }
2430
+ /** Wired to the media element whenever one is created, for video and for audio alike. */
2431
+ watchAirPlay(media) {
2432
+ const target = media;
2433
+ if (!('webkitShowPlaybackTargetPicker' in target))
2434
+ return;
2435
+ this.airPlayActive = target.webkitCurrentPlaybackTargetIsWireless === true;
2436
+ media.addEventListener('webkitplaybacktargetavailabilitychanged', (event) => {
2437
+ this.airPlayAvailable = event.availability === 'available';
2438
+ });
2439
+ media.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', () => {
2440
+ this.airPlayActive = target.webkitCurrentPlaybackTargetIsWireless === true;
2441
+ });
2442
+ }
747
2443
  castLabel() {
748
2444
  if (this.castEnabled)
749
2445
  return 'Play on device';
@@ -771,7 +2467,9 @@ class SmartPlayerComponent {
771
2467
  void this.requestCast();
772
2468
  }
773
2469
  captionsLabel() {
774
- return this.selectedTextTrackId !== null ? 'Captions on' : 'Load subtitles';
2470
+ const labels = this.playerMenuLabels;
2471
+ const shown = this.textTracks.find((track) => track.id === this.selectedTextTrackId);
2472
+ return shown ? `${labels.subtitles} - ${shown.label || shown.language || shown.id}` : labels.subtitles;
775
2473
  }
776
2474
  volumeLabel() {
777
2475
  if (this.volume <= 0.001)
@@ -786,28 +2484,29 @@ class SmartPlayerComponent {
786
2484
  statusLabel() {
787
2485
  const kind = this.state?.kind;
788
2486
  if (kind === 'playing')
789
- return this.playbackRate === 1 ? 'Playing' : `Playing ${this.playbackRate}x`;
2487
+ return this.playbackRate === 1 ? this.playingStatusLabel : `${this.playingStatusLabel} ${this.playbackRate}x`;
790
2488
  if (kind === 'paused')
791
- return 'Paused';
2489
+ return this.pausedStatusLabel;
792
2490
  if (kind === 'buffering')
793
- return 'Buffering';
2491
+ return this.bufferingStatusLabel;
794
2492
  if (kind === 'loading')
795
- return 'Loading';
2493
+ return this.loadingStatusLabel;
796
2494
  if (kind === 'ended')
797
- return 'Ended';
2495
+ return this.endedStatusLabel;
798
2496
  if (kind === 'error')
799
- return 'Error';
800
- return 'Ready';
2497
+ return this.errorStatusLabel;
2498
+ return this.readyStatusLabel;
801
2499
  }
802
2500
  keyboardHintLabel() {
803
2501
  return this.isAudioMode ? 'Space play • Arrows seek • C device' : 'Space play • Arrows seek • F fullscreen • C device';
804
2502
  }
805
2503
  tracksButtonLabel() {
806
- return this.textTracks.length || this.audioTracks.length ? 'Tracks' : 'Media';
2504
+ return this.textTracks.length || this.audioTracks.length ? this.playerMenuLabels.tracks : this.playerMenuLabels.media;
807
2505
  }
808
2506
  onPrimarySurfaceDoubleClick() {
809
2507
  if (this.isAudioMode)
810
2508
  return;
2509
+ this.clearSurfaceClickTimer();
811
2510
  this.toggleFullscreen();
812
2511
  }
813
2512
  bufferPercent() {
@@ -825,6 +2524,25 @@ class SmartPlayerComponent {
825
2524
  return `${m}:${String(r).padStart(2, '0')}`;
826
2525
  }
827
2526
  noop() { }
2527
+ /**
2528
+ * Music plays in a real <audio> element: mobile browsers and Android WebViews pause <video> elements as soon as
2529
+ * the page is hidden, which would stop the music when the app goes to the background or the screen turns off.
2530
+ */
2531
+ mediaElementForMode() {
2532
+ const video = this.videoEl.nativeElement;
2533
+ if (!this.isAudioMode || typeof document === 'undefined')
2534
+ return video;
2535
+ const audio = document.createElement('audio');
2536
+ audio.preload = 'metadata';
2537
+ audio.setAttribute('x-webkit-airplay', 'allow');
2538
+ audio.addEventListener('timeupdate', () => this.noop());
2539
+ audio.addEventListener('loadedmetadata', () => this.onLoadedMetadata());
2540
+ audio.addEventListener('ended', () => this.onEnded());
2541
+ video.replaceWith(audio);
2542
+ // The facade and helpers only use HTMLMediaElement members for audio (video-only APIs are feature-checked).
2543
+ this.videoEl = new ElementRef(audio);
2544
+ return this.videoEl.nativeElement;
2545
+ }
828
2546
  onInteraction(persist = false) {
829
2547
  this.controlsVisible = true;
830
2548
  this.clearHideControlsTimer();
@@ -845,11 +2563,18 @@ class SmartPlayerComponent {
845
2563
  const target = event.target;
846
2564
  if (!target)
847
2565
  return;
848
- if (target.closest('.ngxsp-menu, .ngxsp-nfx-chip, .ngxsp-btn, .ngxsp-debug, .ngxsp-nfx-side, .ngxsp-nfx-center, .ngxsp-range')) {
2566
+ if (target.closest('button, input, textarea, select, summary, [contenteditable="true"], .ngxsp-menu, .ngxsp-controls, .ngxsp-debug, .ngxsp-overlay, .ngxsp-range')) {
2567
+ return;
2568
+ }
2569
+ if (event.detail > 1) {
2570
+ this.clearSurfaceClickTimer();
849
2571
  return;
850
2572
  }
851
2573
  this.openMenu = null;
852
2574
  this.onInteraction();
2575
+ if (this.uiSkin === 'vod' && !this.isAudioMode) {
2576
+ this.queueSurfacePlaybackToggle();
2577
+ }
853
2578
  }
854
2579
  onEscape() {
855
2580
  if (this.openMenu !== null) {
@@ -858,11 +2583,13 @@ class SmartPlayerComponent {
858
2583
  }
859
2584
  }
860
2585
  onKeydown(event) {
2586
+ if (!this.documentShortcuts)
2587
+ return;
861
2588
  if (this.isInteractiveTarget(event.target))
862
2589
  return;
863
2590
  if (event.code === 'Space') {
864
2591
  event.preventDefault();
865
- this.togglePlay();
2592
+ this.requestPlaybackToggle();
866
2593
  this.onInteraction();
867
2594
  return;
868
2595
  }
@@ -898,6 +2625,80 @@ class SmartPlayerComponent {
898
2625
  this.onCastClick();
899
2626
  }
900
2627
  }
2628
+ async loadConfig(autoplay) {
2629
+ const sequence = ++this.loadSequence;
2630
+ this.loading = true;
2631
+ this.loadingAutoplay = autoplay;
2632
+ this.loadedSourceSignature = this.sourceSignature(this.config);
2633
+ this.duration = 0;
2634
+ this.currentTime = 0;
2635
+ this.bufferAheadSec = 0;
2636
+ this.tracks = [];
2637
+ this.cdr.markForCheck();
2638
+ try {
2639
+ await this.facade.load(this.config);
2640
+ if (this.pendingSeekSec !== null) {
2641
+ const target = this.pendingSeekSec;
2642
+ // Cleared only when the position really reached an engine; otherwise the metadata event retries it.
2643
+ if (await this.applySeek(target) && this.pendingSeekSec === target)
2644
+ this.pendingSeekSec = null;
2645
+ }
2646
+ else {
2647
+ await this.applyStartTime();
2648
+ }
2649
+ // A newer load owns playback: only the latest request may start it.
2650
+ if (sequence === this.loadSequence && (autoplay || this.pendingPlay)) {
2651
+ this.pendingPlay = false;
2652
+ await this.facade.play().catch(() => { });
2653
+ }
2654
+ }
2655
+ catch {
2656
+ // The facade has already emitted a typed player error.
2657
+ }
2658
+ finally {
2659
+ if (sequence === this.loadSequence) {
2660
+ this.loading = false;
2661
+ // A position asked for while this load was finishing, after it looked for one, is applied now.
2662
+ const target = this.pendingSeekSec;
2663
+ if (target !== null) {
2664
+ void this.applySeek(target).then((applied) => {
2665
+ if (applied && this.pendingSeekSec === target)
2666
+ this.pendingSeekSec = null;
2667
+ });
2668
+ }
2669
+ }
2670
+ }
2671
+ }
2672
+ async applyStartTime() {
2673
+ const start = Number(this.startTimeSec ?? 0);
2674
+ if (!Number.isFinite(start) || start <= 0.2)
2675
+ return;
2676
+ await this.applySeek(start);
2677
+ }
2678
+ async applySeek(timeSec) {
2679
+ try {
2680
+ return await this.facade.seekTo(this.safeSeekTime(timeSec));
2681
+ }
2682
+ catch {
2683
+ // Best effort: some engines only allow seeking after enough metadata is loaded.
2684
+ return false;
2685
+ }
2686
+ }
2687
+ safeSeekTime(timeSec) {
2688
+ return Math.max(0, Math.min(timeSec, (this.duration || timeSec + 1) - 0.3));
2689
+ }
2690
+ sourceSignature(config) {
2691
+ const source = config?.source;
2692
+ if (!source)
2693
+ return '';
2694
+ return [
2695
+ source.type,
2696
+ source.url,
2697
+ source.contentId,
2698
+ source.live ? 'live' : 'vod',
2699
+ source.offlineEligible ? 'offline' : 'online',
2700
+ ].join('|');
2701
+ }
901
2702
  bindRemotePlaybackState() {
902
2703
  const video = this.videoEl?.nativeElement;
903
2704
  const remote = video?.remote;
@@ -927,12 +2728,80 @@ class SmartPlayerComponent {
927
2728
  this.clearHideControlsTimer();
928
2729
  this.controlsVisible = true;
929
2730
  }
2731
+ applyVolume() {
2732
+ try {
2733
+ const media = this.videoEl.nativeElement;
2734
+ media.volume = this.volume;
2735
+ // iOS ignores volume writes: without this, muting and the volume slider have no effect on iPhone and iPad.
2736
+ media.muted = this.volume <= 0.001;
2737
+ }
2738
+ catch {
2739
+ // Some browser environments reject direct volume writes; the engine path remains available.
2740
+ }
2741
+ this.facade.setVolume(this.volume);
2742
+ }
2743
+ syncOutputVolumeInput() {
2744
+ this.setVolume(this.outputVolume, false);
2745
+ }
2746
+ setOutputVolume(value) {
2747
+ this.setVolume(value, false);
2748
+ }
2749
+ setVolume(value, emitChange) {
2750
+ const nextVolume = this.normalizeVolume(value, this.volume);
2751
+ if (Math.abs(nextVolume - this.volume) < 0.001)
2752
+ return;
2753
+ this.volume = nextVolume;
2754
+ if (this.volume > 0)
2755
+ this.lastVolumeBeforeMute = this.volume;
2756
+ if (this.initialized)
2757
+ this.applyVolume();
2758
+ if (emitChange)
2759
+ this.outputVolumeChange.emit(this.volume);
2760
+ }
2761
+ normalizeVolume(value, fallback) {
2762
+ const numeric = Number(value);
2763
+ if (!Number.isFinite(numeric))
2764
+ return fallback;
2765
+ return Math.max(0, Math.min(1, numeric));
2766
+ }
930
2767
  clearHideControlsTimer() {
931
2768
  if (this.hideControlsTimer) {
932
2769
  clearTimeout(this.hideControlsTimer);
933
2770
  this.hideControlsTimer = null;
934
2771
  }
935
2772
  }
2773
+ queueSurfacePlaybackToggle() {
2774
+ this.clearSurfaceClickTimer();
2775
+ this.surfaceClickTimer = setTimeout(() => {
2776
+ this.surfaceClickTimer = null;
2777
+ this.requestPlaybackToggle();
2778
+ this.cdr.markForCheck();
2779
+ }, 180);
2780
+ }
2781
+ clearSurfaceClickTimer() {
2782
+ if (this.surfaceClickTimer) {
2783
+ clearTimeout(this.surfaceClickTimer);
2784
+ this.surfaceClickTimer = null;
2785
+ }
2786
+ }
2787
+ playbackToggleIntent() {
2788
+ return this.state?.kind === 'playing' ? 'pause' : 'play';
2789
+ }
2790
+ showSurfaceGesture(intent) {
2791
+ this.surfaceGesture = intent;
2792
+ this.clearGestureTimer();
2793
+ this.gestureTimer = setTimeout(() => {
2794
+ this.surfaceGesture = null;
2795
+ this.cdr.markForCheck();
2796
+ }, 520);
2797
+ this.cdr.markForCheck();
2798
+ }
2799
+ clearGestureTimer() {
2800
+ if (this.gestureTimer) {
2801
+ clearTimeout(this.gestureTimer);
2802
+ this.gestureTimer = null;
2803
+ }
2804
+ }
936
2805
  isInteractiveTarget(target) {
937
2806
  const el = target;
938
2807
  if (!el)
@@ -945,15 +2814,20 @@ class SmartPlayerComponent {
945
2814
  emitPrev() {
946
2815
  this.prev.emit();
947
2816
  }
948
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
949
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: SmartPlayerComponent, isStandalone: true, selector: "ngx-smart-player", inputs: { config: "config", plugins: "plugins", ports: "ports", engines: "engines", debug: "debug", startTimeSec: "startTimeSec", canPrev: "canPrev", canNext: "canNext", shuffleActive: "shuffleActive", shuffleLabel: "shuffleLabel", repeatMode: "repeatMode", repeatLabel: "repeatLabel", hasPlaylistControls: "hasPlaylistControls", mediaMode: "mediaMode", castEnabled: "castEnabled" }, outputs: { ended: "ended", videoSize: "videoSize", progress: "progress", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", subtitles: "subtitles", cast: "cast" }, host: { listeners: { "document:keydown.escape": "onEscape()", "document:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "videoEl", first: true, predicate: ["video"], descendants: true, static: true }], ngImport: i0, template: `
950
- <div class="ngxsp-root" [class.ngxsp-skin-netflix]="uiSkin==='netflix'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
2817
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2818
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartPlayerComponent, isStandalone: true, selector: "ngx-smart-player", inputs: { config: "config", plugins: "plugins", ports: "ports", engines: "engines", debug: "debug", startTimeSec: "startTimeSec", autoplayOnInit: "autoplayOnInit", playRequest: "playRequest", canPrev: "canPrev", canNext: "canNext", previousLabel: "previousLabel", nextLabel: "nextLabel", playLabel: "playLabel", pauseLabel: "pauseLabel", shuffleActive: "shuffleActive", shuffleLabel: "shuffleLabel", repeatMode: "repeatMode", repeatLabel: "repeatLabel", hasPlaylistControls: "hasPlaylistControls", mediaMode: "mediaMode", castEnabled: "castEnabled", castConnected: "castConnected", documentShortcuts: "documentShortcuts", nowPlayingLabel: "nowPlayingLabel", nowListeningLabel: "nowListeningLabel", fullscreenHintLabel: "fullscreenHintLabel", fullscreenLabel: "fullscreenLabel", readyStatusLabel: "readyStatusLabel", playingStatusLabel: "playingStatusLabel", pausedStatusLabel: "pausedStatusLabel", bufferingStatusLabel: "bufferingStatusLabel", loadingStatusLabel: "loadingStatusLabel", endedStatusLabel: "endedStatusLabel", errorStatusLabel: "errorStatusLabel", outputVolume: "outputVolume", outputVolumeLabel: "outputVolumeLabel", outputMuteLabel: "outputMuteLabel", outputUnmuteLabel: "outputUnmuteLabel", timelineDurationSec: "timelineDurationSec", timelineMarkers: "timelineMarkers", menuLabels: "menuLabels" }, outputs: { ended: "ended", videoSize: "videoSize", progress: "progress", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", subtitles: "subtitles", subtitlesLoaded: "subtitlesLoaded", cast: "cast", stateChange: "stateChange", playbackToggle: "playbackToggle", outputVolumeChange: "outputVolumeChange" }, host: { listeners: { "document:keydown.escape": "onEscape()", "document:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "videoEl", first: true, predicate: ["video"], descendants: true, static: true }, { propertyName: "captionsLayer", first: true, predicate: ["captionsLayer"], descendants: true, static: true }, { propertyName: "subtitleFileInput", first: true, predicate: ["subtitleFileInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
2819
+ <div class="ngxsp-root" [class.ngxsp-skin-vod]="uiSkin==='vod'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
951
2820
  <!-- Surface de présentation : interactions pointeur non essentielles, toutes
952
2821
  dupliquées au clavier par @HostListener('document:keydown') (Espace/flèches/F/M/C)
953
2822
  et par les contrôles focusables. -->
954
2823
  <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
955
2824
  <div class="ngxsp-video" (dblclick)="onPrimarySurfaceDoubleClick()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
956
- <video #video playsinline (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
2825
+ <video #video playsinline x-webkit-airplay="allow" (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
2826
+ <!-- Subtitles loaded from a file, drawn here at the bottom of the picture, above the controls when they show. -->
2827
+ <div #captionsLayer class="ngxsp-captions" aria-hidden="true"></div>
2828
+ @if (!isAudioMode) {
2829
+ <input #subtitleFileInput class="ngxsp-subtitleInput" type="file" accept=".srt,.vtt,text/vtt,application/x-subrip" hidden (change)="onSubtitleFileChosen($event)" />
2830
+ }
957
2831
  @if (isAudioMode) {
958
2832
  <div class="ngxsp-audio-stage" aria-hidden="true">
959
2833
  <div class="ngxsp-audio-disc"></div>
@@ -976,132 +2850,136 @@ class SmartPlayerComponent {
976
2850
  (copyJson)="copyDebug()"
977
2851
  (downloadJson)="downloadDebug()"
978
2852
  />
2853
+ @if (surfaceGesture) {
2854
+ <div class="ngxsp-gesturePulse" aria-hidden="true">
2855
+ @if (surfaceGesture === 'pause') {
2856
+ <ngxsp-smart-icon name="pause" />
2857
+ } @else {
2858
+ <ngxsp-smart-icon name="play" />
2859
+ }
2860
+ </div>
2861
+ }
979
2862
 
980
- @if (uiSkin==='netflix') {
981
- <div class="ngxsp-nfx-top">
982
- <div class="ngxsp-nfx-titleWrap">
983
- <div class="ngxsp-nfx-eyebrow">{{ isAudioMode ? 'Now Listening' : 'Now Playing' }}</div>
984
- <div class="ngxsp-nfx-title">{{displayTitle}}</div>
985
- </div>
986
- <div class="ngxsp-nfx-spacer"></div>
987
- <div class="ngxsp-nfx-status">{{ statusLabel() }}</div>
988
- @if (!isAudioMode) {
989
- <div class="ngxsp-nfx-topHint">Double-click for fullscreen</div>
990
- }
991
- </div>
992
-
993
- <div class="ngxsp-nfx-centerDock">
994
- @if (hasPlaylistControls) {
995
- <button type="button" class="ngxsp-nfx-side" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item">
996
- <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
997
- </button>
998
- }
999
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds">
1000
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1001
- <span class="ngxsp-iconBadge">10</span>
1002
- </button>
1003
- <ngxsp-play-button class="ngxsp-nfx-center" [playing]="state?.kind==='playing'" (playToggle)="togglePlay()">
1004
- @if (state?.kind==='playing') {
1005
- <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1006
- } @else {
1007
- <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1008
- }
1009
- </ngxsp-play-button>
1010
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(10)" aria-label="Seek forward 10 seconds">
1011
- <span class="ngxsp-iconBadge">10</span>
1012
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1013
- </button>
1014
- @if (hasPlaylistControls) {
1015
- <button type="button" class="ngxsp-nfx-side" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item">
1016
- <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1017
- </button>
1018
- }
1019
- </div>
1020
-
1021
- <div class="ngxsp-controls ngxsp-nfx-bottom">
1022
- <div class="ngxsp-nfx-metaRow">
1023
- <div class="ngxsp-nfx-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
1024
- <div class="ngxsp-nfx-volumeGroup">
1025
- <button type="button" class="ngxsp-nfx-chip ngxsp-nfx-chip-quiet" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
1026
- @if (volume <= 0.001) {
1027
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-mute" />
1028
- } @else if (volume < 0.5) {
1029
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-low" />
1030
- } @else {
1031
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-high" />
1032
- }
1033
- </button>
1034
- <input class="ngxsp-range ngxsp-nfx-volume" type="range"
1035
- min="0" max="1" step="0.01"
1036
- [value]="volume"
1037
- (input)="onVolume(($any($event.target).value))"
1038
- aria-label="Volume"
1039
- />
1040
- </div>
1041
- <div class="ngxsp-nfx-spacer"></div>
1042
- @if (hasPlaylistControls) {
1043
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
1044
- <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1045
- </button>
1046
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
1047
- @if (repeatMode === 'one') {
1048
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1049
- } @else {
1050
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1051
- }
1052
- </button>
1053
- }
1054
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()">
1055
- <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1056
- </button>
1057
- <button type="button" class="ngxsp-nfx-chip" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1058
- @if (selectedTextTrackId !== null) {
1059
- <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1060
- } @else {
1061
- <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1062
- }
1063
- </button>
1064
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()">
1065
- <ngxsp-smart-icon class="ngxsp-icon" name="speed" />
1066
- </button>
1067
- @if (showCastButton()) {
1068
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="isCastActive()" (click)="onCastClick()" [attr.title]="castHintLabel()" [attr.aria-label]="castLabel()">
1069
- @if (isCastActive()) {
1070
- <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1071
- } @else {
1072
- <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1073
- }
1074
- </button>
1075
- }
1076
- @if (!isAudioMode && isPiPSupported()) {
1077
- <button type="button" class="ngxsp-nfx-chip" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1078
- @if (isPiPActive()) {
1079
- <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1080
- } @else {
1081
- <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1082
- }
1083
- </button>
1084
- }
2863
+ @if (uiSkin==='vod') {
2864
+ <ngxsp-player-controls-vod
2865
+ [playing]="state?.kind==='playing'"
2866
+ [isAudioMode]="isAudioMode"
2867
+ [displayTitle]="displayTitle"
2868
+ [nowPlayingLabel]="nowPlayingLabel"
2869
+ [nowListeningLabel]="nowListeningLabel"
2870
+ [fullscreenHintLabel]="fullscreenHintLabel"
2871
+ [statusLabel]="statusLabel()"
2872
+ [keyboardHint]="keyboardHintLabel()"
2873
+ [hasPlaylistControls]="hasPlaylistControls"
2874
+ [canPrev]="canPrev"
2875
+ [canNext]="canNext"
2876
+ [previousLabel]="previousLabel"
2877
+ [nextLabel]="nextLabel"
2878
+ [playLabel]="playLabel"
2879
+ [pauseLabel]="pauseLabel"
2880
+ [shuffleActive]="shuffleActive"
2881
+ [shuffleLabel]="shuffleLabel"
2882
+ [repeatMode]="repeatMode"
2883
+ [repeatLabel]="repeatLabel"
2884
+ [captionsActive]="selectedTextTrackId !== null"
2885
+ [tracksLabel]="tracksButtonLabel()"
2886
+ [captionsLabel]="captionsLabel()"
2887
+ [speedLabel]="playbackRateLabel()"
2888
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
2889
+ [muteLabel]="outputMuteLabel"
2890
+ [unmuteLabel]="outputUnmuteLabel"
2891
+ [showAirPlay]="showAirPlayButton()"
2892
+ [airPlayActive]="isAirPlayActive()"
2893
+ [airPlayLabel]="airPlayLabel()"
2894
+ (airPlay)="openAirPlayPicker()"
2895
+ [showCast]="showCastButton()"
2896
+ [castActive]="isCastActive()"
2897
+ [castLabel]="castLabel()"
2898
+ [castHint]="castHintLabel()"
2899
+ [pipSupported]="isPiPSupported()"
2900
+ [pipActive]="isPiPActive()"
2901
+ [pipLabel]="pipLabel()"
2902
+ [fullscreenLabel]="fullscreenLabel"
2903
+ [volume]="volume"
2904
+ [currentTime]="currentTime"
2905
+ [duration]="duration"
2906
+ [buffered]="bufferAheadSec"
2907
+ [bufferPercent]="bufferPercent()"
2908
+ [profile]="sourceProfile"
2909
+ [timelineDurationSec]="timelineDurationSec ?? 0"
2910
+ [timelineMarkers]="timelineMarkers"
2911
+ (playToggle)="requestPlaybackToggle()"
2912
+ (seekRelative)="seekRel($event)"
2913
+ (prev)="emitPrev()"
2914
+ (next)="emitNext()"
2915
+ (toggleShuffle)="toggleShuffle.emit()"
2916
+ (cycleRepeat)="cycleRepeat.emit()"
2917
+ (openMenu)="toggleMenu($event)"
2918
+ (captions)="onCaptionsClick()"
2919
+ (cast)="onCastClick()"
2920
+ (pip)="togglePiP()"
2921
+ (fullscreen)="toggleFullscreen()"
2922
+ (mute)="toggleMute()"
2923
+ (scrub)="onScrub($event)"
2924
+ (volumeChange)="onVolume($event)"
2925
+ >
2926
+ <div ngxsp-player-inline-controls class="ngxsp-inline-projection">
2927
+ <ng-content select="[ngxsp-player-inline-controls]" />
1085
2928
  </div>
1086
-
1087
- <ngxsp-time-slider
1088
- class="ngxsp-nfx-range"
1089
- [currentTime]="currentTime"
1090
- [duration]="duration"
1091
- [buffered]="bufferAheadSec"
1092
- [profile]="sourceProfile"
1093
- (seek)="onScrub($event)"
2929
+ <ngxsp-player-menu
2930
+ [openMenu]="openMenu"
2931
+ [isAudioMode]="isAudioMode"
2932
+ [textTracks]="textTracks"
2933
+ [audioTracks]="audioTracks"
2934
+ [videoTracks]="videoTracks"
2935
+ [selectedTextTrackId]="selectedTextTrackId"
2936
+ [selectedAudioTrackId]="selectedAudioTrackId"
2937
+ [selectedVideoTrackId]="selectedVideoTrackId"
2938
+ [playbackRates]="playbackRates"
2939
+ [playbackRate]="playbackRate"
2940
+ [labels]="playerMenuLabels"
2941
+ (loadSubtitles)="openSubtitlePicker()"
2942
+ (selectText)="selectTextTrack($event)"
2943
+ (selectAudio)="selectAudioTrack($event)"
2944
+ (selectVideo)="selectVideoTrack($event)"
2945
+ (selectRate)="setPlaybackRate($event)"
1094
2946
  />
1095
-
1096
- <div class="ngxsp-nfx-metaRow">
1097
- <div class="ngxsp-nfx-spacer"></div>
1098
- <div class="ngxsp-nfx-label">{{ keyboardHintLabel() }}</div>
1099
- </div>
1100
-
1101
- <div class="ngxsp-buffer" aria-hidden="true">
1102
- <div class="ngxsp-bufferFill" [style.width.%]="bufferPercent()"></div>
1103
- </div>
1104
-
2947
+ </ngxsp-player-controls-vod>
2948
+ } @else if (uiSkin==='edition') {
2949
+ <ngxsp-player-controls-edition
2950
+ [profile]="sourceProfile"
2951
+ [playing]="state?.kind==='playing'"
2952
+ [displayTitle]="displayTitle"
2953
+ [metaLabel]="editionMeta"
2954
+ [statusLabel]="statusLabel()"
2955
+ [captionsActive]="selectedTextTrackId !== null"
2956
+ [speedLabel]="playbackRateLabel()"
2957
+ [showAirPlay]="showAirPlayButton()"
2958
+ [airPlayActive]="isAirPlayActive()"
2959
+ [airPlayLabel]="airPlayLabel()"
2960
+ (airPlay)="openAirPlayPicker()"
2961
+ [showCast]="showCastButton()"
2962
+ [castActive]="isCastActive()"
2963
+ [pipSupported]="isPiPSupported()"
2964
+ [pipActive]="isPiPActive()"
2965
+ [volume]="volume"
2966
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
2967
+ [muteLabel]="outputMuteLabel"
2968
+ [unmuteLabel]="outputUnmuteLabel"
2969
+ [currentTime]="currentTime"
2970
+ [duration]="duration"
2971
+ [buffered]="bufferAheadSec"
2972
+ (playToggle)="requestPlaybackToggle()"
2973
+ (seekRelative)="seekRel($event)"
2974
+ (openMenu)="toggleMenu($event)"
2975
+ (captions)="onCaptionsClick()"
2976
+ (cast)="onCastClick()"
2977
+ (pip)="togglePiP()"
2978
+ (mute)="toggleMute()"
2979
+ (scrub)="onScrub($event)"
2980
+ (volumeChange)="onVolume($event)"
2981
+ (goLive)="reload()"
2982
+ >
1105
2983
  <ngxsp-player-menu
1106
2984
  [openMenu]="openMenu"
1107
2985
  [isAudioMode]="isAudioMode"
@@ -1113,114 +2991,79 @@ class SmartPlayerComponent {
1113
2991
  [selectedVideoTrackId]="selectedVideoTrackId"
1114
2992
  [playbackRates]="playbackRates"
1115
2993
  [playbackRate]="playbackRate"
2994
+ [labels]="playerMenuLabels"
2995
+ (loadSubtitles)="openSubtitlePicker()"
1116
2996
  (selectText)="selectTextTrack($event)"
1117
2997
  (selectAudio)="selectAudioTrack($event)"
1118
2998
  (selectVideo)="selectVideoTrack($event)"
1119
2999
  (selectRate)="setPlaybackRate($event)"
1120
3000
  />
1121
- </div>
3001
+ </ngxsp-player-controls-edition>
1122
3002
  } @else {
1123
- <div class="ngxsp-controls">
1124
- <ngxsp-play-button class="ngxsp-btn" [playing]="state?.kind==='playing'" (playToggle)="togglePlay()">
1125
- @if (state?.kind==='playing') {
1126
- <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1127
- } @else {
1128
- <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1129
- }
1130
- </ngxsp-play-button>
1131
- <button type="button" class="ngxsp-btn" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1132
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1133
- <span class="ngxsp-iconBadge">10</span>
1134
- </button>
1135
- <button type="button" class="ngxsp-btn" (click)="seekRel(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1136
- <span class="ngxsp-iconBadge">10</span>
1137
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1138
- </button>
1139
- @if (hasPlaylistControls) {
1140
- <button type="button" class="ngxsp-btn" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1141
- <button type="button" class="ngxsp-btn" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1142
- <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
1143
- <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1144
- </button>
1145
- <button type="button" class="ngxsp-btn" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
1146
- @if (repeatMode === 'one') {
1147
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1148
- } @else {
1149
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1150
- }
1151
- </button>
1152
- }
1153
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1154
- <button type="button" class="ngxsp-btn" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1155
- @if (selectedTextTrackId !== null) {
1156
- <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1157
- } @else {
1158
- <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1159
- }
1160
- </button>
1161
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1162
- @if (showCastButton()) {
1163
- <button type="button" class="ngxsp-btn" [class.accent]="isCastActive()" (click)="onCastClick()" [attr.title]="castHintLabel()" [attr.aria-label]="castLabel()">
1164
- @if (isCastActive()) {
1165
- <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1166
- } @else {
1167
- <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1168
- }
1169
- </button>
1170
- }
1171
- @if (!isAudioMode && isPiPSupported()) {
1172
- <button type="button" class="ngxsp-btn" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1173
- @if (isPiPActive()) {
1174
- <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1175
- } @else {
1176
- <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1177
- }
1178
- </button>
1179
- }
1180
- <button type="button" class="ngxsp-btn" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
1181
- @if (volume <= 0.001) {
1182
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-mute" />
1183
- } @else if (volume < 0.5) {
1184
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-low" />
1185
- } @else {
1186
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-high" />
1187
- }
1188
- </button>
1189
-
1190
- <ngxsp-time-slider
1191
- style="flex:1 1 160px; min-width:160px"
1192
- [currentTime]="currentTime"
1193
- [duration]="duration"
1194
- [buffered]="bufferAheadSec"
1195
- [profile]="sourceProfile"
1196
- (seek)="onScrub($event)"
1197
- />
1198
-
1199
- <div class="ngxsp-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
1200
-
1201
- <input class="ngxsp-range" style="max-width:140px" type="range"
1202
- min="0" max="1" step="0.01"
1203
- [value]="volume"
1204
- (input)="onVolume(($any($event.target).value))"
1205
- aria-label="Volume"
1206
- />
1207
- </div>
3003
+ <ngxsp-player-controls
3004
+ [playing]="state?.kind==='playing'"
3005
+ [isAudioMode]="isAudioMode"
3006
+ [hasPlaylistControls]="hasPlaylistControls"
3007
+ [canPrev]="canPrev"
3008
+ [canNext]="canNext"
3009
+ [shuffleActive]="shuffleActive"
3010
+ [shuffleLabel]="shuffleLabel"
3011
+ [repeatMode]="repeatMode"
3012
+ [repeatLabel]="repeatLabel"
3013
+ [captionsActive]="selectedTextTrackId !== null"
3014
+ [tracksLabel]="tracksButtonLabel()"
3015
+ [captionsLabel]="captionsLabel()"
3016
+ [speedLabel]="playbackRateLabel()"
3017
+ [volumeLabel]="volumeLabel()"
3018
+ [showCast]="showCastButton()"
3019
+ [castActive]="isCastActive()"
3020
+ [castLabel]="castLabel()"
3021
+ [castHint]="castHintLabel()"
3022
+ [pipSupported]="isPiPSupported()"
3023
+ [pipActive]="isPiPActive()"
3024
+ [pipLabel]="pipLabel()"
3025
+ [volume]="volume"
3026
+ [muteLabel]="outputMuteLabel"
3027
+ [unmuteLabel]="outputUnmuteLabel"
3028
+ [currentTime]="currentTime"
3029
+ [duration]="duration"
3030
+ [buffered]="bufferAheadSec"
3031
+ [profile]="sourceProfile"
3032
+ (playToggle)="requestPlaybackToggle()"
3033
+ (seekRelative)="seekRel($event)"
3034
+ (prev)="emitPrev()"
3035
+ (next)="emitNext()"
3036
+ (toggleShuffle)="toggleShuffle.emit()"
3037
+ (cycleRepeat)="cycleRepeat.emit()"
3038
+ (openMenu)="toggleMenu($event)"
3039
+ (captions)="subtitles.emit()"
3040
+ (cast)="onCastClick()"
3041
+ (pip)="togglePiP()"
3042
+ (mute)="toggleMute()"
3043
+ (scrub)="onScrub($event)"
3044
+ (volumeChange)="onVolume($event)"
3045
+ />
1208
3046
  }
1209
3047
 
1210
3048
  </div>
1211
3049
  </div>
1212
- `, isInline: true, styles: [".ngxsp-root{--ngxsp-bg:#07111f;--ngxsp-bg-elev:#0d1a2b;--ngxsp-bg-soft:#102238;--ngxsp-border:rgba(148,163,184,.18);--ngxsp-border-strong:rgba(191,219,254,.24);--ngxsp-text:#e6eef8;--ngxsp-text-dim:rgba(230,238,248,.72);--ngxsp-accent:#7dd3fc;--ngxsp-accent-soft:rgba(125,211,252,.16);position:relative;width:100%;height:100%;background:radial-gradient(circle at top,rgba(56,189,248,.12),transparent 28%),linear-gradient(180deg,#0a1524,#07111f 56%,#050d18);color:var(--ngxsp-text);font-family:Segoe UI,SF Pro Text,Inter,system-ui,sans-serif;letter-spacing:.01em}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate}.ngxsp-video video{width:100%;height:100%;display:block;background:#02060b;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:clamp(24px,3.8vw,42px);font-weight:800;letter-spacing:-.03em;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:clamp(13px,1.4vw,16px);color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border);color:var(--ngxsp-text);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong);color:#d8f3ff}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:32px;height:32px}.ngxsp-iconBadge{font-size:11px;font-weight:800;line-height:1}.ngxsp-range{flex:1;min-width:160px}.ngxsp-time{color:var(--ngxsp-text-dim);font-variant-numeric:tabular-nums}.ngxsp-skin-netflix .ngxsp-video{position:relative}.ngxsp-skin-netflix .ngxsp-video:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,#0f172a00 30%,#02061747);opacity:.92;pointer-events:none;z-index:0}.ngxsp-skin-netflix .ngxsp-video:after{content:\"\";position:absolute;inset:auto 0 0;height:220px;background:linear-gradient(to top,#040a13f5,#060e1ab8 38%,#0000);opacity:0;transition:opacity .18s ease;pointer-events:none}.ngxsp-skin-netflix.ngxsp-ui-visible .ngxsp-video:after{opacity:1}.ngxsp-mode-audio.ngxsp-skin-netflix .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-nfx-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:20px 20px 24px;background:linear-gradient(to bottom,#040a13eb,#040a1300);pointer-events:none;opacity:0;transition:opacity .18s ease;z-index:3}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-top{opacity:1}.ngxsp-nfx-titleWrap{display:flex;flex-direction:column;gap:4px}.ngxsp-nfx-eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:.18em;color:#bfdbfe9e}.ngxsp-nfx-title{font-weight:700;letter-spacing:.01em;opacity:.98;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.45);font-size:18px}.ngxsp-nfx-topHint{font-size:12px;color:#bfdbfe85;padding-top:6px}.ngxsp-nfx-status{font-size:10px;text-transform:uppercase;letter-spacing:.14em;padding:7px 10px;border-radius:999px;border:1px solid var(--ngxsp-border);background:#0f172a8a;color:#e2e8f0d6}.ngxsp-nfx-spacer{flex:1}.ngxsp-nfx-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:14px;z-index:4;opacity:0;transform:translateY(10px) scale(.98);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-centerDock{opacity:1;transform:translateY(0) scale(1)}.ngxsp-nfx-center{height:92px;width:92px;border-radius:999px;border:1px solid rgba(191,219,254,.2);background:linear-gradient(180deg,#14304af2,#0b1826f5);color:#f8fbff;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);box-shadow:0 22px 60px #0206176b,inset 0 1px #ffffff0f}.ngxsp-nfx-side{height:56px;min-width:56px;border-radius:999px;border:1px solid var(--ngxsp-border);background:linear-gradient(180deg,#0f1c2cd1,#0a1421d1);color:var(--ngxsp-text);padding:0 18px;cursor:pointer;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);font-weight:700;font-size:14px;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 14px 34px #0206173d}.ngxsp-nfx-side-wide{min-width:78px}.ngxsp-nfx-bottom{position:absolute;left:14px;right:14px;bottom:14px;padding:14px 16px 16px;background:linear-gradient(180deg,#0c1827eb,#08111df0);border:1px solid var(--ngxsp-border);border-radius:16px;gap:12px;flex-direction:column;align-items:stretch;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);z-index:4;opacity:0;transform:translateY(12px);transition:opacity .18s ease,transform .18s ease;box-shadow:0 24px 54px #02061761}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-bottom{opacity:1;transform:translateY(0)}.ngxsp-nfx-metaRow{display:flex;align-items:center;gap:10px;width:100%}.ngxsp-nfx-time{color:#e2e8f0e6;font-variant-numeric:tabular-nums;font-size:12px;font-weight:600}.ngxsp-nfx-range{min-width:100%}.ngxsp-nfx-volumeGroup{display:inline-flex;align-items:center;gap:8px}.ngxsp-nfx-volume{max-width:0;min-width:0;opacity:0;transform:scaleX(.72);transform-origin:left center;pointer-events:none;transition:opacity .18s ease,transform .18s ease,max-width .18s ease}.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume{max-width:160px;min-width:96px;opacity:1;transform:scaleX(1);pointer-events:auto}.ngxsp-nfx-label{font-size:12px;color:#bfdbfe8a}.ngxsp-nfx-chip{background:linear-gradient(180deg,#122236e6,#0b1523e6);border:1px solid var(--ngxsp-border);color:var(--ngxsp-text);border-radius:999px;padding:8px 12px;cursor:pointer;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0a}.ngxsp-nfx-chip.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong);color:#d8f3ff}.ngxsp-nfx-chip-quiet{background:linear-gradient(180deg,#0c1827e0,#08111de0)}.ngxsp-nfx-center:hover,.ngxsp-nfx-side:hover,.ngxsp-nfx-chip:hover,.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-nfx-center:disabled,.ngxsp-nfx-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-range{appearance:none;height:4px;border-radius:999px;background:linear-gradient(to right,#7dd3fcf2,#94a3b833);outline:none}.ngxsp-range::-webkit-slider-thumb{appearance:none;width:14px;height:14px;border-radius:50%;background:#f8fbff;box-shadow:0 0 0 3px #7dd3fc29}.ngxsp-range::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#f8fbff;box-shadow:0 0 0 3px #7dd3fc29}.ngxsp-range::-moz-range-track{height:4px;border:none;border-radius:999px;background:#94a3b83d}.ngxsp-buffer{height:4px;border-radius:999px;background:#94a3b81f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:linear-gradient(to right,#7dd3fc4d,#7dd3fceb)}@media(max-width:860px){.ngxsp-nfx-top{padding:14px 14px 18px}.ngxsp-nfx-topHint{display:none}.ngxsp-nfx-centerDock{gap:10px}.ngxsp-nfx-center{height:74px;width:74px}.ngxsp-nfx-side{height:46px;min-width:46px;padding:0 12px}.ngxsp-nfx-side-wide{min-width:64px}.ngxsp-nfx-bottom{left:10px;right:10px;bottom:10px;padding:12px}.ngxsp-nfx-metaRow{flex-wrap:wrap}.ngxsp-nfx-spacer{display:none}.ngxsp-nfx-volumeGroup{order:3;width:100%}.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume{max-width:none;width:100%;min-width:0}.ngxsp-nfx-chip{padding:8px 10px}}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}.ngxsp-icon-lg{width:26px;height:26px}.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-nfx-title{font-size:15px}.ngxsp-nfx-status{display:none}.ngxsp-nfx-time,.ngxsp-nfx-label{font-size:11px}.ngxsp-nfx-bottom{border-radius:12px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: PlayerMenuComponent, selector: "ngxsp-player-menu", inputs: ["openMenu", "isAudioMode", "textTracks", "audioTracks", "videoTracks", "selectedTextTrackId", "selectedAudioTrackId", "selectedVideoTrackId", "playbackRates", "playbackRate"], outputs: ["selectText", "selectAudio", "selectVideo", "selectRate"] }, { kind: "component", type: PlayerOverlaysComponent, selector: "ngxsp-player-overlays", inputs: ["error", "debug", "debugJson"], outputs: ["retry", "requestPlay", "copyJson", "downloadJson"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3050
+ `, isInline: true, styles: [".ngxsp-root{--ngxsp-bg:#000;--ngxsp-bg-elev:var(--nsp-surface-1, #17181d);--ngxsp-bg-soft:var(--nsp-surface-2, #1e1f25);--ngxsp-border:var(--nsp-line, rgb(255 255 255 / .07));--ngxsp-border-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--ngxsp-text:var(--nsp-text, #ededf0);--ngxsp-text-dim:var(--nsp-text-2, #a3a5ae);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-accent-soft:rgb(139 144 255 / .14);--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);position:relative;width:100%;height:100%;background:#000;color:var(--ngxsp-text);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif)}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate;container-type:inline-size}.ngxsp-captions{position:absolute;left:6%;right:6%;bottom:5%;z-index:3;display:flex;flex-direction:column;align-items:center;gap:.15em;pointer-events:none;text-align:center;font-size:clamp(13px,2.6cqw,34px);line-height:1.3;transition:bottom .2s ease}.ngxsp-captions:empty{display:none}.ngxsp-ui-visible .ngxsp-captions{bottom:calc(2% + var(--ngxsp-captions-lift, 96px))}.ngxsp-captions ::ng-deep .ngxsp-cue{max-width:100%;padding:.08em .45em;border-radius:.2em;background:#000000bd;color:#fff;white-space:pre-line;text-shadow:0 1px 2px rgb(0 0 0 / .5)}.ngxsp-mode-audio .ngxsp-captions{display:none}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:36px;font-weight:800;letter-spacing:0;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:16px;color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-skin-vod .ngxsp-video{position:relative}.ngxsp-skin-vod .ngxsp-video:before{content:none}.ngxsp-skin-vod .ngxsp-video:after{content:none}.ngxsp-mode-audio.ngxsp-skin-vod .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-inline-projection{display:inline-flex;flex:0 0 auto;align-items:center;gap:8px;width:max-content}.ngxsp-inline-projection:empty{display:none}.ngxsp-gesturePulse{position:absolute;inset:0;z-index:5;display:grid;place-items:center;pointer-events:none;color:#f8fbff;animation:ngxspGesturePulse .52s ease both}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px;padding:18px;box-sizing:border-box;display:grid;place-items:center;border-radius:50%;background:#00000080;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media(max-width:640px){.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-audio-title{font-size:28px}.ngxsp-audio-subtitle{font-size:14px}.ngxsp-skin-vod .ngxsp-inline-projection{display:none}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}@keyframes ngxspGesturePulse{0%{opacity:0;transform:scale(.82)}24%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.16)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PlayerMenuComponent, selector: "ngxsp-player-menu", inputs: ["openMenu", "isAudioMode", "textTracks", "audioTracks", "videoTracks", "selectedTextTrackId", "selectedAudioTrackId", "selectedVideoTrackId", "playbackRates", "playbackRate", "labels"], outputs: ["selectText", "selectAudio", "selectVideo", "selectRate", "loadSubtitles"] }, { kind: "component", type: PlayerOverlaysComponent, selector: "ngxsp-player-overlays", inputs: ["error", "debug", "debugJson"], outputs: ["retry", "requestPlay", "copyJson", "downloadJson"] }, { kind: "component", type: PlayerControlsComponent, selector: "ngxsp-player-controls", inputs: ["playing", "isAudioMode", "hasPlaylistControls", "canPrev", "canNext", "shuffleActive", "shuffleLabel", "repeatMode", "repeatLabel", "captionsActive", "tracksLabel", "captionsLabel", "speedLabel", "volumeLabel", "muteLabel", "unmuteLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "castLabel", "castHint", "pipSupported", "pipActive", "pipLabel", "volume", "currentTime", "duration", "buffered", "profile"], outputs: ["playToggle", "seekRelative", "prev", "next", "toggleShuffle", "cycleRepeat", "openMenu", "captions", "airPlay", "cast", "pip", "mute", "scrub", "volumeChange"] }, { kind: "component", type: PlayerControlsVodComponent, selector: "ngxsp-player-controls-vod", inputs: ["playing", "isAudioMode", "displayTitle", "nowPlayingLabel", "nowListeningLabel", "fullscreenHintLabel", "statusLabel", "keyboardHint", "hasPlaylistControls", "canPrev", "canNext", "previousLabel", "nextLabel", "playLabel", "pauseLabel", "shuffleActive", "shuffleLabel", "repeatMode", "repeatLabel", "captionsActive", "tracksLabel", "captionsLabel", "speedLabel", "volumeLabel", "muteLabel", "unmuteLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "castLabel", "castHint", "pipSupported", "pipActive", "pipLabel", "fullscreenLabel", "volume", "currentTime", "duration", "buffered", "bufferPercent", "profile", "timelineDurationSec", "timelineMarkers"], outputs: ["playToggle", "seekRelative", "prev", "next", "toggleShuffle", "cycleRepeat", "openMenu", "captions", "airPlay", "cast", "pip", "fullscreen", "mute", "scrub", "volumeChange"] }, { kind: "component", type: PlayerControlsEditionComponent, selector: "ngxsp-player-controls-edition", inputs: ["profile", "playing", "displayTitle", "metaLabel", "statusLabel", "behindSec", "liveLabel", "captionsActive", "speedLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "pipSupported", "pipActive", "volume", "volumeLabel", "muteLabel", "unmuteLabel", "currentTime", "duration", "buffered"], outputs: ["playToggle", "seekRelative", "openMenu", "captions", "airPlay", "cast", "pip", "mute", "scrub", "volumeChange", "goLive"] }, { kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1213
3051
  }
1214
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerComponent, decorators: [{
3052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerComponent, decorators: [{
1215
3053
  type: Component,
1216
- args: [{ selector: 'ngx-smart-player', standalone: true, imports: [CommonModule, SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, PlayerMenuComponent, PlayerOverlaysComponent], template: `
1217
- <div class="ngxsp-root" [class.ngxsp-skin-netflix]="uiSkin==='netflix'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
3054
+ args: [{ selector: 'ngx-smart-player', standalone: true, imports: [CommonModule, PlayerMenuComponent, PlayerOverlaysComponent, PlayerControlsComponent, PlayerControlsVodComponent, PlayerControlsEditionComponent, SmartPlayerIconComponent], template: `
3055
+ <div class="ngxsp-root" [class.ngxsp-skin-vod]="uiSkin==='vod'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
1218
3056
  <!-- Surface de présentation : interactions pointeur non essentielles, toutes
1219
3057
  dupliquées au clavier par @HostListener('document:keydown') (Espace/flèches/F/M/C)
1220
3058
  et par les contrôles focusables. -->
1221
3059
  <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
1222
3060
  <div class="ngxsp-video" (dblclick)="onPrimarySurfaceDoubleClick()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
1223
- <video #video playsinline (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
3061
+ <video #video playsinline x-webkit-airplay="allow" (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
3062
+ <!-- Subtitles loaded from a file, drawn here at the bottom of the picture, above the controls when they show. -->
3063
+ <div #captionsLayer class="ngxsp-captions" aria-hidden="true"></div>
3064
+ @if (!isAudioMode) {
3065
+ <input #subtitleFileInput class="ngxsp-subtitleInput" type="file" accept=".srt,.vtt,text/vtt,application/x-subrip" hidden (change)="onSubtitleFileChosen($event)" />
3066
+ }
1224
3067
  @if (isAudioMode) {
1225
3068
  <div class="ngxsp-audio-stage" aria-hidden="true">
1226
3069
  <div class="ngxsp-audio-disc"></div>
@@ -1243,132 +3086,136 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1243
3086
  (copyJson)="copyDebug()"
1244
3087
  (downloadJson)="downloadDebug()"
1245
3088
  />
3089
+ @if (surfaceGesture) {
3090
+ <div class="ngxsp-gesturePulse" aria-hidden="true">
3091
+ @if (surfaceGesture === 'pause') {
3092
+ <ngxsp-smart-icon name="pause" />
3093
+ } @else {
3094
+ <ngxsp-smart-icon name="play" />
3095
+ }
3096
+ </div>
3097
+ }
1246
3098
 
1247
- @if (uiSkin==='netflix') {
1248
- <div class="ngxsp-nfx-top">
1249
- <div class="ngxsp-nfx-titleWrap">
1250
- <div class="ngxsp-nfx-eyebrow">{{ isAudioMode ? 'Now Listening' : 'Now Playing' }}</div>
1251
- <div class="ngxsp-nfx-title">{{displayTitle}}</div>
1252
- </div>
1253
- <div class="ngxsp-nfx-spacer"></div>
1254
- <div class="ngxsp-nfx-status">{{ statusLabel() }}</div>
1255
- @if (!isAudioMode) {
1256
- <div class="ngxsp-nfx-topHint">Double-click for fullscreen</div>
1257
- }
1258
- </div>
1259
-
1260
- <div class="ngxsp-nfx-centerDock">
1261
- @if (hasPlaylistControls) {
1262
- <button type="button" class="ngxsp-nfx-side" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item">
1263
- <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1264
- </button>
1265
- }
1266
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds">
1267
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1268
- <span class="ngxsp-iconBadge">10</span>
1269
- </button>
1270
- <ngxsp-play-button class="ngxsp-nfx-center" [playing]="state?.kind==='playing'" (playToggle)="togglePlay()">
1271
- @if (state?.kind==='playing') {
1272
- <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1273
- } @else {
1274
- <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1275
- }
1276
- </ngxsp-play-button>
1277
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(10)" aria-label="Seek forward 10 seconds">
1278
- <span class="ngxsp-iconBadge">10</span>
1279
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1280
- </button>
1281
- @if (hasPlaylistControls) {
1282
- <button type="button" class="ngxsp-nfx-side" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item">
1283
- <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1284
- </button>
1285
- }
1286
- </div>
1287
-
1288
- <div class="ngxsp-controls ngxsp-nfx-bottom">
1289
- <div class="ngxsp-nfx-metaRow">
1290
- <div class="ngxsp-nfx-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
1291
- <div class="ngxsp-nfx-volumeGroup">
1292
- <button type="button" class="ngxsp-nfx-chip ngxsp-nfx-chip-quiet" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
1293
- @if (volume <= 0.001) {
1294
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-mute" />
1295
- } @else if (volume < 0.5) {
1296
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-low" />
1297
- } @else {
1298
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-high" />
1299
- }
1300
- </button>
1301
- <input class="ngxsp-range ngxsp-nfx-volume" type="range"
1302
- min="0" max="1" step="0.01"
1303
- [value]="volume"
1304
- (input)="onVolume(($any($event.target).value))"
1305
- aria-label="Volume"
1306
- />
1307
- </div>
1308
- <div class="ngxsp-nfx-spacer"></div>
1309
- @if (hasPlaylistControls) {
1310
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
1311
- <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1312
- </button>
1313
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
1314
- @if (repeatMode === 'one') {
1315
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1316
- } @else {
1317
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1318
- }
1319
- </button>
1320
- }
1321
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()">
1322
- <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1323
- </button>
1324
- <button type="button" class="ngxsp-nfx-chip" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1325
- @if (selectedTextTrackId !== null) {
1326
- <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1327
- } @else {
1328
- <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1329
- }
1330
- </button>
1331
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()">
1332
- <ngxsp-smart-icon class="ngxsp-icon" name="speed" />
1333
- </button>
1334
- @if (showCastButton()) {
1335
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="isCastActive()" (click)="onCastClick()" [attr.title]="castHintLabel()" [attr.aria-label]="castLabel()">
1336
- @if (isCastActive()) {
1337
- <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1338
- } @else {
1339
- <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1340
- }
1341
- </button>
1342
- }
1343
- @if (!isAudioMode && isPiPSupported()) {
1344
- <button type="button" class="ngxsp-nfx-chip" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1345
- @if (isPiPActive()) {
1346
- <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1347
- } @else {
1348
- <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1349
- }
1350
- </button>
1351
- }
3099
+ @if (uiSkin==='vod') {
3100
+ <ngxsp-player-controls-vod
3101
+ [playing]="state?.kind==='playing'"
3102
+ [isAudioMode]="isAudioMode"
3103
+ [displayTitle]="displayTitle"
3104
+ [nowPlayingLabel]="nowPlayingLabel"
3105
+ [nowListeningLabel]="nowListeningLabel"
3106
+ [fullscreenHintLabel]="fullscreenHintLabel"
3107
+ [statusLabel]="statusLabel()"
3108
+ [keyboardHint]="keyboardHintLabel()"
3109
+ [hasPlaylistControls]="hasPlaylistControls"
3110
+ [canPrev]="canPrev"
3111
+ [canNext]="canNext"
3112
+ [previousLabel]="previousLabel"
3113
+ [nextLabel]="nextLabel"
3114
+ [playLabel]="playLabel"
3115
+ [pauseLabel]="pauseLabel"
3116
+ [shuffleActive]="shuffleActive"
3117
+ [shuffleLabel]="shuffleLabel"
3118
+ [repeatMode]="repeatMode"
3119
+ [repeatLabel]="repeatLabel"
3120
+ [captionsActive]="selectedTextTrackId !== null"
3121
+ [tracksLabel]="tracksButtonLabel()"
3122
+ [captionsLabel]="captionsLabel()"
3123
+ [speedLabel]="playbackRateLabel()"
3124
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
3125
+ [muteLabel]="outputMuteLabel"
3126
+ [unmuteLabel]="outputUnmuteLabel"
3127
+ [showAirPlay]="showAirPlayButton()"
3128
+ [airPlayActive]="isAirPlayActive()"
3129
+ [airPlayLabel]="airPlayLabel()"
3130
+ (airPlay)="openAirPlayPicker()"
3131
+ [showCast]="showCastButton()"
3132
+ [castActive]="isCastActive()"
3133
+ [castLabel]="castLabel()"
3134
+ [castHint]="castHintLabel()"
3135
+ [pipSupported]="isPiPSupported()"
3136
+ [pipActive]="isPiPActive()"
3137
+ [pipLabel]="pipLabel()"
3138
+ [fullscreenLabel]="fullscreenLabel"
3139
+ [volume]="volume"
3140
+ [currentTime]="currentTime"
3141
+ [duration]="duration"
3142
+ [buffered]="bufferAheadSec"
3143
+ [bufferPercent]="bufferPercent()"
3144
+ [profile]="sourceProfile"
3145
+ [timelineDurationSec]="timelineDurationSec ?? 0"
3146
+ [timelineMarkers]="timelineMarkers"
3147
+ (playToggle)="requestPlaybackToggle()"
3148
+ (seekRelative)="seekRel($event)"
3149
+ (prev)="emitPrev()"
3150
+ (next)="emitNext()"
3151
+ (toggleShuffle)="toggleShuffle.emit()"
3152
+ (cycleRepeat)="cycleRepeat.emit()"
3153
+ (openMenu)="toggleMenu($event)"
3154
+ (captions)="onCaptionsClick()"
3155
+ (cast)="onCastClick()"
3156
+ (pip)="togglePiP()"
3157
+ (fullscreen)="toggleFullscreen()"
3158
+ (mute)="toggleMute()"
3159
+ (scrub)="onScrub($event)"
3160
+ (volumeChange)="onVolume($event)"
3161
+ >
3162
+ <div ngxsp-player-inline-controls class="ngxsp-inline-projection">
3163
+ <ng-content select="[ngxsp-player-inline-controls]" />
1352
3164
  </div>
1353
-
1354
- <ngxsp-time-slider
1355
- class="ngxsp-nfx-range"
1356
- [currentTime]="currentTime"
1357
- [duration]="duration"
1358
- [buffered]="bufferAheadSec"
1359
- [profile]="sourceProfile"
1360
- (seek)="onScrub($event)"
3165
+ <ngxsp-player-menu
3166
+ [openMenu]="openMenu"
3167
+ [isAudioMode]="isAudioMode"
3168
+ [textTracks]="textTracks"
3169
+ [audioTracks]="audioTracks"
3170
+ [videoTracks]="videoTracks"
3171
+ [selectedTextTrackId]="selectedTextTrackId"
3172
+ [selectedAudioTrackId]="selectedAudioTrackId"
3173
+ [selectedVideoTrackId]="selectedVideoTrackId"
3174
+ [playbackRates]="playbackRates"
3175
+ [playbackRate]="playbackRate"
3176
+ [labels]="playerMenuLabels"
3177
+ (loadSubtitles)="openSubtitlePicker()"
3178
+ (selectText)="selectTextTrack($event)"
3179
+ (selectAudio)="selectAudioTrack($event)"
3180
+ (selectVideo)="selectVideoTrack($event)"
3181
+ (selectRate)="setPlaybackRate($event)"
1361
3182
  />
1362
-
1363
- <div class="ngxsp-nfx-metaRow">
1364
- <div class="ngxsp-nfx-spacer"></div>
1365
- <div class="ngxsp-nfx-label">{{ keyboardHintLabel() }}</div>
1366
- </div>
1367
-
1368
- <div class="ngxsp-buffer" aria-hidden="true">
1369
- <div class="ngxsp-bufferFill" [style.width.%]="bufferPercent()"></div>
1370
- </div>
1371
-
3183
+ </ngxsp-player-controls-vod>
3184
+ } @else if (uiSkin==='edition') {
3185
+ <ngxsp-player-controls-edition
3186
+ [profile]="sourceProfile"
3187
+ [playing]="state?.kind==='playing'"
3188
+ [displayTitle]="displayTitle"
3189
+ [metaLabel]="editionMeta"
3190
+ [statusLabel]="statusLabel()"
3191
+ [captionsActive]="selectedTextTrackId !== null"
3192
+ [speedLabel]="playbackRateLabel()"
3193
+ [showAirPlay]="showAirPlayButton()"
3194
+ [airPlayActive]="isAirPlayActive()"
3195
+ [airPlayLabel]="airPlayLabel()"
3196
+ (airPlay)="openAirPlayPicker()"
3197
+ [showCast]="showCastButton()"
3198
+ [castActive]="isCastActive()"
3199
+ [pipSupported]="isPiPSupported()"
3200
+ [pipActive]="isPiPActive()"
3201
+ [volume]="volume"
3202
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
3203
+ [muteLabel]="outputMuteLabel"
3204
+ [unmuteLabel]="outputUnmuteLabel"
3205
+ [currentTime]="currentTime"
3206
+ [duration]="duration"
3207
+ [buffered]="bufferAheadSec"
3208
+ (playToggle)="requestPlaybackToggle()"
3209
+ (seekRelative)="seekRel($event)"
3210
+ (openMenu)="toggleMenu($event)"
3211
+ (captions)="onCaptionsClick()"
3212
+ (cast)="onCastClick()"
3213
+ (pip)="togglePiP()"
3214
+ (mute)="toggleMute()"
3215
+ (scrub)="onScrub($event)"
3216
+ (volumeChange)="onVolume($event)"
3217
+ (goLive)="reload()"
3218
+ >
1372
3219
  <ngxsp-player-menu
1373
3220
  [openMenu]="openMenu"
1374
3221
  [isAudioMode]="isAudioMode"
@@ -1380,103 +3227,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1380
3227
  [selectedVideoTrackId]="selectedVideoTrackId"
1381
3228
  [playbackRates]="playbackRates"
1382
3229
  [playbackRate]="playbackRate"
3230
+ [labels]="playerMenuLabels"
3231
+ (loadSubtitles)="openSubtitlePicker()"
1383
3232
  (selectText)="selectTextTrack($event)"
1384
3233
  (selectAudio)="selectAudioTrack($event)"
1385
3234
  (selectVideo)="selectVideoTrack($event)"
1386
3235
  (selectRate)="setPlaybackRate($event)"
1387
3236
  />
1388
- </div>
3237
+ </ngxsp-player-controls-edition>
1389
3238
  } @else {
1390
- <div class="ngxsp-controls">
1391
- <ngxsp-play-button class="ngxsp-btn" [playing]="state?.kind==='playing'" (playToggle)="togglePlay()">
1392
- @if (state?.kind==='playing') {
1393
- <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1394
- } @else {
1395
- <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1396
- }
1397
- </ngxsp-play-button>
1398
- <button type="button" class="ngxsp-btn" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1399
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1400
- <span class="ngxsp-iconBadge">10</span>
1401
- </button>
1402
- <button type="button" class="ngxsp-btn" (click)="seekRel(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1403
- <span class="ngxsp-iconBadge">10</span>
1404
- <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1405
- </button>
1406
- @if (hasPlaylistControls) {
1407
- <button type="button" class="ngxsp-btn" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1408
- <button type="button" class="ngxsp-btn" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1409
- <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
1410
- <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1411
- </button>
1412
- <button type="button" class="ngxsp-btn" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
1413
- @if (repeatMode === 'one') {
1414
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1415
- } @else {
1416
- <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1417
- }
1418
- </button>
1419
- }
1420
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1421
- <button type="button" class="ngxsp-btn" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1422
- @if (selectedTextTrackId !== null) {
1423
- <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1424
- } @else {
1425
- <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1426
- }
1427
- </button>
1428
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1429
- @if (showCastButton()) {
1430
- <button type="button" class="ngxsp-btn" [class.accent]="isCastActive()" (click)="onCastClick()" [attr.title]="castHintLabel()" [attr.aria-label]="castLabel()">
1431
- @if (isCastActive()) {
1432
- <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1433
- } @else {
1434
- <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1435
- }
1436
- </button>
1437
- }
1438
- @if (!isAudioMode && isPiPSupported()) {
1439
- <button type="button" class="ngxsp-btn" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1440
- @if (isPiPActive()) {
1441
- <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1442
- } @else {
1443
- <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1444
- }
1445
- </button>
1446
- }
1447
- <button type="button" class="ngxsp-btn" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
1448
- @if (volume <= 0.001) {
1449
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-mute" />
1450
- } @else if (volume < 0.5) {
1451
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-low" />
1452
- } @else {
1453
- <ngxsp-smart-icon class="ngxsp-icon" name="volume-high" />
1454
- }
1455
- </button>
1456
-
1457
- <ngxsp-time-slider
1458
- style="flex:1 1 160px; min-width:160px"
1459
- [currentTime]="currentTime"
1460
- [duration]="duration"
1461
- [buffered]="bufferAheadSec"
1462
- [profile]="sourceProfile"
1463
- (seek)="onScrub($event)"
1464
- />
1465
-
1466
- <div class="ngxsp-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
1467
-
1468
- <input class="ngxsp-range" style="max-width:140px" type="range"
1469
- min="0" max="1" step="0.01"
1470
- [value]="volume"
1471
- (input)="onVolume(($any($event.target).value))"
1472
- aria-label="Volume"
1473
- />
1474
- </div>
3239
+ <ngxsp-player-controls
3240
+ [playing]="state?.kind==='playing'"
3241
+ [isAudioMode]="isAudioMode"
3242
+ [hasPlaylistControls]="hasPlaylistControls"
3243
+ [canPrev]="canPrev"
3244
+ [canNext]="canNext"
3245
+ [shuffleActive]="shuffleActive"
3246
+ [shuffleLabel]="shuffleLabel"
3247
+ [repeatMode]="repeatMode"
3248
+ [repeatLabel]="repeatLabel"
3249
+ [captionsActive]="selectedTextTrackId !== null"
3250
+ [tracksLabel]="tracksButtonLabel()"
3251
+ [captionsLabel]="captionsLabel()"
3252
+ [speedLabel]="playbackRateLabel()"
3253
+ [volumeLabel]="volumeLabel()"
3254
+ [showCast]="showCastButton()"
3255
+ [castActive]="isCastActive()"
3256
+ [castLabel]="castLabel()"
3257
+ [castHint]="castHintLabel()"
3258
+ [pipSupported]="isPiPSupported()"
3259
+ [pipActive]="isPiPActive()"
3260
+ [pipLabel]="pipLabel()"
3261
+ [volume]="volume"
3262
+ [muteLabel]="outputMuteLabel"
3263
+ [unmuteLabel]="outputUnmuteLabel"
3264
+ [currentTime]="currentTime"
3265
+ [duration]="duration"
3266
+ [buffered]="bufferAheadSec"
3267
+ [profile]="sourceProfile"
3268
+ (playToggle)="requestPlaybackToggle()"
3269
+ (seekRelative)="seekRel($event)"
3270
+ (prev)="emitPrev()"
3271
+ (next)="emitNext()"
3272
+ (toggleShuffle)="toggleShuffle.emit()"
3273
+ (cycleRepeat)="cycleRepeat.emit()"
3274
+ (openMenu)="toggleMenu($event)"
3275
+ (captions)="subtitles.emit()"
3276
+ (cast)="onCastClick()"
3277
+ (pip)="togglePiP()"
3278
+ (mute)="toggleMute()"
3279
+ (scrub)="onScrub($event)"
3280
+ (volumeChange)="onVolume($event)"
3281
+ />
1475
3282
  }
1476
3283
 
1477
3284
  </div>
1478
3285
  </div>
1479
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".ngxsp-root{--ngxsp-bg:#07111f;--ngxsp-bg-elev:#0d1a2b;--ngxsp-bg-soft:#102238;--ngxsp-border:rgba(148,163,184,.18);--ngxsp-border-strong:rgba(191,219,254,.24);--ngxsp-text:#e6eef8;--ngxsp-text-dim:rgba(230,238,248,.72);--ngxsp-accent:#7dd3fc;--ngxsp-accent-soft:rgba(125,211,252,.16);position:relative;width:100%;height:100%;background:radial-gradient(circle at top,rgba(56,189,248,.12),transparent 28%),linear-gradient(180deg,#0a1524,#07111f 56%,#050d18);color:var(--ngxsp-text);font-family:Segoe UI,SF Pro Text,Inter,system-ui,sans-serif;letter-spacing:.01em}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate}.ngxsp-video video{width:100%;height:100%;display:block;background:#02060b;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:clamp(24px,3.8vw,42px);font-weight:800;letter-spacing:-.03em;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:clamp(13px,1.4vw,16px);color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border);color:var(--ngxsp-text);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong);color:#d8f3ff}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:32px;height:32px}.ngxsp-iconBadge{font-size:11px;font-weight:800;line-height:1}.ngxsp-range{flex:1;min-width:160px}.ngxsp-time{color:var(--ngxsp-text-dim);font-variant-numeric:tabular-nums}.ngxsp-skin-netflix .ngxsp-video{position:relative}.ngxsp-skin-netflix .ngxsp-video:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,#0f172a00 30%,#02061747);opacity:.92;pointer-events:none;z-index:0}.ngxsp-skin-netflix .ngxsp-video:after{content:\"\";position:absolute;inset:auto 0 0;height:220px;background:linear-gradient(to top,#040a13f5,#060e1ab8 38%,#0000);opacity:0;transition:opacity .18s ease;pointer-events:none}.ngxsp-skin-netflix.ngxsp-ui-visible .ngxsp-video:after{opacity:1}.ngxsp-mode-audio.ngxsp-skin-netflix .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-nfx-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:20px 20px 24px;background:linear-gradient(to bottom,#040a13eb,#040a1300);pointer-events:none;opacity:0;transition:opacity .18s ease;z-index:3}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-top{opacity:1}.ngxsp-nfx-titleWrap{display:flex;flex-direction:column;gap:4px}.ngxsp-nfx-eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:.18em;color:#bfdbfe9e}.ngxsp-nfx-title{font-weight:700;letter-spacing:.01em;opacity:.98;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.45);font-size:18px}.ngxsp-nfx-topHint{font-size:12px;color:#bfdbfe85;padding-top:6px}.ngxsp-nfx-status{font-size:10px;text-transform:uppercase;letter-spacing:.14em;padding:7px 10px;border-radius:999px;border:1px solid var(--ngxsp-border);background:#0f172a8a;color:#e2e8f0d6}.ngxsp-nfx-spacer{flex:1}.ngxsp-nfx-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:14px;z-index:4;opacity:0;transform:translateY(10px) scale(.98);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-centerDock{opacity:1;transform:translateY(0) scale(1)}.ngxsp-nfx-center{height:92px;width:92px;border-radius:999px;border:1px solid rgba(191,219,254,.2);background:linear-gradient(180deg,#14304af2,#0b1826f5);color:#f8fbff;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);box-shadow:0 22px 60px #0206176b,inset 0 1px #ffffff0f}.ngxsp-nfx-side{height:56px;min-width:56px;border-radius:999px;border:1px solid var(--ngxsp-border);background:linear-gradient(180deg,#0f1c2cd1,#0a1421d1);color:var(--ngxsp-text);padding:0 18px;cursor:pointer;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);font-weight:700;font-size:14px;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 14px 34px #0206173d}.ngxsp-nfx-side-wide{min-width:78px}.ngxsp-nfx-bottom{position:absolute;left:14px;right:14px;bottom:14px;padding:14px 16px 16px;background:linear-gradient(180deg,#0c1827eb,#08111df0);border:1px solid var(--ngxsp-border);border-radius:16px;gap:12px;flex-direction:column;align-items:stretch;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);z-index:4;opacity:0;transform:translateY(12px);transition:opacity .18s ease,transform .18s ease;box-shadow:0 24px 54px #02061761}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-bottom{opacity:1;transform:translateY(0)}.ngxsp-nfx-metaRow{display:flex;align-items:center;gap:10px;width:100%}.ngxsp-nfx-time{color:#e2e8f0e6;font-variant-numeric:tabular-nums;font-size:12px;font-weight:600}.ngxsp-nfx-range{min-width:100%}.ngxsp-nfx-volumeGroup{display:inline-flex;align-items:center;gap:8px}.ngxsp-nfx-volume{max-width:0;min-width:0;opacity:0;transform:scaleX(.72);transform-origin:left center;pointer-events:none;transition:opacity .18s ease,transform .18s ease,max-width .18s ease}.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume{max-width:160px;min-width:96px;opacity:1;transform:scaleX(1);pointer-events:auto}.ngxsp-nfx-label{font-size:12px;color:#bfdbfe8a}.ngxsp-nfx-chip{background:linear-gradient(180deg,#122236e6,#0b1523e6);border:1px solid var(--ngxsp-border);color:var(--ngxsp-text);border-radius:999px;padding:8px 12px;cursor:pointer;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0a}.ngxsp-nfx-chip.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong);color:#d8f3ff}.ngxsp-nfx-chip-quiet{background:linear-gradient(180deg,#0c1827e0,#08111de0)}.ngxsp-nfx-center:hover,.ngxsp-nfx-side:hover,.ngxsp-nfx-chip:hover,.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-nfx-center:disabled,.ngxsp-nfx-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-range{appearance:none;height:4px;border-radius:999px;background:linear-gradient(to right,#7dd3fcf2,#94a3b833);outline:none}.ngxsp-range::-webkit-slider-thumb{appearance:none;width:14px;height:14px;border-radius:50%;background:#f8fbff;box-shadow:0 0 0 3px #7dd3fc29}.ngxsp-range::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#f8fbff;box-shadow:0 0 0 3px #7dd3fc29}.ngxsp-range::-moz-range-track{height:4px;border:none;border-radius:999px;background:#94a3b83d}.ngxsp-buffer{height:4px;border-radius:999px;background:#94a3b81f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:linear-gradient(to right,#7dd3fc4d,#7dd3fceb)}@media(max-width:860px){.ngxsp-nfx-top{padding:14px 14px 18px}.ngxsp-nfx-topHint{display:none}.ngxsp-nfx-centerDock{gap:10px}.ngxsp-nfx-center{height:74px;width:74px}.ngxsp-nfx-side{height:46px;min-width:46px;padding:0 12px}.ngxsp-nfx-side-wide{min-width:64px}.ngxsp-nfx-bottom{left:10px;right:10px;bottom:10px;padding:12px}.ngxsp-nfx-metaRow{flex-wrap:wrap}.ngxsp-nfx-spacer{display:none}.ngxsp-nfx-volumeGroup{order:3;width:100%}.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume{max-width:none;width:100%;min-width:0}.ngxsp-nfx-chip{padding:8px 10px}}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}.ngxsp-icon-lg{width:26px;height:26px}.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-nfx-title{font-size:15px}.ngxsp-nfx-status{display:none}.ngxsp-nfx-time,.ngxsp-nfx-label{font-size:11px}.ngxsp-nfx-bottom{border-radius:12px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}\n"] }]
3286
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".ngxsp-root{--ngxsp-bg:#000;--ngxsp-bg-elev:var(--nsp-surface-1, #17181d);--ngxsp-bg-soft:var(--nsp-surface-2, #1e1f25);--ngxsp-border:var(--nsp-line, rgb(255 255 255 / .07));--ngxsp-border-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--ngxsp-text:var(--nsp-text, #ededf0);--ngxsp-text-dim:var(--nsp-text-2, #a3a5ae);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-accent-soft:rgb(139 144 255 / .14);--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);position:relative;width:100%;height:100%;background:#000;color:var(--ngxsp-text);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif)}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate;container-type:inline-size}.ngxsp-captions{position:absolute;left:6%;right:6%;bottom:5%;z-index:3;display:flex;flex-direction:column;align-items:center;gap:.15em;pointer-events:none;text-align:center;font-size:clamp(13px,2.6cqw,34px);line-height:1.3;transition:bottom .2s ease}.ngxsp-captions:empty{display:none}.ngxsp-ui-visible .ngxsp-captions{bottom:calc(2% + var(--ngxsp-captions-lift, 96px))}.ngxsp-captions ::ng-deep .ngxsp-cue{max-width:100%;padding:.08em .45em;border-radius:.2em;background:#000000bd;color:#fff;white-space:pre-line;text-shadow:0 1px 2px rgb(0 0 0 / .5)}.ngxsp-mode-audio .ngxsp-captions{display:none}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:36px;font-weight:800;letter-spacing:0;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:16px;color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-skin-vod .ngxsp-video{position:relative}.ngxsp-skin-vod .ngxsp-video:before{content:none}.ngxsp-skin-vod .ngxsp-video:after{content:none}.ngxsp-mode-audio.ngxsp-skin-vod .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-inline-projection{display:inline-flex;flex:0 0 auto;align-items:center;gap:8px;width:max-content}.ngxsp-inline-projection:empty{display:none}.ngxsp-gesturePulse{position:absolute;inset:0;z-index:5;display:grid;place-items:center;pointer-events:none;color:#f8fbff;animation:ngxspGesturePulse .52s ease both}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px;padding:18px;box-sizing:border-box;display:grid;place-items:center;border-radius:50%;background:#00000080;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media(max-width:640px){.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-audio-title{font-size:28px}.ngxsp-audio-subtitle{font-size:14px}.ngxsp-skin-vod .ngxsp-inline-projection{display:none}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}@keyframes ngxspGesturePulse{0%{opacity:0;transform:scale(.82)}24%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.16)}}\n"] }]
1480
3287
  }], propDecorators: { config: [{
1481
3288
  type: Input,
1482
3289
  args: [{ required: true }]
@@ -1490,10 +3297,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1490
3297
  type: Input
1491
3298
  }], startTimeSec: [{
1492
3299
  type: Input
3300
+ }], autoplayOnInit: [{
3301
+ type: Input
3302
+ }], playRequest: [{
3303
+ type: Input
1493
3304
  }], canPrev: [{
1494
3305
  type: Input
1495
3306
  }], canNext: [{
1496
3307
  type: Input
3308
+ }], previousLabel: [{
3309
+ type: Input
3310
+ }], nextLabel: [{
3311
+ type: Input
3312
+ }], playLabel: [{
3313
+ type: Input
3314
+ }], pauseLabel: [{
3315
+ type: Input
1497
3316
  }], shuffleActive: [{
1498
3317
  type: Input
1499
3318
  }], shuffleLabel: [{
@@ -1508,6 +3327,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1508
3327
  type: Input
1509
3328
  }], castEnabled: [{
1510
3329
  type: Input
3330
+ }], castConnected: [{
3331
+ type: Input
3332
+ }], documentShortcuts: [{
3333
+ type: Input
3334
+ }], nowPlayingLabel: [{
3335
+ type: Input
3336
+ }], nowListeningLabel: [{
3337
+ type: Input
3338
+ }], fullscreenHintLabel: [{
3339
+ type: Input
3340
+ }], fullscreenLabel: [{
3341
+ type: Input
3342
+ }], readyStatusLabel: [{
3343
+ type: Input
3344
+ }], playingStatusLabel: [{
3345
+ type: Input
3346
+ }], pausedStatusLabel: [{
3347
+ type: Input
3348
+ }], bufferingStatusLabel: [{
3349
+ type: Input
3350
+ }], loadingStatusLabel: [{
3351
+ type: Input
3352
+ }], endedStatusLabel: [{
3353
+ type: Input
3354
+ }], errorStatusLabel: [{
3355
+ type: Input
3356
+ }], outputVolume: [{
3357
+ type: Input
3358
+ }], outputVolumeLabel: [{
3359
+ type: Input
3360
+ }], outputMuteLabel: [{
3361
+ type: Input
3362
+ }], outputUnmuteLabel: [{
3363
+ type: Input
3364
+ }], timelineDurationSec: [{
3365
+ type: Input
3366
+ }], timelineMarkers: [{
3367
+ type: Input
1511
3368
  }], ended: [{
1512
3369
  type: Output
1513
3370
  }], videoSize: [{
@@ -1524,11 +3381,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1524
3381
  type: Output
1525
3382
  }], subtitles: [{
1526
3383
  type: Output
3384
+ }], subtitlesLoaded: [{
3385
+ type: Output
3386
+ }], menuLabels: [{
3387
+ type: Input
1527
3388
  }], cast: [{
1528
3389
  type: Output
3390
+ }], stateChange: [{
3391
+ type: Output
3392
+ }], playbackToggle: [{
3393
+ type: Output
3394
+ }], outputVolumeChange: [{
3395
+ type: Output
1529
3396
  }], videoEl: [{
1530
3397
  type: ViewChild,
1531
3398
  args: ['video', { static: true }]
3399
+ }], captionsLayer: [{
3400
+ type: ViewChild,
3401
+ args: ['captionsLayer', { static: true }]
3402
+ }], subtitleFileInput: [{
3403
+ type: ViewChild,
3404
+ args: ['subtitleFileInput']
1532
3405
  }], onEscape: [{
1533
3406
  type: HostListener,
1534
3407
  args: ['document:keydown.escape']
@@ -1560,5 +3433,5 @@ function provideSmartPlayer(cfg) {
1560
3433
  * Generated bundle index. Do not edit.
1561
3434
  */
1562
3435
 
1563
- export { SMART_PLAYER_METADATA_PROVIDERS, SMART_PLAYER_SUBTITLE_PROVIDERS, SmartPlayerComponent, provideSmartPlayer };
3436
+ export { DEFAULT_PLAYER_MENU_LABELS, PlayerControlsComponent, PlayerControlsEditionComponent, PlayerControlsVodComponent, SMART_PLAYER_METADATA_PROVIDERS, SMART_PLAYER_SUBTITLE_PROVIDERS, SmartConfirmDialogComponent, SmartPlayerComponent, SmartPlayerIconComponent, SmartSettingsComponent, provideSmartPlayer };
1564
3437
  //# sourceMappingURL=ngxsplayer-ngx-smart-player.mjs.map