diff options
author | Chocobozzz <me@florianbigard.com> | 2022-03-14 14:28:20 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-03-14 14:36:35 +0100 |
commit | 57d6503286b114fee61b5e4725825e2490dcac29 (patch) | |
tree | 2d3d23f697b2986d7e41bb443754394296b66ec3 /client/src/assets/player/shared/settings | |
parent | 9597920ee3d4ac99803e7107983ddf98a9dfb3c4 (diff) | |
download | PeerTube-57d6503286b114fee61b5e4725825e2490dcac29.tar.gz PeerTube-57d6503286b114fee61b5e4725825e2490dcac29.tar.zst PeerTube-57d6503286b114fee61b5e4725825e2490dcac29.zip |
Reorganize player files
Diffstat (limited to 'client/src/assets/player/shared/settings')
8 files changed, 895 insertions, 0 deletions
diff --git a/client/src/assets/player/shared/settings/index.ts b/client/src/assets/player/shared/settings/index.ts new file mode 100644 index 000000000..736d50c16 --- /dev/null +++ b/client/src/assets/player/shared/settings/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export * from './resolution-menu-button' | ||
2 | export * from './resolution-menu-item' | ||
3 | export * from './settings-dialog' | ||
4 | export * from './settings-menu-button' | ||
5 | export * from './settings-menu-item' | ||
6 | export * from './settings-panel-child' | ||
7 | export * from './settings-panel' | ||
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts new file mode 100644 index 000000000..8bd5b4f03 --- /dev/null +++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
3 | |||
4 | const Menu = videojs.getComponent('Menu') | ||
5 | const MenuButton = videojs.getComponent('MenuButton') | ||
6 | class ResolutionMenuButton extends MenuButton { | ||
7 | labelEl_: HTMLElement | ||
8 | |||
9 | constructor (player: videojs.Player, options?: videojs.MenuButtonOptions) { | ||
10 | super(player, options) | ||
11 | |||
12 | this.controlText('Quality') | ||
13 | |||
14 | player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) | ||
15 | |||
16 | // For parent | ||
17 | player.peertubeResolutions().on('resolutionChanged', () => { | ||
18 | setTimeout(() => this.trigger('labelUpdated')) | ||
19 | }) | ||
20 | } | ||
21 | |||
22 | createEl () { | ||
23 | const el = super.createEl() | ||
24 | |||
25 | this.labelEl_ = videojs.dom.createEl('div', { | ||
26 | className: 'vjs-resolution-value' | ||
27 | }) as HTMLElement | ||
28 | |||
29 | el.appendChild(this.labelEl_) | ||
30 | |||
31 | return el | ||
32 | } | ||
33 | |||
34 | updateARIAAttributes () { | ||
35 | this.el().setAttribute('aria-label', 'Quality') | ||
36 | } | ||
37 | |||
38 | createMenu () { | ||
39 | return new Menu(this.player_) | ||
40 | } | ||
41 | |||
42 | buildCSSClass () { | ||
43 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
44 | } | ||
45 | |||
46 | buildWrapperCSSClass () { | ||
47 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
48 | } | ||
49 | |||
50 | private addClickListener (component: any) { | ||
51 | component.on('click', () => { | ||
52 | const children = this.menu.children() | ||
53 | |||
54 | for (const child of children) { | ||
55 | if (component !== child) { | ||
56 | (child as videojs.MenuItem).selected(false) | ||
57 | } | ||
58 | } | ||
59 | }) | ||
60 | } | ||
61 | |||
62 | private buildQualities () { | ||
63 | for (const d of this.player().peertubeResolutions().getResolutions()) { | ||
64 | const label = d.label === '0p' | ||
65 | ? this.player().localize('Audio-only') | ||
66 | : d.label | ||
67 | |||
68 | this.menu.addChild(new ResolutionMenuItem( | ||
69 | this.player_, | ||
70 | { | ||
71 | id: d.id, | ||
72 | label, | ||
73 | selected: d.selected | ||
74 | }) | ||
75 | ) | ||
76 | } | ||
77 | |||
78 | for (const m of this.menu.children()) { | ||
79 | this.addClickListener(m) | ||
80 | } | ||
81 | |||
82 | this.trigger('menuChanged') | ||
83 | } | ||
84 | } | ||
85 | |||
86 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | ||
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts new file mode 100644 index 000000000..6047f52f7 --- /dev/null +++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const MenuItem = videojs.getComponent('MenuItem') | ||
4 | |||
5 | export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { | ||
6 | id: number | ||
7 | } | ||
8 | |||
9 | class ResolutionMenuItem extends MenuItem { | ||
10 | private readonly resolutionId: number | ||
11 | private readonly label: string | ||
12 | |||
13 | private autoResolutionEnabled: boolean | ||
14 | private autoResolutionChosen: string | ||
15 | |||
16 | constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { | ||
17 | options.selectable = true | ||
18 | |||
19 | super(player, options) | ||
20 | |||
21 | this.autoResolutionEnabled = true | ||
22 | this.autoResolutionChosen = '' | ||
23 | |||
24 | this.resolutionId = options.id | ||
25 | this.label = options.label | ||
26 | |||
27 | player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection()) | ||
28 | |||
29 | // We only want to disable the "Auto" item | ||
30 | if (this.resolutionId === -1) { | ||
31 | player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution()) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | handleClick (event: any) { | ||
36 | // Auto button disabled? | ||
37 | if (this.autoResolutionEnabled === false && this.resolutionId === -1) return | ||
38 | |||
39 | super.handleClick(event) | ||
40 | |||
41 | this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false }) | ||
42 | } | ||
43 | |||
44 | updateSelection () { | ||
45 | const selectedResolution = this.player().peertubeResolutions().getSelected() | ||
46 | |||
47 | if (this.resolutionId === -1) { | ||
48 | this.autoResolutionChosen = this.player().peertubeResolutions().getAutoResolutionChosen()?.label | ||
49 | } | ||
50 | |||
51 | this.selected(this.resolutionId === selectedResolution.id) | ||
52 | } | ||
53 | |||
54 | updateAutoResolution () { | ||
55 | const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld() | ||
56 | |||
57 | // Check if the auto resolution is enabled or not | ||
58 | if (enabled === false) { | ||
59 | this.addClass('disabled') | ||
60 | } else { | ||
61 | this.removeClass('disabled') | ||
62 | } | ||
63 | |||
64 | this.autoResolutionEnabled = enabled | ||
65 | } | ||
66 | |||
67 | getLabel () { | ||
68 | if (this.resolutionId === -1) { | ||
69 | return this.label + ' <small>' + this.autoResolutionChosen + '</small>' | ||
70 | } | ||
71 | |||
72 | return this.label | ||
73 | } | ||
74 | } | ||
75 | videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
76 | |||
77 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/shared/settings/settings-dialog.ts b/client/src/assets/player/shared/settings/settings-dialog.ts new file mode 100644 index 000000000..8cd98967f --- /dev/null +++ b/client/src/assets/player/shared/settings/settings-dialog.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsDialog extends Component { | ||
6 | constructor (player: videojs.Player) { | ||
7 | super(player) | ||
8 | |||
9 | this.hide() | ||
10 | } | ||
11 | |||
12 | /** | ||
13 | * Create the component's DOM element | ||
14 | * | ||
15 | */ | ||
16 | createEl () { | ||
17 | const uniqueId = this.id() | ||
18 | const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId | ||
19 | const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId | ||
20 | |||
21 | return super.createEl('div', { | ||
22 | className: 'vjs-settings-dialog vjs-modal-overlay', | ||
23 | innerHTML: '', | ||
24 | tabIndex: -1 | ||
25 | }, { | ||
26 | role: 'dialog', | ||
27 | 'aria-labelledby': dialogLabelId, | ||
28 | 'aria-describedby': dialogDescriptionId | ||
29 | }) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | Component.registerComponent('SettingsDialog', SettingsDialog) | ||
34 | |||
35 | export { SettingsDialog } | ||
diff --git a/client/src/assets/player/shared/settings/settings-menu-button.ts b/client/src/assets/player/shared/settings/settings-menu-button.ts new file mode 100644 index 000000000..64866aab2 --- /dev/null +++ b/client/src/assets/player/shared/settings/settings-menu-button.ts | |||
@@ -0,0 +1,277 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { toTitleCase } from '../common' | ||
3 | import { SettingsDialog } from './settings-dialog' | ||
4 | import { SettingsMenuItem } from './settings-menu-item' | ||
5 | import { SettingsPanel } from './settings-panel' | ||
6 | import { SettingsPanelChild } from './settings-panel-child' | ||
7 | |||
8 | const Button = videojs.getComponent('Button') | ||
9 | const Menu = videojs.getComponent('Menu') | ||
10 | const Component = videojs.getComponent('Component') | ||
11 | |||
12 | export interface SettingsButtonOptions extends videojs.ComponentOptions { | ||
13 | entries: any[] | ||
14 | setup?: { | ||
15 | maxHeightOffset: number | ||
16 | } | ||
17 | } | ||
18 | |||
19 | class SettingsButton extends Button { | ||
20 | dialog: SettingsDialog | ||
21 | dialogEl: HTMLElement | ||
22 | menu: videojs.Menu | ||
23 | panel: SettingsPanel | ||
24 | panelChild: SettingsPanelChild | ||
25 | |||
26 | addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem | ||
27 | disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem | ||
28 | documentClickHandler: typeof SettingsButton.prototype.onDocumentClick | ||
29 | userInactiveHandler: typeof SettingsButton.prototype.onUserInactive | ||
30 | |||
31 | private settingsButtonOptions: SettingsButtonOptions | ||
32 | |||
33 | constructor (player: videojs.Player, options?: SettingsButtonOptions) { | ||
34 | super(player, options) | ||
35 | |||
36 | this.settingsButtonOptions = options | ||
37 | |||
38 | this.controlText('Settings') | ||
39 | |||
40 | this.dialog = this.player().addChild('settingsDialog') | ||
41 | this.dialogEl = this.dialog.el() as HTMLElement | ||
42 | this.menu = null | ||
43 | this.panel = this.dialog.addChild('settingsPanel') | ||
44 | this.panelChild = this.panel.addChild('settingsPanelChild') | ||
45 | |||
46 | this.addClass('vjs-settings') | ||
47 | this.el().setAttribute('aria-label', 'Settings Button') | ||
48 | |||
49 | // Event handlers | ||
50 | this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) | ||
51 | this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this) | ||
52 | this.documentClickHandler = this.onDocumentClick.bind(this) | ||
53 | this.userInactiveHandler = this.onUserInactive.bind(this) | ||
54 | |||
55 | this.buildMenu() | ||
56 | this.bindEvents() | ||
57 | |||
58 | // Prepare the dialog | ||
59 | this.player().one('play', () => this.hideDialog()) | ||
60 | } | ||
61 | |||
62 | onDocumentClick (event: MouseEvent) { | ||
63 | const element = event.target as HTMLElement | ||
64 | |||
65 | if (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) { | ||
66 | return | ||
67 | } | ||
68 | |||
69 | if (!this.dialog.hasClass('vjs-hidden')) { | ||
70 | this.hideDialog() | ||
71 | } | ||
72 | } | ||
73 | |||
74 | onDisposeSettingsItem (event: any, name: string) { | ||
75 | if (name === undefined) { | ||
76 | const children = this.menu.children() | ||
77 | |||
78 | while (children.length > 0) { | ||
79 | children[0].dispose() | ||
80 | this.menu.removeChild(children[0]) | ||
81 | } | ||
82 | |||
83 | this.addClass('vjs-hidden') | ||
84 | } else { | ||
85 | const item = this.menu.getChild(name) | ||
86 | |||
87 | if (item) { | ||
88 | item.dispose() | ||
89 | this.menu.removeChild(item) | ||
90 | } | ||
91 | } | ||
92 | |||
93 | this.hideDialog() | ||
94 | |||
95 | if (this.settingsButtonOptions.entries.length === 0) { | ||
96 | this.addClass('vjs-hidden') | ||
97 | } | ||
98 | } | ||
99 | |||
100 | dispose () { | ||
101 | document.removeEventListener('click', this.documentClickHandler) | ||
102 | |||
103 | if (this.isInIframe()) { | ||
104 | window.removeEventListener('blur', this.documentClickHandler) | ||
105 | } | ||
106 | } | ||
107 | |||
108 | onAddSettingsItem (event: any, data: any) { | ||
109 | const [ entry, options ] = data | ||
110 | |||
111 | this.addMenuItem(entry, options) | ||
112 | this.removeClass('vjs-hidden') | ||
113 | } | ||
114 | |||
115 | onUserInactive () { | ||
116 | if (!this.dialog.hasClass('vjs-hidden')) { | ||
117 | this.hideDialog() | ||
118 | } | ||
119 | } | ||
120 | |||
121 | bindEvents () { | ||
122 | document.addEventListener('click', this.documentClickHandler) | ||
123 | if (this.isInIframe()) { | ||
124 | window.addEventListener('blur', this.documentClickHandler) | ||
125 | } | ||
126 | |||
127 | this.player().on('addsettingsitem', this.addSettingsItemHandler) | ||
128 | this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) | ||
129 | this.player().on('userinactive', this.userInactiveHandler) | ||
130 | } | ||
131 | |||
132 | buildCSSClass () { | ||
133 | return `vjs-icon-settings ${super.buildCSSClass()}` | ||
134 | } | ||
135 | |||
136 | handleClick () { | ||
137 | if (this.dialog.hasClass('vjs-hidden')) { | ||
138 | this.showDialog() | ||
139 | } else { | ||
140 | this.hideDialog() | ||
141 | } | ||
142 | } | ||
143 | |||
144 | showDialog () { | ||
145 | this.player().peertube().onMenuOpened(); | ||
146 | |||
147 | (this.menu.el() as HTMLElement).style.opacity = '1' | ||
148 | |||
149 | this.dialog.show() | ||
150 | this.el().setAttribute('aria-expanded', 'true') | ||
151 | |||
152 | this.setDialogSize(this.getComponentSize(this.menu)) | ||
153 | |||
154 | const firstChild = this.menu.children()[0] | ||
155 | if (firstChild) firstChild.focus() | ||
156 | } | ||
157 | |||
158 | hideDialog () { | ||
159 | this.player_.peertube().onMenuClosed() | ||
160 | |||
161 | this.dialog.hide() | ||
162 | this.el().setAttribute('aria-expanded', 'false') | ||
163 | |||
164 | this.setDialogSize(this.getComponentSize(this.menu)); | ||
165 | (this.menu.el() as HTMLElement).style.opacity = '1' | ||
166 | this.resetChildren() | ||
167 | } | ||
168 | |||
169 | getComponentSize (element: videojs.Component | HTMLElement) { | ||
170 | let width: number = null | ||
171 | let height: number = null | ||
172 | |||
173 | // Could be component or just DOM element | ||
174 | if (element instanceof Component) { | ||
175 | const el = element.el() as HTMLElement | ||
176 | |||
177 | width = el.offsetWidth | ||
178 | height = el.offsetHeight; | ||
179 | |||
180 | (element as any).width = width; | ||
181 | (element as any).height = height | ||
182 | } else { | ||
183 | width = element.offsetWidth | ||
184 | height = element.offsetHeight | ||
185 | } | ||
186 | |||
187 | return [ width, height ] | ||
188 | } | ||
189 | |||
190 | setDialogSize ([ width, height ]: number[]) { | ||
191 | if (typeof height !== 'number') { | ||
192 | return | ||
193 | } | ||
194 | |||
195 | const offset = this.settingsButtonOptions.setup.maxHeightOffset | ||
196 | const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset | ||
197 | |||
198 | const panelEl = this.panel.el() as HTMLElement | ||
199 | |||
200 | if (height > maxHeight) { | ||
201 | height = maxHeight | ||
202 | width += 17 | ||
203 | panelEl.style.maxHeight = `${height}px` | ||
204 | } else if (panelEl.style.maxHeight !== '') { | ||
205 | panelEl.style.maxHeight = '' | ||
206 | } | ||
207 | |||
208 | this.dialogEl.style.width = `${width}px` | ||
209 | this.dialogEl.style.height = `${height}px` | ||
210 | } | ||
211 | |||
212 | buildMenu () { | ||
213 | this.menu = new Menu(this.player()) | ||
214 | this.menu.addClass('vjs-main-menu') | ||
215 | const entries = this.settingsButtonOptions.entries | ||
216 | |||
217 | if (entries.length === 0) { | ||
218 | this.addClass('vjs-hidden') | ||
219 | this.panelChild.addChild(this.menu) | ||
220 | return | ||
221 | } | ||
222 | |||
223 | for (const entry of entries) { | ||
224 | this.addMenuItem(entry, this.settingsButtonOptions) | ||
225 | } | ||
226 | |||
227 | this.panelChild.addChild(this.menu) | ||
228 | } | ||
229 | |||
230 | addMenuItem (entry: any, options: any) { | ||
231 | const openSubMenu = function (this: any) { | ||
232 | if (videojs.dom.hasClass(this.el_, 'open')) { | ||
233 | videojs.dom.removeClass(this.el_, 'open') | ||
234 | } else { | ||
235 | videojs.dom.addClass(this.el_, 'open') | ||
236 | } | ||
237 | } | ||
238 | |||
239 | options.name = toTitleCase(entry) | ||
240 | |||
241 | const newOptions = Object.assign({}, options, { entry, menuButton: this }) | ||
242 | const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) | ||
243 | |||
244 | this.menu.addChild(settingsMenuItem) | ||
245 | |||
246 | // Hide children to avoid sub menus stacking on top of each other | ||
247 | // or having multiple menus open | ||
248 | settingsMenuItem.on('click', videojs.bind(this, this.hideChildren)) | ||
249 | |||
250 | // Whether to add or remove selected class on the settings sub menu element | ||
251 | settingsMenuItem.on('click', openSubMenu) | ||
252 | } | ||
253 | |||
254 | resetChildren () { | ||
255 | for (const menuChild of this.menu.children()) { | ||
256 | (menuChild as SettingsMenuItem).reset() | ||
257 | } | ||
258 | } | ||
259 | |||
260 | /** | ||
261 | * Hide all the sub menus | ||
262 | */ | ||
263 | hideChildren () { | ||
264 | for (const menuChild of this.menu.children()) { | ||
265 | (menuChild as SettingsMenuItem).hideSubMenu() | ||
266 | } | ||
267 | } | ||
268 | |||
269 | isInIframe () { | ||
270 | return window.self !== window.top | ||
271 | } | ||
272 | |||
273 | } | ||
274 | |||
275 | Component.registerComponent('SettingsButton', SettingsButton) | ||
276 | |||
277 | export { SettingsButton } | ||
diff --git a/client/src/assets/player/shared/settings/settings-menu-item.ts b/client/src/assets/player/shared/settings/settings-menu-item.ts new file mode 100644 index 000000000..8d1819a2d --- /dev/null +++ b/client/src/assets/player/shared/settings/settings-menu-item.ts | |||
@@ -0,0 +1,377 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { toTitleCase } from '../common' | ||
3 | import { SettingsDialog } from './settings-dialog' | ||
4 | import { SettingsButton } from './settings-menu-button' | ||
5 | import { SettingsPanel } from './settings-panel' | ||
6 | import { SettingsPanelChild } from './settings-panel-child' | ||
7 | |||
8 | const MenuItem = videojs.getComponent('MenuItem') | ||
9 | const component = videojs.getComponent('Component') | ||
10 | |||
11 | export interface SettingsMenuItemOptions extends videojs.MenuItemOptions { | ||
12 | entry: string | ||
13 | menuButton: SettingsButton | ||
14 | } | ||
15 | |||
16 | class SettingsMenuItem extends MenuItem { | ||
17 | settingsButton: SettingsButton | ||
18 | dialog: SettingsDialog | ||
19 | mainMenu: videojs.Menu | ||
20 | panel: SettingsPanel | ||
21 | panelChild: SettingsPanelChild | ||
22 | panelChildEl: HTMLElement | ||
23 | size: number[] | ||
24 | menuToLoad: string | ||
25 | subMenu: SettingsButton | ||
26 | |||
27 | submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick | ||
28 | transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd | ||
29 | |||
30 | settingsSubMenuTitleEl_: HTMLElement | ||
31 | settingsSubMenuValueEl_: HTMLElement | ||
32 | settingsSubMenuEl_: HTMLElement | ||
33 | |||
34 | constructor (player: videojs.Player, options?: SettingsMenuItemOptions) { | ||
35 | super(player, options) | ||
36 | |||
37 | this.settingsButton = options.menuButton | ||
38 | this.dialog = this.settingsButton.dialog | ||
39 | this.mainMenu = this.settingsButton.menu | ||
40 | this.panel = this.dialog.getChild('settingsPanel') | ||
41 | this.panelChild = this.panel.getChild('settingsPanelChild') | ||
42 | this.panelChildEl = this.panelChild.el() as HTMLElement | ||
43 | |||
44 | this.size = null | ||
45 | |||
46 | // keep state of what menu type is loading next | ||
47 | this.menuToLoad = 'mainmenu' | ||
48 | |||
49 | const subMenuName = toTitleCase(options.entry) | ||
50 | const SubMenuComponent = videojs.getComponent(subMenuName) | ||
51 | |||
52 | if (!SubMenuComponent) { | ||
53 | throw new Error(`Component ${subMenuName} does not exist`) | ||
54 | } | ||
55 | |||
56 | const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this }) | ||
57 | |||
58 | this.subMenu = new SubMenuComponent(this.player(), newOptions) as SettingsButton | ||
59 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] | ||
60 | this.settingsSubMenuEl_.className += ' ' + subMenuClass | ||
61 | |||
62 | this.eventHandlers() | ||
63 | |||
64 | player.ready(() => { | ||
65 | // Voodoo magic for IOS | ||
66 | setTimeout(() => { | ||
67 | // Player was destroyed | ||
68 | if (!this.player_) return | ||
69 | |||
70 | this.build() | ||
71 | |||
72 | // Update on rate change | ||
73 | player.on('ratechange', this.submenuClickHandler) | ||
74 | |||
75 | if (subMenuName === 'CaptionsButton') { | ||
76 | // Hack to regenerate captions on HTTP fallback | ||
77 | player.on('captionsChanged', () => { | ||
78 | setTimeout(() => { | ||
79 | this.settingsSubMenuEl_.innerHTML = '' | ||
80 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | ||
81 | this.update() | ||
82 | this.bindClickEvents() | ||
83 | }, 0) | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | this.reset() | ||
88 | }, 0) | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | eventHandlers () { | ||
93 | this.submenuClickHandler = this.onSubmenuClick.bind(this) | ||
94 | this.transitionEndHandler = this.onTransitionEnd.bind(this) | ||
95 | } | ||
96 | |||
97 | onSubmenuClick (event: any) { | ||
98 | let target = null | ||
99 | |||
100 | if (event.type === 'tap') { | ||
101 | target = event.target | ||
102 | } else { | ||
103 | target = event.currentTarget || event.target | ||
104 | } | ||
105 | |||
106 | if (target?.classList.contains('vjs-back-button')) { | ||
107 | this.loadMainMenu() | ||
108 | return | ||
109 | } | ||
110 | |||
111 | // To update the sub menu value on click, setTimeout is needed because | ||
112 | // updating the value is not instant | ||
113 | setTimeout(() => this.update(event), 0) | ||
114 | |||
115 | // Seems like videojs adds a vjs-hidden class on the caption menu after a click | ||
116 | // We don't need it | ||
117 | this.subMenu.menu.removeClass('vjs-hidden') | ||
118 | } | ||
119 | |||
120 | /** | ||
121 | * Create the component's DOM element | ||
122 | * | ||
123 | */ | ||
124 | createEl () { | ||
125 | const el = videojs.dom.createEl('li', { | ||
126 | className: 'vjs-menu-item', | ||
127 | tabIndex: -1 | ||
128 | }) | ||
129 | |||
130 | this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', { | ||
131 | className: 'vjs-settings-sub-menu-title' | ||
132 | }) as HTMLElement | ||
133 | |||
134 | el.appendChild(this.settingsSubMenuTitleEl_) | ||
135 | |||
136 | this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', { | ||
137 | className: 'vjs-settings-sub-menu-value' | ||
138 | }) as HTMLElement | ||
139 | |||
140 | el.appendChild(this.settingsSubMenuValueEl_) | ||
141 | |||
142 | this.settingsSubMenuEl_ = videojs.dom.createEl('div', { | ||
143 | className: 'vjs-settings-sub-menu' | ||
144 | }) as HTMLElement | ||
145 | |||
146 | return el as HTMLLIElement | ||
147 | } | ||
148 | |||
149 | /** | ||
150 | * Handle click on menu item | ||
151 | * | ||
152 | * @method handleClick | ||
153 | */ | ||
154 | handleClick (event: videojs.EventTarget.Event) { | ||
155 | this.menuToLoad = 'submenu' | ||
156 | // Remove open class to ensure only the open submenu gets this class | ||
157 | videojs.dom.removeClass(this.el(), 'open') | ||
158 | |||
159 | super.handleClick(event); | ||
160 | |||
161 | (this.mainMenu.el() as HTMLElement).style.opacity = '0' | ||
162 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element | ||
163 | if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { | ||
164 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
165 | |||
166 | // animation not played without timeout | ||
167 | setTimeout(() => { | ||
168 | this.settingsSubMenuEl_.style.opacity = '1' | ||
169 | this.settingsSubMenuEl_.style.marginRight = '0px' | ||
170 | }, 0) | ||
171 | |||
172 | this.settingsButton.setDialogSize(this.size) | ||
173 | |||
174 | const firstChild = this.subMenu.menu.children()[0] | ||
175 | if (firstChild) firstChild.focus() | ||
176 | } else { | ||
177 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
178 | } | ||
179 | } | ||
180 | |||
181 | /** | ||
182 | * Create back button | ||
183 | * | ||
184 | * @method createBackButton | ||
185 | */ | ||
186 | createBackButton () { | ||
187 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) | ||
188 | |||
189 | button.addClass('vjs-back-button'); | ||
190 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) | ||
191 | } | ||
192 | |||
193 | /** | ||
194 | * Add/remove prefixed event listener for CSS Transition | ||
195 | * | ||
196 | * @method PrefixedEvent | ||
197 | */ | ||
198 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { | ||
199 | const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] | ||
200 | |||
201 | for (let p = 0; p < prefix.length; p++) { | ||
202 | if (!prefix[p]) { | ||
203 | type = type.toLowerCase() | ||
204 | } | ||
205 | |||
206 | if (action === 'addEvent') { | ||
207 | element.addEventListener(prefix[p] + type, callback, false) | ||
208 | } else if (action === 'removeEvent') { | ||
209 | element.removeEventListener(prefix[p] + type, callback, false) | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | |||
214 | onTransitionEnd (event: any) { | ||
215 | if (event.propertyName !== 'margin-right') { | ||
216 | return | ||
217 | } | ||
218 | |||
219 | if (this.menuToLoad === 'mainmenu') { | ||
220 | // hide submenu | ||
221 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
222 | |||
223 | // reset opacity to 0 | ||
224 | this.settingsSubMenuEl_.style.opacity = '0' | ||
225 | } | ||
226 | } | ||
227 | |||
228 | reset () { | ||
229 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
230 | this.settingsSubMenuEl_.style.opacity = '0' | ||
231 | this.setMargin() | ||
232 | } | ||
233 | |||
234 | loadMainMenu () { | ||
235 | const mainMenuEl = this.mainMenu.el() as HTMLElement | ||
236 | this.menuToLoad = 'mainmenu' | ||
237 | this.mainMenu.show() | ||
238 | mainMenuEl.style.opacity = '0' | ||
239 | |||
240 | // back button will always take you to main menu, so set dialog sizes | ||
241 | const mainMenuAny = this.mainMenu as any | ||
242 | this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ]) | ||
243 | |||
244 | // animation not triggered without timeout (some async stuff ?!?) | ||
245 | setTimeout(() => { | ||
246 | // animate margin and opacity before hiding the submenu | ||
247 | // this triggers CSS Transition event | ||
248 | this.setMargin() | ||
249 | mainMenuEl.style.opacity = '1' | ||
250 | |||
251 | const firstChild = this.mainMenu.children()[0] | ||
252 | if (firstChild) firstChild.focus() | ||
253 | }, 0) | ||
254 | } | ||
255 | |||
256 | build () { | ||
257 | this.subMenu.on('labelUpdated', () => { | ||
258 | this.update() | ||
259 | }) | ||
260 | this.subMenu.on('menuChanged', () => { | ||
261 | this.bindClickEvents() | ||
262 | this.setSize() | ||
263 | this.update() | ||
264 | }) | ||
265 | |||
266 | this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText()) | ||
267 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) | ||
268 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) | ||
269 | this.update() | ||
270 | |||
271 | this.createBackButton() | ||
272 | this.setSize() | ||
273 | this.bindClickEvents() | ||
274 | |||
275 | // prefixed event listeners for CSS TransitionEnd | ||
276 | this.PrefixedEvent( | ||
277 | this.settingsSubMenuEl_, | ||
278 | 'TransitionEnd', | ||
279 | this.transitionEndHandler, | ||
280 | 'addEvent' | ||
281 | ) | ||
282 | } | ||
283 | |||
284 | update (event?: any) { | ||
285 | let target: HTMLElement = null | ||
286 | const subMenu = this.subMenu.name() | ||
287 | |||
288 | if (event && event.type === 'tap') { | ||
289 | target = event.target | ||
290 | } else if (event) { | ||
291 | target = event.currentTarget | ||
292 | } | ||
293 | |||
294 | // Playback rate menu button doesn't get a vjs-selected class | ||
295 | // or sets options_['selected'] on the selected playback rate. | ||
296 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | ||
297 | if (subMenu === 'PlaybackRateMenuButton') { | ||
298 | const html = (this.subMenu as any).labelEl_.innerHTML | ||
299 | |||
300 | setTimeout(() => { | ||
301 | this.settingsSubMenuValueEl_.innerHTML = html | ||
302 | }, 250) | ||
303 | } else { | ||
304 | // Loop trough the submenu items to find the selected child | ||
305 | for (const subMenuItem of this.subMenu.menu.children_) { | ||
306 | if (!(subMenuItem instanceof component)) { | ||
307 | continue | ||
308 | } | ||
309 | |||
310 | if (subMenuItem.hasClass('vjs-selected')) { | ||
311 | const subMenuItemUntyped = subMenuItem as any | ||
312 | |||
313 | // Prefer to use the function | ||
314 | if (typeof subMenuItemUntyped.getLabel === 'function') { | ||
315 | this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel() | ||
316 | break | ||
317 | } | ||
318 | |||
319 | this.settingsSubMenuValueEl_.innerHTML = this.player().localize(subMenuItemUntyped.options_.label) | ||
320 | } | ||
321 | } | ||
322 | } | ||
323 | |||
324 | if (target && !target.classList.contains('vjs-back-button')) { | ||
325 | this.settingsButton.hideDialog() | ||
326 | } | ||
327 | } | ||
328 | |||
329 | bindClickEvents () { | ||
330 | for (const item of this.subMenu.menu.children()) { | ||
331 | if (!(item instanceof component)) { | ||
332 | continue | ||
333 | } | ||
334 | item.on([ 'tap', 'click' ], this.submenuClickHandler) | ||
335 | } | ||
336 | } | ||
337 | |||
338 | // save size of submenus on first init | ||
339 | // if number of submenu items change dynamically more logic will be needed | ||
340 | setSize () { | ||
341 | this.dialog.removeClass('vjs-hidden') | ||
342 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
343 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | ||
344 | this.setMargin() | ||
345 | this.dialog.addClass('vjs-hidden') | ||
346 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
347 | } | ||
348 | |||
349 | setMargin () { | ||
350 | if (!this.size) return | ||
351 | |||
352 | const [ width ] = this.size | ||
353 | |||
354 | this.settingsSubMenuEl_.style.marginRight = `-${width}px` | ||
355 | } | ||
356 | |||
357 | /** | ||
358 | * Hide the sub menu | ||
359 | */ | ||
360 | hideSubMenu () { | ||
361 | // after removing settings item this.el_ === null | ||
362 | if (!this.el()) { | ||
363 | return | ||
364 | } | ||
365 | |||
366 | if (videojs.dom.hasClass(this.el(), 'open')) { | ||
367 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
368 | videojs.dom.removeClass(this.el(), 'open') | ||
369 | } | ||
370 | } | ||
371 | |||
372 | } | ||
373 | |||
374 | (SettingsMenuItem as any).prototype.contentElType = 'button' | ||
375 | videojs.registerComponent('SettingsMenuItem', SettingsMenuItem) | ||
376 | |||
377 | export { SettingsMenuItem } | ||
diff --git a/client/src/assets/player/shared/settings/settings-panel-child.ts b/client/src/assets/player/shared/settings/settings-panel-child.ts new file mode 100644 index 000000000..161420c38 --- /dev/null +++ b/client/src/assets/player/shared/settings/settings-panel-child.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanelChild extends Component { | ||
6 | |||
7 | createEl () { | ||
8 | return super.createEl('div', { | ||
9 | className: 'vjs-settings-panel-child', | ||
10 | innerHTML: '', | ||
11 | tabIndex: -1 | ||
12 | }) | ||
13 | } | ||
14 | } | ||
15 | |||
16 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | ||
17 | |||
18 | export { SettingsPanelChild } | ||
diff --git a/client/src/assets/player/shared/settings/settings-panel.ts b/client/src/assets/player/shared/settings/settings-panel.ts new file mode 100644 index 000000000..28b579bdd --- /dev/null +++ b/client/src/assets/player/shared/settings/settings-panel.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanel extends Component { | ||
6 | |||
7 | createEl () { | ||
8 | return super.createEl('div', { | ||
9 | className: 'vjs-settings-panel', | ||
10 | innerHTML: '', | ||
11 | tabIndex: -1 | ||
12 | }) | ||
13 | } | ||
14 | } | ||
15 | |||
16 | Component.registerComponent('SettingsPanel', SettingsPanel) | ||
17 | |||
18 | export { SettingsPanel } | ||