From 9af2accee68082e4e1160a4e4a7036451262be02 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Mar 2022 10:27:05 +0100 Subject: Reorganize videojs components --- .../control-bar/next-previous-video-button.ts | 50 +++ .../assets/player/control-bar/p2p-info-button.ts | 124 +++++++ .../player/control-bar/peertube-link-button.ts | 45 +++ .../control-bar/peertube-load-progress-bar.ts | 33 ++ .../assets/player/control-bar/theater-button.ts | 53 +++ .../src/assets/player/peertube-player-manager.ts | 24 +- client/src/assets/player/peertube-plugin.ts | 4 +- .../player/settings/resolution-menu-button.ts | 86 +++++ .../assets/player/settings/resolution-menu-item.ts | 77 +++++ .../src/assets/player/settings/settings-dialog.ts | 35 ++ .../assets/player/settings/settings-menu-button.ts | 279 +++++++++++++++ .../assets/player/settings/settings-menu-item.ts | 378 +++++++++++++++++++++ .../assets/player/settings/settings-panel-child.ts | 18 + .../src/assets/player/settings/settings-panel.ts | 18 + .../next-previous-video-button.ts | 50 --- .../player/videojs-components/p2p-info-button.ts | 124 ------- .../videojs-components/peertube-link-button.ts | 45 --- .../peertube-load-progress-bar.ts | 33 -- .../videojs-components/resolution-menu-button.ts | 86 ----- .../videojs-components/resolution-menu-item.ts | 77 ----- .../player/videojs-components/settings-dialog.ts | 35 -- .../videojs-components/settings-menu-button.ts | 279 --------------- .../videojs-components/settings-menu-item.ts | 378 --------------------- .../videojs-components/settings-panel-child.ts | 18 - .../player/videojs-components/settings-panel.ts | 18 - .../player/videojs-components/theater-button.ts | 53 --- 26 files changed, 1210 insertions(+), 1210 deletions(-) create mode 100644 client/src/assets/player/control-bar/next-previous-video-button.ts create mode 100644 client/src/assets/player/control-bar/p2p-info-button.ts create mode 100644 client/src/assets/player/control-bar/peertube-link-button.ts create mode 100644 client/src/assets/player/control-bar/peertube-load-progress-bar.ts create mode 100644 client/src/assets/player/control-bar/theater-button.ts create mode 100644 client/src/assets/player/settings/resolution-menu-button.ts create mode 100644 client/src/assets/player/settings/resolution-menu-item.ts create mode 100644 client/src/assets/player/settings/settings-dialog.ts create mode 100644 client/src/assets/player/settings/settings-menu-button.ts create mode 100644 client/src/assets/player/settings/settings-menu-item.ts create mode 100644 client/src/assets/player/settings/settings-panel-child.ts create mode 100644 client/src/assets/player/settings/settings-panel.ts delete mode 100644 client/src/assets/player/videojs-components/next-previous-video-button.ts delete mode 100644 client/src/assets/player/videojs-components/p2p-info-button.ts delete mode 100644 client/src/assets/player/videojs-components/peertube-link-button.ts delete mode 100644 client/src/assets/player/videojs-components/peertube-load-progress-bar.ts delete mode 100644 client/src/assets/player/videojs-components/resolution-menu-button.ts delete mode 100644 client/src/assets/player/videojs-components/resolution-menu-item.ts delete mode 100644 client/src/assets/player/videojs-components/settings-dialog.ts delete mode 100644 client/src/assets/player/videojs-components/settings-menu-button.ts delete mode 100644 client/src/assets/player/videojs-components/settings-menu-item.ts delete mode 100644 client/src/assets/player/videojs-components/settings-panel-child.ts delete mode 100644 client/src/assets/player/videojs-components/settings-panel.ts delete mode 100644 client/src/assets/player/videojs-components/theater-button.ts diff --git a/client/src/assets/player/control-bar/next-previous-video-button.ts b/client/src/assets/player/control-bar/next-previous-video-button.ts new file mode 100644 index 000000000..fe17ce2ce --- /dev/null +++ b/client/src/assets/player/control-bar/next-previous-video-button.ts @@ -0,0 +1,50 @@ +import videojs from 'video.js' +import { NextPreviousVideoButtonOptions } from '../peertube-videojs-typings' + +const Button = videojs.getComponent('Button') + +class NextPreviousVideoButton extends Button { + private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions + + constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) { + super(player, options as any) + + this.nextPreviousVideoButtonOptions = options + + this.update() + } + + createEl () { + const type = (this.options_ as NextPreviousVideoButtonOptions).type + + const button = videojs.dom.createEl('button', { + className: 'vjs-' + type + '-video' + }) as HTMLButtonElement + const nextIcon = videojs.dom.createEl('span', { + className: 'icon icon-' + type + }) + button.appendChild(nextIcon) + + if (type === 'next') { + button.title = this.player_.localize('Next video') + } else { + button.title = this.player_.localize('Previous video') + } + + return button + } + + handleClick () { + this.nextPreviousVideoButtonOptions.handler() + } + + update () { + const disabled = this.nextPreviousVideoButtonOptions.isDisabled() + + if (disabled) this.addClass('vjs-disabled') + else this.removeClass('vjs-disabled') + } +} + +videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) +videojs.registerComponent('PreviousVideoButton', NextPreviousVideoButton) diff --git a/client/src/assets/player/control-bar/p2p-info-button.ts b/client/src/assets/player/control-bar/p2p-info-button.ts new file mode 100644 index 000000000..081dee1d3 --- /dev/null +++ b/client/src/assets/player/control-bar/p2p-info-button.ts @@ -0,0 +1,124 @@ +import videojs from 'video.js' +import { PeerTubeP2PInfoButtonOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' +import { bytes } from '../utils' + +const Button = videojs.getComponent('Button') +class P2pInfoButton extends Button { + + constructor (player: videojs.Player, options?: PeerTubeP2PInfoButtonOptions) { + super(player, options as any) + } + + createEl () { + const div = videojs.dom.createEl('div', { + className: 'vjs-peertube' + }) + const subDivWebtorrent = videojs.dom.createEl('div', { + className: 'vjs-peertube-hidden' // Hide the stats before we get the info + }) as HTMLDivElement + div.appendChild(subDivWebtorrent) + + // Stop here if P2P is not enabled + const p2pEnabled = (this.options_ as PeerTubeP2PInfoButtonOptions).p2pEnabled + if (!p2pEnabled) return div as HTMLButtonElement + + const downloadIcon = videojs.dom.createEl('span', { + className: 'icon icon-download' + }) + subDivWebtorrent.appendChild(downloadIcon) + + const downloadSpeedText = videojs.dom.createEl('span', { + className: 'download-speed-text' + }) + const downloadSpeedNumber = videojs.dom.createEl('span', { + className: 'download-speed-number' + }) + const downloadSpeedUnit = videojs.dom.createEl('span') + downloadSpeedText.appendChild(downloadSpeedNumber) + downloadSpeedText.appendChild(downloadSpeedUnit) + subDivWebtorrent.appendChild(downloadSpeedText) + + const uploadIcon = videojs.dom.createEl('span', { + className: 'icon icon-upload' + }) + subDivWebtorrent.appendChild(uploadIcon) + + const uploadSpeedText = videojs.dom.createEl('span', { + className: 'upload-speed-text' + }) + const uploadSpeedNumber = videojs.dom.createEl('span', { + className: 'upload-speed-number' + }) + const uploadSpeedUnit = videojs.dom.createEl('span') + uploadSpeedText.appendChild(uploadSpeedNumber) + uploadSpeedText.appendChild(uploadSpeedUnit) + subDivWebtorrent.appendChild(uploadSpeedText) + + const peersText = videojs.dom.createEl('span', { + className: 'peers-text' + }) + const peersNumber = videojs.dom.createEl('span', { + className: 'peers-number' + }) + subDivWebtorrent.appendChild(peersNumber) + subDivWebtorrent.appendChild(peersText) + + const subDivHttp = videojs.dom.createEl('div', { + className: 'vjs-peertube-hidden' + }) + const subDivHttpText = videojs.dom.createEl('span', { + className: 'http-fallback', + textContent: 'HTTP' + }) + + subDivHttp.appendChild(subDivHttpText) + div.appendChild(subDivHttp) + + this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { + // We are in HTTP fallback + if (!data) { + subDivHttp.className = 'vjs-peertube-displayed' + subDivWebtorrent.className = 'vjs-peertube-hidden' + + return + } + + const p2pStats = data.p2p + const httpStats = data.http + + const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) + const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) + const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) + const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) + const numPeers = p2pStats.numPeers + + subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + + if (data.source === 'p2p-media-loader') { + const downloadedFromServer = bytes(httpStats.downloaded).join(' ') + const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') + + subDivWebtorrent.title += + ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + + ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' + } + subDivWebtorrent.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') + + downloadSpeedNumber.textContent = downloadSpeed[0] + downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] + + uploadSpeedNumber.textContent = uploadSpeed[0] + uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] + + peersNumber.textContent = numPeers.toString() + peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) + + subDivHttp.className = 'vjs-peertube-hidden' + subDivWebtorrent.className = 'vjs-peertube-displayed' + }) + + return div as HTMLButtonElement + } +} + +videojs.registerComponent('P2PInfoButton', P2pInfoButton) diff --git a/client/src/assets/player/control-bar/peertube-link-button.ts b/client/src/assets/player/control-bar/peertube-link-button.ts new file mode 100644 index 000000000..c49cee566 --- /dev/null +++ b/client/src/assets/player/control-bar/peertube-link-button.ts @@ -0,0 +1,45 @@ +import videojs from 'video.js' +import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' +import { PeerTubeLinkButtonOptions } from '../peertube-videojs-typings' + +const Button = videojs.getComponent('Button') +class PeerTubeLinkButton extends Button { + + constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { + super(player, options as any) + } + + createEl () { + return this.buildElement() + } + + updateHref () { + this.el().setAttribute('href', this.buildLink()) + } + + handleClick () { + this.player().pause() + } + + private buildElement () { + const el = videojs.dom.createEl('a', { + href: this.buildLink(), + innerHTML: 'PeerTube', + title: this.player().localize('Video page (new window)'), + className: 'vjs-peertube-link', + target: '_blank' + }) + + el.addEventListener('mouseenter', () => this.updateHref()) + + return el as HTMLButtonElement + } + + private buildLink () { + const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID }) + + return decorateVideoLink({ url, startTime: this.player().currentTime() }) + } +} + +videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) diff --git a/client/src/assets/player/control-bar/peertube-load-progress-bar.ts b/client/src/assets/player/control-bar/peertube-load-progress-bar.ts new file mode 100644 index 000000000..623e70eb2 --- /dev/null +++ b/client/src/assets/player/control-bar/peertube-load-progress-bar.ts @@ -0,0 +1,33 @@ +import videojs from 'video.js' + +const Component = videojs.getComponent('Component') + +class PeerTubeLoadProgressBar extends Component { + + constructor (player: videojs.Player, options?: videojs.ComponentOptions) { + super(player, options) + + this.on(player, 'progress', this.update) + } + + createEl () { + return super.createEl('div', { + className: 'vjs-load-progress', + innerHTML: `${this.localize('Loaded')}: 0%` + }) + } + + dispose () { + super.dispose() + } + + update () { + const torrent = this.player().webtorrent().getTorrent() + if (!torrent) return + + (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' + } + +} + +Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar) diff --git a/client/src/assets/player/control-bar/theater-button.ts b/client/src/assets/player/control-bar/theater-button.ts new file mode 100644 index 000000000..f862ee224 --- /dev/null +++ b/client/src/assets/player/control-bar/theater-button.ts @@ -0,0 +1,53 @@ +import videojs from 'video.js' +import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' + +const Button = videojs.getComponent('Button') +class TheaterButton extends Button { + + private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' + + constructor (player: videojs.Player, options: videojs.ComponentOptions) { + super(player, options) + + const enabled = getStoredTheater() + if (enabled === true) { + this.player().addClass(TheaterButton.THEATER_MODE_CLASS) + + this.handleTheaterChange() + } + + this.controlText('Theater mode') + + this.player().theaterEnabled = enabled + } + + buildCSSClass () { + return `vjs-theater-control ${super.buildCSSClass()}` + } + + handleTheaterChange () { + const theaterEnabled = this.isTheaterEnabled() + + if (theaterEnabled) { + this.controlText('Normal mode') + } else { + this.controlText('Theater mode') + } + + saveTheaterInStore(theaterEnabled) + + this.player_.trigger('theaterChange', theaterEnabled) + } + + handleClick () { + this.player_.toggleClass(TheaterButton.THEATER_MODE_CLASS) + + this.handleTheaterChange() + } + + private isTheaterEnabled () { + return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) + } +} + +videojs.registerComponent('TheaterButton', TheaterButton) diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 9d2b29811..81ddb8814 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -6,18 +6,18 @@ import './stats/stats-plugin' import './bezels/bezels-plugin' import './peertube-plugin' import './peertube-resolutions-plugin' -import './videojs-components/next-previous-video-button' -import './videojs-components/p2p-info-button' -import './videojs-components/peertube-link-button' -import './videojs-components/peertube-load-progress-bar' -import './videojs-components/resolution-menu-button' -import './videojs-components/resolution-menu-item' -import './videojs-components/settings-dialog' -import './videojs-components/settings-menu-button' -import './videojs-components/settings-menu-item' -import './videojs-components/settings-panel' -import './videojs-components/settings-panel-child' -import './videojs-components/theater-button' +import './control-bar/next-previous-video-button' +import './control-bar/p2p-info-button' +import './control-bar/peertube-link-button' +import './control-bar/peertube-load-progress-bar' +import './control-bar/resolution-menu-button' +import './control-bar/resolution-menu-item' +import './control-bar/settings-dialog' +import './control-bar/settings-menu-button' +import './control-bar/settings-menu-item' +import './control-bar/settings-panel' +import './control-bar/settings-panel-child' +import './control-bar/theater-button' import './playlist/playlist-plugin' import './mobile/peertube-mobile-plugin' import './mobile/peertube-mobile-buttons' diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index b5c42d1c5..4ffc9ce3e 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts @@ -1,3 +1,4 @@ +import debug from 'debug' import videojs from 'video.js' import { timeToInt } from '@shared/core-utils' import { @@ -10,9 +11,8 @@ import { saveVolumeInStore } from './peertube-player-local-storage' import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings' +import { SettingsButton } from './settings/settings-menu-button' import { isMobile } from './utils' -import { SettingsButton } from './videojs-components/settings-menu-button' -import debug from 'debug' const logger = debug('peertube:player:peertube') diff --git a/client/src/assets/player/settings/resolution-menu-button.ts b/client/src/assets/player/settings/resolution-menu-button.ts new file mode 100644 index 000000000..8bd5b4f03 --- /dev/null +++ b/client/src/assets/player/settings/resolution-menu-button.ts @@ -0,0 +1,86 @@ +import videojs from 'video.js' +import { ResolutionMenuItem } from './resolution-menu-item' + +const Menu = videojs.getComponent('Menu') +const MenuButton = videojs.getComponent('MenuButton') +class ResolutionMenuButton extends MenuButton { + labelEl_: HTMLElement + + constructor (player: videojs.Player, options?: videojs.MenuButtonOptions) { + super(player, options) + + this.controlText('Quality') + + player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) + + // For parent + player.peertubeResolutions().on('resolutionChanged', () => { + setTimeout(() => this.trigger('labelUpdated')) + }) + } + + createEl () { + const el = super.createEl() + + this.labelEl_ = videojs.dom.createEl('div', { + className: 'vjs-resolution-value' + }) as HTMLElement + + el.appendChild(this.labelEl_) + + return el + } + + updateARIAAttributes () { + this.el().setAttribute('aria-label', 'Quality') + } + + createMenu () { + return new Menu(this.player_) + } + + buildCSSClass () { + return super.buildCSSClass() + ' vjs-resolution-button' + } + + buildWrapperCSSClass () { + return 'vjs-resolution-control ' + super.buildWrapperCSSClass() + } + + private addClickListener (component: any) { + component.on('click', () => { + const children = this.menu.children() + + for (const child of children) { + if (component !== child) { + (child as videojs.MenuItem).selected(false) + } + } + }) + } + + private buildQualities () { + for (const d of this.player().peertubeResolutions().getResolutions()) { + const label = d.label === '0p' + ? this.player().localize('Audio-only') + : d.label + + this.menu.addChild(new ResolutionMenuItem( + this.player_, + { + id: d.id, + label, + selected: d.selected + }) + ) + } + + for (const m of this.menu.children()) { + this.addClickListener(m) + } + + this.trigger('menuChanged') + } +} + +videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) diff --git a/client/src/assets/player/settings/resolution-menu-item.ts b/client/src/assets/player/settings/resolution-menu-item.ts new file mode 100644 index 000000000..6047f52f7 --- /dev/null +++ b/client/src/assets/player/settings/resolution-menu-item.ts @@ -0,0 +1,77 @@ +import videojs from 'video.js' + +const MenuItem = videojs.getComponent('MenuItem') + +export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { + id: number +} + +class ResolutionMenuItem extends MenuItem { + private readonly resolutionId: number + private readonly label: string + + private autoResolutionEnabled: boolean + private autoResolutionChosen: string + + constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { + options.selectable = true + + super(player, options) + + this.autoResolutionEnabled = true + this.autoResolutionChosen = '' + + this.resolutionId = options.id + this.label = options.label + + player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection()) + + // We only want to disable the "Auto" item + if (this.resolutionId === -1) { + player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution()) + } + } + + handleClick (event: any) { + // Auto button disabled? + if (this.autoResolutionEnabled === false && this.resolutionId === -1) return + + super.handleClick(event) + + this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false }) + } + + updateSelection () { + const selectedResolution = this.player().peertubeResolutions().getSelected() + + if (this.resolutionId === -1) { + this.autoResolutionChosen = this.player().peertubeResolutions().getAutoResolutionChosen()?.label + } + + this.selected(this.resolutionId === selectedResolution.id) + } + + updateAutoResolution () { + const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld() + + // Check if the auto resolution is enabled or not + if (enabled === false) { + this.addClass('disabled') + } else { + this.removeClass('disabled') + } + + this.autoResolutionEnabled = enabled + } + + getLabel () { + if (this.resolutionId === -1) { + return this.label + ' ' + this.autoResolutionChosen + '' + } + + return this.label + } +} +videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem) + +export { ResolutionMenuItem } diff --git a/client/src/assets/player/settings/settings-dialog.ts b/client/src/assets/player/settings/settings-dialog.ts new file mode 100644 index 000000000..8cd98967f --- /dev/null +++ b/client/src/assets/player/settings/settings-dialog.ts @@ -0,0 +1,35 @@ +import videojs from 'video.js' + +const Component = videojs.getComponent('Component') + +class SettingsDialog extends Component { + constructor (player: videojs.Player) { + super(player) + + this.hide() + } + + /** + * Create the component's DOM element + * + */ + createEl () { + const uniqueId = this.id() + const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId + const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId + + return super.createEl('div', { + className: 'vjs-settings-dialog vjs-modal-overlay', + innerHTML: '', + tabIndex: -1 + }, { + role: 'dialog', + 'aria-labelledby': dialogLabelId, + 'aria-describedby': dialogDescriptionId + }) + } +} + +Component.registerComponent('SettingsDialog', SettingsDialog) + +export { SettingsDialog } diff --git a/client/src/assets/player/settings/settings-menu-button.ts b/client/src/assets/player/settings/settings-menu-button.ts new file mode 100644 index 000000000..6de390f4d --- /dev/null +++ b/client/src/assets/player/settings/settings-menu-button.ts @@ -0,0 +1,279 @@ +// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu +import { SettingsMenuItem } from './settings-menu-item' +import { toTitleCase } from '../utils' +import videojs from 'video.js' + +import { SettingsDialog } from './settings-dialog' +import { SettingsPanel } from './settings-panel' +import { SettingsPanelChild } from './settings-panel-child' + +const Button = videojs.getComponent('Button') +const Menu = videojs.getComponent('Menu') +const Component = videojs.getComponent('Component') + +export interface SettingsButtonOptions extends videojs.ComponentOptions { + entries: any[] + setup?: { + maxHeightOffset: number + } +} + +class SettingsButton extends Button { + dialog: SettingsDialog + dialogEl: HTMLElement + menu: videojs.Menu + panel: SettingsPanel + panelChild: SettingsPanelChild + + addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem + disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem + documentClickHandler: typeof SettingsButton.prototype.onDocumentClick + userInactiveHandler: typeof SettingsButton.prototype.onUserInactive + + private settingsButtonOptions: SettingsButtonOptions + + constructor (player: videojs.Player, options?: SettingsButtonOptions) { + super(player, options) + + this.settingsButtonOptions = options + + this.controlText('Settings') + + this.dialog = this.player().addChild('settingsDialog') + this.dialogEl = this.dialog.el() as HTMLElement + this.menu = null + this.panel = this.dialog.addChild('settingsPanel') + this.panelChild = this.panel.addChild('settingsPanelChild') + + this.addClass('vjs-settings') + this.el().setAttribute('aria-label', 'Settings Button') + + // Event handlers + this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) + this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this) + this.documentClickHandler = this.onDocumentClick.bind(this) + this.userInactiveHandler = this.onUserInactive.bind(this) + + this.buildMenu() + this.bindEvents() + + // Prepare the dialog + this.player().one('play', () => this.hideDialog()) + } + + onDocumentClick (event: MouseEvent) { + const element = event.target as HTMLElement + + if (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) { + return + } + + if (!this.dialog.hasClass('vjs-hidden')) { + this.hideDialog() + } + } + + onDisposeSettingsItem (event: any, name: string) { + if (name === undefined) { + const children = this.menu.children() + + while (children.length > 0) { + children[0].dispose() + this.menu.removeChild(children[0]) + } + + this.addClass('vjs-hidden') + } else { + const item = this.menu.getChild(name) + + if (item) { + item.dispose() + this.menu.removeChild(item) + } + } + + this.hideDialog() + + if (this.settingsButtonOptions.entries.length === 0) { + this.addClass('vjs-hidden') + } + } + + dispose () { + document.removeEventListener('click', this.documentClickHandler) + + if (this.isInIframe()) { + window.removeEventListener('blur', this.documentClickHandler) + } + } + + onAddSettingsItem (event: any, data: any) { + const [ entry, options ] = data + + this.addMenuItem(entry, options) + this.removeClass('vjs-hidden') + } + + onUserInactive () { + if (!this.dialog.hasClass('vjs-hidden')) { + this.hideDialog() + } + } + + bindEvents () { + document.addEventListener('click', this.documentClickHandler) + if (this.isInIframe()) { + window.addEventListener('blur', this.documentClickHandler) + } + + this.player().on('addsettingsitem', this.addSettingsItemHandler) + this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) + this.player().on('userinactive', this.userInactiveHandler) + } + + buildCSSClass () { + return `vjs-icon-settings ${super.buildCSSClass()}` + } + + handleClick () { + if (this.dialog.hasClass('vjs-hidden')) { + this.showDialog() + } else { + this.hideDialog() + } + } + + showDialog () { + this.player().peertube().onMenuOpened(); + + (this.menu.el() as HTMLElement).style.opacity = '1' + + this.dialog.show() + this.el().setAttribute('aria-expanded', 'true') + + this.setDialogSize(this.getComponentSize(this.menu)) + + const firstChild = this.menu.children()[0] + if (firstChild) firstChild.focus() + } + + hideDialog () { + this.player_.peertube().onMenuClosed() + + this.dialog.hide() + this.el().setAttribute('aria-expanded', 'false') + + this.setDialogSize(this.getComponentSize(this.menu)); + (this.menu.el() as HTMLElement).style.opacity = '1' + this.resetChildren() + } + + getComponentSize (element: videojs.Component | HTMLElement) { + let width: number = null + let height: number = null + + // Could be component or just DOM element + if (element instanceof Component) { + const el = element.el() as HTMLElement + + width = el.offsetWidth + height = el.offsetHeight; + + (element as any).width = width; + (element as any).height = height + } else { + width = element.offsetWidth + height = element.offsetHeight + } + + return [ width, height ] + } + + setDialogSize ([ width, height ]: number[]) { + if (typeof height !== 'number') { + return + } + + const offset = this.settingsButtonOptions.setup.maxHeightOffset + const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset + + const panelEl = this.panel.el() as HTMLElement + + if (height > maxHeight) { + height = maxHeight + width += 17 + panelEl.style.maxHeight = `${height}px` + } else if (panelEl.style.maxHeight !== '') { + panelEl.style.maxHeight = '' + } + + this.dialogEl.style.width = `${width}px` + this.dialogEl.style.height = `${height}px` + } + + buildMenu () { + this.menu = new Menu(this.player()) + this.menu.addClass('vjs-main-menu') + const entries = this.settingsButtonOptions.entries + + if (entries.length === 0) { + this.addClass('vjs-hidden') + this.panelChild.addChild(this.menu) + return + } + + for (const entry of entries) { + this.addMenuItem(entry, this.settingsButtonOptions) + } + + this.panelChild.addChild(this.menu) + } + + addMenuItem (entry: any, options: any) { + const openSubMenu = function (this: any) { + if (videojs.dom.hasClass(this.el_, 'open')) { + videojs.dom.removeClass(this.el_, 'open') + } else { + videojs.dom.addClass(this.el_, 'open') + } + } + + options.name = toTitleCase(entry) + + const newOptions = Object.assign({}, options, { entry, menuButton: this }) + const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) + + this.menu.addChild(settingsMenuItem) + + // Hide children to avoid sub menus stacking on top of each other + // or having multiple menus open + settingsMenuItem.on('click', videojs.bind(this, this.hideChildren)) + + // Whether to add or remove selected class on the settings sub menu element + settingsMenuItem.on('click', openSubMenu) + } + + resetChildren () { + for (const menuChild of this.menu.children()) { + (menuChild as SettingsMenuItem).reset() + } + } + + /** + * Hide all the sub menus + */ + hideChildren () { + for (const menuChild of this.menu.children()) { + (menuChild as SettingsMenuItem).hideSubMenu() + } + } + + isInIframe () { + return window.self !== window.top + } + +} + +Component.registerComponent('SettingsButton', SettingsButton) + +export { SettingsButton } diff --git a/client/src/assets/player/settings/settings-menu-item.ts b/client/src/assets/player/settings/settings-menu-item.ts new file mode 100644 index 000000000..31d42c456 --- /dev/null +++ b/client/src/assets/player/settings/settings-menu-item.ts @@ -0,0 +1,378 @@ +import videojs from 'video.js' +// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu +import { toTitleCase } from '../utils' +import { SettingsDialog } from './settings-dialog' +import { SettingsButton } from './settings-menu-button' +import { SettingsPanel } from './settings-panel' +import { SettingsPanelChild } from './settings-panel-child' + +const MenuItem = videojs.getComponent('MenuItem') +const component = videojs.getComponent('Component') + +export interface SettingsMenuItemOptions extends videojs.MenuItemOptions { + entry: string + menuButton: SettingsButton +} + +class SettingsMenuItem extends MenuItem { + settingsButton: SettingsButton + dialog: SettingsDialog + mainMenu: videojs.Menu + panel: SettingsPanel + panelChild: SettingsPanelChild + panelChildEl: HTMLElement + size: number[] + menuToLoad: string + subMenu: SettingsButton + + submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick + transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd + + settingsSubMenuTitleEl_: HTMLElement + settingsSubMenuValueEl_: HTMLElement + settingsSubMenuEl_: HTMLElement + + constructor (player: videojs.Player, options?: SettingsMenuItemOptions) { + super(player, options) + + this.settingsButton = options.menuButton + this.dialog = this.settingsButton.dialog + this.mainMenu = this.settingsButton.menu + this.panel = this.dialog.getChild('settingsPanel') + this.panelChild = this.panel.getChild('settingsPanelChild') + this.panelChildEl = this.panelChild.el() as HTMLElement + + this.size = null + + // keep state of what menu type is loading next + this.menuToLoad = 'mainmenu' + + const subMenuName = toTitleCase(options.entry) + const SubMenuComponent = videojs.getComponent(subMenuName) + + if (!SubMenuComponent) { + throw new Error(`Component ${subMenuName} does not exist`) + } + + const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this }) + + this.subMenu = new SubMenuComponent(this.player(), newOptions) as SettingsButton + const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] + this.settingsSubMenuEl_.className += ' ' + subMenuClass + + this.eventHandlers() + + player.ready(() => { + // Voodoo magic for IOS + setTimeout(() => { + // Player was destroyed + if (!this.player_) return + + this.build() + + // Update on rate change + player.on('ratechange', this.submenuClickHandler) + + if (subMenuName === 'CaptionsButton') { + // Hack to regenerate captions on HTTP fallback + player.on('captionsChanged', () => { + setTimeout(() => { + this.settingsSubMenuEl_.innerHTML = '' + this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) + this.update() + this.bindClickEvents() + }, 0) + }) + } + + this.reset() + }, 0) + }) + } + + eventHandlers () { + this.submenuClickHandler = this.onSubmenuClick.bind(this) + this.transitionEndHandler = this.onTransitionEnd.bind(this) + } + + onSubmenuClick (event: any) { + let target = null + + if (event.type === 'tap') { + target = event.target + } else { + target = event.currentTarget || event.target + } + + if (target?.classList.contains('vjs-back-button')) { + this.loadMainMenu() + return + } + + // To update the sub menu value on click, setTimeout is needed because + // updating the value is not instant + setTimeout(() => this.update(event), 0) + + // Seems like videojs adds a vjs-hidden class on the caption menu after a click + // We don't need it + this.subMenu.menu.removeClass('vjs-hidden') + } + + /** + * Create the component's DOM element + * + */ + createEl () { + const el = videojs.dom.createEl('li', { + className: 'vjs-menu-item', + tabIndex: -1 + }) + + this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', { + className: 'vjs-settings-sub-menu-title' + }) as HTMLElement + + el.appendChild(this.settingsSubMenuTitleEl_) + + this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', { + className: 'vjs-settings-sub-menu-value' + }) as HTMLElement + + el.appendChild(this.settingsSubMenuValueEl_) + + this.settingsSubMenuEl_ = videojs.dom.createEl('div', { + className: 'vjs-settings-sub-menu' + }) as HTMLElement + + return el as HTMLLIElement + } + + /** + * Handle click on menu item + * + * @method handleClick + */ + handleClick (event: videojs.EventTarget.Event) { + this.menuToLoad = 'submenu' + // Remove open class to ensure only the open submenu gets this class + videojs.dom.removeClass(this.el(), 'open') + + super.handleClick(event); + + (this.mainMenu.el() as HTMLElement).style.opacity = '0' + // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element + if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { + videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') + + // animation not played without timeout + setTimeout(() => { + this.settingsSubMenuEl_.style.opacity = '1' + this.settingsSubMenuEl_.style.marginRight = '0px' + }, 0) + + this.settingsButton.setDialogSize(this.size) + + const firstChild = this.subMenu.menu.children()[0] + if (firstChild) firstChild.focus() + } else { + videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + } + } + + /** + * Create back button + * + * @method createBackButton + */ + createBackButton () { + const button = this.subMenu.menu.addChild('MenuItem', {}, 0) + + button.addClass('vjs-back-button'); + (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) + } + + /** + * Add/remove prefixed event listener for CSS Transition + * + * @method PrefixedEvent + */ + PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { + const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] + + for (let p = 0; p < prefix.length; p++) { + if (!prefix[p]) { + type = type.toLowerCase() + } + + if (action === 'addEvent') { + element.addEventListener(prefix[p] + type, callback, false) + } else if (action === 'removeEvent') { + element.removeEventListener(prefix[p] + type, callback, false) + } + } + } + + onTransitionEnd (event: any) { + if (event.propertyName !== 'margin-right') { + return + } + + if (this.menuToLoad === 'mainmenu') { + // hide submenu + videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + + // reset opacity to 0 + this.settingsSubMenuEl_.style.opacity = '0' + } + } + + reset () { + videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + this.settingsSubMenuEl_.style.opacity = '0' + this.setMargin() + } + + loadMainMenu () { + const mainMenuEl = this.mainMenu.el() as HTMLElement + this.menuToLoad = 'mainmenu' + this.mainMenu.show() + mainMenuEl.style.opacity = '0' + + // back button will always take you to main menu, so set dialog sizes + const mainMenuAny = this.mainMenu as any + this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ]) + + // animation not triggered without timeout (some async stuff ?!?) + setTimeout(() => { + // animate margin and opacity before hiding the submenu + // this triggers CSS Transition event + this.setMargin() + mainMenuEl.style.opacity = '1' + + const firstChild = this.mainMenu.children()[0] + if (firstChild) firstChild.focus() + }, 0) + } + + build () { + this.subMenu.on('labelUpdated', () => { + this.update() + }) + this.subMenu.on('menuChanged', () => { + this.bindClickEvents() + this.setSize() + this.update() + }) + + this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText()) + this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) + this.panelChildEl.appendChild(this.settingsSubMenuEl_) + this.update() + + this.createBackButton() + this.setSize() + this.bindClickEvents() + + // prefixed event listeners for CSS TransitionEnd + this.PrefixedEvent( + this.settingsSubMenuEl_, + 'TransitionEnd', + this.transitionEndHandler, + 'addEvent' + ) + } + + update (event?: any) { + let target: HTMLElement = null + const subMenu = this.subMenu.name() + + if (event && event.type === 'tap') { + target = event.target + } else if (event) { + target = event.currentTarget + } + + // Playback rate menu button doesn't get a vjs-selected class + // or sets options_['selected'] on the selected playback rate. + // Thus we get the submenu value based on the labelEl of playbackRateMenuButton + if (subMenu === 'PlaybackRateMenuButton') { + const html = (this.subMenu as any).labelEl_.innerHTML + + setTimeout(() => { + this.settingsSubMenuValueEl_.innerHTML = html + }, 250) + } else { + // Loop trough the submenu items to find the selected child + for (const subMenuItem of this.subMenu.menu.children_) { + if (!(subMenuItem instanceof component)) { + continue + } + + if (subMenuItem.hasClass('vjs-selected')) { + const subMenuItemUntyped = subMenuItem as any + + // Prefer to use the function + if (typeof subMenuItemUntyped.getLabel === 'function') { + this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel() + break + } + + this.settingsSubMenuValueEl_.innerHTML = this.player().localize(subMenuItemUntyped.options_.label) + } + } + } + + if (target && !target.classList.contains('vjs-back-button')) { + this.settingsButton.hideDialog() + } + } + + bindClickEvents () { + for (const item of this.subMenu.menu.children()) { + if (!(item instanceof component)) { + continue + } + item.on([ 'tap', 'click' ], this.submenuClickHandler) + } + } + + // save size of submenus on first init + // if number of submenu items change dynamically more logic will be needed + setSize () { + this.dialog.removeClass('vjs-hidden') + videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') + this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) + this.setMargin() + this.dialog.addClass('vjs-hidden') + videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + } + + setMargin () { + if (!this.size) return + + const [ width ] = this.size + + this.settingsSubMenuEl_.style.marginRight = `-${width}px` + } + + /** + * Hide the sub menu + */ + hideSubMenu () { + // after removing settings item this.el_ === null + if (!this.el()) { + return + } + + if (videojs.dom.hasClass(this.el(), 'open')) { + videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + videojs.dom.removeClass(this.el(), 'open') + } + } + +} + +(SettingsMenuItem as any).prototype.contentElType = 'button' +videojs.registerComponent('SettingsMenuItem', SettingsMenuItem) + +export { SettingsMenuItem } diff --git a/client/src/assets/player/settings/settings-panel-child.ts b/client/src/assets/player/settings/settings-panel-child.ts new file mode 100644 index 000000000..161420c38 --- /dev/null +++ b/client/src/assets/player/settings/settings-panel-child.ts @@ -0,0 +1,18 @@ +import videojs from 'video.js' + +const Component = videojs.getComponent('Component') + +class SettingsPanelChild extends Component { + + createEl () { + return super.createEl('div', { + className: 'vjs-settings-panel-child', + innerHTML: '', + tabIndex: -1 + }) + } +} + +Component.registerComponent('SettingsPanelChild', SettingsPanelChild) + +export { SettingsPanelChild } diff --git a/client/src/assets/player/settings/settings-panel.ts b/client/src/assets/player/settings/settings-panel.ts new file mode 100644 index 000000000..28b579bdd --- /dev/null +++ b/client/src/assets/player/settings/settings-panel.ts @@ -0,0 +1,18 @@ +import videojs from 'video.js' + +const Component = videojs.getComponent('Component') + +class SettingsPanel extends Component { + + createEl () { + return super.createEl('div', { + className: 'vjs-settings-panel', + innerHTML: '', + tabIndex: -1 + }) + } +} + +Component.registerComponent('SettingsPanel', SettingsPanel) + +export { SettingsPanel } diff --git a/client/src/assets/player/videojs-components/next-previous-video-button.ts b/client/src/assets/player/videojs-components/next-previous-video-button.ts deleted file mode 100644 index fe17ce2ce..000000000 --- a/client/src/assets/player/videojs-components/next-previous-video-button.ts +++ /dev/null @@ -1,50 +0,0 @@ -import videojs from 'video.js' -import { NextPreviousVideoButtonOptions } from '../peertube-videojs-typings' - -const Button = videojs.getComponent('Button') - -class NextPreviousVideoButton extends Button { - private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions - - constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) { - super(player, options as any) - - this.nextPreviousVideoButtonOptions = options - - this.update() - } - - createEl () { - const type = (this.options_ as NextPreviousVideoButtonOptions).type - - const button = videojs.dom.createEl('button', { - className: 'vjs-' + type + '-video' - }) as HTMLButtonElement - const nextIcon = videojs.dom.createEl('span', { - className: 'icon icon-' + type - }) - button.appendChild(nextIcon) - - if (type === 'next') { - button.title = this.player_.localize('Next video') - } else { - button.title = this.player_.localize('Previous video') - } - - return button - } - - handleClick () { - this.nextPreviousVideoButtonOptions.handler() - } - - update () { - const disabled = this.nextPreviousVideoButtonOptions.isDisabled() - - if (disabled) this.addClass('vjs-disabled') - else this.removeClass('vjs-disabled') - } -} - -videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) -videojs.registerComponent('PreviousVideoButton', NextPreviousVideoButton) diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts deleted file mode 100644 index 081dee1d3..000000000 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ /dev/null @@ -1,124 +0,0 @@ -import videojs from 'video.js' -import { PeerTubeP2PInfoButtonOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' -import { bytes } from '../utils' - -const Button = videojs.getComponent('Button') -class P2pInfoButton extends Button { - - constructor (player: videojs.Player, options?: PeerTubeP2PInfoButtonOptions) { - super(player, options as any) - } - - createEl () { - const div = videojs.dom.createEl('div', { - className: 'vjs-peertube' - }) - const subDivWebtorrent = videojs.dom.createEl('div', { - className: 'vjs-peertube-hidden' // Hide the stats before we get the info - }) as HTMLDivElement - div.appendChild(subDivWebtorrent) - - // Stop here if P2P is not enabled - const p2pEnabled = (this.options_ as PeerTubeP2PInfoButtonOptions).p2pEnabled - if (!p2pEnabled) return div as HTMLButtonElement - - const downloadIcon = videojs.dom.createEl('span', { - className: 'icon icon-download' - }) - subDivWebtorrent.appendChild(downloadIcon) - - const downloadSpeedText = videojs.dom.createEl('span', { - className: 'download-speed-text' - }) - const downloadSpeedNumber = videojs.dom.createEl('span', { - className: 'download-speed-number' - }) - const downloadSpeedUnit = videojs.dom.createEl('span') - downloadSpeedText.appendChild(downloadSpeedNumber) - downloadSpeedText.appendChild(downloadSpeedUnit) - subDivWebtorrent.appendChild(downloadSpeedText) - - const uploadIcon = videojs.dom.createEl('span', { - className: 'icon icon-upload' - }) - subDivWebtorrent.appendChild(uploadIcon) - - const uploadSpeedText = videojs.dom.createEl('span', { - className: 'upload-speed-text' - }) - const uploadSpeedNumber = videojs.dom.createEl('span', { - className: 'upload-speed-number' - }) - const uploadSpeedUnit = videojs.dom.createEl('span') - uploadSpeedText.appendChild(uploadSpeedNumber) - uploadSpeedText.appendChild(uploadSpeedUnit) - subDivWebtorrent.appendChild(uploadSpeedText) - - const peersText = videojs.dom.createEl('span', { - className: 'peers-text' - }) - const peersNumber = videojs.dom.createEl('span', { - className: 'peers-number' - }) - subDivWebtorrent.appendChild(peersNumber) - subDivWebtorrent.appendChild(peersText) - - const subDivHttp = videojs.dom.createEl('div', { - className: 'vjs-peertube-hidden' - }) - const subDivHttpText = videojs.dom.createEl('span', { - className: 'http-fallback', - textContent: 'HTTP' - }) - - subDivHttp.appendChild(subDivHttpText) - div.appendChild(subDivHttp) - - this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { - // We are in HTTP fallback - if (!data) { - subDivHttp.className = 'vjs-peertube-displayed' - subDivWebtorrent.className = 'vjs-peertube-hidden' - - return - } - - const p2pStats = data.p2p - const httpStats = data.http - - const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) - const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) - const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) - const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) - const numPeers = p2pStats.numPeers - - subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' - - if (data.source === 'p2p-media-loader') { - const downloadedFromServer = bytes(httpStats.downloaded).join(' ') - const downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') - - subDivWebtorrent.title += - ' * ' + this.player().localize('From servers: ') + downloadedFromServer + '\n' + - ' * ' + this.player().localize('From peers: ') + downloadedFromPeers + '\n' - } - subDivWebtorrent.title += this.player().localize('Total uploaded: ') + totalUploaded.join(' ') - - downloadSpeedNumber.textContent = downloadSpeed[0] - downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] - - uploadSpeedNumber.textContent = uploadSpeed[0] - uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] - - peersNumber.textContent = numPeers.toString() - peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) - - subDivHttp.className = 'vjs-peertube-hidden' - subDivWebtorrent.className = 'vjs-peertube-displayed' - }) - - return div as HTMLButtonElement - } -} - -videojs.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 deleted file mode 100644 index c49cee566..000000000 --- a/client/src/assets/player/videojs-components/peertube-link-button.ts +++ /dev/null @@ -1,45 +0,0 @@ -import videojs from 'video.js' -import { buildVideoLink, decorateVideoLink } from '@shared/core-utils' -import { PeerTubeLinkButtonOptions } from '../peertube-videojs-typings' - -const Button = videojs.getComponent('Button') -class PeerTubeLinkButton extends Button { - - constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { - super(player, options as any) - } - - createEl () { - return this.buildElement() - } - - updateHref () { - this.el().setAttribute('href', this.buildLink()) - } - - handleClick () { - this.player().pause() - } - - private buildElement () { - const el = videojs.dom.createEl('a', { - href: this.buildLink(), - innerHTML: 'PeerTube', - title: this.player().localize('Video page (new window)'), - className: 'vjs-peertube-link', - target: '_blank' - }) - - el.addEventListener('mouseenter', () => this.updateHref()) - - return el as HTMLButtonElement - } - - private buildLink () { - const url = buildVideoLink({ shortUUID: (this.options_ as PeerTubeLinkButtonOptions).shortUUID }) - - return decorateVideoLink({ url, startTime: this.player().currentTime() }) - } -} - -videojs.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 deleted file mode 100644 index 623e70eb2..000000000 --- a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts +++ /dev/null @@ -1,33 +0,0 @@ -import videojs from 'video.js' - -const Component = videojs.getComponent('Component') - -class PeerTubeLoadProgressBar extends Component { - - constructor (player: videojs.Player, options?: videojs.ComponentOptions) { - super(player, options) - - this.on(player, 'progress', this.update) - } - - createEl () { - return super.createEl('div', { - className: 'vjs-load-progress', - innerHTML: `${this.localize('Loaded')}: 0%` - }) - } - - dispose () { - super.dispose() - } - - update () { - const torrent = this.player().webtorrent().getTorrent() - if (!torrent) return - - (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' - } - -} - -Component.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 deleted file mode 100644 index 8bd5b4f03..000000000 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ /dev/null @@ -1,86 +0,0 @@ -import videojs from 'video.js' -import { ResolutionMenuItem } from './resolution-menu-item' - -const Menu = videojs.getComponent('Menu') -const MenuButton = videojs.getComponent('MenuButton') -class ResolutionMenuButton extends MenuButton { - labelEl_: HTMLElement - - constructor (player: videojs.Player, options?: videojs.MenuButtonOptions) { - super(player, options) - - this.controlText('Quality') - - player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) - - // For parent - player.peertubeResolutions().on('resolutionChanged', () => { - setTimeout(() => this.trigger('labelUpdated')) - }) - } - - createEl () { - const el = super.createEl() - - this.labelEl_ = videojs.dom.createEl('div', { - className: 'vjs-resolution-value' - }) as HTMLElement - - el.appendChild(this.labelEl_) - - return el - } - - updateARIAAttributes () { - this.el().setAttribute('aria-label', 'Quality') - } - - createMenu () { - return new Menu(this.player_) - } - - buildCSSClass () { - return super.buildCSSClass() + ' vjs-resolution-button' - } - - buildWrapperCSSClass () { - return 'vjs-resolution-control ' + super.buildWrapperCSSClass() - } - - private addClickListener (component: any) { - component.on('click', () => { - const children = this.menu.children() - - for (const child of children) { - if (component !== child) { - (child as videojs.MenuItem).selected(false) - } - } - }) - } - - private buildQualities () { - for (const d of this.player().peertubeResolutions().getResolutions()) { - const label = d.label === '0p' - ? this.player().localize('Audio-only') - : d.label - - this.menu.addChild(new ResolutionMenuItem( - this.player_, - { - id: d.id, - label, - selected: d.selected - }) - ) - } - - for (const m of this.menu.children()) { - this.addClickListener(m) - } - - this.trigger('menuChanged') - } -} - -videojs.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 deleted file mode 100644 index 6047f52f7..000000000 --- a/client/src/assets/player/videojs-components/resolution-menu-item.ts +++ /dev/null @@ -1,77 +0,0 @@ -import videojs from 'video.js' - -const MenuItem = videojs.getComponent('MenuItem') - -export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { - id: number -} - -class ResolutionMenuItem extends MenuItem { - private readonly resolutionId: number - private readonly label: string - - private autoResolutionEnabled: boolean - private autoResolutionChosen: string - - constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { - options.selectable = true - - super(player, options) - - this.autoResolutionEnabled = true - this.autoResolutionChosen = '' - - this.resolutionId = options.id - this.label = options.label - - player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection()) - - // We only want to disable the "Auto" item - if (this.resolutionId === -1) { - player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution()) - } - } - - handleClick (event: any) { - // Auto button disabled? - if (this.autoResolutionEnabled === false && this.resolutionId === -1) return - - super.handleClick(event) - - this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false }) - } - - updateSelection () { - const selectedResolution = this.player().peertubeResolutions().getSelected() - - if (this.resolutionId === -1) { - this.autoResolutionChosen = this.player().peertubeResolutions().getAutoResolutionChosen()?.label - } - - this.selected(this.resolutionId === selectedResolution.id) - } - - updateAutoResolution () { - const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld() - - // Check if the auto resolution is enabled or not - if (enabled === false) { - this.addClass('disabled') - } else { - this.removeClass('disabled') - } - - this.autoResolutionEnabled = enabled - } - - getLabel () { - if (this.resolutionId === -1) { - return this.label + ' ' + this.autoResolutionChosen + '' - } - - return this.label - } -} -videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem) - -export { ResolutionMenuItem } diff --git a/client/src/assets/player/videojs-components/settings-dialog.ts b/client/src/assets/player/videojs-components/settings-dialog.ts deleted file mode 100644 index 8cd98967f..000000000 --- a/client/src/assets/player/videojs-components/settings-dialog.ts +++ /dev/null @@ -1,35 +0,0 @@ -import videojs from 'video.js' - -const Component = videojs.getComponent('Component') - -class SettingsDialog extends Component { - constructor (player: videojs.Player) { - super(player) - - this.hide() - } - - /** - * Create the component's DOM element - * - */ - createEl () { - const uniqueId = this.id() - const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId - const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId - - return super.createEl('div', { - className: 'vjs-settings-dialog vjs-modal-overlay', - innerHTML: '', - tabIndex: -1 - }, { - role: 'dialog', - 'aria-labelledby': dialogLabelId, - 'aria-describedby': dialogDescriptionId - }) - } -} - -Component.registerComponent('SettingsDialog', SettingsDialog) - -export { SettingsDialog } diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts deleted file mode 100644 index 6de390f4d..000000000 --- a/client/src/assets/player/videojs-components/settings-menu-button.ts +++ /dev/null @@ -1,279 +0,0 @@ -// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu -import { SettingsMenuItem } from './settings-menu-item' -import { toTitleCase } from '../utils' -import videojs from 'video.js' - -import { SettingsDialog } from './settings-dialog' -import { SettingsPanel } from './settings-panel' -import { SettingsPanelChild } from './settings-panel-child' - -const Button = videojs.getComponent('Button') -const Menu = videojs.getComponent('Menu') -const Component = videojs.getComponent('Component') - -export interface SettingsButtonOptions extends videojs.ComponentOptions { - entries: any[] - setup?: { - maxHeightOffset: number - } -} - -class SettingsButton extends Button { - dialog: SettingsDialog - dialogEl: HTMLElement - menu: videojs.Menu - panel: SettingsPanel - panelChild: SettingsPanelChild - - addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem - disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem - documentClickHandler: typeof SettingsButton.prototype.onDocumentClick - userInactiveHandler: typeof SettingsButton.prototype.onUserInactive - - private settingsButtonOptions: SettingsButtonOptions - - constructor (player: videojs.Player, options?: SettingsButtonOptions) { - super(player, options) - - this.settingsButtonOptions = options - - this.controlText('Settings') - - this.dialog = this.player().addChild('settingsDialog') - this.dialogEl = this.dialog.el() as HTMLElement - this.menu = null - this.panel = this.dialog.addChild('settingsPanel') - this.panelChild = this.panel.addChild('settingsPanelChild') - - this.addClass('vjs-settings') - this.el().setAttribute('aria-label', 'Settings Button') - - // Event handlers - this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) - this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this) - this.documentClickHandler = this.onDocumentClick.bind(this) - this.userInactiveHandler = this.onUserInactive.bind(this) - - this.buildMenu() - this.bindEvents() - - // Prepare the dialog - this.player().one('play', () => this.hideDialog()) - } - - onDocumentClick (event: MouseEvent) { - const element = event.target as HTMLElement - - if (element?.classList?.contains('vjs-settings') || element?.parentElement?.classList?.contains('vjs-settings')) { - return - } - - if (!this.dialog.hasClass('vjs-hidden')) { - this.hideDialog() - } - } - - onDisposeSettingsItem (event: any, name: string) { - if (name === undefined) { - const children = this.menu.children() - - while (children.length > 0) { - children[0].dispose() - this.menu.removeChild(children[0]) - } - - this.addClass('vjs-hidden') - } else { - const item = this.menu.getChild(name) - - if (item) { - item.dispose() - this.menu.removeChild(item) - } - } - - this.hideDialog() - - if (this.settingsButtonOptions.entries.length === 0) { - this.addClass('vjs-hidden') - } - } - - dispose () { - document.removeEventListener('click', this.documentClickHandler) - - if (this.isInIframe()) { - window.removeEventListener('blur', this.documentClickHandler) - } - } - - onAddSettingsItem (event: any, data: any) { - const [ entry, options ] = data - - this.addMenuItem(entry, options) - this.removeClass('vjs-hidden') - } - - onUserInactive () { - if (!this.dialog.hasClass('vjs-hidden')) { - this.hideDialog() - } - } - - bindEvents () { - document.addEventListener('click', this.documentClickHandler) - if (this.isInIframe()) { - window.addEventListener('blur', this.documentClickHandler) - } - - this.player().on('addsettingsitem', this.addSettingsItemHandler) - this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) - this.player().on('userinactive', this.userInactiveHandler) - } - - buildCSSClass () { - return `vjs-icon-settings ${super.buildCSSClass()}` - } - - handleClick () { - if (this.dialog.hasClass('vjs-hidden')) { - this.showDialog() - } else { - this.hideDialog() - } - } - - showDialog () { - this.player().peertube().onMenuOpened(); - - (this.menu.el() as HTMLElement).style.opacity = '1' - - this.dialog.show() - this.el().setAttribute('aria-expanded', 'true') - - this.setDialogSize(this.getComponentSize(this.menu)) - - const firstChild = this.menu.children()[0] - if (firstChild) firstChild.focus() - } - - hideDialog () { - this.player_.peertube().onMenuClosed() - - this.dialog.hide() - this.el().setAttribute('aria-expanded', 'false') - - this.setDialogSize(this.getComponentSize(this.menu)); - (this.menu.el() as HTMLElement).style.opacity = '1' - this.resetChildren() - } - - getComponentSize (element: videojs.Component | HTMLElement) { - let width: number = null - let height: number = null - - // Could be component or just DOM element - if (element instanceof Component) { - const el = element.el() as HTMLElement - - width = el.offsetWidth - height = el.offsetHeight; - - (element as any).width = width; - (element as any).height = height - } else { - width = element.offsetWidth - height = element.offsetHeight - } - - return [ width, height ] - } - - setDialogSize ([ width, height ]: number[]) { - if (typeof height !== 'number') { - return - } - - const offset = this.settingsButtonOptions.setup.maxHeightOffset - const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset - - const panelEl = this.panel.el() as HTMLElement - - if (height > maxHeight) { - height = maxHeight - width += 17 - panelEl.style.maxHeight = `${height}px` - } else if (panelEl.style.maxHeight !== '') { - panelEl.style.maxHeight = '' - } - - this.dialogEl.style.width = `${width}px` - this.dialogEl.style.height = `${height}px` - } - - buildMenu () { - this.menu = new Menu(this.player()) - this.menu.addClass('vjs-main-menu') - const entries = this.settingsButtonOptions.entries - - if (entries.length === 0) { - this.addClass('vjs-hidden') - this.panelChild.addChild(this.menu) - return - } - - for (const entry of entries) { - this.addMenuItem(entry, this.settingsButtonOptions) - } - - this.panelChild.addChild(this.menu) - } - - addMenuItem (entry: any, options: any) { - const openSubMenu = function (this: any) { - if (videojs.dom.hasClass(this.el_, 'open')) { - videojs.dom.removeClass(this.el_, 'open') - } else { - videojs.dom.addClass(this.el_, 'open') - } - } - - options.name = toTitleCase(entry) - - const newOptions = Object.assign({}, options, { entry, menuButton: this }) - const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) - - this.menu.addChild(settingsMenuItem) - - // Hide children to avoid sub menus stacking on top of each other - // or having multiple menus open - settingsMenuItem.on('click', videojs.bind(this, this.hideChildren)) - - // Whether to add or remove selected class on the settings sub menu element - settingsMenuItem.on('click', openSubMenu) - } - - resetChildren () { - for (const menuChild of this.menu.children()) { - (menuChild as SettingsMenuItem).reset() - } - } - - /** - * Hide all the sub menus - */ - hideChildren () { - for (const menuChild of this.menu.children()) { - (menuChild as SettingsMenuItem).hideSubMenu() - } - } - - isInIframe () { - return window.self !== window.top - } - -} - -Component.registerComponent('SettingsButton', SettingsButton) - -export { SettingsButton } diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts deleted file mode 100644 index 31d42c456..000000000 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ /dev/null @@ -1,378 +0,0 @@ -import videojs from 'video.js' -// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu -import { toTitleCase } from '../utils' -import { SettingsDialog } from './settings-dialog' -import { SettingsButton } from './settings-menu-button' -import { SettingsPanel } from './settings-panel' -import { SettingsPanelChild } from './settings-panel-child' - -const MenuItem = videojs.getComponent('MenuItem') -const component = videojs.getComponent('Component') - -export interface SettingsMenuItemOptions extends videojs.MenuItemOptions { - entry: string - menuButton: SettingsButton -} - -class SettingsMenuItem extends MenuItem { - settingsButton: SettingsButton - dialog: SettingsDialog - mainMenu: videojs.Menu - panel: SettingsPanel - panelChild: SettingsPanelChild - panelChildEl: HTMLElement - size: number[] - menuToLoad: string - subMenu: SettingsButton - - submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick - transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd - - settingsSubMenuTitleEl_: HTMLElement - settingsSubMenuValueEl_: HTMLElement - settingsSubMenuEl_: HTMLElement - - constructor (player: videojs.Player, options?: SettingsMenuItemOptions) { - super(player, options) - - this.settingsButton = options.menuButton - this.dialog = this.settingsButton.dialog - this.mainMenu = this.settingsButton.menu - this.panel = this.dialog.getChild('settingsPanel') - this.panelChild = this.panel.getChild('settingsPanelChild') - this.panelChildEl = this.panelChild.el() as HTMLElement - - this.size = null - - // keep state of what menu type is loading next - this.menuToLoad = 'mainmenu' - - const subMenuName = toTitleCase(options.entry) - const SubMenuComponent = videojs.getComponent(subMenuName) - - if (!SubMenuComponent) { - throw new Error(`Component ${subMenuName} does not exist`) - } - - const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this }) - - this.subMenu = new SubMenuComponent(this.player(), newOptions) as SettingsButton - const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] - this.settingsSubMenuEl_.className += ' ' + subMenuClass - - this.eventHandlers() - - player.ready(() => { - // Voodoo magic for IOS - setTimeout(() => { - // Player was destroyed - if (!this.player_) return - - this.build() - - // Update on rate change - player.on('ratechange', this.submenuClickHandler) - - if (subMenuName === 'CaptionsButton') { - // Hack to regenerate captions on HTTP fallback - player.on('captionsChanged', () => { - setTimeout(() => { - this.settingsSubMenuEl_.innerHTML = '' - this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) - this.update() - this.bindClickEvents() - }, 0) - }) - } - - this.reset() - }, 0) - }) - } - - eventHandlers () { - this.submenuClickHandler = this.onSubmenuClick.bind(this) - this.transitionEndHandler = this.onTransitionEnd.bind(this) - } - - onSubmenuClick (event: any) { - let target = null - - if (event.type === 'tap') { - target = event.target - } else { - target = event.currentTarget || event.target - } - - if (target?.classList.contains('vjs-back-button')) { - this.loadMainMenu() - return - } - - // To update the sub menu value on click, setTimeout is needed because - // updating the value is not instant - setTimeout(() => this.update(event), 0) - - // Seems like videojs adds a vjs-hidden class on the caption menu after a click - // We don't need it - this.subMenu.menu.removeClass('vjs-hidden') - } - - /** - * Create the component's DOM element - * - */ - createEl () { - const el = videojs.dom.createEl('li', { - className: 'vjs-menu-item', - tabIndex: -1 - }) - - this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', { - className: 'vjs-settings-sub-menu-title' - }) as HTMLElement - - el.appendChild(this.settingsSubMenuTitleEl_) - - this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', { - className: 'vjs-settings-sub-menu-value' - }) as HTMLElement - - el.appendChild(this.settingsSubMenuValueEl_) - - this.settingsSubMenuEl_ = videojs.dom.createEl('div', { - className: 'vjs-settings-sub-menu' - }) as HTMLElement - - return el as HTMLLIElement - } - - /** - * Handle click on menu item - * - * @method handleClick - */ - handleClick (event: videojs.EventTarget.Event) { - this.menuToLoad = 'submenu' - // Remove open class to ensure only the open submenu gets this class - videojs.dom.removeClass(this.el(), 'open') - - super.handleClick(event); - - (this.mainMenu.el() as HTMLElement).style.opacity = '0' - // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element - if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { - videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') - - // animation not played without timeout - setTimeout(() => { - this.settingsSubMenuEl_.style.opacity = '1' - this.settingsSubMenuEl_.style.marginRight = '0px' - }, 0) - - this.settingsButton.setDialogSize(this.size) - - const firstChild = this.subMenu.menu.children()[0] - if (firstChild) firstChild.focus() - } else { - videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') - } - } - - /** - * Create back button - * - * @method createBackButton - */ - createBackButton () { - const button = this.subMenu.menu.addChild('MenuItem', {}, 0) - - button.addClass('vjs-back-button'); - (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) - } - - /** - * Add/remove prefixed event listener for CSS Transition - * - * @method PrefixedEvent - */ - PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { - const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] - - for (let p = 0; p < prefix.length; p++) { - if (!prefix[p]) { - type = type.toLowerCase() - } - - if (action === 'addEvent') { - element.addEventListener(prefix[p] + type, callback, false) - } else if (action === 'removeEvent') { - element.removeEventListener(prefix[p] + type, callback, false) - } - } - } - - onTransitionEnd (event: any) { - if (event.propertyName !== 'margin-right') { - return - } - - if (this.menuToLoad === 'mainmenu') { - // hide submenu - videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') - - // reset opacity to 0 - this.settingsSubMenuEl_.style.opacity = '0' - } - } - - reset () { - videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') - this.settingsSubMenuEl_.style.opacity = '0' - this.setMargin() - } - - loadMainMenu () { - const mainMenuEl = this.mainMenu.el() as HTMLElement - this.menuToLoad = 'mainmenu' - this.mainMenu.show() - mainMenuEl.style.opacity = '0' - - // back button will always take you to main menu, so set dialog sizes - const mainMenuAny = this.mainMenu as any - this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ]) - - // animation not triggered without timeout (some async stuff ?!?) - setTimeout(() => { - // animate margin and opacity before hiding the submenu - // this triggers CSS Transition event - this.setMargin() - mainMenuEl.style.opacity = '1' - - const firstChild = this.mainMenu.children()[0] - if (firstChild) firstChild.focus() - }, 0) - } - - build () { - this.subMenu.on('labelUpdated', () => { - this.update() - }) - this.subMenu.on('menuChanged', () => { - this.bindClickEvents() - this.setSize() - this.update() - }) - - this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText()) - this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) - this.panelChildEl.appendChild(this.settingsSubMenuEl_) - this.update() - - this.createBackButton() - this.setSize() - this.bindClickEvents() - - // prefixed event listeners for CSS TransitionEnd - this.PrefixedEvent( - this.settingsSubMenuEl_, - 'TransitionEnd', - this.transitionEndHandler, - 'addEvent' - ) - } - - update (event?: any) { - let target: HTMLElement = null - const subMenu = this.subMenu.name() - - if (event && event.type === 'tap') { - target = event.target - } else if (event) { - target = event.currentTarget - } - - // Playback rate menu button doesn't get a vjs-selected class - // or sets options_['selected'] on the selected playback rate. - // Thus we get the submenu value based on the labelEl of playbackRateMenuButton - if (subMenu === 'PlaybackRateMenuButton') { - const html = (this.subMenu as any).labelEl_.innerHTML - - setTimeout(() => { - this.settingsSubMenuValueEl_.innerHTML = html - }, 250) - } else { - // Loop trough the submenu items to find the selected child - for (const subMenuItem of this.subMenu.menu.children_) { - if (!(subMenuItem instanceof component)) { - continue - } - - if (subMenuItem.hasClass('vjs-selected')) { - const subMenuItemUntyped = subMenuItem as any - - // Prefer to use the function - if (typeof subMenuItemUntyped.getLabel === 'function') { - this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel() - break - } - - this.settingsSubMenuValueEl_.innerHTML = this.player().localize(subMenuItemUntyped.options_.label) - } - } - } - - if (target && !target.classList.contains('vjs-back-button')) { - this.settingsButton.hideDialog() - } - } - - bindClickEvents () { - for (const item of this.subMenu.menu.children()) { - if (!(item instanceof component)) { - continue - } - item.on([ 'tap', 'click' ], this.submenuClickHandler) - } - } - - // save size of submenus on first init - // if number of submenu items change dynamically more logic will be needed - setSize () { - this.dialog.removeClass('vjs-hidden') - videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') - this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) - this.setMargin() - this.dialog.addClass('vjs-hidden') - videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') - } - - setMargin () { - if (!this.size) return - - const [ width ] = this.size - - this.settingsSubMenuEl_.style.marginRight = `-${width}px` - } - - /** - * Hide the sub menu - */ - hideSubMenu () { - // after removing settings item this.el_ === null - if (!this.el()) { - return - } - - if (videojs.dom.hasClass(this.el(), 'open')) { - videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') - videojs.dom.removeClass(this.el(), 'open') - } - } - -} - -(SettingsMenuItem as any).prototype.contentElType = 'button' -videojs.registerComponent('SettingsMenuItem', SettingsMenuItem) - -export { SettingsMenuItem } diff --git a/client/src/assets/player/videojs-components/settings-panel-child.ts b/client/src/assets/player/videojs-components/settings-panel-child.ts deleted file mode 100644 index 161420c38..000000000 --- a/client/src/assets/player/videojs-components/settings-panel-child.ts +++ /dev/null @@ -1,18 +0,0 @@ -import videojs from 'video.js' - -const Component = videojs.getComponent('Component') - -class SettingsPanelChild extends Component { - - createEl () { - return super.createEl('div', { - className: 'vjs-settings-panel-child', - innerHTML: '', - tabIndex: -1 - }) - } -} - -Component.registerComponent('SettingsPanelChild', SettingsPanelChild) - -export { SettingsPanelChild } diff --git a/client/src/assets/player/videojs-components/settings-panel.ts b/client/src/assets/player/videojs-components/settings-panel.ts deleted file mode 100644 index 28b579bdd..000000000 --- a/client/src/assets/player/videojs-components/settings-panel.ts +++ /dev/null @@ -1,18 +0,0 @@ -import videojs from 'video.js' - -const Component = videojs.getComponent('Component') - -class SettingsPanel extends Component { - - createEl () { - return super.createEl('div', { - className: 'vjs-settings-panel', - innerHTML: '', - tabIndex: -1 - }) - } -} - -Component.registerComponent('SettingsPanel', SettingsPanel) - -export { SettingsPanel } diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts deleted file mode 100644 index f862ee224..000000000 --- a/client/src/assets/player/videojs-components/theater-button.ts +++ /dev/null @@ -1,53 +0,0 @@ -import videojs from 'video.js' -import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' - -const Button = videojs.getComponent('Button') -class TheaterButton extends Button { - - private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' - - constructor (player: videojs.Player, options: videojs.ComponentOptions) { - super(player, options) - - const enabled = getStoredTheater() - if (enabled === true) { - this.player().addClass(TheaterButton.THEATER_MODE_CLASS) - - this.handleTheaterChange() - } - - this.controlText('Theater mode') - - this.player().theaterEnabled = enabled - } - - buildCSSClass () { - return `vjs-theater-control ${super.buildCSSClass()}` - } - - handleTheaterChange () { - const theaterEnabled = this.isTheaterEnabled() - - if (theaterEnabled) { - this.controlText('Normal mode') - } else { - this.controlText('Theater mode') - } - - saveTheaterInStore(theaterEnabled) - - this.player_.trigger('theaterChange', theaterEnabled) - } - - handleClick () { - this.player_.toggleClass(TheaterButton.THEATER_MODE_CLASS) - - this.handleTheaterChange() - } - - private isTheaterEnabled () { - return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) - } -} - -videojs.registerComponent('TheaterButton', TheaterButton) -- cgit v1.2.3