diff options
-rw-r--r-- | client/src/assets/player/images/tick-white.svg | 5 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 18 | ||||
-rw-r--r-- | client/src/assets/player/peertube-videojs-typings.ts | 25 | ||||
-rw-r--r-- | client/src/assets/player/playlist/playlist-button.ts | 61 | ||||
-rw-r--r-- | client/src/assets/player/playlist/playlist-menu-item.ts | 98 | ||||
-rw-r--r-- | client/src/assets/player/playlist/playlist-menu.ts | 124 | ||||
-rw-r--r-- | client/src/assets/player/playlist/playlist-plugin.ts | 35 | ||||
-rw-r--r-- | client/src/sass/include/_miniature.scss | 13 | ||||
-rw-r--r-- | client/src/sass/include/_mixins.scss | 15 | ||||
-rw-r--r-- | client/src/sass/player/index.scss | 3 | ||||
-rw-r--r-- | client/src/sass/player/playlist.scss | 165 | ||||
-rw-r--r-- | client/src/standalone/videos/embed.ts | 29 | ||||
-rwxr-xr-x | scripts/i18n/create-custom-files.ts | 4 |
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' | |||
18 | import './videojs-components/settings-panel' | 18 | import './videojs-components/settings-panel' |
19 | import './videojs-components/settings-panel-child' | 19 | import './videojs-components/settings-panel-child' |
20 | import './videojs-components/theater-button' | 20 | import './videojs-components/theater-button' |
21 | import './playlist/playlist-plugin' | ||
21 | import videojs from 'video.js' | 22 | import videojs from 'video.js' |
22 | import { VideoFile } from '@shared/models' | ||
23 | import { isDefaultLocale } from '@shared/core-utils/i18n' | 23 | import { isDefaultLocale } from '@shared/core-utils/i18n' |
24 | import { VideoFile } from '@shared/models' | ||
24 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 25 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
25 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | 26 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' |
26 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | 27 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' |
27 | import { getStoredP2PEnabled } from './peertube-player-local-storage' | 28 | import { getStoredP2PEnabled } from './peertube-player-local-storage' |
28 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings' | 29 | import { |
30 | P2PMediaLoaderPluginOptions, | ||
31 | PlaylistPluginOptions, | ||
32 | UserWatching, | ||
33 | VideoJSCaption, | ||
34 | VideoJSPluginOptions | ||
35 | } from './peertube-videojs-typings' | ||
29 | import { TranslationsManager } from './translations-manager' | 36 | import { TranslationsManager } from './translations-manager' |
30 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils' | 37 | import { 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 @@ | |||
1 | import { Config, Level } from 'hls.js' | 1 | import { Config, Level } from 'hls.js' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { VideoFile } from '@shared/models' | 3 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
4 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | 4 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' |
5 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 5 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
6 | import { PlayerMode } from './peertube-player-manager' | 6 | import { PlayerMode } from './peertube-player-manager' |
7 | import { PeerTubePlugin } from './peertube-plugin' | 7 | import { PeerTubePlugin } from './peertube-plugin' |
8 | import { PlaylistPlugin } from './playlist/playlist-plugin' | ||
8 | import { EndCardOptions } from './upnext/end-card' | 9 | import { EndCardOptions } from './upnext/end-card' |
9 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | 10 | import { 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 | ||
111 | type PlaylistPluginOptions = { | ||
112 | elements: VideoPlaylistElement[] | ||
113 | |||
114 | playlist: VideoPlaylist | ||
115 | |||
116 | getCurrentPosition: () => number | ||
117 | |||
118 | onItemClicked: (element: VideoPlaylistElement) => void | ||
119 | } | ||
120 | |||
108 | type WebtorrentPluginOptions = { | 121 | type WebtorrentPluginOptions = { |
109 | playerElement: HTMLVideoElement | 122 | playerElement: HTMLVideoElement |
110 | 123 | ||
@@ -125,6 +138,8 @@ type P2PMediaLoaderPluginOptions = { | |||
125 | } | 138 | } |
126 | 139 | ||
127 | type VideoJSPluginOptions = { | 140 | type 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 | ||
188 | type PlaylistItemOptions = { | ||
189 | element: VideoPlaylistElement | ||
190 | |||
191 | onClicked: Function | ||
192 | } | ||
193 | |||
173 | export { | 194 | export { |
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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PlaylistPluginOptions } from '../peertube-videojs-typings' | ||
3 | import { PlaylistMenu } from './playlist-menu' | ||
4 | |||
5 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
6 | |||
7 | class 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 | |||
59 | videojs.registerComponent('PlaylistButton', PlaylistButton) | ||
60 | |||
61 | export { 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { VideoPlaylistElement } from '@shared/models' | ||
3 | import { PlaylistItemOptions } from '../peertube-videojs-typings' | ||
4 | |||
5 | const Component = videojs.getComponent('Component') | ||
6 | |||
7 | class 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 | |||
96 | Component.registerComponent('PlaylistMenuItem', PlaylistMenuItem) | ||
97 | |||
98 | export { 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { VideoPlaylistElement } from '@shared/models' | ||
3 | import { PlaylistPluginOptions } from '../peertube-videojs-typings' | ||
4 | import { PlaylistMenuItem } from './playlist-menu-item' | ||
5 | |||
6 | const Component = videojs.getComponent('Component') | ||
7 | |||
8 | class 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 | |||
122 | Component.registerComponent('PlaylistMenu', PlaylistMenu) | ||
123 | |||
124 | export { 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PlaylistPluginOptions } from '../peertube-videojs-typings' | ||
3 | import { PlaylistButton } from './playlist-button' | ||
4 | import { PlaylistMenu } from './playlist-menu' | ||
5 | |||
6 | const Plugin = videojs.getPlugin('plugin') | ||
7 | |||
8 | class 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 | |||
34 | videojs.registerPlugin('playlist', PlaylistPlugin) | ||
35 | export { 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 | ||