Handle basic playlist in embed
This commit is contained in:
parent
5abc96fca2
commit
4572c3d0d9
13 changed files with 570 additions and 25 deletions
|
@ -1,8 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<defs></defs>
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
<g transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
|
||||||
<g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
|
|
||||||
<g id="8" transform="translate(356.000000, 115.000000)">
|
<g id="8" transform="translate(356.000000, 115.000000)">
|
||||||
<path d="M21,6 L9,18" id="Path-14"></path>
|
<path d="M21,6 L9,18" id="Path-14"></path>
|
||||||
<path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path>
|
<path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path>
|
||||||
|
|
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 692 B |
|
@ -18,14 +18,21 @@ import './videojs-components/settings-menu-item'
|
||||||
import './videojs-components/settings-panel'
|
import './videojs-components/settings-panel'
|
||||||
import './videojs-components/settings-panel-child'
|
import './videojs-components/settings-panel-child'
|
||||||
import './videojs-components/theater-button'
|
import './videojs-components/theater-button'
|
||||||
|
import './playlist/playlist-plugin'
|
||||||
import videojs from 'video.js'
|
import videojs from 'video.js'
|
||||||
import { VideoFile } from '@shared/models'
|
|
||||||
import { isDefaultLocale } from '@shared/core-utils/i18n'
|
import { isDefaultLocale } from '@shared/core-utils/i18n'
|
||||||
|
import { VideoFile } from '@shared/models'
|
||||||
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
||||||
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
|
import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
|
||||||
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
|
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
|
||||||
import { getStoredP2PEnabled } from './peertube-player-local-storage'
|
import { getStoredP2PEnabled } from './peertube-player-local-storage'
|
||||||
import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings'
|
import {
|
||||||
|
P2PMediaLoaderPluginOptions,
|
||||||
|
PlaylistPluginOptions,
|
||||||
|
UserWatching,
|
||||||
|
VideoJSCaption,
|
||||||
|
VideoJSPluginOptions
|
||||||
|
} from './peertube-videojs-typings'
|
||||||
import { TranslationsManager } from './translations-manager'
|
import { TranslationsManager } from './translations-manager'
|
||||||
import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils'
|
import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils'
|
||||||
|
|
||||||
|
@ -71,6 +78,9 @@ export interface CommonOptions extends CustomizationOptions {
|
||||||
|
|
||||||
autoplay: boolean
|
autoplay: boolean
|
||||||
nextVideo?: Function
|
nextVideo?: Function
|
||||||
|
|
||||||
|
playlist?: PlaylistPluginOptions
|
||||||
|
|
||||||
videoDuration: number
|
videoDuration: number
|
||||||
enableHotkeys: boolean
|
enableHotkeys: boolean
|
||||||
inactivityTimeout: number
|
inactivityTimeout: number
|
||||||
|
@ -203,6 +213,10 @@ export class PeertubePlayerManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (commonOptions.playlist) {
|
||||||
|
plugins.playlist = commonOptions.playlist
|
||||||
|
}
|
||||||
|
|
||||||
if (commonOptions.enableHotkeys === true) {
|
if (commonOptions.enableHotkeys === true) {
|
||||||
PeertubePlayerManager.addHotkeysOptions(plugins)
|
PeertubePlayerManager.addHotkeysOptions(plugins)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Config, Level } from 'hls.js'
|
import { Config, Level } from 'hls.js'
|
||||||
import videojs from 'video.js'
|
import videojs from 'video.js'
|
||||||
import { VideoFile } from '@shared/models'
|
import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
|
||||||
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
|
import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
|
||||||
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
|
||||||
import { PlayerMode } from './peertube-player-manager'
|
import { PlayerMode } from './peertube-player-manager'
|
||||||
import { PeerTubePlugin } from './peertube-plugin'
|
import { PeerTubePlugin } from './peertube-plugin'
|
||||||
|
import { PlaylistPlugin } from './playlist/playlist-plugin'
|
||||||
import { EndCardOptions } from './upnext/end-card'
|
import { EndCardOptions } from './upnext/end-card'
|
||||||
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
|
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
|
||||||
|
|
||||||
|
@ -45,6 +46,8 @@ declare module 'video.js' {
|
||||||
dock (options: { title: string, description: string }): void
|
dock (options: { title: string, description: string }): void
|
||||||
|
|
||||||
upnext (options: Partial<EndCardOptions>): void
|
upnext (options: Partial<EndCardOptions>): void
|
||||||
|
|
||||||
|
playlist (): PlaylistPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +108,16 @@ type PeerTubePluginOptions = {
|
||||||
stopTime: number | string
|
stopTime: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaylistPluginOptions = {
|
||||||
|
elements: VideoPlaylistElement[]
|
||||||
|
|
||||||
|
playlist: VideoPlaylist
|
||||||
|
|
||||||
|
getCurrentPosition: () => number
|
||||||
|
|
||||||
|
onItemClicked: (element: VideoPlaylistElement) => void
|
||||||
|
}
|
||||||
|
|
||||||
type WebtorrentPluginOptions = {
|
type WebtorrentPluginOptions = {
|
||||||
playerElement: HTMLVideoElement
|
playerElement: HTMLVideoElement
|
||||||
|
|
||||||
|
@ -125,6 +138,8 @@ type P2PMediaLoaderPluginOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoJSPluginOptions = {
|
type VideoJSPluginOptions = {
|
||||||
|
playlist?: PlaylistPluginOptions
|
||||||
|
|
||||||
peertube: PeerTubePluginOptions
|
peertube: PeerTubePluginOptions
|
||||||
|
|
||||||
webtorrent?: WebtorrentPluginOptions
|
webtorrent?: WebtorrentPluginOptions
|
||||||
|
@ -170,10 +185,18 @@ type PlayerNetworkInfo = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaylistItemOptions = {
|
||||||
|
element: VideoPlaylistElement
|
||||||
|
|
||||||
|
onClicked: Function
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
PlayerNetworkInfo,
|
PlayerNetworkInfo,
|
||||||
|
PlaylistItemOptions,
|
||||||
ResolutionUpdateData,
|
ResolutionUpdateData,
|
||||||
AutoResolutionUpdateData,
|
AutoResolutionUpdateData,
|
||||||
|
PlaylistPluginOptions,
|
||||||
VideoJSCaption,
|
VideoJSCaption,
|
||||||
UserWatching,
|
UserWatching,
|
||||||
PeerTubePluginOptions,
|
PeerTubePluginOptions,
|
||||||
|
|
61
client/src/assets/player/playlist/playlist-button.ts
Normal file
61
client/src/assets/player/playlist/playlist-button.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import videojs from 'video.js'
|
||||||
|
import { PlaylistPluginOptions } from '../peertube-videojs-typings'
|
||||||
|
import { PlaylistMenu } from './playlist-menu'
|
||||||
|
|
||||||
|
const ClickableComponent = videojs.getComponent('ClickableComponent')
|
||||||
|
|
||||||
|
class PlaylistButton extends ClickableComponent {
|
||||||
|
private playlistInfoElement: HTMLElement
|
||||||
|
private wrapper: HTMLElement
|
||||||
|
|
||||||
|
constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) {
|
||||||
|
super(player, options as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEl () {
|
||||||
|
this.wrapper = super.createEl('div', {
|
||||||
|
className: 'vjs-playlist-button',
|
||||||
|
innerHTML: '',
|
||||||
|
tabIndex: -1
|
||||||
|
}) as HTMLElement
|
||||||
|
|
||||||
|
const icon = super.createEl('div', {
|
||||||
|
className: 'vjs-playlist-icon',
|
||||||
|
innerHTML: '',
|
||||||
|
tabIndex: -1
|
||||||
|
})
|
||||||
|
|
||||||
|
this.playlistInfoElement = super.createEl('div', {
|
||||||
|
className: 'vjs-playlist-info',
|
||||||
|
innerHTML: '',
|
||||||
|
tabIndex: -1
|
||||||
|
}) as HTMLElement
|
||||||
|
|
||||||
|
this.wrapper.appendChild(icon)
|
||||||
|
this.wrapper.appendChild(this.playlistInfoElement)
|
||||||
|
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
return this.wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
const options = this.options_ as PlaylistPluginOptions
|
||||||
|
|
||||||
|
this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength
|
||||||
|
this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ])
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick () {
|
||||||
|
const playlistMenu = this.getPlaylistMenu()
|
||||||
|
playlistMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlaylistMenu () {
|
||||||
|
return (this.options_ as any).playlistMenu as PlaylistMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videojs.registerComponent('PlaylistButton', PlaylistButton)
|
||||||
|
|
||||||
|
export { PlaylistButton }
|
98
client/src/assets/player/playlist/playlist-menu-item.ts
Normal file
98
client/src/assets/player/playlist/playlist-menu-item.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import videojs from 'video.js'
|
||||||
|
import { VideoPlaylistElement } from '@shared/models'
|
||||||
|
import { PlaylistItemOptions } from '../peertube-videojs-typings'
|
||||||
|
|
||||||
|
const Component = videojs.getComponent('Component')
|
||||||
|
|
||||||
|
class PlaylistMenuItem extends Component {
|
||||||
|
private element: VideoPlaylistElement
|
||||||
|
|
||||||
|
constructor (player: videojs.Player, options?: PlaylistItemOptions) {
|
||||||
|
super(player, options as any)
|
||||||
|
|
||||||
|
this.emitTapEvents()
|
||||||
|
|
||||||
|
this.element = options.element
|
||||||
|
|
||||||
|
this.on([ 'click', 'tap' ], () => this.switchPlaylistItem())
|
||||||
|
this.on('keydown', event => this.handleKeyDown(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
createEl () {
|
||||||
|
const options = this.options_ as PlaylistItemOptions
|
||||||
|
|
||||||
|
const li = super.createEl('li', {
|
||||||
|
className: 'vjs-playlist-menu-item',
|
||||||
|
innerHTML: ''
|
||||||
|
}) as HTMLElement
|
||||||
|
|
||||||
|
const positionBlock = super.createEl('div', {
|
||||||
|
className: 'item-position-block'
|
||||||
|
})
|
||||||
|
|
||||||
|
const position = super.createEl('div', {
|
||||||
|
className: 'item-position',
|
||||||
|
innerHTML: options.element.position
|
||||||
|
})
|
||||||
|
|
||||||
|
const player = super.createEl('div', {
|
||||||
|
className: 'item-player'
|
||||||
|
})
|
||||||
|
|
||||||
|
positionBlock.appendChild(position)
|
||||||
|
positionBlock.appendChild(player)
|
||||||
|
|
||||||
|
li.appendChild(positionBlock)
|
||||||
|
|
||||||
|
const thumbnail = super.createEl('img', {
|
||||||
|
src: window.location.origin + options.element.video.thumbnailPath
|
||||||
|
})
|
||||||
|
|
||||||
|
const infoBlock = super.createEl('div', {
|
||||||
|
className: 'info-block'
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = super.createEl('div', {
|
||||||
|
innerHTML: options.element.video.name,
|
||||||
|
className: 'title'
|
||||||
|
})
|
||||||
|
|
||||||
|
const channel = super.createEl('div', {
|
||||||
|
innerHTML: options.element.video.channel.displayName,
|
||||||
|
className: 'channel'
|
||||||
|
})
|
||||||
|
|
||||||
|
infoBlock.appendChild(title)
|
||||||
|
infoBlock.appendChild(channel)
|
||||||
|
|
||||||
|
li.append(thumbnail)
|
||||||
|
li.append(infoBlock)
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected (selected: boolean) {
|
||||||
|
if (selected) this.addClass('vjs-selected')
|
||||||
|
else this.removeClass('vjs-selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement () {
|
||||||
|
return this.element
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown (event: KeyboardEvent) {
|
||||||
|
if (event.code === 'Space' || event.code === 'Enter') {
|
||||||
|
this.switchPlaylistItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private switchPlaylistItem () {
|
||||||
|
const options = this.options_ as PlaylistItemOptions
|
||||||
|
|
||||||
|
options.onClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.registerComponent('PlaylistMenuItem', PlaylistMenuItem)
|
||||||
|
|
||||||
|
export { PlaylistMenuItem }
|
124
client/src/assets/player/playlist/playlist-menu.ts
Normal file
124
client/src/assets/player/playlist/playlist-menu.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import videojs from 'video.js'
|
||||||
|
import { VideoPlaylistElement } from '@shared/models'
|
||||||
|
import { PlaylistPluginOptions } from '../peertube-videojs-typings'
|
||||||
|
import { PlaylistMenuItem } from './playlist-menu-item'
|
||||||
|
|
||||||
|
const Component = videojs.getComponent('Component')
|
||||||
|
|
||||||
|
class PlaylistMenu extends Component {
|
||||||
|
private menuItems: PlaylistMenuItem[]
|
||||||
|
|
||||||
|
constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
|
||||||
|
super(player, options as any)
|
||||||
|
|
||||||
|
this.player().on('userinactive', () => {
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.player().on('click', event => {
|
||||||
|
let current = event.target as HTMLElement
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (
|
||||||
|
current.classList.contains('vjs-playlist-menu') ||
|
||||||
|
current.classList.contains('vjs-playlist-button')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement
|
||||||
|
} while (current)
|
||||||
|
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEl () {
|
||||||
|
this.menuItems = []
|
||||||
|
|
||||||
|
const options = this.getOptions()
|
||||||
|
|
||||||
|
const menu = super.createEl('div', {
|
||||||
|
className: 'vjs-playlist-menu',
|
||||||
|
innerHTML: '',
|
||||||
|
tabIndex: -1
|
||||||
|
})
|
||||||
|
|
||||||
|
const header = super.createEl('div', {
|
||||||
|
className: 'header'
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerLeft = super.createEl('div')
|
||||||
|
|
||||||
|
const leftTitle = super.createEl('div', {
|
||||||
|
innerHTML: options.playlist.displayName,
|
||||||
|
className: 'title'
|
||||||
|
})
|
||||||
|
|
||||||
|
const leftSubtitle = super.createEl('div', {
|
||||||
|
innerHTML: this.player().localize('By {1}', [ options.playlist.videoChannel.displayName ]),
|
||||||
|
className: 'channel'
|
||||||
|
})
|
||||||
|
|
||||||
|
headerLeft.appendChild(leftTitle)
|
||||||
|
headerLeft.appendChild(leftSubtitle)
|
||||||
|
|
||||||
|
const tick = super.createEl('div', {
|
||||||
|
className: 'cross'
|
||||||
|
})
|
||||||
|
tick.addEventListener('click', () => this.close())
|
||||||
|
|
||||||
|
header.appendChild(headerLeft)
|
||||||
|
header.appendChild(tick)
|
||||||
|
|
||||||
|
const list = super.createEl('ol')
|
||||||
|
|
||||||
|
for (const playlistElement of options.elements) {
|
||||||
|
const item = new PlaylistMenuItem(this.player(), {
|
||||||
|
element: playlistElement,
|
||||||
|
onClicked: () => this.onItemClicked(playlistElement)
|
||||||
|
})
|
||||||
|
|
||||||
|
list.appendChild(item.el())
|
||||||
|
|
||||||
|
this.menuItems.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.appendChild(header)
|
||||||
|
menu.appendChild(list)
|
||||||
|
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
const options = this.getOptions()
|
||||||
|
|
||||||
|
this.updateSelected(options.getCurrentPosition())
|
||||||
|
}
|
||||||
|
|
||||||
|
open () {
|
||||||
|
this.player().addClass('playlist-menu-displayed')
|
||||||
|
}
|
||||||
|
|
||||||
|
close () {
|
||||||
|
this.player().removeClass('playlist-menu-displayed')
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelected (newPosition: number) {
|
||||||
|
for (const item of this.menuItems) {
|
||||||
|
item.setSelected(item.getElement().position === newPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOptions () {
|
||||||
|
return this.options_ as PlaylistPluginOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
private onItemClicked (element: VideoPlaylistElement) {
|
||||||
|
this.getOptions().onItemClicked(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.registerComponent('PlaylistMenu', PlaylistMenu)
|
||||||
|
|
||||||
|
export { PlaylistMenu }
|
35
client/src/assets/player/playlist/playlist-plugin.ts
Normal file
35
client/src/assets/player/playlist/playlist-plugin.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import videojs from 'video.js'
|
||||||
|
import { PlaylistPluginOptions } from '../peertube-videojs-typings'
|
||||||
|
import { PlaylistButton } from './playlist-button'
|
||||||
|
import { PlaylistMenu } from './playlist-menu'
|
||||||
|
|
||||||
|
const Plugin = videojs.getPlugin('plugin')
|
||||||
|
|
||||||
|
class PlaylistPlugin extends Plugin {
|
||||||
|
private playlistMenu: PlaylistMenu
|
||||||
|
private playlistButton: PlaylistButton
|
||||||
|
private options: PlaylistPluginOptions
|
||||||
|
|
||||||
|
constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
|
||||||
|
super(player, options)
|
||||||
|
|
||||||
|
this.options = options
|
||||||
|
|
||||||
|
this.player.ready(() => {
|
||||||
|
player.addClass('vjs-playlist')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.playlistMenu = new PlaylistMenu(player, options)
|
||||||
|
this.playlistButton = new PlaylistButton(player, Object.assign({}, options, { playlistMenu: this.playlistMenu }))
|
||||||
|
|
||||||
|
player.addChild(this.playlistMenu, options)
|
||||||
|
player.addChild(this.playlistButton, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelected () {
|
||||||
|
this.playlistMenu.updateSelected(this.options.getCurrentPosition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videojs.registerPlugin('playlist', PlaylistPlugin)
|
||||||
|
export { PlaylistPlugin }
|
|
@ -52,18 +52,7 @@ $play-overlay-width: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 0;
|
@include play-icon($play-overlay-height, $play-overlay-width);
|
||||||
height: 0;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%) scale(0.5);
|
|
||||||
|
|
||||||
border-top: ($play-overlay-height / 2) solid transparent;
|
|
||||||
border-bottom: ($play-overlay-height / 2) solid transparent;
|
|
||||||
|
|
||||||
border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1019,3 +1019,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin play-icon ($width, $height) {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(0.5);
|
||||||
|
|
||||||
|
border-top: ($height / 2) solid transparent;
|
||||||
|
border-bottom: ($height / 2) solid transparent;
|
||||||
|
|
||||||
|
border-left: $width solid rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
@import './spinner';
|
@import './spinner';
|
||||||
@import './upnext';
|
@import './upnext';
|
||||||
@import './bezels.scss';
|
@import './bezels.scss';
|
||||||
|
@import './playlist.scss';
|
||||||
|
|
165
client/src/sass/player/playlist.scss
Normal file
165
client/src/sass/player/playlist.scss
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
$playlist-menu-width: 350px;
|
||||||
|
|
||||||
|
.vjs-playlist-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: $playlist-menu-width;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 101;
|
||||||
|
transition: right 0.2s;
|
||||||
|
|
||||||
|
// Hidden
|
||||||
|
right: -$playlist-menu-width;
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid $header-border-color;
|
||||||
|
padding: 20px 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cross {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
mask-image: url('#{$assets-path}/images/feather/x.svg');
|
||||||
|
-webkit-mask-image: url('#{$assets-path}/images/feather/x.svg');
|
||||||
|
background-color: white;
|
||||||
|
mask-size: cover;
|
||||||
|
-webkit-mask-size: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-menu-displayed {
|
||||||
|
|
||||||
|
.vjs-playlist-menu {
|
||||||
|
right: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playlist-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $playlist-menu-width) {
|
||||||
|
.vjs-playlist-menu {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-menu-displayed .vjs-playlist-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playlist-button {
|
||||||
|
font-size: 15px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playlist-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
mask-image: url('#{$assets-path}/images/feather/list.svg');
|
||||||
|
-webkit-mask-image: url('#{$assets-path}/images/feather/list.svg');
|
||||||
|
background-color: white;
|
||||||
|
mask-size: cover;
|
||||||
|
-webkit-mask-size: cover;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playing.vjs-user-inactive .vjs-playlist-button {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playing.vjs-no-flex.vjs-user-inactive .vjs-playlist-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-playlist-menu-item {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.item-position-block {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-player {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@include play-icon(20px, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vjs-selected {
|
||||||
|
background-color: rgba(150, 150, 150, 0.3);
|
||||||
|
|
||||||
|
.item-position {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-player {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(150, 150, 150, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 80px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-block {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -324,7 +324,11 @@ export class PeerTubeEmbed {
|
||||||
|
|
||||||
this.currentPlaylistElement = next
|
this.currentPlaylistElement = next
|
||||||
|
|
||||||
const res = await this.loadVideo(this.currentPlaylistElement.video.uuid)
|
return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadVideoAndBuildPlayer (uuid: string) {
|
||||||
|
const res = await this.loadVideo(uuid)
|
||||||
if (res === undefined) return
|
if (res === undefined) return
|
||||||
|
|
||||||
return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
|
return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
|
||||||
|
@ -386,6 +390,22 @@ export class PeerTubeEmbed {
|
||||||
|
|
||||||
this.loadParams(videoInfo)
|
this.loadParams(videoInfo)
|
||||||
|
|
||||||
|
const playlistPlugin = this.currentPlaylistElement
|
||||||
|
? {
|
||||||
|
elements: this.playlistElements,
|
||||||
|
playlist: this.playlist,
|
||||||
|
|
||||||
|
getCurrentPosition: () => this.currentPlaylistElement.position,
|
||||||
|
|
||||||
|
onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
|
||||||
|
this.currentPlaylistElement = videoPlaylistElement
|
||||||
|
|
||||||
|
this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
const options: PeertubePlayerManagerOptions = {
|
const options: PeertubePlayerManagerOptions = {
|
||||||
common: {
|
common: {
|
||||||
// Autoplay in playlist mode
|
// Autoplay in playlist mode
|
||||||
|
@ -399,6 +419,7 @@ export class PeerTubeEmbed {
|
||||||
subtitle: this.subtitle,
|
subtitle: this.subtitle,
|
||||||
|
|
||||||
nextVideo: () => this.autoplayNext(),
|
nextVideo: () => this.autoplayNext(),
|
||||||
|
playlist: playlistPlugin,
|
||||||
|
|
||||||
videoCaptions,
|
videoCaptions,
|
||||||
inactivityTimeout: 2500,
|
inactivityTimeout: 2500,
|
||||||
|
@ -452,6 +473,7 @@ export class PeerTubeEmbed {
|
||||||
|
|
||||||
if (this.isPlaylistEmbed()) {
|
if (this.isPlaylistEmbed()) {
|
||||||
await this.buildPlaylistManager()
|
await this.buildPlaylistManager()
|
||||||
|
this.player.playlist().updateSelected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,10 +502,7 @@ export class PeerTubeEmbed {
|
||||||
videoId = this.getResourceId()
|
videoId = this.getResourceId()
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.loadVideo(videoId)
|
return this.loadVideoAndBuildPlayer(videoId)
|
||||||
if (res === undefined) return
|
|
||||||
|
|
||||||
return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError (err: Error, translations?: { [ id: string ]: string }) {
|
private handleError (err: Error, translations?: { [ id: string ]: string }) {
|
||||||
|
|
|
@ -50,7 +50,9 @@ values(VIDEO_CATEGORIES)
|
||||||
'Sorry',
|
'Sorry',
|
||||||
'This video is not available because the remote instance is not responding.',
|
'This video is not available because the remote instance is not responding.',
|
||||||
'This playlist does not exist',
|
'This playlist does not exist',
|
||||||
'We cannot fetch the playlist. Please try again later.'
|
'We cannot fetch the playlist. Please try again later.',
|
||||||
|
'Playlist: {1}',
|
||||||
|
'By {1}'
|
||||||
])
|
])
|
||||||
.forEach(v => { serverKeys[v] = v })
|
.forEach(v => { serverKeys[v] = v })
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue