aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-08-05 09:44:58 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-08-07 08:58:29 +0200
commit4572c3d0d92f5b1b79b34dbe2c7b6557a8a5b7e4 (patch)
tree2c1aa81a536b50d6da0181aba6fce1db972f6191
parent5abc96fca2496f33075796db208fccc3543e0f65 (diff)
downloadPeerTube-4572c3d0d92f5b1b79b34dbe2c7b6557a8a5b7e4.tar.gz
PeerTube-4572c3d0d92f5b1b79b34dbe2c7b6557a8a5b7e4.tar.zst
PeerTube-4572c3d0d92f5b1b79b34dbe2c7b6557a8a5b7e4.zip
Handle basic playlist in embed
-rw-r--r--client/src/assets/player/images/tick-white.svg5
-rw-r--r--client/src/assets/player/peertube-player-manager.ts18
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts25
-rw-r--r--client/src/assets/player/playlist/playlist-button.ts61
-rw-r--r--client/src/assets/player/playlist/playlist-menu-item.ts98
-rw-r--r--client/src/assets/player/playlist/playlist-menu.ts124
-rw-r--r--client/src/assets/player/playlist/playlist-plugin.ts35
-rw-r--r--client/src/sass/include/_miniature.scss13
-rw-r--r--client/src/sass/include/_mixins.scss15
-rw-r--r--client/src/sass/player/index.scss3
-rw-r--r--client/src/sass/player/playlist.scss165
-rw-r--r--client/src/standalone/videos/embed.ts29
-rwxr-xr-xscripts/i18n/create-custom-files.ts4
13 files changed, 570 insertions, 25 deletions
diff --git a/client/src/assets/player/images/tick-white.svg b/client/src/assets/player/images/tick-white.svg
index d329e6bfb..8868a2481 100644
--- a/client/src/assets/player/images/tick-white.svg
+++ b/client/src/assets/player/images/tick-white.svg
@@ -1,8 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?> 1<?xml version="1.0" encoding="UTF-8"?>
2<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"> 2<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">
3 <defs></defs> 3 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> 4 <g transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
5 <g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
6 <g id="8" transform="translate(356.000000, 115.000000)"> 5 <g id="8" transform="translate(356.000000, 115.000000)">
7 <path d="M21,6 L9,18" id="Path-14"></path> 6 <path d="M21,6 L9,18" id="Path-14"></path>
8 <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> 7 <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path>
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 6a6d63462..dcfa3a593 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -18,14 +18,21 @@ import './videojs-components/settings-menu-item'
18import './videojs-components/settings-panel' 18import './videojs-components/settings-panel'
19import './videojs-components/settings-panel-child' 19import './videojs-components/settings-panel-child'
20import './videojs-components/theater-button' 20import './videojs-components/theater-button'
21import './playlist/playlist-plugin'
21import videojs from 'video.js' 22import videojs from 'video.js'
22import { VideoFile } from '@shared/models'
23import { isDefaultLocale } from '@shared/core-utils/i18n' 23import { isDefaultLocale } from '@shared/core-utils/i18n'
24import { VideoFile } from '@shared/models'
24import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 25import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
25import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' 26import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
26import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' 27import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
27import { getStoredP2PEnabled } from './peertube-player-local-storage' 28import { getStoredP2PEnabled } from './peertube-player-local-storage'
28import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings' 29import {
30 P2PMediaLoaderPluginOptions,
31 PlaylistPluginOptions,
32 UserWatching,
33 VideoJSCaption,
34 VideoJSPluginOptions
35} from './peertube-videojs-typings'
29import { TranslationsManager } from './translations-manager' 36import { TranslationsManager } from './translations-manager'
30import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils' 37import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils'
31 38
@@ -71,6 +78,9 @@ export interface CommonOptions extends CustomizationOptions {
71 78
72 autoplay: boolean 79 autoplay: boolean
73 nextVideo?: Function 80 nextVideo?: Function
81
82 playlist?: PlaylistPluginOptions
83
74 videoDuration: number 84 videoDuration: number
75 enableHotkeys: boolean 85 enableHotkeys: boolean
76 inactivityTimeout: number 86 inactivityTimeout: number
@@ -203,6 +213,10 @@ export class PeertubePlayerManager {
203 } 213 }
204 } 214 }
205 215
216 if (commonOptions.playlist) {
217 plugins.playlist = commonOptions.playlist
218 }
219
206 if (commonOptions.enableHotkeys === true) { 220 if (commonOptions.enableHotkeys === true) {
207 PeertubePlayerManager.addHotkeysOptions(plugins) 221 PeertubePlayerManager.addHotkeysOptions(plugins)
208 } 222 }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index 1506a04ac..b72c4b0f9 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,10 +1,11 @@
1import { Config, Level } from 'hls.js' 1import { Config, Level } from 'hls.js'
2import videojs from 'video.js' 2import videojs from 'video.js'
3import { VideoFile } from '@shared/models' 3import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
4import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 4import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
5import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 5import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
6import { PlayerMode } from './peertube-player-manager' 6import { PlayerMode } from './peertube-player-manager'
7import { PeerTubePlugin } from './peertube-plugin' 7import { PeerTubePlugin } from './peertube-plugin'
8import { PlaylistPlugin } from './playlist/playlist-plugin'
8import { EndCardOptions } from './upnext/end-card' 9import { EndCardOptions } from './upnext/end-card'
9import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' 10import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
10 11
@@ -45,6 +46,8 @@ declare module 'video.js' {
45 dock (options: { title: string, description: string }): void 46 dock (options: { title: string, description: string }): void
46 47
47 upnext (options: Partial<EndCardOptions>): void 48 upnext (options: Partial<EndCardOptions>): void
49
50 playlist (): PlaylistPlugin
48 } 51 }
49} 52}
50 53
@@ -105,6 +108,16 @@ type PeerTubePluginOptions = {
105 stopTime: number | string 108 stopTime: number | string
106} 109}
107 110
111type PlaylistPluginOptions = {
112 elements: VideoPlaylistElement[]
113
114 playlist: VideoPlaylist
115
116 getCurrentPosition: () => number
117
118 onItemClicked: (element: VideoPlaylistElement) => void
119}
120
108type WebtorrentPluginOptions = { 121type WebtorrentPluginOptions = {
109 playerElement: HTMLVideoElement 122 playerElement: HTMLVideoElement
110 123
@@ -125,6 +138,8 @@ type P2PMediaLoaderPluginOptions = {
125} 138}
126 139
127type VideoJSPluginOptions = { 140type VideoJSPluginOptions = {
141 playlist?: PlaylistPluginOptions
142
128 peertube: PeerTubePluginOptions 143 peertube: PeerTubePluginOptions
129 144
130 webtorrent?: WebtorrentPluginOptions 145 webtorrent?: WebtorrentPluginOptions
@@ -170,10 +185,18 @@ type PlayerNetworkInfo = {
170 } 185 }
171} 186}
172 187
188type PlaylistItemOptions = {
189 element: VideoPlaylistElement
190
191 onClicked: Function
192}
193
173export { 194export {
174 PlayerNetworkInfo, 195 PlayerNetworkInfo,
196 PlaylistItemOptions,
175 ResolutionUpdateData, 197 ResolutionUpdateData,
176 AutoResolutionUpdateData, 198 AutoResolutionUpdateData,
199 PlaylistPluginOptions,
177 VideoJSCaption, 200 VideoJSCaption,
178 UserWatching, 201 UserWatching,
179 PeerTubePluginOptions, 202 PeerTubePluginOptions,
diff --git a/client/src/assets/player/playlist/playlist-button.ts b/client/src/assets/player/playlist/playlist-button.ts
new file mode 100644
index 000000000..a7996ec60
--- /dev/null
+++ b/client/src/assets/player/playlist/playlist-button.ts
@@ -0,0 +1,61 @@
1import videojs from 'video.js'
2import { PlaylistPluginOptions } from '../peertube-videojs-typings'
3import { PlaylistMenu } from './playlist-menu'
4
5const ClickableComponent = videojs.getComponent('ClickableComponent')
6
7class PlaylistButton extends ClickableComponent {
8 private playlistInfoElement: HTMLElement
9 private wrapper: HTMLElement
10
11 constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) {
12 super(player, options as any)
13 }
14
15 createEl () {
16 this.wrapper = super.createEl('div', {
17 className: 'vjs-playlist-button',
18 innerHTML: '',
19 tabIndex: -1
20 }) as HTMLElement
21
22 const icon = super.createEl('div', {
23 className: 'vjs-playlist-icon',
24 innerHTML: '',
25 tabIndex: -1
26 })
27
28 this.playlistInfoElement = super.createEl('div', {
29 className: 'vjs-playlist-info',
30 innerHTML: '',
31 tabIndex: -1
32 }) as HTMLElement
33
34 this.wrapper.appendChild(icon)
35 this.wrapper.appendChild(this.playlistInfoElement)
36
37 this.update()
38
39 return this.wrapper
40 }
41
42 update () {
43 const options = this.options_ as PlaylistPluginOptions
44
45 this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength
46 this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ])
47 }
48
49 handleClick () {
50 const playlistMenu = this.getPlaylistMenu()
51 playlistMenu.open()
52 }
53
54 private getPlaylistMenu () {
55 return (this.options_ as any).playlistMenu as PlaylistMenu
56 }
57}
58
59videojs.registerComponent('PlaylistButton', PlaylistButton)
60
61export { PlaylistButton }
diff --git a/client/src/assets/player/playlist/playlist-menu-item.ts b/client/src/assets/player/playlist/playlist-menu-item.ts
new file mode 100644
index 000000000..916c6338f
--- /dev/null
+++ b/client/src/assets/player/playlist/playlist-menu-item.ts
@@ -0,0 +1,98 @@
1import videojs from 'video.js'
2import { VideoPlaylistElement } from '@shared/models'
3import { PlaylistItemOptions } from '../peertube-videojs-typings'
4
5const Component = videojs.getComponent('Component')
6
7class PlaylistMenuItem extends Component {
8 private element: VideoPlaylistElement
9
10 constructor (player: videojs.Player, options?: PlaylistItemOptions) {
11 super(player, options as any)
12
13 this.emitTapEvents()
14
15 this.element = options.element
16
17 this.on([ 'click', 'tap' ], () => this.switchPlaylistItem())
18 this.on('keydown', event => this.handleKeyDown(event))
19 }
20
21 createEl () {
22 const options = this.options_ as PlaylistItemOptions
23
24 const li = super.createEl('li', {
25 className: 'vjs-playlist-menu-item',
26 innerHTML: ''
27 }) as HTMLElement
28
29 const positionBlock = super.createEl('div', {
30 className: 'item-position-block'
31 })
32
33 const position = super.createEl('div', {
34 className: 'item-position',
35 innerHTML: options.element.position
36 })
37
38 const player = super.createEl('div', {
39 className: 'item-player'
40 })
41
42 positionBlock.appendChild(position)
43 positionBlock.appendChild(player)
44
45 li.appendChild(positionBlock)
46
47 const thumbnail = super.createEl('img', {
48 src: window.location.origin + options.element.video.thumbnailPath
49 })
50
51 const infoBlock = super.createEl('div', {
52 className: 'info-block'
53 })
54
55 const title = super.createEl('div', {
56 innerHTML: options.element.video.name,
57 className: 'title'
58 })
59
60 const channel = super.createEl('div', {
61 innerHTML: options.element.video.channel.displayName,
62 className: 'channel'
63 })
64
65 infoBlock.appendChild(title)
66 infoBlock.appendChild(channel)
67
68 li.append(thumbnail)
69 li.append(infoBlock)
70
71 return li
72 }
73
74 setSelected (selected: boolean) {
75 if (selected) this.addClass('vjs-selected')
76 else this.removeClass('vjs-selected')
77 }
78
79 getElement () {
80 return this.element
81 }
82
83 private handleKeyDown (event: KeyboardEvent) {
84 if (event.code === 'Space' || event.code === 'Enter') {
85 this.switchPlaylistItem()
86 }
87 }
88
89 private switchPlaylistItem () {
90 const options = this.options_ as PlaylistItemOptions
91
92 options.onClicked()
93 }
94}
95
96Component.registerComponent('PlaylistMenuItem', PlaylistMenuItem)
97
98export { PlaylistMenuItem }
diff --git a/client/src/assets/player/playlist/playlist-menu.ts b/client/src/assets/player/playlist/playlist-menu.ts
new file mode 100644
index 000000000..7d7d9e12f
--- /dev/null
+++ b/client/src/assets/player/playlist/playlist-menu.ts
@@ -0,0 +1,124 @@
1import videojs from 'video.js'
2import { VideoPlaylistElement } from '@shared/models'
3import { PlaylistPluginOptions } from '../peertube-videojs-typings'
4import { PlaylistMenuItem } from './playlist-menu-item'
5
6const Component = videojs.getComponent('Component')
7
8class PlaylistMenu extends Component {
9 private menuItems: PlaylistMenuItem[]
10
11 constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
12 super(player, options as any)
13
14 this.player().on('userinactive', () => {
15 this.close()
16 })
17
18 this.player().on('click', event => {
19 let current = event.target as HTMLElement
20
21 do {
22 if (
23 current.classList.contains('vjs-playlist-menu') ||
24 current.classList.contains('vjs-playlist-button')
25 ) {
26 return
27 }
28
29 current = current.parentElement
30 } while (current)
31
32 this.close()
33 })
34 }
35
36 createEl () {
37 this.menuItems = []
38
39 const options = this.getOptions()
40
41 const menu = super.createEl('div', {
42 className: 'vjs-playlist-menu',
43 innerHTML: '',
44 tabIndex: -1
45 })
46
47 const header = super.createEl('div', {
48 className: 'header'
49 })
50
51 const headerLeft = super.createEl('div')
52
53 const leftTitle = super.createEl('div', {
54 innerHTML: options.playlist.displayName,
55 className: 'title'
56 })
57
58 const leftSubtitle = super.createEl('div', {
59 innerHTML: this.player().localize('By {1}', [ options.playlist.videoChannel.displayName ]),
60 className: 'channel'
61 })
62
63 headerLeft.appendChild(leftTitle)
64 headerLeft.appendChild(leftSubtitle)
65
66 const tick = super.createEl('div', {
67 className: 'cross'
68 })
69 tick.addEventListener('click', () => this.close())
70
71 header.appendChild(headerLeft)
72 header.appendChild(tick)
73
74 const list = super.createEl('ol')
75
76 for (const playlistElement of options.elements) {
77 const item = new PlaylistMenuItem(this.player(), {
78 element: playlistElement,
79 onClicked: () => this.onItemClicked(playlistElement)
80 })
81
82 list.appendChild(item.el())
83
84 this.menuItems.push(item)
85 }
86
87 menu.appendChild(header)
88 menu.appendChild(list)
89
90 return menu
91 }
92
93 update () {
94 const options = this.getOptions()
95
96 this.updateSelected(options.getCurrentPosition())
97 }
98
99 open () {
100 this.player().addClass('playlist-menu-displayed')
101 }
102
103 close () {
104 this.player().removeClass('playlist-menu-displayed')
105 }
106
107 updateSelected (newPosition: number) {
108 for (const item of this.menuItems) {
109 item.setSelected(item.getElement().position === newPosition)
110 }
111 }
112
113 private getOptions () {
114 return this.options_ as PlaylistPluginOptions
115 }
116
117 private onItemClicked (element: VideoPlaylistElement) {
118 this.getOptions().onItemClicked(element)
119 }
120}
121
122Component.registerComponent('PlaylistMenu', PlaylistMenu)
123
124export { PlaylistMenu }
diff --git a/client/src/assets/player/playlist/playlist-plugin.ts b/client/src/assets/player/playlist/playlist-plugin.ts
new file mode 100644
index 000000000..b69d82e3c
--- /dev/null
+++ b/client/src/assets/player/playlist/playlist-plugin.ts
@@ -0,0 +1,35 @@
1import videojs from 'video.js'
2import { PlaylistPluginOptions } from '../peertube-videojs-typings'
3import { PlaylistButton } from './playlist-button'
4import { PlaylistMenu } from './playlist-menu'
5
6const Plugin = videojs.getPlugin('plugin')
7
8class PlaylistPlugin extends Plugin {
9 private playlistMenu: PlaylistMenu
10 private playlistButton: PlaylistButton
11 private options: PlaylistPluginOptions
12
13 constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
14 super(player, options)
15
16 this.options = options
17
18 this.player.ready(() => {
19 player.addClass('vjs-playlist')
20 })
21
22 this.playlistMenu = new PlaylistMenu(player, options)
23 this.playlistButton = new PlaylistButton(player, Object.assign({}, options, { playlistMenu: this.playlistMenu }))
24
25 player.addChild(this.playlistMenu, options)
26 player.addChild(this.playlistButton, options)
27 }
28
29 updateSelected () {
30 this.playlistMenu.updateSelected(this.options.getCurrentPosition())
31 }
32}
33
34videojs.registerPlugin('playlist', PlaylistPlugin)
35export { PlaylistPlugin }
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index 976bbf4d6..97b4c690b 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -52,18 +52,7 @@ $play-overlay-width: 18px;
52 } 52 }
53 53
54 .icon { 54 .icon {
55 width: 0; 55 @include play-icon($play-overlay-height, $play-overlay-width);
56 height: 0;
57
58 position: absolute;
59 left: 50%;
60 top: 50%;
61 transform: translate(-50%, -50%) scale(0.5);
62
63 border-top: ($play-overlay-height / 2) solid transparent;
64 border-bottom: ($play-overlay-height / 2) solid transparent;
65
66 border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
67 } 56 }
68 } 57 }
69 58
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index ee2fe0497..e4c2dffa0 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -1019,3 +1019,18 @@
1019 } 1019 }
1020 } 1020 }
1021} 1021}
1022
1023@mixin play-icon ($width, $height) {
1024 width: 0;
1025 height: 0;
1026
1027 position: absolute;
1028 left: 50%;
1029 top: 50%;
1030 transform: translate(-50%, -50%) scale(0.5);
1031
1032 border-top: ($height / 2) solid transparent;
1033 border-bottom: ($height / 2) solid transparent;
1034
1035 border-left: $width solid rgba(255, 255, 255, 0.95);
1036}
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss
index 58ce3ac96..fe92ce5e0 100644
--- a/client/src/sass/player/index.scss
+++ b/client/src/sass/player/index.scss
@@ -4,4 +4,5 @@
4@import './settings-menu'; 4@import './settings-menu';
5@import './spinner'; 5@import './spinner';
6@import './upnext'; 6@import './upnext';
7@import './bezels.scss'; \ No newline at end of file 7@import './bezels.scss';
8@import './playlist.scss';
diff --git a/client/src/sass/player/playlist.scss b/client/src/sass/player/playlist.scss
new file mode 100644
index 000000000..c242acba8
--- /dev/null
+++ b/client/src/sass/player/playlist.scss
@@ -0,0 +1,165 @@
1$playlist-menu-width: 350px;
2
3.vjs-playlist-menu {
4 position: absolute;
5 right: 0;
6 height: 100%;
7 width: $playlist-menu-width;
8 background: rgba(0, 0, 0, 0.8);
9 z-index: 101;
10 transition: right 0.2s;
11
12 // Hidden
13 right: -$playlist-menu-width;
14
15 ol {
16 padding: 0;
17 margin: 0;
18 }
19
20 .header {
21 border-bottom: 1px solid $header-border-color;
22 padding: 20px 10px;
23 display: flex;
24 justify-content: space-between;
25
26 .title {
27 font-size: 14px;
28 margin-bottom: 5px;
29 white-space: nowrap;
30 text-overflow: ellipsis;
31 }
32
33 .channel {
34 font-size: 11px;
35 color: #bfbfbf;
36 white-space: nowrap;
37 text-overflow: ellipsis;
38 }
39
40 .cross {
41 cursor: pointer;
42 width: 20px;
43 height: 20px;
44 mask-image: url('#{$assets-path}/images/feather/x.svg');
45 -webkit-mask-image: url('#{$assets-path}/images/feather/x.svg');
46 background-color: white;
47 mask-size: cover;
48 -webkit-mask-size: cover;
49 }
50 }
51}
52
53.playlist-menu-displayed {
54
55 .vjs-playlist-menu {
56 right: 0;
57 display: block;
58 }
59
60 .vjs-playlist-button {
61 display: none;
62 }
63}
64
65@media screen and (max-width: $playlist-menu-width) {
66 .vjs-playlist-menu {
67 width: 100%;
68 min-width: unset;
69 display: none;
70 }
71
72 .playlist-menu-displayed .vjs-playlist-menu {
73 display: block;
74 }
75}
76
77.vjs-playlist-button {
78 font-size: 15px;
79 position: absolute;
80 right: 0;
81 top: 0;
82 z-index: 100;
83 padding: 1em;
84 cursor: pointer;
85}
86
87.vjs-playlist-icon {
88 width: 22px;
89 height: 22px;
90 mask-image: url('#{$assets-path}/images/feather/list.svg');
91 -webkit-mask-image: url('#{$assets-path}/images/feather/list.svg');
92 background-color: white;
93 mask-size: cover;
94 -webkit-mask-size: cover;
95 margin-bottom: 3px;
96}
97
98.vjs-playing.vjs-user-inactive .vjs-playlist-button {
99 opacity: 0;
100
101 transition: opacity 1s;
102}
103
104.vjs-playing.vjs-no-flex.vjs-user-inactive .vjs-playlist-button {
105 display: none;
106}
107
108.vjs-playlist-menu-item {
109 cursor: pointer;
110 display: flex;
111 padding: 10px 0;
112
113 .item-position-block {
114 position: relative;
115 display: flex;
116 align-items: center;
117 justify-content: center;
118 width: 30px;
119 }
120
121 .item-player {
122 display: none;
123
124 @include play-icon(20px, 16px);
125 }
126
127 &.vjs-selected {
128 background-color: rgba(150, 150, 150, 0.3);
129
130 .item-position {
131 display: none;
132 }
133
134 .item-player {
135 display: block;
136 }
137 }
138
139 &:hover {
140 background-color: rgba(150, 150, 150, 0.2);
141 }
142
143 img {
144 width: 80px;
145 height: 40px;
146 }
147
148 .info-block {
149 margin-left: 10px;
150
151 .title {
152 font-size: 13px;
153 margin-bottom: 5px;
154 white-space: nowrap;
155 text-overflow: ellipsis;
156 }
157
158 .channel {
159 font-size: 11px;
160 color: #bfbfbf;
161 white-space: nowrap;
162 text-overflow: ellipsis;
163 }
164 }
165}
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 71bd04e76..17b0ee9ef 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -324,7 +324,11 @@ export class PeerTubeEmbed {
324 324
325 this.currentPlaylistElement = next 325 this.currentPlaylistElement = next
326 326
327 const res = await this.loadVideo(this.currentPlaylistElement.video.uuid) 327 return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
328 }
329
330 private async loadVideoAndBuildPlayer (uuid: string) {
331 const res = await this.loadVideo(uuid)
328 if (res === undefined) return 332 if (res === undefined) return
329 333
330 return this.buildVideoPlayer(res.videoResponse, res.captionsPromise) 334 return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
@@ -386,6 +390,22 @@ export class PeerTubeEmbed {
386 390
387 this.loadParams(videoInfo) 391 this.loadParams(videoInfo)
388 392
393 const playlistPlugin = this.currentPlaylistElement
394 ? {
395 elements: this.playlistElements,
396 playlist: this.playlist,
397
398 getCurrentPosition: () => this.currentPlaylistElement.position,
399
400 onItemClicked: (videoPlaylistElement: VideoPlaylistElement) => {
401 this.currentPlaylistElement = videoPlaylistElement
402
403 this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
404 .catch(err => console.error(err))
405 }
406 }
407 : undefined
408
389 const options: PeertubePlayerManagerOptions = { 409 const options: PeertubePlayerManagerOptions = {
390 common: { 410 common: {
391 // Autoplay in playlist mode 411 // Autoplay in playlist mode
@@ -399,6 +419,7 @@ export class PeerTubeEmbed {
399 subtitle: this.subtitle, 419 subtitle: this.subtitle,
400 420
401 nextVideo: () => this.autoplayNext(), 421 nextVideo: () => this.autoplayNext(),
422 playlist: playlistPlugin,
402 423
403 videoCaptions, 424 videoCaptions,
404 inactivityTimeout: 2500, 425 inactivityTimeout: 2500,
@@ -452,6 +473,7 @@ export class PeerTubeEmbed {
452 473
453 if (this.isPlaylistEmbed()) { 474 if (this.isPlaylistEmbed()) {
454 await this.buildPlaylistManager() 475 await this.buildPlaylistManager()
476 this.player.playlist().updateSelected()
455 } 477 }
456 } 478 }
457 479
@@ -480,10 +502,7 @@ export class PeerTubeEmbed {
480 videoId = this.getResourceId() 502 videoId = this.getResourceId()
481 } 503 }
482 504
483 const res = await this.loadVideo(videoId) 505 return this.loadVideoAndBuildPlayer(videoId)
484 if (res === undefined) return
485
486 return this.buildVideoPlayer(res.videoResponse, res.captionsPromise)
487 } 506 }
488 507
489 private handleError (err: Error, translations?: { [ id: string ]: string }) { 508 private handleError (err: Error, translations?: { [ id: string ]: string }) {
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts
index 298eda71b..89a967b14 100755
--- a/scripts/i18n/create-custom-files.ts
+++ b/scripts/i18n/create-custom-files.ts
@@ -50,7 +50,9 @@ values(VIDEO_CATEGORIES)
50 'Sorry', 50 'Sorry',
51 'This video is not available because the remote instance is not responding.', 51 'This video is not available because the remote instance is not responding.',
52 'This playlist does not exist', 52 'This playlist does not exist',
53 'We cannot fetch the playlist. Please try again later.' 53 'We cannot fetch the playlist. Please try again later.',
54 'Playlist: {1}',
55 'By {1}'
54 ]) 56 ])
55 .forEach(v => { serverKeys[v] = v }) 57 .forEach(v => { serverKeys[v] = v })
56 58