diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-31 15:57:32 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-08-01 09:11:04 +0200 |
commit | bfbd912886eba17b4aa9a40dcef2fddc685d85bf (patch) | |
tree | 85e0f22980210a8ccd0888eb5e1790b152074677 /client | |
parent | 85394ba22a07bde1dfccebf3f591a5d6dbe9df56 (diff) | |
download | PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.tar.gz PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.tar.zst PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.zip |
Fix broken playlist api
Diffstat (limited to 'client')
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' | |||
3 | import { AuthService } from '../../core/auth' | 3 | import { AuthService } from '../../core/auth' |
4 | import { ConfirmService } from '../../core/confirm' | 4 | import { ConfirmService } from '../../core/confirm' |
5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
6 | import { Video } from '@app/shared/video/video.model' | 6 | import { Subscription } from 'rxjs' |
7 | import { Subject, Subscription } from 'rxjs' | ||
8 | import { ActivatedRoute } from '@angular/router' | 7 | import { ActivatedRoute } from '@angular/router' |
9 | import { VideoService } from '@app/shared/video/video.service' | ||
10 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 8 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
11 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 9 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' | 11 | import { CdkDragDrop } from '@angular/cdk/drag-drop' |
14 | import { throttleTime } from 'rxjs/operators' | 12 | import { 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 | }) |
21 | export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { | 19 | export 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 | |||
5 | my-video-thumbnail { | 8 | my-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 | |||
18 | my-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 @@ | |||
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' | 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' |
2 | import { Video } from '@app/shared/video/video.model' | 2 | import { Video } from '@app/shared/video/video.model' |
3 | import { VideoPlaylistElementUpdate } from '@shared/models' | 3 | import { VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models' |
4 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist. | |||
9 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | 9 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' |
10 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 10 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
11 | import { secondsToTime } from '../../../assets/player/utils' | 11 | import { secondsToTime } from '../../../assets/player/utils' |
12 | import { 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 @@ | |||
1 | import { VideoPlaylistElement as ServerVideoPlaylistElement, VideoPlaylistElementType } from '../../../../../shared/models/videos' | ||
2 | import { Video } from '@app/shared/video/video.model' | ||
3 | |||
4 | export 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' | |||
18 | import { RestService } from '@app/shared/rest' | 18 | import { RestService } from '@app/shared/rest' |
19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | 19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' |
20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' | 20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' |
21 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
22 | import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model' | ||
23 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
21 | 24 | ||
22 | @Injectable() | 25 | @Injectable() |
23 | export class VideoPlaylistService { | 26 | export 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 @@ | |||
1 | import { User } from '../' | 1 | import { User } from '../' |
2 | import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' | 2 | import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' |
3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
4 | import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' | 4 | import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' |
5 | import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' | 5 | import { 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' | |||
31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' | 31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' |
32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
33 | import { I18n } from '@ngx-translate/i18n-polyfill' | 33 | import { I18n } from '@ngx-translate/i18n-polyfill' |
34 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
35 | 34 | ||
36 | export interface VideosProvider { | 35 | export 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 2 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
3 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 3 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
4 | import { Video } from '@app/shared/video/video.model' | ||
5 | import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' | 4 | import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' |
6 | import { VideoService } from '@app/shared/video/video.service' | ||
7 | import { Router } from '@angular/router' | 5 | import { Router } from '@angular/router' |
8 | import { AuthService } from '@app/core' | 6 | import { AuthService } from '@app/core' |
7 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
8 | import { 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 |