aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html7
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss5
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html6
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts66
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.ts8
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html100
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss32
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts61
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element.model.ts24
-rw-r--r--client/src/app/shared/video-playlist/video-playlist.service.ts46
-rw-r--r--client/src/app/shared/video/video.model.ts6
-rw-r--r--client/src/app/shared/video/video.service.ts18
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.html6
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.scss5
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.ts43
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/assets/player/peertube-player-manager.ts6
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts3
-rw-r--r--client/src/standalone/videos/embed.ts2
19 files changed, 263 insertions, 183 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index cb23bb522..ea5f61b18 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -18,10 +18,13 @@
18 <div *ngIf="getVideosOf(videoChannel)" class="videos"> 18 <div *ngIf="getVideosOf(videoChannel)" class="videos">
19 <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div> 19 <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div>
20 20
21 <my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> 21 <my-video-miniature
22 *ngFor="let video of getVideosOf(videoChannel)"
23 [video]="video" [user]="user" [displayVideoActions]="true"
24 ></my-video-miniature>
22 </div> 25 </div>
23 26
24 <a class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> 27 <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)">
25 Show this channel 28 Show this channel
26 </a> 29 </a>
27 </div> 30 </div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 98931f0c2..7f7652460 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -23,6 +23,11 @@
23 height: 50px; 23 height: 50px;
24 } 24 }
25 } 25 }
26
27 my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown {
28 // Fix our overflow
29 position: absolute;
30 }
26} 31}
27 32
28 33
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html
index 284694b7f..4de4e69da 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html
@@ -14,10 +14,10 @@
14 class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" 14 class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
15 cdkDropList (cdkDropListDropped)="drop($event)" 15 cdkDropList (cdkDropListDropped)="drop($event)"
16 > 16 >
17 <div class="video" *ngFor="let video of videos; trackBy: trackByFn" cdkDrag (cdkDragMoved)="onDragMove($event)"> 17 <div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag>
18 <my-video-playlist-element-miniature 18 <my-video-playlist-element-miniature
19 [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)" 19 [playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
20 [position]="video.playlistElement.position" 20 [position]="playlistElement.position"
21 > 21 >
22 </my-video-playlist-element-miniature> 22 </my-video-playlist-element-miniature>
23 </div> 23 </div>
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
index d5122aeba..6434b9e50 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts
@@ -3,15 +3,13 @@ import { Notifier, ServerService } from '@app/core'
3import { AuthService } from '../../core/auth' 3import { AuthService } from '../../core/auth'
4import { ConfirmService } from '../../core/confirm' 4import { ConfirmService } from '../../core/confirm'
5import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 5import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
6import { Video } from '@app/shared/video/video.model' 6import { Subscription } from 'rxjs'
7import { Subject, Subscription } from 'rxjs'
8import { ActivatedRoute } from '@angular/router' 7import { ActivatedRoute } from '@angular/router'
9import { VideoService } from '@app/shared/video/video.service'
10import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 8import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
11import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 9import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
12import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
13import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' 11import { CdkDragDrop } from '@angular/cdk/drag-drop'
14import { throttleTime } from 'rxjs/operators' 12import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
15 13
16@Component({ 14@Component({
17 selector: 'my-account-video-playlist-elements', 15 selector: 'my-account-video-playlist-elements',
@@ -19,7 +17,7 @@ import { throttleTime } from 'rxjs/operators'
19 styleUrls: [ './my-account-video-playlist-elements.component.scss' ] 17 styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
20}) 18})
21export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { 19export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
22 videos: Video[] = [] 20 playlistElements: VideoPlaylistElement[] = []
23 playlist: VideoPlaylist 21 playlist: VideoPlaylist
24 22
25 pagination: ComponentPagination = { 23 pagination: ComponentPagination = {
@@ -30,7 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
30 28
31 private videoPlaylistId: string | number 29 private videoPlaylistId: string | number
32 private paramsSub: Subscription 30 private paramsSub: Subscription
33 private dragMoveSubject = new Subject<number>()
34 31
35 constructor ( 32 constructor (
36 private authService: AuthService, 33 private authService: AuthService,
@@ -39,7 +36,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
39 private confirmService: ConfirmService, 36 private confirmService: ConfirmService,
40 private route: ActivatedRoute, 37 private route: ActivatedRoute,
41 private i18n: I18n, 38 private i18n: I18n,
42 private videoService: VideoService,
43 private videoPlaylistService: VideoPlaylistService 39 private videoPlaylistService: VideoPlaylistService
44 ) {} 40 ) {}
45 41
@@ -50,10 +46,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
50 46
51 this.loadPlaylistInfo() 47 this.loadPlaylistInfo()
52 }) 48 })
53
54 this.dragMoveSubject.asObservable()
55 .pipe(throttleTime(200))
56 .subscribe(y => this.checkScroll(y))
57 } 49 }
58 50
59 ngOnDestroy () { 51 ngOnDestroy () {
@@ -66,8 +58,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
66 58
67 if (previousIndex === newIndex) return 59 if (previousIndex === newIndex) return
68 60
69 const oldPosition = this.videos[previousIndex].playlistElement.position 61 const oldPosition = this.playlistElements[previousIndex].position
70 let insertAfter = this.videos[newIndex].playlistElement.position 62 let insertAfter = this.playlistElements[newIndex].position
71 63
72 if (oldPosition > insertAfter) insertAfter-- 64 if (oldPosition > insertAfter) insertAfter--
73 65
@@ -78,42 +70,16 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
78 err => this.notifier.error(err.message) 70 err => this.notifier.error(err.message)
79 ) 71 )
80 72
81 const video = this.videos[previousIndex] 73 const element = this.playlistElements[previousIndex]
82 74
83 this.videos.splice(previousIndex, 1) 75 this.playlistElements.splice(previousIndex, 1)
84 this.videos.splice(newIndex, 0, video) 76 this.playlistElements.splice(newIndex, 0, element)
85 77
86 this.reorderClientPositions() 78 this.reorderClientPositions()
87 } 79 }
88 80
89 onDragMove (event: CdkDragMove<any>) { 81 onElementRemoved (element: VideoPlaylistElement) {
90 this.dragMoveSubject.next(event.pointerPosition.y) 82 this.playlistElements = this.playlistElements.filter(v => v.id !== element.id)
91 }
92
93 checkScroll (pointerY: number) {
94 // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed
95 // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented
96 // if (pointerY < 150) {
97 // window.scrollBy({
98 // left: 0,
99 // top: -20,
100 // behavior: 'smooth'
101 // })
102 //
103 // return
104 // }
105 //
106 // if (window.innerHeight - pointerY <= 50) {
107 // window.scrollBy({
108 // left: 0,
109 // top: 20,
110 // behavior: 'smooth'
111 // })
112 // }
113 }
114
115 onElementRemoved (video: Video) {
116 this.videos = this.videos.filter(v => v.id !== video.id)
117 this.reorderClientPositions() 83 this.reorderClientPositions()
118 } 84 }
119 85
@@ -125,14 +91,14 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
125 this.loadElements() 91 this.loadElements()
126 } 92 }
127 93
128 trackByFn (index: number, elem: Video) { 94 trackByFn (index: number, elem: VideoPlaylistElement) {
129 return elem.id 95 return elem.id
130 } 96 }
131 97
132 private loadElements () { 98 private loadElements () {
133 this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) 99 this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
134 .subscribe(({ total, data }) => { 100 .subscribe(({ total, data }) => {
135 this.videos = this.videos.concat(data) 101 this.playlistElements = this.playlistElements.concat(data)
136 this.pagination.totalItems = total 102 this.pagination.totalItems = total
137 }) 103 })
138 } 104 }
@@ -147,8 +113,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
147 private reorderClientPositions () { 113 private reorderClientPositions () {
148 let i = 1 114 let i = 1
149 115
150 for (const video of this.videos) { 116 for (const element of this.playlistElements) {
151 video.playlistElement.position = i 117 element.position = i
152 i++ 118 i++
153 } 119 }
154 } 120 }
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
index c6cff03a4..08ceb21bc 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
@@ -37,6 +37,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
37 } 37 }
38 displayOptions = false 38 displayOptions = false
39 39
40 private playlistElementId: number
41
40 constructor ( 42 constructor (
41 protected formValidatorService: FormValidatorService, 43 protected formValidatorService: FormValidatorService,
42 private authService: AuthService, 44 private authService: AuthService,
@@ -96,6 +98,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
96 startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, 98 startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined,
97 stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined 99 stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined
98 }) 100 })
101
102 this.playlistElementId = existingPlaylist ? existingPlaylist.playlistElementId : undefined
99 } 103 }
100 104
101 this.cd.markForCheck() 105 this.cd.markForCheck()
@@ -177,7 +181,9 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
177 } 181 }
178 182
179 private removeVideoFromPlaylist (playlist: PlaylistSummary) { 183 private removeVideoFromPlaylist (playlist: PlaylistSummary) {
180 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.video.id) 184 if (!this.playlistElementId) return
185
186 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.playlistElementId)
181 .subscribe( 187 .subscribe(
182 () => { 188 () => {
183 this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) 189 this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName }))
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html
index ab5a78928..25d4783fb 100644
--- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html
@@ -6,66 +6,82 @@
6 </div> 6 </div>
7 7
8 <my-video-thumbnail 8 <my-video-thumbnail
9 [video]="video" [nsfw]="isVideoBlur(video)" 9 *ngIf="playlistElement.video"
10 [video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)"
10 [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" 11 [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
11 ></my-video-thumbnail> 12 ></my-video-thumbnail>
12 13
14 <div class="fake-thumbnail" *ngIf="!playlistElement.video"></div>
15
13 <div class="video-info"> 16 <div class="video-info">
14 <a tabindex="-1" class="video-info-name" 17 <ng-container *ngIf="playlistElement.video">
15 [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" 18 <a tabindex="-1" class="video-info-name"
16 [attr.title]="video.name" 19 [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
17 >{{ video.name }}</a> 20 [attr.title]="playlistElement.video.name"
21 >{{ playlistElement.video.name }}</a>
22
23 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]">
24 {{ playlistElement.video.byAccount }}
25 </a>
26 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
18 27
19 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> 28 <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(playlistElement) }}</span>
20 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span> 29 </ng-container>
21 30
22 <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video) }}</span> 31 <span *ngIf="!playlistElement.video" class="video-info-name">
32 <ng-container i18n *ngIf="isUnavailable(playlistElement)">Unavailable</ng-container>
33 <ng-container i18n *ngIf="isPrivate(playlistElement)">Private</ng-container>
34 <ng-container i18n *ngIf="isDeleted(playlistElement)">Deleted</ng-container>
35 </span>
23 </div> 36 </div>
24 </a> 37 </a>
25 38
26 <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()" 39 <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right"
27 autoClose="outside"> 40 (openChange)="onDropdownOpenChange()" autoClose="outside"
41 >
28 <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> 42 <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
29 43
30 <div ngbDropdownMenu> 44 <div ngbDropdownMenu>
31 <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)"> 45 <ng-container *ngIf="playlistElement.video">
32 <my-global-icon iconName="edit"></my-global-icon> 46 <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, playlistElement)">
33 <ng-container i18n>Edit starts/stops at</ng-container> 47 <my-global-icon iconName="edit"></my-global-icon>
34 </div> 48 <ng-container i18n>Edit starts/stops at</ng-container>
49 </div>
35 50
36 <div class="timestamp-options" *ngIf="displayTimestampOptions"> 51 <div class="timestamp-options" *ngIf="displayTimestampOptions">
37 <div> 52 <div>
38 <my-peertube-checkbox 53 <my-peertube-checkbox
39 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" 54 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
40 i18n-labelText labelText="Start at" 55 i18n-labelText labelText="Start at"
41 ></my-peertube-checkbox> 56 ></my-peertube-checkbox>
42 57
43 <my-timestamp-input 58 <my-timestamp-input
44 [timestamp]="timestampOptions.startTimestamp" 59 [timestamp]="timestampOptions.startTimestamp"
45 [maxTimestamp]="video.duration" 60 [maxTimestamp]="playlistElement.video.duration"
46 [disabled]="!timestampOptions.startTimestampEnabled" 61 [disabled]="!timestampOptions.startTimestampEnabled"
47 [(ngModel)]="timestampOptions.startTimestamp" 62 [(ngModel)]="timestampOptions.startTimestamp"
48 ></my-timestamp-input> 63 ></my-timestamp-input>
49 </div> 64 </div>
50 65
51 <div> 66 <div>
52 <my-peertube-checkbox 67 <my-peertube-checkbox
53 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" 68 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
54 i18n-labelText labelText="Stop at" 69 i18n-labelText labelText="Stop at"
55 ></my-peertube-checkbox> 70 ></my-peertube-checkbox>
56 71
57 <my-timestamp-input 72 <my-timestamp-input
58 [timestamp]="timestampOptions.stopTimestamp" 73 [timestamp]="timestampOptions.stopTimestamp"
59 [maxTimestamp]="video.duration" 74 [maxTimestamp]="playlistElement.video.duration"
60 [disabled]="!timestampOptions.stopTimestampEnabled" 75 [disabled]="!timestampOptions.stopTimestampEnabled"
61 [(ngModel)]="timestampOptions.stopTimestamp" 76 [(ngModel)]="timestampOptions.stopTimestamp"
62 ></my-timestamp-input> 77 ></my-timestamp-input>
63 </div> 78 </div>
64 79
65 <input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)"> 80 <input type="submit" i18n-value value="Save" (click)="updateTimestamps(playlistElement)">
66 </div> 81 </div>
82 </ng-container>
67 83
68 <span class="dropdown-item" (click)="removeFromPlaylist(video)"> 84 <span class="dropdown-item" (click)="removeFromPlaylist(playlistElement)">
69 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container> 85 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container>
70 </span> 86 </span>
71 </div> 87 </div>
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
index cb7072d7f..9f4061b02 100644
--- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
@@ -2,9 +2,21 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5$thumbnail-width: 130px;
6$thumbnail-height: 72px;
7
5my-video-thumbnail { 8my-video-thumbnail {
6 @include thumbnail-size-component(130px, 72px); 9 @include thumbnail-size-component($thumbnail-width, $thumbnail-height);
10}
7 11
12.fake-thumbnail {
13 width: $thumbnail-width;
14 height: $thumbnail-height;
15 background-color: #ececec;
16}
17
18my-video-thumbnail,
19.fake-thumbnail {
8 display: flex; // Avoids an issue with line-height that adds space below the element 20 display: flex; // Avoids an issue with line-height that adds space below the element
9 margin-right: 10px; 21 margin-right: 10px;
10} 22}
@@ -31,6 +43,7 @@ my-video-thumbnail {
31 a { 43 a {
32 @include disable-default-a-behaviour; 44 @include disable-default-a-behaviour;
33 45
46 color: var(--mainForegroundColor);
34 display: flex; 47 display: flex;
35 min-width: 0; 48 min-width: 0;
36 align-items: center; 49 align-items: center;
@@ -58,7 +71,6 @@ my-video-thumbnail {
58 min-width: 0; 71 min-width: 0;
59 72
60 a { 73 a {
61 color: var(--mainForegroundColor);
62 width: auto; 74 width: auto;
63 75
64 &:hover { 76 &:hover {
@@ -66,20 +78,20 @@ my-video-thumbnail {
66 } 78 }
67 } 79 }
68 80
69 .video-info-name {
70 font-size: 18px;
71 font-weight: $font-semibold;
72 display: inline-block;
73
74 @include ellipsis;
75 }
76
77 .video-info-account, .video-info-timestamp { 81 .video-info-account, .video-info-timestamp {
78 color: $grey-foreground-color; 82 color: $grey-foreground-color;
79 } 83 }
80 } 84 }
81 } 85 }
82 86
87 .video-info-name {
88 font-size: 18px;
89 font-weight: $font-semibold;
90 display: inline-block;
91
92 @include ellipsis;
93 }
94
83 .more { 95 .more {
84 justify-self: flex-end; 96 justify-self: flex-end;
85 margin-left: auto; 97 margin-left: auto;
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
index 62cf6536d..a8e5a4885 100644
--- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts
@@ -1,6 +1,6 @@
1import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' 1import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
2import { Video } from '@app/shared/video/video.model' 2import { Video } from '@app/shared/video/video.model'
3import { VideoPlaylistElementUpdate } from '@shared/models' 3import { VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models'
4import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' 4import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.
9import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' 9import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
10import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 10import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
11import { secondsToTime } from '../../../assets/player/utils' 11import { secondsToTime } from '../../../assets/player/utils'
12import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
12 13
13@Component({ 14@Component({
14 selector: 'my-video-playlist-element-miniature', 15 selector: 'my-video-playlist-element-miniature',
@@ -20,14 +21,14 @@ export class VideoPlaylistElementMiniatureComponent {
20 @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown 21 @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown
21 22
22 @Input() playlist: VideoPlaylist 23 @Input() playlist: VideoPlaylist
23 @Input() video: Video 24 @Input() playlistElement: VideoPlaylistElement
24 @Input() owned = false 25 @Input() owned = false
25 @Input() playing = false 26 @Input() playing = false
26 @Input() rowLink = false 27 @Input() rowLink = false
27 @Input() accountLink = true 28 @Input() accountLink = true
28 @Input() position: number 29 @Input() position: number // Keep this property because we're in the OnPush change detection strategy
29 30
30 @Output() elementRemoved = new EventEmitter<Video>() 31 @Output() elementRemoved = new EventEmitter<VideoPlaylistElement>()
31 32
32 displayTimestampOptions = false 33 displayTimestampOptions = false
33 34
@@ -50,6 +51,18 @@ export class VideoPlaylistElementMiniatureComponent {
50 private cdr: ChangeDetectorRef 51 private cdr: ChangeDetectorRef
51 ) {} 52 ) {}
52 53
54 isUnavailable (e: VideoPlaylistElement) {
55 return e.type === VideoPlaylistElementType.UNAVAILABLE
56 }
57
58 isPrivate (e: VideoPlaylistElement) {
59 return e.type === VideoPlaylistElementType.PRIVATE
60 }
61
62 isDeleted (e: VideoPlaylistElement) {
63 return e.type === VideoPlaylistElementType.DELETED
64 }
65
53 buildRouterLink () { 66 buildRouterLink () {
54 if (!this.playlist) return null 67 if (!this.playlist) return null
55 68
@@ -57,12 +70,12 @@ export class VideoPlaylistElementMiniatureComponent {
57 } 70 }
58 71
59 buildRouterQuery () { 72 buildRouterQuery () {
60 if (!this.video) return {} 73 if (!this.playlistElement || !this.playlistElement.video) return {}
61 74
62 return { 75 return {
63 videoId: this.video.uuid, 76 videoId: this.playlistElement.video.uuid,
64 start: this.video.playlistElement.startTimestamp, 77 start: this.playlistElement.startTimestamp,
65 stop: this.video.playlistElement.stopTimestamp 78 stop: this.playlistElement.stopTimestamp
66 } 79 }
67 } 80 }
68 81
@@ -70,13 +83,13 @@ export class VideoPlaylistElementMiniatureComponent {
70 return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) 83 return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
71 } 84 }
72 85
73 removeFromPlaylist (video: Video) { 86 removeFromPlaylist (playlistElement: VideoPlaylistElement) {
74 this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id) 87 this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, playlistElement.id)
75 .subscribe( 88 .subscribe(
76 () => { 89 () => {
77 this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) 90 this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
78 91
79 this.elementRemoved.emit(this.video) 92 this.elementRemoved.emit(playlistElement)
80 }, 93 },
81 94
82 err => this.notifier.error(err.message) 95 err => this.notifier.error(err.message)
@@ -85,19 +98,19 @@ export class VideoPlaylistElementMiniatureComponent {
85 this.moreDropdown.close() 98 this.moreDropdown.close()
86 } 99 }
87 100
88 updateTimestamps (video: Video) { 101 updateTimestamps (playlistElement: VideoPlaylistElement) {
89 const body: VideoPlaylistElementUpdate = {} 102 const body: VideoPlaylistElementUpdate = {}
90 103
91 body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null 104 body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
92 body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null 105 body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
93 106
94 this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body) 107 this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, playlistElement.id, body)
95 .subscribe( 108 .subscribe(
96 () => { 109 () => {
97 this.notifier.success(this.i18n('Timestamps updated')) 110 this.notifier.success(this.i18n('Timestamps updated'))
98 111
99 video.playlistElement.startTimestamp = body.startTimestamp 112 playlistElement.startTimestamp = body.startTimestamp
100 video.playlistElement.stopTimestamp = body.stopTimestamp 113 playlistElement.stopTimestamp = body.stopTimestamp
101 114
102 this.cdr.detectChanges() 115 this.cdr.detectChanges()
103 }, 116 },
@@ -108,9 +121,9 @@ export class VideoPlaylistElementMiniatureComponent {
108 this.moreDropdown.close() 121 this.moreDropdown.close()
109 } 122 }
110 123
111 formatTimestamp (video: Video) { 124 formatTimestamp (playlistElement: VideoPlaylistElement) {
112 const start = video.playlistElement.startTimestamp 125 const start = playlistElement.startTimestamp
113 const stop = video.playlistElement.stopTimestamp 126 const stop = playlistElement.stopTimestamp
114 127
115 const startFormatted = secondsToTime(start, true, ':') 128 const startFormatted = secondsToTime(start, true, ':')
116 const stopFormatted = secondsToTime(stop, true, ':') 129 const stopFormatted = secondsToTime(stop, true, ':')
@@ -127,7 +140,7 @@ export class VideoPlaylistElementMiniatureComponent {
127 this.displayTimestampOptions = false 140 this.displayTimestampOptions = false
128 } 141 }
129 142
130 toggleDisplayTimestampsOptions (event: Event, video: Video) { 143 toggleDisplayTimestampsOptions (event: Event, playlistElement: VideoPlaylistElement) {
131 event.preventDefault() 144 event.preventDefault()
132 145
133 this.displayTimestampOptions = !this.displayTimestampOptions 146 this.displayTimestampOptions = !this.displayTimestampOptions
@@ -137,17 +150,17 @@ export class VideoPlaylistElementMiniatureComponent {
137 startTimestampEnabled: false, 150 startTimestampEnabled: false,
138 stopTimestampEnabled: false, 151 stopTimestampEnabled: false,
139 startTimestamp: 0, 152 startTimestamp: 0,
140 stopTimestamp: video.duration 153 stopTimestamp: playlistElement.video.duration
141 } 154 }
142 155
143 if (video.playlistElement.startTimestamp) { 156 if (playlistElement.startTimestamp) {
144 this.timestampOptions.startTimestampEnabled = true 157 this.timestampOptions.startTimestampEnabled = true
145 this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp 158 this.timestampOptions.startTimestamp = playlistElement.startTimestamp
146 } 159 }
147 160
148 if (video.playlistElement.stopTimestamp) { 161 if (playlistElement.stopTimestamp) {
149 this.timestampOptions.stopTimestampEnabled = true 162 this.timestampOptions.stopTimestampEnabled = true
150 this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp 163 this.timestampOptions.stopTimestamp = playlistElement.stopTimestamp
151 } 164 }
152 } 165 }
153 166
diff --git a/client/src/app/shared/video-playlist/video-playlist-element.model.ts b/client/src/app/shared/video-playlist/video-playlist-element.model.ts
new file mode 100644
index 000000000..f1c46d1eb
--- /dev/null
+++ b/client/src/app/shared/video-playlist/video-playlist-element.model.ts
@@ -0,0 +1,24 @@
1import { VideoPlaylistElement as ServerVideoPlaylistElement, VideoPlaylistElementType } from '../../../../../shared/models/videos'
2import { Video } from '@app/shared/video/video.model'
3
4export class VideoPlaylistElement implements ServerVideoPlaylistElement {
5 id: number
6 position: number
7 startTimestamp: number
8 stopTimestamp: number
9
10 type: VideoPlaylistElementType
11
12 video?: Video
13
14 constructor (hash: ServerVideoPlaylistElement, translations: {}) {
15 this.id = hash.id
16 this.position = hash.position
17 this.startTimestamp = hash.startTimestamp
18 this.stopTimestamp = hash.stopTimestamp
19
20 this.type = hash.type
21
22 if (hash.video) this.video = new Video(hash.video, translations)
23 }
24}
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts
index da7437507..b93a19356 100644
--- a/client/src/app/shared/video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/video-playlist/video-playlist.service.ts
@@ -18,6 +18,9 @@ import { Account } from '@app/shared/account/account.model'
18import { RestService } from '@app/shared/rest' 18import { RestService } from '@app/shared/rest'
19import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' 19import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
20import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' 20import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model'
21import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
22import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model'
23import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
21 24
22@Injectable() 25@Injectable()
23export class VideoPlaylistService { 26export class VideoPlaylistService {
@@ -110,16 +113,16 @@ export class VideoPlaylistService {
110 ) 113 )
111 } 114 }
112 115
113 updateVideoOfPlaylist (playlistId: number, videoId: number, body: VideoPlaylistElementUpdate) { 116 updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate) {
114 return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId, body) 117 return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body)
115 .pipe( 118 .pipe(
116 map(this.restExtractor.extractDataBool), 119 map(this.restExtractor.extractDataBool),
117 catchError(err => this.restExtractor.handleError(err)) 120 catchError(err => this.restExtractor.handleError(err))
118 ) 121 )
119 } 122 }
120 123
121 removeVideoFromPlaylist (playlistId: number, videoId: number) { 124 removeVideoFromPlaylist (playlistId: number, playlistElementId: number) {
122 return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId) 125 return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId)
123 .pipe( 126 .pipe(
124 map(this.restExtractor.extractDataBool), 127 map(this.restExtractor.extractDataBool),
125 catchError(err => this.restExtractor.handleError(err)) 128 catchError(err => this.restExtractor.handleError(err))
@@ -139,6 +142,24 @@ export class VideoPlaylistService {
139 ) 142 )
140 } 143 }
141 144
145 getPlaylistVideos (
146 videoPlaylistId: number | string,
147 componentPagination: ComponentPagination
148 ): Observable<ResultList<VideoPlaylistElement>> {
149 const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos'
150 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
151
152 let params = new HttpParams()
153 params = this.restService.addRestGetParams(params, pagination)
154
155 return this.authHttp
156 .get<ResultList<ServerVideoPlaylistElement>>(path, { params })
157 .pipe(
158 switchMap(res => this.extractVideoPlaylistElements(res)),
159 catchError(err => this.restExtractor.handleError(err))
160 )
161 }
162
142 doesVideoExistInPlaylist (videoId: number) { 163 doesVideoExistInPlaylist (videoId: number) {
143 this.videoExistsInPlaylistSubject.next(videoId) 164 this.videoExistsInPlaylistSubject.next(videoId)
144 165
@@ -167,6 +188,23 @@ export class VideoPlaylistService {
167 .pipe(map(translations => new VideoPlaylist(playlist, translations))) 188 .pipe(map(translations => new VideoPlaylist(playlist, translations)))
168 } 189 }
169 190
191 extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) {
192 return this.serverService.localeObservable
193 .pipe(
194 map(translations => {
195 const elementsJson = result.data
196 const total = result.total
197 const elements: VideoPlaylistElement[] = []
198
199 for (const elementJson of elementsJson) {
200 elements.push(new VideoPlaylistElement(elementJson, translations))
201 }
202
203 return { total, data: elements }
204 })
205 )
206 }
207
170 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { 208 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
171 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' 209 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
172 let params = new HttpParams() 210 let params = new HttpParams()
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 6f9de9241..fb98d5382 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -1,5 +1,5 @@
1import { User } from '../' 1import { User } from '../'
2import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' 2import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
3import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 3import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
4import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' 4import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
5import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' 5import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
@@ -48,8 +48,6 @@ export class Video implements VideoServerModel {
48 blacklisted?: boolean 48 blacklisted?: boolean
49 blacklistedReason?: string 49 blacklistedReason?: string
50 50
51 playlistElement?: PlaylistElement
52
53 account: { 51 account: {
54 id: number 52 id: number
55 name: string 53 name: string
@@ -126,8 +124,6 @@ export class Video implements VideoServerModel {
126 this.blacklistedReason = hash.blacklistedReason 124 this.blacklistedReason = hash.blacklistedReason
127 125
128 this.userHistory = hash.userHistory 126 this.userHistory = hash.userHistory
129
130 this.playlistElement = hash.playlistElement
131 } 127 }
132 128
133 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 129 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index 114b014ad..45366e3e3 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -31,7 +31,6 @@ import { ServerService } from '@app/core'
31import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' 31import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
32import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 32import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
33import { I18n } from '@ngx-translate/i18n-polyfill' 33import { I18n } from '@ngx-translate/i18n-polyfill'
34import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
35 34
36export interface VideosProvider { 35export interface VideosProvider {
37 getVideos (parameters: { 36 getVideos (parameters: {
@@ -172,23 +171,6 @@ export class VideoService implements VideosProvider {
172 ) 171 )
173 } 172 }
174 173
175 getPlaylistVideos (
176 videoPlaylistId: number | string,
177 videoPagination: ComponentPagination
178 ): Observable<ResultList<Video>> {
179 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
180
181 let params = new HttpParams()
182 params = this.restService.addRestGetParams(params, pagination)
183
184 return this.authHttp
185 .get<ResultList<Video>>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos', { params })
186 .pipe(
187 switchMap(res => this.extractVideos(res)),
188 catchError(err => this.restExtractor.handleError(err))
189 )
190 }
191
192 getUserSubscriptionVideos (parameters: { 174 getUserSubscriptionVideos (parameters: {
193 videoPagination: ComponentPagination, 175 videoPagination: ComponentPagination,
194 sort: VideoSortField 176 sort: VideoSortField
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.html b/client/src/app/videos/+video-watch/video-watch-playlist.component.html
index c168a3130..c89936bd1 100644
--- a/client/src/app/videos/+video-watch/video-watch-playlist.component.html
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.html
@@ -16,10 +16,10 @@
16 </div> 16 </div>
17 </div> 17 </div>
18 18
19 <div *ngFor="let playlistVideo of playlistVideos"> 19 <div *ngFor="let playlistElement of playlistElements">
20 <my-video-playlist-element-miniature 20 <my-video-playlist-element-miniature
21 [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)" 21 [playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
22 [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position" 22 [playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position"
23 ></my-video-playlist-element-miniature> 23 ></my-video-playlist-element-miniature>
24 </div> 24 </div>
25</div> 25</div>
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss
index eeb763bd9..4c24d6b05 100644
--- a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss
@@ -53,6 +53,11 @@
53 my-video-thumbnail { 53 my-video-thumbnail {
54 @include thumbnail-size-component(90px, 50px); 54 @include thumbnail-size-component(90px, 50px);
55 } 55 }
56
57 .fake-thumbnail {
58 width: 90px;
59 height: 50px;
60 }
56 } 61 }
57 } 62 }
58} 63}
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
index 2fb0cb0e5..6e8d58cd8 100644
--- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
@@ -1,11 +1,11 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 2import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
3import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 3import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
4import { Video } from '@app/shared/video/video.model'
5import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' 4import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models'
6import { VideoService } from '@app/shared/video/video.service'
7import { Router } from '@angular/router' 5import { Router } from '@angular/router'
8import { AuthService } from '@app/core' 6import { AuthService } from '@app/core'
7import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
8import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
9 9
10@Component({ 10@Component({
11 selector: 'my-video-watch-playlist', 11 selector: 'my-video-watch-playlist',
@@ -16,7 +16,7 @@ export class VideoWatchPlaylistComponent {
16 @Input() video: VideoDetails 16 @Input() video: VideoDetails
17 @Input() playlist: VideoPlaylist 17 @Input() playlist: VideoPlaylist
18 18
19 playlistVideos: Video[] = [] 19 playlistElements: VideoPlaylistElement[] = []
20 playlistPagination: ComponentPagination = { 20 playlistPagination: ComponentPagination = {
21 currentPage: 1, 21 currentPage: 1,
22 itemsPerPage: 30, 22 itemsPerPage: 30,
@@ -28,7 +28,7 @@ export class VideoWatchPlaylistComponent {
28 28
29 constructor ( 29 constructor (
30 private auth: AuthService, 30 private auth: AuthService,
31 private videoService: VideoService, 31 private videoPlaylist: VideoPlaylistService,
32 private router: Router 32 private router: Router
33 ) {} 33 ) {}
34 34
@@ -40,8 +40,8 @@ export class VideoWatchPlaylistComponent {
40 this.loadPlaylistElements(this.playlist,false) 40 this.loadPlaylistElements(this.playlist,false)
41 } 41 }
42 42
43 onElementRemoved (video: Video) { 43 onElementRemoved (playlistElement: VideoPlaylistElement) {
44 this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id) 44 this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id)
45 45
46 this.playlistPagination.totalItems-- 46 this.playlistPagination.totalItems--
47 } 47 }
@@ -65,12 +65,13 @@ export class VideoWatchPlaylistComponent {
65 } 65 }
66 66
67 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { 67 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
68 this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) 68 this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination)
69 .subscribe(({ total, data }) => { 69 .subscribe(({ total, data }) => {
70 this.playlistVideos = this.playlistVideos.concat(data) 70 this.playlistElements = this.playlistElements.concat(data)
71 this.playlistPagination.totalItems = total 71 this.playlistPagination.totalItems = total
72 72
73 if (total === 0) { 73 const firstAvailableVideos = this.playlistElements.find(e => !!e.video)
74 if (!firstAvailableVideos) {
74 this.noPlaylistVideos = true 75 this.noPlaylistVideos = true
75 return 76 return
76 } 77 }
@@ -79,7 +80,7 @@ export class VideoWatchPlaylistComponent {
79 80
80 if (redirectToFirst) { 81 if (redirectToFirst) {
81 const extras = { 82 const extras = {
82 queryParams: { videoId: this.playlistVideos[ 0 ].uuid }, 83 queryParams: { videoId: firstAvailableVideos.video.uuid },
83 replaceUrl: true 84 replaceUrl: true
84 } 85 }
85 this.router.navigate([], extras) 86 this.router.navigate([], extras)
@@ -88,11 +89,11 @@ export class VideoWatchPlaylistComponent {
88 } 89 }
89 90
90 updatePlaylistIndex (video: VideoDetails) { 91 updatePlaylistIndex (video: VideoDetails) {
91 if (this.playlistVideos.length === 0 || !video) return 92 if (this.playlistElements.length === 0 || !video) return
92 93
93 for (const playlistVideo of this.playlistVideos) { 94 for (const playlistElement of this.playlistElements) {
94 if (playlistVideo.id === video.id) { 95 if (playlistElement.video && playlistElement.video.id === video.id) {
95 this.currentPlaylistPosition = playlistVideo.playlistElement.position 96 this.currentPlaylistPosition = playlistElement.position
96 return 97 return
97 } 98 }
98 } 99 }
@@ -103,11 +104,17 @@ export class VideoWatchPlaylistComponent {
103 104
104 navigateToNextPlaylistVideo () { 105 navigateToNextPlaylistVideo () {
105 if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { 106 if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
106 const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1) 107 const next = this.playlistElements.find(e => e.position === this.currentPlaylistPosition + 1)
107 108
108 const start = next.playlistElement.startTimestamp 109 if (!next || !next.video) {
109 const stop = next.playlistElement.stopTimestamp 110 this.currentPlaylistPosition++
110 this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } }) 111 this.navigateToNextPlaylistVideo()
112 return
113 }
114
115 const start = next.startTimestamp
116 const stop = next.stopTimestamp
117 this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } })
111 } 118 }
112 } 119 }
113} 120}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 0d499d47f..d7c7b7497 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -464,7 +464,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
464 } 464 }
465 465
466 this.zone.runOutsideAngular(async () => { 466 this.zone.runOutsideAngular(async () => {
467 this.player = await PeertubePlayerManager.initialize(mode, options) 467 this.player = await PeertubePlayerManager.initialize(mode, options, player => this.player = player)
468 468
469 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) 469 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
470 470
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 083c621d2..6c8b13087 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -86,6 +86,7 @@ export class PeertubePlayerManager {
86 86
87 private static videojsLocaleCache: { [ path: string ]: any } = {} 87 private static videojsLocaleCache: { [ path: string ]: any } = {}
88 private static playerElementClassName: string 88 private static playerElementClassName: string
89 private static onPlayerChange: (player: any) => void
89 90
90 static getServerTranslations (serverUrl: string, locale: string) { 91 static getServerTranslations (serverUrl: string, locale: string) {
91 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) 92 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
@@ -100,9 +101,10 @@ export class PeertubePlayerManager {
100 }) 101 })
101 } 102 }
102 103
103 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { 104 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) {
104 let p2pMediaLoader: any 105 let p2pMediaLoader: any
105 106
107 this.onPlayerChange = onPlayerChange
106 this.playerElementClassName = options.common.playerElement.className 108 this.playerElementClassName = options.common.playerElement.className
107 109
108 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') 110 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
@@ -171,6 +173,8 @@ export class PeertubePlayerManager {
171 const player = this 173 const player = this
172 174
173 self.addContextMenu(mode, player, options.common.embedUrl) 175 self.addContextMenu(mode, player, options.common.embedUrl)
176
177 PeertubePlayerManager.onPlayerChange(player)
174 }) 178 })
175 } 179 }
176 180
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 78879a2ec..24b7e0c70 100644
--- a/client/src/assets/player/videojs-components/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -43,6 +43,9 @@ class SettingsMenuItem extends MenuItem {
43 player.ready(() => { 43 player.ready(() => {
44 // Voodoo magic for IOS 44 // Voodoo magic for IOS
45 setTimeout(() => { 45 setTimeout(() => {
46 // Player was destroyed
47 if (!this.player_) return
48
46 this.build() 49 this.build()
47 50
48 // Update on rate change 51 // Update on rate change
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cfe8e94b1..6ff3efef1 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -212,7 +212,7 @@ export class PeerTubeEmbed {
212 }) 212 })
213 } 213 }
214 214
215 this.player = await PeertubePlayerManager.initialize(this.mode, options) 215 this.player = await PeertubePlayerManager.initialize(this.mode, options, player => this.player = player)
216 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) 216 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
217 217
218 window[ 'videojsPlayer' ] = this.player 218 window[ 'videojsPlayer' ] = this.player