diff options
Diffstat (limited to 'client/src/assets/player/playlist')
4 files changed, 318 insertions, 0 deletions
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 } | ||