aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/videojs-components
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/assets/player/videojs-components
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/assets/player/videojs-components')
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts105
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts40
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts38
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts109
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts83
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts288
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts335
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts50
8 files changed, 1048 insertions, 0 deletions
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
new file mode 100644
index 000000000..6424787b2
--- /dev/null
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -0,0 +1,105 @@
1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { bytes } from '../utils'
3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class P2pInfoButton extends Button {
6
7 createEl () {
8 const div = videojsUntyped.dom.createEl('div', {
9 className: 'vjs-peertube'
10 })
11 const subDivWebtorrent = videojsUntyped.dom.createEl('div', {
12 className: 'vjs-peertube-hidden' // Hide the stats before we get the info
13 })
14 div.appendChild(subDivWebtorrent)
15
16 const downloadIcon = videojsUntyped.dom.createEl('span', {
17 className: 'icon icon-download'
18 })
19 subDivWebtorrent.appendChild(downloadIcon)
20
21 const downloadSpeedText = videojsUntyped.dom.createEl('span', {
22 className: 'download-speed-text'
23 })
24 const downloadSpeedNumber = videojsUntyped.dom.createEl('span', {
25 className: 'download-speed-number'
26 })
27 const downloadSpeedUnit = videojsUntyped.dom.createEl('span')
28 downloadSpeedText.appendChild(downloadSpeedNumber)
29 downloadSpeedText.appendChild(downloadSpeedUnit)
30 subDivWebtorrent.appendChild(downloadSpeedText)
31
32 const uploadIcon = videojsUntyped.dom.createEl('span', {
33 className: 'icon icon-upload'
34 })
35 subDivWebtorrent.appendChild(uploadIcon)
36
37 const uploadSpeedText = videojsUntyped.dom.createEl('span', {
38 className: 'upload-speed-text'
39 })
40 const uploadSpeedNumber = videojsUntyped.dom.createEl('span', {
41 className: 'upload-speed-number'
42 })
43 const uploadSpeedUnit = videojsUntyped.dom.createEl('span')
44 uploadSpeedText.appendChild(uploadSpeedNumber)
45 uploadSpeedText.appendChild(uploadSpeedUnit)
46 subDivWebtorrent.appendChild(uploadSpeedText)
47
48 const peersText = videojsUntyped.dom.createEl('span', {
49 className: 'peers-text'
50 })
51 const peersNumber = videojsUntyped.dom.createEl('span', {
52 className: 'peers-number'
53 })
54 subDivWebtorrent.appendChild(peersNumber)
55 subDivWebtorrent.appendChild(peersText)
56
57 const subDivHttp = videojsUntyped.dom.createEl('div', {
58 className: 'vjs-peertube-hidden'
59 })
60 const subDivHttpText = videojsUntyped.dom.createEl('span', {
61 className: 'http-fallback',
62 textContent: 'HTTP'
63 })
64
65 subDivHttp.appendChild(subDivHttpText)
66 div.appendChild(subDivHttp)
67
68 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
69 // We are in HTTP fallback
70 if (!data) {
71 subDivHttp.className = 'vjs-peertube-displayed'
72 subDivWebtorrent.className = 'vjs-peertube-hidden'
73
74 return
75 }
76
77 const p2pStats = data.p2p
78 const httpStats = data.http
79
80 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
81 const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed)
82 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
84 const numPeers = p2pStats.numPeers
85
86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
87 this.player_.localize('Total uploaded: ' + totalUploaded.join(' '))
88
89 downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
90 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
91
92 uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
94
95 peersNumber.textContent = numPeers
96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer'))
97
98 subDivHttp.className = 'vjs-peertube-hidden'
99 subDivWebtorrent.className = 'vjs-peertube-displayed'
100 })
101
102 return div
103 }
104}
105Button.registerComponent('P2PInfoButton', P2pInfoButton)
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts
new file mode 100644
index 000000000..fed8ea33e
--- /dev/null
+++ b/client/src/assets/player/videojs-components/peertube-link-button.ts
@@ -0,0 +1,40 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from '../utils'
3// FIXME: something weird with our path definition in tsconfig and typings
4// @ts-ignore
5import { Player } from 'video.js'
6
7const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
8class PeerTubeLinkButton extends Button {
9
10 constructor (player: Player, options: any) {
11 super(player, options)
12 }
13
14 createEl () {
15 return this.buildElement()
16 }
17
18 updateHref () {
19 this.el().setAttribute('href', buildVideoLink(this.player().currentTime()))
20 }
21
22 handleClick () {
23 this.player_.pause()
24 }
25
26 private buildElement () {
27 const el = videojsUntyped.dom.createEl('a', {
28 href: buildVideoLink(),
29 innerHTML: 'PeerTube',
30 title: this.player_.localize('Go to the video page'),
31 className: 'vjs-peertube-link',
32 target: '_blank'
33 })
34
35 el.addEventListener('mouseenter', () => this.updateHref())
36
37 return el
38 }
39}
40Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
new file mode 100644
index 000000000..9a0e3b550
--- /dev/null
+++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
@@ -0,0 +1,38 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
5
6const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
7
8class PeerTubeLoadProgressBar extends Component {
9
10 constructor (player: Player, options: any) {
11 super(player, options)
12 this.partEls_ = []
13 this.on(player, 'progress', this.update)
14 }
15
16 createEl () {
17 return super.createEl('div', {
18 className: 'vjs-load-progress',
19 innerHTML: `<span class="vjs-control-text"><span>${this.localize('Loaded')}</span>: 0%</span>`
20 })
21 }
22
23 dispose () {
24 this.partEls_ = null
25
26 super.dispose()
27 }
28
29 update () {
30 const torrent = this.player().webtorrent().getTorrent()
31 if (!torrent) return
32
33 this.el_.style.width = (torrent.progress * 100) + '%'
34 }
35
36}
37
38Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar)
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
new file mode 100644
index 000000000..abcc16411
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -0,0 +1,109 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement
12
13 constructor (player: Player, options: any) {
14 super(player, options)
15 this.player = player
16
17 player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
18
19 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
20 }
21
22 createEl () {
23 const el = super.createEl()
24
25 this.labelEl_ = videojsUntyped.dom.createEl('div', {
26 className: 'vjs-resolution-value'
27 })
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 let children = this.menu.children()
53
54 for (const child of children) {
55 if (component !== child) {
56 child.selected(false)
57 }
58 }
59 })
60 }
61
62 private buildQualities (data: LoadedQualityData) {
63 // The automatic resolution item will need other labels
64 const labels: { [ id: number ]: string } = {}
65
66 data.qualityData.video.sort((a, b) => {
67 if (a.id > b.id) return -1
68 if (a.id === b.id) return 0
69 return 1
70 })
71
72 for (const d of data.qualityData.video) {
73 // Skip auto resolution, we'll add it ourselves
74 if (d.id === -1) continue
75
76 this.menu.addChild(new ResolutionMenuItem(
77 this.player_,
78 {
79 id: d.id,
80 label: d.label,
81 selected: d.selected,
82 callback: data.qualitySwitchCallback
83 })
84 )
85
86 labels[d.id] = d.label
87 }
88
89 this.menu.addChild(new ResolutionMenuItem(
90 this.player_,
91 {
92 id: -1,
93 label: this.player_.localize('Auto'),
94 labels,
95 callback: data.qualitySwitchCallback,
96 selected: true // By default, in auto mode
97 }
98 ))
99
100 for (const m of this.menu.children()) {
101 this.addClickListener(m)
102 }
103
104 this.trigger('menuChanged')
105 }
106}
107ResolutionMenuButton.prototype.controlText_ = 'Quality'
108
109MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
new file mode 100644
index 000000000..6c42fefd2
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -0,0 +1,83 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem {
9 private readonly id: number
10 private readonly label: string
11 // Only used for the automatic item
12 private readonly labels: { [id: number]: string }
13 private readonly callback: Function
14
15 private autoResolutionPossible: boolean
16 private currentResolutionLabel: string
17
18 constructor (player: Player, options: any) {
19 options.selectable = true
20
21 super(player, options)
22
23 this.autoResolutionPossible = true
24 this.currentResolutionLabel = ''
25
26 this.label = options.label
27 this.labels = options.labels
28 this.id = options.id
29 this.callback = options.callback
30
31 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
32
33 // We only want to disable the "Auto" item
34 if (this.id === -1) {
35 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
36 }
37 }
38
39 handleClick (event: any) {
40 // Auto button disabled?
41 if (this.autoResolutionPossible === false && this.id === -1) return
42
43 super.handleClick(event)
44
45 this.callback(this.id, 'video')
46 }
47
48 updateSelection (data: ResolutionUpdateData) {
49 if (this.id === -1) {
50 this.currentResolutionLabel = this.labels[data.id]
51 }
52
53 // Automatic resolution only
54 if (data.auto === true) {
55 this.selected(this.id === -1)
56 return
57 }
58
59 this.selected(this.id === data.id)
60 }
61
62 updateAutoResolution (data: AutoResolutionUpdateData) {
63 // Check if the auto resolution is enabled or not
64 if (data.possible === false) {
65 this.addClass('disabled')
66 } else {
67 this.removeClass('disabled')
68 }
69
70 this.autoResolutionPossible = data.possible
71 }
72
73 getLabel () {
74 if (this.id === -1) {
75 return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
76 }
77
78 return this.label
79 }
80}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82
83export { ResolutionMenuItem }
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
new file mode 100644
index 000000000..14cb8ba43
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -0,0 +1,288 @@
1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from '../utils'
11
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
14const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
15
16class SettingsButton extends Button {
17 constructor (player: videojs.Player, options: any) {
18 super(player, options)
19
20 this.playerComponent = player
21 this.dialog = this.playerComponent.addChild('settingsDialog')
22 this.dialogEl = this.dialog.el_
23 this.menu = null
24 this.panel = this.dialog.addChild('settingsPanel')
25 this.panelChild = this.panel.addChild('settingsPanelChild')
26
27 this.addClass('vjs-settings')
28 this.el_.setAttribute('aria-label', 'Settings Button')
29
30 // Event handlers
31 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
32 this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this)
33 this.playerClickHandler = this.onPlayerClick.bind(this)
34 this.userInactiveHandler = this.onUserInactive.bind(this)
35
36 this.buildMenu()
37 this.bindEvents()
38
39 // Prepare the dialog
40 this.player().one('play', () => this.hideDialog())
41 }
42
43 onPlayerClick (event: MouseEvent) {
44 const element = event.target as HTMLElement
45 if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) {
46 return
47 }
48
49 if (!this.dialog.hasClass('vjs-hidden')) {
50 this.hideDialog()
51 }
52 }
53
54 onDisposeSettingsItem (event: any, name: string) {
55 if (name === undefined) {
56 let children = this.menu.children()
57
58 while (children.length > 0) {
59 children[0].dispose()
60 this.menu.removeChild(children[0])
61 }
62
63 this.addClass('vjs-hidden')
64 } else {
65 let item = this.menu.getChild(name)
66
67 if (item) {
68 item.dispose()
69 this.menu.removeChild(item)
70 }
71 }
72
73 this.hideDialog()
74
75 if (this.options_.entries.length === 0) {
76 this.addClass('vjs-hidden')
77 }
78 }
79
80 onAddSettingsItem (event: any, data: any) {
81 const [ entry, options ] = data
82
83 this.addMenuItem(entry, options)
84 this.removeClass('vjs-hidden')
85 }
86
87 onUserInactive () {
88 if (!this.dialog.hasClass('vjs-hidden')) {
89 this.hideDialog()
90 }
91 }
92
93 bindEvents () {
94 this.playerComponent.on('click', this.playerClickHandler)
95 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler)
96 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler)
97 this.playerComponent.on('userinactive', this.userInactiveHandler)
98 }
99
100 buildCSSClass () {
101 return `vjs-icon-settings ${super.buildCSSClass()}`
102 }
103
104 handleClick () {
105 if (this.dialog.hasClass('vjs-hidden')) {
106 this.showDialog()
107 } else {
108 this.hideDialog()
109 }
110 }
111
112 showDialog () {
113 this.menu.el_.style.opacity = '1'
114 this.dialog.show()
115
116 this.setDialogSize(this.getComponentSize(this.menu))
117 }
118
119 hideDialog () {
120 this.dialog.hide()
121 this.setDialogSize(this.getComponentSize(this.menu))
122 this.menu.el_.style.opacity = '1'
123 this.resetChildren()
124 }
125
126 getComponentSize (element: any) {
127 let width: number = null
128 let height: number = null
129
130 // Could be component or just DOM element
131 if (element instanceof Component) {
132 width = element.el_.offsetWidth
133 height = element.el_.offsetHeight
134
135 // keep width/height as properties for direct use
136 element.width = width
137 element.height = height
138 } else {
139 width = element.offsetWidth
140 height = element.offsetHeight
141 }
142
143 return [ width, height ]
144 }
145
146 setDialogSize ([ width, height ]: number[]) {
147 if (typeof height !== 'number') {
148 return
149 }
150
151 let offset = this.options_.setup.maxHeightOffset
152 let maxHeight = this.playerComponent.el_.offsetHeight - offset
153
154 if (height > maxHeight) {
155 height = maxHeight
156 width += 17
157 this.panel.el_.style.maxHeight = `${height}px`
158 } else if (this.panel.el_.style.maxHeight !== '') {
159 this.panel.el_.style.maxHeight = ''
160 }
161
162 this.dialogEl.style.width = `${width}px`
163 this.dialogEl.style.height = `${height}px`
164 }
165
166 buildMenu () {
167 this.menu = new Menu(this.player())
168 this.menu.addClass('vjs-main-menu')
169 let entries = this.options_.entries
170
171 if (entries.length === 0) {
172 this.addClass('vjs-hidden')
173 this.panelChild.addChild(this.menu)
174 return
175 }
176
177 for (let entry of entries) {
178 this.addMenuItem(entry, this.options_)
179 }
180
181 this.panelChild.addChild(this.menu)
182 }
183
184 addMenuItem (entry: any, options: any) {
185 const openSubMenu = function (this: any) {
186 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
187 videojsUntyped.dom.removeClass(this.el_, 'open')
188 } else {
189 videojsUntyped.dom.addClass(this.el_, 'open')
190 }
191 }
192
193 options.name = toTitleCase(entry)
194 let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
195
196 this.menu.addChild(settingsMenuItem)
197
198 // Hide children to avoid sub menus stacking on top of each other
199 // or having multiple menus open
200 settingsMenuItem.on('click', videojs.bind(this, this.hideChildren))
201
202 // Whether to add or remove selected class on the settings sub menu element
203 settingsMenuItem.on('click', openSubMenu)
204 }
205
206 resetChildren () {
207 for (let menuChild of this.menu.children()) {
208 menuChild.reset()
209 }
210 }
211
212 /**
213 * Hide all the sub menus
214 */
215 hideChildren () {
216 for (let menuChild of this.menu.children()) {
217 menuChild.hideSubMenu()
218 }
219 }
220
221}
222
223class SettingsPanel extends Component {
224 constructor (player: videojs.Player, options: any) {
225 super(player, options)
226 }
227
228 createEl () {
229 return super.createEl('div', {
230 className: 'vjs-settings-panel',
231 innerHTML: '',
232 tabIndex: -1
233 })
234 }
235}
236
237class SettingsPanelChild extends Component {
238 constructor (player: videojs.Player, options: any) {
239 super(player, options)
240 }
241
242 createEl () {
243 return super.createEl('div', {
244 className: 'vjs-settings-panel-child',
245 innerHTML: '',
246 tabIndex: -1
247 })
248 }
249}
250
251class SettingsDialog extends Component {
252 constructor (player: videojs.Player, options: any) {
253 super(player, options)
254 this.hide()
255 }
256
257 /**
258 * Create the component's DOM element
259 *
260 * @return {Element}
261 * @method createEl
262 */
263 createEl () {
264 const uniqueId = this.id_
265 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
266 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
267
268 return super.createEl('div', {
269 className: 'vjs-settings-dialog vjs-modal-overlay',
270 innerHTML: '',
271 tabIndex: -1
272 }, {
273 'role': 'dialog',
274 'aria-labelledby': dialogLabelId,
275 'aria-describedby': dialogDescriptionId
276 })
277 }
278
279}
280
281SettingsButton.prototype.controlText_ = 'Settings'
282
283Component.registerComponent('SettingsButton', SettingsButton)
284Component.registerComponent('SettingsDialog', SettingsDialog)
285Component.registerComponent('SettingsPanel', SettingsPanel)
286Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
287
288export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
new file mode 100644
index 000000000..f14959f9c
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -0,0 +1,335 @@
1// Author: Yanko Shterev
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
13
14class SettingsMenuItem extends MenuItem {
15
16 constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) {
17 super(player, options)
18
19 this.settingsButton = menuButton
20 this.dialog = this.settingsButton.dialog
21 this.mainMenu = this.settingsButton.menu
22 this.panel = this.dialog.getChild('settingsPanel')
23 this.panelChild = this.panel.getChild('settingsPanelChild')
24 this.panelChildEl = this.panelChild.el_
25
26 this.size = null
27
28 // keep state of what menu type is loading next
29 this.menuToLoad = 'mainmenu'
30
31 const subMenuName = toTitleCase(entry)
32 const SubMenuComponent = videojsUntyped.getComponent(subMenuName)
33
34 if (!SubMenuComponent) {
35 throw new Error(`Component ${subMenuName} does not exist`)
36 }
37 this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this)
38 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0]
39 this.settingsSubMenuEl_.className += ' ' + subMenuClass
40
41 this.eventHandlers()
42
43 player.ready(() => {
44 // Voodoo magic for IOS
45 setTimeout(() => {
46 this.build()
47
48 // Update on rate change
49 player.on('ratechange', this.submenuClickHandler)
50
51 if (subMenuName === 'CaptionsButton') {
52 // Hack to regenerate captions on HTTP fallback
53 player.on('captionsChanged', () => {
54 setTimeout(() => {
55 this.settingsSubMenuEl_.innerHTML = ''
56 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
57 this.update()
58 this.bindClickEvents()
59
60 }, 0)
61 })
62 }
63
64 this.reset()
65 }, 0)
66 })
67 }
68
69 eventHandlers () {
70 this.submenuClickHandler = this.onSubmenuClick.bind(this)
71 this.transitionEndHandler = this.onTransitionEnd.bind(this)
72 }
73
74 onSubmenuClick (event: any) {
75 let target = null
76
77 if (event.type === 'tap') {
78 target = event.target
79 } else {
80 target = event.currentTarget
81 }
82
83 if (target && target.classList.contains('vjs-back-button')) {
84 this.loadMainMenu()
85 return
86 }
87
88 // To update the sub menu value on click, setTimeout is needed because
89 // updating the value is not instant
90 setTimeout(() => this.update(event), 0)
91 }
92
93 /**
94 * Create the component's DOM element
95 *
96 * @return {Element}
97 * @method createEl
98 */
99 createEl () {
100 const el = videojsUntyped.dom.createEl('li', {
101 className: 'vjs-menu-item'
102 })
103
104 this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', {
105 className: 'vjs-settings-sub-menu-title'
106 })
107
108 el.appendChild(this.settingsSubMenuTitleEl_)
109
110 this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', {
111 className: 'vjs-settings-sub-menu-value'
112 })
113
114 el.appendChild(this.settingsSubMenuValueEl_)
115
116 this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', {
117 className: 'vjs-settings-sub-menu'
118 })
119
120 return el
121 }
122
123 /**
124 * Handle click on menu item
125 *
126 * @method handleClick
127 */
128 handleClick () {
129 this.menuToLoad = 'submenu'
130 // Remove open class to ensure only the open submenu gets this class
131 videojsUntyped.dom.removeClass(this.el_, 'open')
132
133 super.handleClick()
134
135 this.mainMenu.el_.style.opacity = '0'
136 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
137 if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
138 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
139
140 // animation not played without timeout
141 setTimeout(() => {
142 this.settingsSubMenuEl_.style.opacity = '1'
143 this.settingsSubMenuEl_.style.marginRight = '0px'
144 }, 0)
145
146 this.settingsButton.setDialogSize(this.size)
147 } else {
148 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
149 }
150 }
151
152 /**
153 * Create back button
154 *
155 * @method createBackButton
156 */
157 createBackButton () {
158 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
159 button.name_ = 'BackButton'
160 button.addClass('vjs-back-button')
161 button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_)
162 }
163
164 /**
165 * Add/remove prefixed event listener for CSS Transition
166 *
167 * @method PrefixedEvent
168 */
169 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
170 let prefix = ['webkit', 'moz', 'MS', 'o', '']
171
172 for (let p = 0; p < prefix.length; p++) {
173 if (!prefix[p]) {
174 type = type.toLowerCase()
175 }
176
177 if (action === 'addEvent') {
178 element.addEventListener(prefix[p] + type, callback, false)
179 } else if (action === 'removeEvent') {
180 element.removeEventListener(prefix[p] + type, callback, false)
181 }
182 }
183 }
184
185 onTransitionEnd (event: any) {
186 if (event.propertyName !== 'margin-right') {
187 return
188 }
189
190 if (this.menuToLoad === 'mainmenu') {
191 // hide submenu
192 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
193
194 // reset opacity to 0
195 this.settingsSubMenuEl_.style.opacity = '0'
196 }
197 }
198
199 reset () {
200 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
201 this.settingsSubMenuEl_.style.opacity = '0'
202 this.setMargin()
203 }
204
205 loadMainMenu () {
206 this.menuToLoad = 'mainmenu'
207 this.mainMenu.show()
208 this.mainMenu.el_.style.opacity = '0'
209
210 // back button will always take you to main menu, so set dialog sizes
211 this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height])
212
213 // animation not triggered without timeout (some async stuff ?!?)
214 setTimeout(() => {
215 // animate margin and opacity before hiding the submenu
216 // this triggers CSS Transition event
217 this.setMargin()
218 this.mainMenu.el_.style.opacity = '1'
219 }, 0)
220 }
221
222 build () {
223 this.subMenu.on('updateLabel', () => {
224 this.update()
225 })
226 this.subMenu.on('menuChanged', () => {
227 this.bindClickEvents()
228 this.setSize()
229 this.update()
230 })
231
232 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
233 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
234 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
235 this.update()
236
237 this.createBackButton()
238 this.setSize()
239 this.bindClickEvents()
240
241 // prefixed event listeners for CSS TransitionEnd
242 this.PrefixedEvent(
243 this.settingsSubMenuEl_,
244 'TransitionEnd',
245 this.transitionEndHandler,
246 'addEvent'
247 )
248 }
249
250 update (event?: any) {
251 let target: HTMLElement = null
252 let subMenu = this.subMenu.name()
253
254 if (event && event.type === 'tap') {
255 target = event.target
256 } else if (event) {
257 target = event.currentTarget
258 }
259
260 // Playback rate menu button doesn't get a vjs-selected class
261 // or sets options_['selected'] on the selected playback rate.
262 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
263 if (subMenu === 'PlaybackRateMenuButton') {
264 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250)
265 } else {
266 // Loop trough the submenu items to find the selected child
267 for (let subMenuItem of this.subMenu.menu.children_) {
268 if (!(subMenuItem instanceof component)) {
269 continue
270 }
271
272 if (subMenuItem.hasClass('vjs-selected')) {
273 // Prefer to use the function
274 if (typeof subMenuItem.getLabel === 'function') {
275 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel()
276 break
277 }
278
279 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
280 }
281 }
282 }
283
284 if (target && !target.classList.contains('vjs-back-button')) {
285 this.settingsButton.hideDialog()
286 }
287 }
288
289 bindClickEvents () {
290 for (let item of this.subMenu.menu.children()) {
291 if (!(item instanceof component)) {
292 continue
293 }
294 item.on(['tap', 'click'], this.submenuClickHandler)
295 }
296 }
297
298 // save size of submenus on first init
299 // if number of submenu items change dynamically more logic will be needed
300 setSize () {
301 this.dialog.removeClass('vjs-hidden')
302 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
303 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
304 this.setMargin()
305 this.dialog.addClass('vjs-hidden')
306 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
307 }
308
309 setMargin () {
310 let [width] = this.size
311
312 this.settingsSubMenuEl_.style.marginRight = `-${width}px`
313 }
314
315 /**
316 * Hide the sub menu
317 */
318 hideSubMenu () {
319 // after removing settings item this.el_ === null
320 if (!this.el_) {
321 return
322 }
323
324 if (videojsUntyped.dom.hasClass(this.el_, 'open')) {
325 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
326 videojsUntyped.dom.removeClass(this.el_, 'open')
327 }
328 }
329
330}
331
332SettingsMenuItem.prototype.contentElType = 'button'
333videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem)
334
335export { SettingsMenuItem }
diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts
new file mode 100644
index 000000000..1e11a9546
--- /dev/null
+++ b/client/src/assets/player/videojs-components/theater-button.ts
@@ -0,0 +1,50 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
9class TheaterButton extends Button {
10
11 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled'
12
13 constructor (player: videojs.Player, options: any) {
14 super(player, options)
15
16 const enabled = getStoredTheater()
17 if (enabled === true) {
18 this.player_.addClass(TheaterButton.THEATER_MODE_CLASS)
19 this.handleTheaterChange()
20 }
21 }
22
23 buildCSSClass () {
24 return `vjs-theater-control ${super.buildCSSClass()}`
25 }
26
27 handleTheaterChange () {
28 if (this.isTheaterEnabled()) {
29 this.controlText('Normal mode')
30 } else {
31 this.controlText('Theater mode')
32 }
33
34 saveTheaterInStore(this.isTheaterEnabled())
35 }
36
37 handleClick () {
38 this.player_.toggleClass(TheaterButton.THEATER_MODE_CLASS)
39
40 this.handleTheaterChange()
41 }
42
43 private isTheaterEnabled () {
44 return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS)
45 }
46}
47
48TheaterButton.prototype.controlText_ = 'Theater mode'
49
50TheaterButton.registerComponent('TheaterButton', TheaterButton)