diff options
author | Chocobozzz <me@florianbigard.com> | 2020-06-23 14:10:17 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-06-23 16:00:49 +0200 |
commit | 67ed6552b831df66713bac9e672738796128d33f (patch) | |
tree | 59c97d41e0b49d75a90aa3de987968ab9b1ff447 /client/src/app/shared/video-playlist | |
parent | 0c4bacbff53bc732f5a2677d62a6ead7752e2405 (diff) | |
download | PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.gz PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.zst PeerTube-67ed6552b831df66713bac9e672738796128d33f.zip |
Reorganize client shared modules
Diffstat (limited to 'client/src/app/shared/video-playlist')
12 files changed, 0 insertions, 1584 deletions
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html deleted file mode 100644 index a40e0699e..000000000 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html +++ /dev/null | |||
@@ -1,82 +0,0 @@ | |||
1 | <div class="root"> | ||
2 | <div class="header"> | ||
3 | <div class="first-row"> | ||
4 | <div i18n class="title">Save to</div> | ||
5 | |||
6 | <div class="options" (click)="displayOptions = !displayOptions"> | ||
7 | <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> | ||
8 | |||
9 | <span i18n>Options</span> | ||
10 | </div> | ||
11 | </div> | ||
12 | |||
13 | <div class="options-row" *ngIf="displayOptions"> | ||
14 | <div> | ||
15 | <my-peertube-checkbox | ||
16 | inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" | ||
17 | i18n-labelText labelText="Start at" | ||
18 | ></my-peertube-checkbox> | ||
19 | |||
20 | <my-timestamp-input | ||
21 | [timestamp]="timestampOptions.startTimestamp" | ||
22 | [maxTimestamp]="video.duration" | ||
23 | [disabled]="!timestampOptions.startTimestampEnabled" | ||
24 | [(ngModel)]="timestampOptions.startTimestamp" | ||
25 | ></my-timestamp-input> | ||
26 | </div> | ||
27 | |||
28 | <div> | ||
29 | <my-peertube-checkbox | ||
30 | inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" | ||
31 | i18n-labelText labelText="Stop at" | ||
32 | ></my-peertube-checkbox> | ||
33 | |||
34 | <my-timestamp-input | ||
35 | [timestamp]="timestampOptions.stopTimestamp" | ||
36 | [maxTimestamp]="video.duration" | ||
37 | [disabled]="!timestampOptions.stopTimestampEnabled" | ||
38 | [(ngModel)]="timestampOptions.stopTimestamp" | ||
39 | ></my-timestamp-input> | ||
40 | </div> | ||
41 | </div> | ||
42 | </div> | ||
43 | |||
44 | <div class="input-container"> | ||
45 | <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> | ||
46 | </div> | ||
47 | |||
48 | <div class="playlists"> | ||
49 | <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> | ||
50 | <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> | ||
51 | |||
52 | <div class="display-name"> | ||
53 | {{ playlist.displayName }} | ||
54 | |||
55 | <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> | ||
56 | {{ formatTimestamp(playlist) }} | ||
57 | </div> | ||
58 | </div> | ||
59 | </div> | ||
60 | </div> | ||
61 | |||
62 | <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened"> | ||
63 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | ||
64 | |||
65 | <span i18n>Create a private playlist</span> | ||
66 | </div> | ||
67 | |||
68 | <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form"> | ||
69 | <div class="form-group"> | ||
70 | <label i18n for="displayName">Display name</label> | ||
71 | <input | ||
72 | type="text" id="displayName" | ||
73 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
74 | > | ||
75 | <div *ngIf="formErrors['displayName']" class="form-error"> | ||
76 | {{ formErrors['displayName'] }} | ||
77 | </div> | ||
78 | </div> | ||
79 | |||
80 | <input type="submit" i18n-value value="Create" [disabled]="!form.valid"> | ||
81 | </form> | ||
82 | </div> | ||
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss deleted file mode 100644 index 47baa997b..000000000 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss +++ /dev/null | |||
@@ -1,107 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .header, | ||
5 | .dropdown-item, | ||
6 | .input-container { | ||
7 | padding: 8px 24px; | ||
8 | } | ||
9 | |||
10 | .header { | ||
11 | min-width: 240px; | ||
12 | margin-bottom: 10px; | ||
13 | border-bottom: 1px solid $separator-border-color; | ||
14 | |||
15 | .first-row { | ||
16 | display: flex; | ||
17 | align-items: center; | ||
18 | |||
19 | .title { | ||
20 | font-size: 18px; | ||
21 | flex-grow: 1; | ||
22 | } | ||
23 | |||
24 | .options { | ||
25 | display: flex; | ||
26 | align-items: center; | ||
27 | font-size: 14px; | ||
28 | cursor: pointer; | ||
29 | |||
30 | my-global-icon { | ||
31 | @include apply-svg-color(#333); | ||
32 | |||
33 | width: 16px; | ||
34 | height: 23px; | ||
35 | margin-right: 3px; | ||
36 | } | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .options-row { | ||
41 | margin-top: 10px; | ||
42 | padding-left: 10px; | ||
43 | |||
44 | > div { | ||
45 | display: flex; | ||
46 | align-items: center; | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | |||
51 | .playlists { | ||
52 | max-height: 180px; | ||
53 | overflow-y: auto; | ||
54 | } | ||
55 | |||
56 | .playlist { | ||
57 | display: inline-flex; | ||
58 | cursor: pointer; | ||
59 | |||
60 | my-peertube-checkbox { | ||
61 | margin-right: 10px; | ||
62 | align-self: center; | ||
63 | } | ||
64 | |||
65 | .display-name { | ||
66 | display: flex; | ||
67 | align-items: flex-end; | ||
68 | |||
69 | .timestamp-info { | ||
70 | font-size: 0.9em; | ||
71 | color: pvar(--greyForegroundColor); | ||
72 | margin-left: 5px; | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | |||
77 | .new-playlist-button, | ||
78 | .new-playlist-block { | ||
79 | padding-top: 10px; | ||
80 | border-top: 1px solid $separator-border-color; | ||
81 | } | ||
82 | |||
83 | .new-playlist-button { | ||
84 | cursor: pointer; | ||
85 | |||
86 | my-global-icon { | ||
87 | @include apply-svg-color(#333); | ||
88 | |||
89 | position: relative; | ||
90 | left: -1px; | ||
91 | top: -1px; | ||
92 | margin-right: 4px; | ||
93 | width: 21px; | ||
94 | height: 21px; | ||
95 | } | ||
96 | } | ||
97 | |||
98 | input[type=text] { | ||
99 | @include peertube-input-text(200px); | ||
100 | |||
101 | display: block; | ||
102 | } | ||
103 | |||
104 | input[type=submit] { | ||
105 | @include peertube-button; | ||
106 | @include orange-button; | ||
107 | } | ||
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 deleted file mode 100644 index 0c593a79a..000000000 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts +++ /dev/null | |||
@@ -1,280 +0,0 @@ | |||
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' | ||
2 | import { CachedPlaylist, VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
3 | import { AuthService, Notifier } from '@app/core' | ||
4 | import { Subject, Subscription } from 'rxjs' | ||
5 | import { debounceTime, filter } from 'rxjs/operators' | ||
6 | import { Video, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' | ||
7 | import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/forms' | ||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
9 | import { secondsToTime } from '../../../assets/player/utils' | ||
10 | import * as debug from 'debug' | ||
11 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | ||
12 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | ||
13 | |||
14 | const logger = debug('peertube:playlists:VideoAddToPlaylistComponent') | ||
15 | |||
16 | type PlaylistSummary = { | ||
17 | id: number | ||
18 | inPlaylist: boolean | ||
19 | displayName: string | ||
20 | |||
21 | playlistElementId?: number | ||
22 | startTimestamp?: number | ||
23 | stopTimestamp?: number | ||
24 | } | ||
25 | |||
26 | @Component({ | ||
27 | selector: 'my-video-add-to-playlist', | ||
28 | styleUrls: [ './video-add-to-playlist.component.scss' ], | ||
29 | templateUrl: './video-add-to-playlist.component.html', | ||
30 | changeDetection: ChangeDetectionStrategy.OnPush | ||
31 | }) | ||
32 | export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, OnChanges, OnDestroy, DisableForReuseHook { | ||
33 | @Input() video: Video | ||
34 | @Input() currentVideoTimestamp: number | ||
35 | @Input() lazyLoad = false | ||
36 | |||
37 | isNewPlaylistBlockOpened = false | ||
38 | videoPlaylistSearch: string | ||
39 | videoPlaylistSearchChanged = new Subject<string>() | ||
40 | videoPlaylists: PlaylistSummary[] = [] | ||
41 | timestampOptions: { | ||
42 | startTimestampEnabled: boolean | ||
43 | startTimestamp: number | ||
44 | stopTimestampEnabled: boolean | ||
45 | stopTimestamp: number | ||
46 | } | ||
47 | displayOptions = false | ||
48 | |||
49 | private disabled = false | ||
50 | |||
51 | private listenToPlaylistChangeSub: Subscription | ||
52 | private playlistsData: CachedPlaylist[] = [] | ||
53 | |||
54 | constructor ( | ||
55 | protected formValidatorService: FormValidatorService, | ||
56 | private authService: AuthService, | ||
57 | private notifier: Notifier, | ||
58 | private i18n: I18n, | ||
59 | private videoPlaylistService: VideoPlaylistService, | ||
60 | private videoPlaylistValidatorsService: VideoPlaylistValidatorsService, | ||
61 | private cd: ChangeDetectorRef | ||
62 | ) { | ||
63 | super() | ||
64 | } | ||
65 | |||
66 | get user () { | ||
67 | return this.authService.getUser() | ||
68 | } | ||
69 | |||
70 | ngOnInit () { | ||
71 | this.buildForm({ | ||
72 | displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME | ||
73 | }) | ||
74 | |||
75 | this.videoPlaylistService.listenToMyAccountPlaylistsChange() | ||
76 | .subscribe(result => { | ||
77 | this.playlistsData = result.data | ||
78 | |||
79 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | ||
80 | }) | ||
81 | |||
82 | this.videoPlaylistSearchChanged | ||
83 | .pipe(debounceTime(500)) | ||
84 | .subscribe(() => this.load()) | ||
85 | |||
86 | if (this.lazyLoad === false) this.load() | ||
87 | } | ||
88 | |||
89 | ngOnChanges (simpleChanges: SimpleChanges) { | ||
90 | if (simpleChanges['video']) { | ||
91 | this.reload() | ||
92 | } | ||
93 | } | ||
94 | |||
95 | ngOnDestroy () { | ||
96 | this.unsubscribePlaylistChanges() | ||
97 | } | ||
98 | |||
99 | disableForReuse () { | ||
100 | this.disabled = true | ||
101 | } | ||
102 | |||
103 | enabledForReuse () { | ||
104 | this.disabled = false | ||
105 | } | ||
106 | |||
107 | reload () { | ||
108 | logger('Reloading component') | ||
109 | |||
110 | this.videoPlaylists = [] | ||
111 | this.videoPlaylistSearch = undefined | ||
112 | |||
113 | this.resetOptions(true) | ||
114 | this.load() | ||
115 | |||
116 | this.cd.markForCheck() | ||
117 | } | ||
118 | |||
119 | load () { | ||
120 | logger('Loading component') | ||
121 | |||
122 | this.listenToPlaylistChanges() | ||
123 | |||
124 | this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch) | ||
125 | .subscribe(playlistsResult => { | ||
126 | this.playlistsData = playlistsResult.data | ||
127 | |||
128 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | ||
129 | }) | ||
130 | } | ||
131 | |||
132 | openChange (opened: boolean) { | ||
133 | if (opened === false) { | ||
134 | this.isNewPlaylistBlockOpened = false | ||
135 | this.displayOptions = false | ||
136 | } | ||
137 | } | ||
138 | |||
139 | openCreateBlock (event: Event) { | ||
140 | event.preventDefault() | ||
141 | |||
142 | this.isNewPlaylistBlockOpened = true | ||
143 | } | ||
144 | |||
145 | togglePlaylist (event: Event, playlist: PlaylistSummary) { | ||
146 | event.preventDefault() | ||
147 | |||
148 | if (playlist.inPlaylist === true) { | ||
149 | this.removeVideoFromPlaylist(playlist) | ||
150 | } else { | ||
151 | this.addVideoInPlaylist(playlist) | ||
152 | } | ||
153 | |||
154 | playlist.inPlaylist = !playlist.inPlaylist | ||
155 | this.resetOptions() | ||
156 | |||
157 | this.cd.markForCheck() | ||
158 | } | ||
159 | |||
160 | createPlaylist () { | ||
161 | const displayName = this.form.value[ 'displayName' ] | ||
162 | |||
163 | const videoPlaylistCreate: VideoPlaylistCreate = { | ||
164 | displayName, | ||
165 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
166 | } | ||
167 | |||
168 | this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe( | ||
169 | () => { | ||
170 | this.isNewPlaylistBlockOpened = false | ||
171 | |||
172 | this.cd.markForCheck() | ||
173 | }, | ||
174 | |||
175 | err => this.notifier.error(err.message) | ||
176 | ) | ||
177 | } | ||
178 | |||
179 | resetOptions (resetTimestamp = false) { | ||
180 | this.displayOptions = false | ||
181 | |||
182 | this.timestampOptions = {} as any | ||
183 | this.timestampOptions.startTimestampEnabled = false | ||
184 | this.timestampOptions.stopTimestampEnabled = false | ||
185 | |||
186 | if (resetTimestamp) { | ||
187 | this.timestampOptions.startTimestamp = 0 | ||
188 | this.timestampOptions.stopTimestamp = this.video.duration | ||
189 | } | ||
190 | } | ||
191 | |||
192 | formatTimestamp (playlist: PlaylistSummary) { | ||
193 | const start = playlist.startTimestamp ? secondsToTime(playlist.startTimestamp) : '' | ||
194 | const stop = playlist.stopTimestamp ? secondsToTime(playlist.stopTimestamp) : '' | ||
195 | |||
196 | return `(${start}-${stop})` | ||
197 | } | ||
198 | |||
199 | onVideoPlaylistSearchChanged () { | ||
200 | this.videoPlaylistSearchChanged.next() | ||
201 | } | ||
202 | |||
203 | private removeVideoFromPlaylist (playlist: PlaylistSummary) { | ||
204 | if (!playlist.playlistElementId) return | ||
205 | |||
206 | this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, playlist.playlistElementId, this.video.id) | ||
207 | .subscribe( | ||
208 | () => { | ||
209 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) | ||
210 | }, | ||
211 | |||
212 | err => { | ||
213 | this.notifier.error(err.message) | ||
214 | }, | ||
215 | |||
216 | () => this.cd.markForCheck() | ||
217 | ) | ||
218 | } | ||
219 | |||
220 | private listenToPlaylistChanges () { | ||
221 | this.unsubscribePlaylistChanges() | ||
222 | |||
223 | this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id) | ||
224 | .pipe(filter(() => this.disabled === false)) | ||
225 | .subscribe(existResult => this.rebuildPlaylists(existResult)) | ||
226 | } | ||
227 | |||
228 | private unsubscribePlaylistChanges () { | ||
229 | if (this.listenToPlaylistChangeSub) { | ||
230 | this.listenToPlaylistChangeSub.unsubscribe() | ||
231 | this.listenToPlaylistChangeSub = undefined | ||
232 | } | ||
233 | } | ||
234 | |||
235 | private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { | ||
236 | logger('Got existing results for %d.', this.video.id, existResult) | ||
237 | |||
238 | this.videoPlaylists = [] | ||
239 | for (const playlist of this.playlistsData) { | ||
240 | const existingPlaylist = existResult.find(p => p.playlistId === playlist.id) | ||
241 | |||
242 | this.videoPlaylists.push({ | ||
243 | id: playlist.id, | ||
244 | displayName: playlist.displayName, | ||
245 | inPlaylist: !!existingPlaylist, | ||
246 | playlistElementId: existingPlaylist ? existingPlaylist.playlistElementId : undefined, | ||
247 | startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, | ||
248 | stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined | ||
249 | }) | ||
250 | } | ||
251 | |||
252 | logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists) | ||
253 | |||
254 | this.cd.markForCheck() | ||
255 | } | ||
256 | |||
257 | private addVideoInPlaylist (playlist: PlaylistSummary) { | ||
258 | const body: VideoPlaylistElementCreate = { videoId: this.video.id } | ||
259 | |||
260 | if (this.timestampOptions.startTimestampEnabled) body.startTimestamp = this.timestampOptions.startTimestamp | ||
261 | if (this.timestampOptions.stopTimestampEnabled) body.stopTimestamp = this.timestampOptions.stopTimestamp | ||
262 | |||
263 | this.videoPlaylistService.addVideoInPlaylist(playlist.id, body) | ||
264 | .subscribe( | ||
265 | () => { | ||
266 | const message = body.startTimestamp || body.stopTimestamp | ||
267 | ? this.i18n('Video added in {{n}} at timestamps {{t}}', { n: playlist.displayName, t: this.formatTimestamp(playlist) }) | ||
268 | : this.i18n('Video added in {{n}}', { n: playlist.displayName }) | ||
269 | |||
270 | this.notifier.success(message) | ||
271 | }, | ||
272 | |||
273 | err => { | ||
274 | this.notifier.error(err.message) | ||
275 | }, | ||
276 | |||
277 | () => this.cd.markForCheck() | ||
278 | ) | ||
279 | } | ||
280 | } | ||
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 deleted file mode 100644 index e3f7ef017..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html +++ /dev/null | |||
@@ -1,92 +0,0 @@ | |||
1 | <div class="video" [ngClass]="{ playing: playing }"> | ||
2 | <a [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"> | ||
3 | <div class="position"> | ||
4 | <my-global-icon *ngIf="playing" iconName="play"></my-global-icon> | ||
5 | <ng-container *ngIf="!playing">{{ position }}</ng-container> | ||
6 | </div> | ||
7 | |||
8 | <my-video-thumbnail | ||
9 | *ngIf="playlistElement.video" | ||
10 | [video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)" | ||
11 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" | ||
12 | ></my-video-thumbnail> | ||
13 | |||
14 | <div class="fake-thumbnail" *ngIf="!playlistElement.video"></div> | ||
15 | |||
16 | <div class="video-info"> | ||
17 | <ng-container *ngIf="playlistElement.video"> | ||
18 | <a tabindex="-1" class="video-info-name" | ||
19 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" | ||
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> | ||
27 | |||
28 | <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(playlistElement) }}</span> | ||
29 | </ng-container> | ||
30 | |||
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> | ||
36 | </div> | ||
37 | </a> | ||
38 | |||
39 | <my-edit-button *ngIf="owned && touchScreenEditButton" [routerLink]="[ '/my-account', 'video-playlists', playlist.uuid ]"></my-edit-button> | ||
40 | |||
41 | <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom auto" | ||
42 | (openChange)="onDropdownOpenChange()" autoClose="outside" | ||
43 | > | ||
44 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> | ||
45 | |||
46 | <div ngbDropdownMenu> | ||
47 | <ng-container *ngIf="playlistElement.video"> | ||
48 | <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, playlistElement)"> | ||
49 | <my-global-icon iconName="edit" aria-hidden="true"></my-global-icon> | ||
50 | <ng-container i18n>Edit starts/stops at</ng-container> | ||
51 | </div> | ||
52 | |||
53 | <div class="timestamp-options" *ngIf="displayTimestampOptions"> | ||
54 | <div> | ||
55 | <my-peertube-checkbox | ||
56 | inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" | ||
57 | i18n-labelText labelText="Start at" | ||
58 | ></my-peertube-checkbox> | ||
59 | |||
60 | <my-timestamp-input | ||
61 | [timestamp]="timestampOptions.startTimestamp" | ||
62 | [maxTimestamp]="playlistElement.video.duration" | ||
63 | [disabled]="!timestampOptions.startTimestampEnabled" | ||
64 | [(ngModel)]="timestampOptions.startTimestamp" | ||
65 | ></my-timestamp-input> | ||
66 | </div> | ||
67 | |||
68 | <div> | ||
69 | <my-peertube-checkbox | ||
70 | inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" | ||
71 | i18n-labelText labelText="Stop at" | ||
72 | ></my-peertube-checkbox> | ||
73 | |||
74 | <my-timestamp-input | ||
75 | [timestamp]="timestampOptions.stopTimestamp" | ||
76 | [maxTimestamp]="playlistElement.video.duration" | ||
77 | [disabled]="!timestampOptions.stopTimestampEnabled" | ||
78 | [(ngModel)]="timestampOptions.stopTimestamp" | ||
79 | ></my-timestamp-input> | ||
80 | </div> | ||
81 | |||
82 | <input type="submit" i18n-value value="Save" (click)="updateTimestamps(playlistElement)"> | ||
83 | </div> | ||
84 | </ng-container> | ||
85 | |||
86 | <span class="dropdown-item" (click)="removeFromPlaylist(playlistElement)"> | ||
87 | <my-global-icon iconName="delete" aria-hidden="true"></my-global-icon> | ||
88 | <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container> | ||
89 | </span> | ||
90 | </div> | ||
91 | </div> | ||
92 | </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 deleted file mode 100644 index afd775b25..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss +++ /dev/null | |||
@@ -1,224 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | @import '_miniature'; | ||
4 | |||
5 | $thumbnail-width: 130px; | ||
6 | $thumbnail-height: 72px; | ||
7 | |||
8 | my-video-thumbnail { | ||
9 | @include thumbnail-size-component($thumbnail-width, $thumbnail-height); | ||
10 | } | ||
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 { | ||
20 | display: flex; // Avoids an issue with line-height that adds space below the element | ||
21 | margin-right: 10px; | ||
22 | } | ||
23 | |||
24 | .video { | ||
25 | display: flex; | ||
26 | align-items: center; | ||
27 | background-color: pvar(--mainBackgroundColor); | ||
28 | padding: 10px; | ||
29 | border-bottom: 1px solid $separator-border-color; | ||
30 | |||
31 | &:hover { | ||
32 | background-color: rgba(0, 0, 0, 0.05); | ||
33 | |||
34 | .more { | ||
35 | opacity: 1; | ||
36 | } | ||
37 | } | ||
38 | |||
39 | @media not all and (hover: hover) and (pointer: fine) { | ||
40 | .more { | ||
41 | opacity: 1 !important; | ||
42 | } | ||
43 | } | ||
44 | |||
45 | &.playing { | ||
46 | background-color: rgba(0, 0, 0, 0.02); | ||
47 | } | ||
48 | |||
49 | a { | ||
50 | @include disable-default-a-behaviour; | ||
51 | |||
52 | color: pvar(--mainForegroundColor); | ||
53 | display: flex; | ||
54 | min-width: 0; | ||
55 | align-items: center; | ||
56 | cursor: pointer; | ||
57 | |||
58 | .position { | ||
59 | font-weight: $font-semibold; | ||
60 | margin-right: 10px; | ||
61 | color: pvar(--greyForegroundColor); | ||
62 | min-width: 25px; | ||
63 | |||
64 | my-global-icon { | ||
65 | @include apply-svg-color(pvar(--greyForegroundColor)); | ||
66 | |||
67 | width: 17px; | ||
68 | position: relative; | ||
69 | left: -2px; | ||
70 | } | ||
71 | } | ||
72 | |||
73 | .video-info { | ||
74 | display: flex; | ||
75 | flex-direction: column; | ||
76 | align-self: flex-start; | ||
77 | min-width: 0; | ||
78 | |||
79 | a { | ||
80 | width: auto; | ||
81 | } | ||
82 | |||
83 | .video-info-account, .video-info-timestamp { | ||
84 | color: pvar(--greyForegroundColor); | ||
85 | } | ||
86 | } | ||
87 | } | ||
88 | |||
89 | .video-info-name { | ||
90 | font-size: 18px; | ||
91 | font-weight: $font-semibold; | ||
92 | display: inline-block; | ||
93 | |||
94 | @include ellipsis; | ||
95 | } | ||
96 | |||
97 | .more, my-edit-button { | ||
98 | justify-self: flex-end; | ||
99 | margin-left: auto; | ||
100 | cursor: pointer; | ||
101 | min-width: 24px; | ||
102 | } | ||
103 | |||
104 | .more { | ||
105 | opacity: 0; | ||
106 | |||
107 | &.show { | ||
108 | opacity: 1; | ||
109 | } | ||
110 | |||
111 | .icon-more { | ||
112 | @include apply-svg-color(pvar(--greyForegroundColor)); | ||
113 | |||
114 | display: flex; | ||
115 | |||
116 | &::after { | ||
117 | border: none; | ||
118 | } | ||
119 | } | ||
120 | |||
121 | .dropdown-item { | ||
122 | @include dropdown-with-icon-item; | ||
123 | } | ||
124 | |||
125 | .timestamp-options { | ||
126 | padding-top: 0; | ||
127 | padding-left: 35px; | ||
128 | margin-bottom: 15px; | ||
129 | |||
130 | > div { | ||
131 | display: flex; | ||
132 | align-items: center; | ||
133 | } | ||
134 | |||
135 | input { | ||
136 | @include peertube-button; | ||
137 | @include orange-button; | ||
138 | |||
139 | margin-top: 10px; | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | |||
145 | @mixin more-dropdown-control { | ||
146 | .video { | ||
147 | my-edit-button { | ||
148 | display: none; | ||
149 | |||
150 | + .more { | ||
151 | display: inline-flex; | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | } | ||
156 | |||
157 | @mixin edit-button-control { | ||
158 | .video { | ||
159 | my-edit-button { | ||
160 | display: none; | ||
161 | } | ||
162 | |||
163 | &.playing { | ||
164 | my-edit-button { | ||
165 | display: inline-flex; | ||
166 | height: max-content; | ||
167 | } | ||
168 | } | ||
169 | |||
170 | my-edit-button + .more { | ||
171 | display: none; | ||
172 | } | ||
173 | } | ||
174 | } | ||
175 | |||
176 | @mixin edit-button-in-mobile-view { | ||
177 | .video { | ||
178 | my-edit-button { | ||
179 | ::ng-deep .action-button-edit { | ||
180 | padding: 0 13px; | ||
181 | |||
182 | .button-label { | ||
183 | display: none; | ||
184 | } | ||
185 | } | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | |||
190 | @media screen and (min-width: $small-view) { | ||
191 | :host-context(.expanded) { | ||
192 | @include more-dropdown-control(); | ||
193 | } | ||
194 | } | ||
195 | |||
196 | @media screen and (max-width: $small-view) { | ||
197 | :host-context(.expanded) { | ||
198 | @include edit-button-control(); | ||
199 | } | ||
200 | } | ||
201 | |||
202 | @media screen and (max-width: $mobile-view) { | ||
203 | :host-context(.expanded) { | ||
204 | @include edit-button-in-mobile-view(); | ||
205 | } | ||
206 | } | ||
207 | |||
208 | @media screen and (min-width: #{$small-view + $menu-width}) { | ||
209 | :host-context(.main-col:not(.expanded)) { | ||
210 | @include more-dropdown-control(); | ||
211 | } | ||
212 | } | ||
213 | |||
214 | @media screen and (max-width: #{$small-view + $menu-width}) { | ||
215 | :host-context(.main-col:not(.expanded)) { | ||
216 | @include edit-button-control(); | ||
217 | } | ||
218 | } | ||
219 | |||
220 | @media screen and (max-width: #{$mobile-view + $menu-width}) { | ||
221 | :host-context(.main-col:not(.expanded)) { | ||
222 | @include edit-button-in-mobile-view(); | ||
223 | } | ||
224 | } | ||
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 deleted file mode 100644 index fad03e045..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts +++ /dev/null | |||
@@ -1,187 +0,0 @@ | |||
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { Video } from '@app/shared/video/video.model' | ||
3 | import { ServerConfig, VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models' | ||
4 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' | ||
5 | import { ActivatedRoute } from '@angular/router' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { VideoService } from '@app/shared/video/video.service' | ||
8 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
9 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | ||
10 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
11 | import { secondsToTime } from '../../../assets/player/utils' | ||
12 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-video-playlist-element-miniature', | ||
16 | styleUrls: [ './video-playlist-element-miniature.component.scss' ], | ||
17 | templateUrl: './video-playlist-element-miniature.component.html', | ||
18 | changeDetection: ChangeDetectionStrategy.OnPush | ||
19 | }) | ||
20 | export class VideoPlaylistElementMiniatureComponent implements OnInit { | ||
21 | @ViewChild('moreDropdown') moreDropdown: NgbDropdown | ||
22 | |||
23 | @Input() playlist: VideoPlaylist | ||
24 | @Input() playlistElement: VideoPlaylistElement | ||
25 | @Input() owned = false | ||
26 | @Input() playing = false | ||
27 | @Input() rowLink = false | ||
28 | @Input() accountLink = true | ||
29 | @Input() position: number // Keep this property because we're in the OnPush change detection strategy | ||
30 | @Input() touchScreenEditButton = false | ||
31 | |||
32 | @Output() elementRemoved = new EventEmitter<VideoPlaylistElement>() | ||
33 | |||
34 | displayTimestampOptions = false | ||
35 | |||
36 | timestampOptions: { | ||
37 | startTimestampEnabled: boolean | ||
38 | startTimestamp: number | ||
39 | stopTimestampEnabled: boolean | ||
40 | stopTimestamp: number | ||
41 | } = {} as any | ||
42 | |||
43 | private serverConfig: ServerConfig | ||
44 | |||
45 | constructor ( | ||
46 | private authService: AuthService, | ||
47 | private serverService: ServerService, | ||
48 | private notifier: Notifier, | ||
49 | private confirmService: ConfirmService, | ||
50 | private route: ActivatedRoute, | ||
51 | private i18n: I18n, | ||
52 | private videoService: VideoService, | ||
53 | private videoPlaylistService: VideoPlaylistService, | ||
54 | private cdr: ChangeDetectorRef | ||
55 | ) {} | ||
56 | |||
57 | ngOnInit (): void { | ||
58 | this.serverConfig = this.serverService.getTmpConfig() | ||
59 | this.serverService.getConfig() | ||
60 | .subscribe(config => { | ||
61 | this.serverConfig = config | ||
62 | this.cdr.detectChanges() | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | isUnavailable (e: VideoPlaylistElement) { | ||
67 | return e.type === VideoPlaylistElementType.UNAVAILABLE | ||
68 | } | ||
69 | |||
70 | isPrivate (e: VideoPlaylistElement) { | ||
71 | return e.type === VideoPlaylistElementType.PRIVATE | ||
72 | } | ||
73 | |||
74 | isDeleted (e: VideoPlaylistElement) { | ||
75 | return e.type === VideoPlaylistElementType.DELETED | ||
76 | } | ||
77 | |||
78 | buildRouterLink () { | ||
79 | if (!this.playlist) return null | ||
80 | |||
81 | return [ '/videos/watch/playlist', this.playlist.uuid ] | ||
82 | } | ||
83 | |||
84 | buildRouterQuery () { | ||
85 | if (!this.playlistElement || !this.playlistElement.video) return {} | ||
86 | |||
87 | return { | ||
88 | videoId: this.playlistElement.video.uuid, | ||
89 | start: this.playlistElement.startTimestamp, | ||
90 | stop: this.playlistElement.stopTimestamp, | ||
91 | resume: true | ||
92 | } | ||
93 | } | ||
94 | |||
95 | isVideoBlur (video: Video) { | ||
96 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverConfig) | ||
97 | } | ||
98 | |||
99 | removeFromPlaylist (playlistElement: VideoPlaylistElement) { | ||
100 | const videoId = this.playlistElement.video ? this.playlistElement.video.id : undefined | ||
101 | |||
102 | this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, playlistElement.id, videoId) | ||
103 | .subscribe( | ||
104 | () => { | ||
105 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) | ||
106 | |||
107 | this.elementRemoved.emit(playlistElement) | ||
108 | }, | ||
109 | |||
110 | err => this.notifier.error(err.message) | ||
111 | ) | ||
112 | |||
113 | this.moreDropdown.close() | ||
114 | } | ||
115 | |||
116 | updateTimestamps (playlistElement: VideoPlaylistElement) { | ||
117 | const body: VideoPlaylistElementUpdate = {} | ||
118 | |||
119 | body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null | ||
120 | body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null | ||
121 | |||
122 | this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, playlistElement.id, body, this.playlistElement.video.id) | ||
123 | .subscribe( | ||
124 | () => { | ||
125 | this.notifier.success(this.i18n('Timestamps updated')) | ||
126 | |||
127 | playlistElement.startTimestamp = body.startTimestamp | ||
128 | playlistElement.stopTimestamp = body.stopTimestamp | ||
129 | |||
130 | this.cdr.detectChanges() | ||
131 | }, | ||
132 | |||
133 | err => this.notifier.error(err.message) | ||
134 | ) | ||
135 | |||
136 | this.moreDropdown.close() | ||
137 | } | ||
138 | |||
139 | formatTimestamp (playlistElement: VideoPlaylistElement) { | ||
140 | const start = playlistElement.startTimestamp | ||
141 | const stop = playlistElement.stopTimestamp | ||
142 | |||
143 | const startFormatted = secondsToTime(start, true, ':') | ||
144 | const stopFormatted = secondsToTime(stop, true, ':') | ||
145 | |||
146 | if (start === null && stop === null) return '' | ||
147 | |||
148 | if (start !== null && stop === null) return this.i18n('Starts at ') + startFormatted | ||
149 | if (start === null && stop !== null) return this.i18n('Stops at ') + stopFormatted | ||
150 | |||
151 | return this.i18n('Starts at ') + startFormatted + this.i18n(' and stops at ') + stopFormatted | ||
152 | } | ||
153 | |||
154 | onDropdownOpenChange () { | ||
155 | this.displayTimestampOptions = false | ||
156 | } | ||
157 | |||
158 | toggleDisplayTimestampsOptions (event: Event, playlistElement: VideoPlaylistElement) { | ||
159 | event.preventDefault() | ||
160 | |||
161 | this.displayTimestampOptions = !this.displayTimestampOptions | ||
162 | |||
163 | if (this.displayTimestampOptions === true) { | ||
164 | this.timestampOptions = { | ||
165 | startTimestampEnabled: false, | ||
166 | stopTimestampEnabled: false, | ||
167 | startTimestamp: 0, | ||
168 | stopTimestamp: playlistElement.video.duration | ||
169 | } | ||
170 | |||
171 | if (playlistElement.startTimestamp) { | ||
172 | this.timestampOptions.startTimestampEnabled = true | ||
173 | this.timestampOptions.startTimestamp = playlistElement.startTimestamp | ||
174 | } | ||
175 | |||
176 | if (playlistElement.stopTimestamp) { | ||
177 | this.timestampOptions.stopTimestampEnabled = true | ||
178 | this.timestampOptions.stopTimestamp = playlistElement.stopTimestamp | ||
179 | } | ||
180 | } | ||
181 | |||
182 | // FIXME: why do we have to use setTimeout here? | ||
183 | setTimeout(() => { | ||
184 | this.cdr.detectChanges() | ||
185 | }) | ||
186 | } | ||
187 | } | ||
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 deleted file mode 100644 index f1c46d1eb..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-element.model.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
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-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-miniature.component.html deleted file mode 100644 index 86f6664cb..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-miniature.component.html +++ /dev/null | |||
@@ -1,34 +0,0 @@ | |||
1 | <div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> | ||
2 | <a | ||
3 | [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" | ||
4 | class="miniature-thumbnail" | ||
5 | > | ||
6 | <img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" /> | ||
7 | |||
8 | <div class="miniature-playlist-info-overlay"> | ||
9 | <ng-container i18n>{playlist.videosLength, plural, =0 {No videos} =1 {1 video} other {{{ playlist.videosLength }} videos}}</ng-container> | ||
10 | </div> | ||
11 | |||
12 | <div class="play-overlay"> | ||
13 | <div class="icon"></div> | ||
14 | </div> | ||
15 | </a> | ||
16 | |||
17 | <div class="miniature-info"> | ||
18 | <a tabindex="-1" class="miniature-name" [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"> | ||
19 | {{ playlist.displayName }} | ||
20 | </a> | ||
21 | |||
22 | <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> | ||
23 | {{ playlist.videoChannelBy }} | ||
24 | </a> | ||
25 | |||
26 | <div class="privacy-date"> | ||
27 | <span class="video-info-privacy" *ngIf="displayPrivacy">{{ playlist.privacy.label }}</span> | ||
28 | |||
29 | <span i18n class="updated-at">Updated {{ playlist.updatedAt | myFromNow }}</span> | ||
30 | </div> | ||
31 | |||
32 | <div *ngIf="displayDescription" class="video-info-description">{{ playlist.description }}</div> | ||
33 | </div> | ||
34 | </div> | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss deleted file mode 100644 index 1b16dbb01..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-miniature.component.scss +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | @import '_miniature'; | ||
4 | |||
5 | .miniature { | ||
6 | display: inline-block; | ||
7 | |||
8 | &.no-videos:not(.to-manage){ | ||
9 | a { | ||
10 | cursor: default !important; | ||
11 | } | ||
12 | } | ||
13 | |||
14 | &.to-manage, | ||
15 | &.no-videos { | ||
16 | .play-overlay { | ||
17 | display: none; | ||
18 | } | ||
19 | } | ||
20 | |||
21 | .miniature-thumbnail { | ||
22 | @include miniature-thumbnail; | ||
23 | |||
24 | .miniature-playlist-info-overlay { | ||
25 | @include static-thumbnail-overlay; | ||
26 | |||
27 | position: absolute; | ||
28 | right: 0; | ||
29 | bottom: 0; | ||
30 | height: $video-thumbnail-height; | ||
31 | padding: 0 10px; | ||
32 | display: flex; | ||
33 | align-items: center; | ||
34 | font-size: 14px; | ||
35 | font-weight: $font-semibold; | ||
36 | } | ||
37 | } | ||
38 | |||
39 | .miniature-info { | ||
40 | width: 200px; | ||
41 | margin-top: 2px; | ||
42 | line-height: normal; | ||
43 | |||
44 | .miniature-name { | ||
45 | @include miniature-name; | ||
46 | |||
47 | @include ellipsis-multiline(1.3em, 2); | ||
48 | |||
49 | margin: 0; | ||
50 | } | ||
51 | |||
52 | .by { | ||
53 | @include disable-default-a-behaviour; | ||
54 | |||
55 | display: block; | ||
56 | color: pvar(--greyForegroundColor); | ||
57 | } | ||
58 | |||
59 | .privacy-date { | ||
60 | margin-top: 5px; | ||
61 | |||
62 | .video-info-privacy { | ||
63 | font-size: 14px; | ||
64 | font-weight: $font-semibold; | ||
65 | |||
66 | &::after { | ||
67 | content: '-'; | ||
68 | margin: 0 3px; | ||
69 | } | ||
70 | } | ||
71 | } | ||
72 | |||
73 | .video-info-description { | ||
74 | margin-top: 10px; | ||
75 | color: pvar(--greyForegroundColor); | ||
76 | } | ||
77 | } | ||
78 | } | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts deleted file mode 100644 index 523e96f2a..000000000 --- a/client/src/app/shared/video-playlist/video-playlist-miniature.component.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-video-playlist-miniature', | ||
6 | styleUrls: [ './video-playlist-miniature.component.scss' ], | ||
7 | templateUrl: './video-playlist-miniature.component.html' | ||
8 | }) | ||
9 | export class VideoPlaylistMiniatureComponent { | ||
10 | @Input() playlist: VideoPlaylist | ||
11 | @Input() toManage = false | ||
12 | @Input() displayChannel = false | ||
13 | @Input() displayDescription = false | ||
14 | @Input() displayPrivacy = false | ||
15 | |||
16 | getPlaylistUrl () { | ||
17 | if (this.toManage) return [ '/my-account/video-playlists', this.playlist.uuid ] | ||
18 | if (this.playlist.videosLength === 0) return null | ||
19 | |||
20 | return [ '/videos/watch/playlist', this.playlist.uuid ] | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist.model.ts b/client/src/app/shared/video-playlist/video-playlist.model.ts deleted file mode 100644 index 6f27e7475..000000000 --- a/client/src/app/shared/video-playlist/video-playlist.model.ts +++ /dev/null | |||
@@ -1,97 +0,0 @@ | |||
1 | import { | ||
2 | VideoChannelSummary, | ||
3 | VideoConstant, | ||
4 | VideoPlaylist as ServerVideoPlaylist, | ||
5 | VideoPlaylistPrivacy, | ||
6 | VideoPlaylistType | ||
7 | } from '../../../../../shared/models/videos' | ||
8 | import { AccountSummary, peertubeTranslate } from '@shared/models' | ||
9 | import { Actor } from '@app/shared/actor/actor.model' | ||
10 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | ||
11 | |||
12 | export class VideoPlaylist implements ServerVideoPlaylist { | ||
13 | id: number | ||
14 | uuid: string | ||
15 | isLocal: boolean | ||
16 | |||
17 | displayName: string | ||
18 | description: string | ||
19 | privacy: VideoConstant<VideoPlaylistPrivacy> | ||
20 | |||
21 | thumbnailPath: string | ||
22 | |||
23 | videosLength: number | ||
24 | |||
25 | type: VideoConstant<VideoPlaylistType> | ||
26 | |||
27 | createdAt: Date | string | ||
28 | updatedAt: Date | string | ||
29 | |||
30 | ownerAccount: AccountSummary | ||
31 | videoChannel?: VideoChannelSummary | ||
32 | |||
33 | thumbnailUrl: string | ||
34 | |||
35 | ownerBy: string | ||
36 | ownerAvatarUrl: string | ||
37 | |||
38 | videoChannelBy?: string | ||
39 | videoChannelAvatarUrl?: string | ||
40 | |||
41 | private thumbnailVersion: number | ||
42 | private originThumbnailUrl: string | ||
43 | |||
44 | constructor (hash: ServerVideoPlaylist, translations: {}) { | ||
45 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
46 | |||
47 | this.id = hash.id | ||
48 | this.uuid = hash.uuid | ||
49 | this.isLocal = hash.isLocal | ||
50 | |||
51 | this.displayName = hash.displayName | ||
52 | |||
53 | this.description = hash.description | ||
54 | this.privacy = hash.privacy | ||
55 | |||
56 | this.thumbnailPath = hash.thumbnailPath | ||
57 | |||
58 | if (this.thumbnailPath) { | ||
59 | this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath | ||
60 | this.originThumbnailUrl = this.thumbnailUrl | ||
61 | } else { | ||
62 | this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg' | ||
63 | } | ||
64 | |||
65 | this.videosLength = hash.videosLength | ||
66 | |||
67 | this.type = hash.type | ||
68 | |||
69 | this.createdAt = new Date(hash.createdAt) | ||
70 | this.updatedAt = new Date(hash.updatedAt) | ||
71 | |||
72 | this.ownerAccount = hash.ownerAccount | ||
73 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) | ||
74 | this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) | ||
75 | |||
76 | if (hash.videoChannel) { | ||
77 | this.videoChannel = hash.videoChannel | ||
78 | this.videoChannelBy = Actor.CREATE_BY_STRING(hash.videoChannel.name, hash.videoChannel.host) | ||
79 | this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.videoChannel) | ||
80 | } | ||
81 | |||
82 | this.privacy.label = peertubeTranslate(this.privacy.label, translations) | ||
83 | |||
84 | if (this.type.id === VideoPlaylistType.WATCH_LATER) { | ||
85 | this.displayName = peertubeTranslate(this.displayName, translations) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | refreshThumbnail () { | ||
90 | if (!this.originThumbnailUrl) return | ||
91 | |||
92 | if (!this.thumbnailVersion) this.thumbnailVersion = 0 | ||
93 | this.thumbnailVersion++ | ||
94 | |||
95 | this.thumbnailUrl = this.originThumbnailUrl + '?v' + this.thumbnailVersion | ||
96 | } | ||
97 | } | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts deleted file mode 100644 index 38d915c6b..000000000 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ /dev/null | |||
@@ -1,357 +0,0 @@ | |||
1 | import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' | ||
2 | import { Injectable, NgZone } from '@angular/core' | ||
3 | import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' | ||
4 | import { RestExtractor } from '../rest/rest-extractor.service' | ||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
6 | import { ResultList, VideoPlaylistElementCreate, VideoPlaylistElementUpdate } from '../../../../../shared' | ||
7 | import { environment } from '../../../environments/environment' | ||
8 | import { VideoPlaylist as VideoPlaylistServerModel } from '@shared/models/videos/playlist/video-playlist.model' | ||
9 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | ||
10 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | ||
11 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | ||
12 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' | ||
13 | import { objectToFormData } from '@app/shared/misc/utils' | ||
14 | import { AuthUser, ServerService } from '@app/core' | ||
15 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
16 | import { AccountService } from '@app/shared/account/account.service' | ||
17 | import { Account } from '@app/shared/account/account.model' | ||
18 | import { RestService } from '@app/shared/rest' | ||
19 | import { VideoExistInPlaylist, VideosExistInPlaylists } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | ||
20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' | ||
21 | import { ComponentPaginationLight } 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' | ||
24 | import { uniq } from 'lodash-es' | ||
25 | import * as debug from 'debug' | ||
26 | import { enterZone, leaveZone } from '@app/shared/rxjs/zone' | ||
27 | |||
28 | const logger = debug('peertube:playlists:VideoPlaylistService') | ||
29 | |||
30 | export type CachedPlaylist = VideoPlaylist | { id: number, displayName: string } | ||
31 | |||
32 | @Injectable() | ||
33 | export class VideoPlaylistService { | ||
34 | static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' | ||
35 | static MY_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/users/me/video-playlists/' | ||
36 | |||
37 | // Use a replay subject because we "next" a value before subscribing | ||
38 | private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1) | ||
39 | private videoExistsInPlaylistCacheSubject = new Subject<VideosExistInPlaylists>() | ||
40 | private readonly videoExistsInPlaylistObservable: Observable<VideosExistInPlaylists> | ||
41 | |||
42 | private videoExistsObservableCache: { [ id: number ]: Observable<VideoExistInPlaylist[]> } = {} | ||
43 | private videoExistsCache: { [ id: number ]: VideoExistInPlaylist[] } = {} | ||
44 | |||
45 | private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined | ||
46 | private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>> | ||
47 | private myAccountPlaylistCacheSubject = new Subject<ResultList<CachedPlaylist>>() | ||
48 | |||
49 | constructor ( | ||
50 | private authHttp: HttpClient, | ||
51 | private serverService: ServerService, | ||
52 | private restExtractor: RestExtractor, | ||
53 | private restService: RestService, | ||
54 | private ngZone: NgZone | ||
55 | ) { | ||
56 | this.videoExistsInPlaylistObservable = merge( | ||
57 | this.videoExistsInPlaylistNotifier.pipe( | ||
58 | // We leave Angular zone so Protractor does not get stuck | ||
59 | bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), | ||
60 | filter(videoIds => videoIds.length !== 0), | ||
61 | map(videoIds => uniq(videoIds)), | ||
62 | observeOn(enterZone(this.ngZone, asyncScheduler)), | ||
63 | switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), | ||
64 | share() | ||
65 | ), | ||
66 | |||
67 | this.videoExistsInPlaylistCacheSubject | ||
68 | ) | ||
69 | } | ||
70 | |||
71 | listChannelPlaylists (videoChannel: VideoChannel, componentPagination: ComponentPaginationLight): Observable<ResultList<VideoPlaylist>> { | ||
72 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists' | ||
73 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
74 | |||
75 | let params = new HttpParams() | ||
76 | params = this.restService.addRestGetParams(params, pagination) | ||
77 | |||
78 | return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) | ||
79 | .pipe( | ||
80 | switchMap(res => this.extractPlaylists(res)), | ||
81 | catchError(err => this.restExtractor.handleError(err)) | ||
82 | ) | ||
83 | } | ||
84 | |||
85 | listMyPlaylistWithCache (user: AuthUser, search?: string) { | ||
86 | if (!search) { | ||
87 | if (this.myAccountPlaylistCacheRunning) return this.myAccountPlaylistCacheRunning | ||
88 | if (this.myAccountPlaylistCache) return of(this.myAccountPlaylistCache) | ||
89 | } | ||
90 | |||
91 | const obs = this.listAccountPlaylists(user.account, undefined, '-updatedAt', search) | ||
92 | .pipe( | ||
93 | tap(result => { | ||
94 | if (!search) { | ||
95 | this.myAccountPlaylistCacheRunning = undefined | ||
96 | this.myAccountPlaylistCache = result | ||
97 | } | ||
98 | }), | ||
99 | share() | ||
100 | ) | ||
101 | |||
102 | if (!search) this.myAccountPlaylistCacheRunning = obs | ||
103 | return obs | ||
104 | } | ||
105 | |||
106 | listAccountPlaylists ( | ||
107 | account: Account, | ||
108 | componentPagination: ComponentPaginationLight, | ||
109 | sort: string, | ||
110 | search?: string | ||
111 | ): Observable<ResultList<VideoPlaylist>> { | ||
112 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' | ||
113 | const pagination = componentPagination | ||
114 | ? this.restService.componentPaginationToRestPagination(componentPagination) | ||
115 | : undefined | ||
116 | |||
117 | let params = new HttpParams() | ||
118 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
119 | if (search) params = this.restService.addObjectParams(params, { search }) | ||
120 | |||
121 | return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) | ||
122 | .pipe( | ||
123 | switchMap(res => this.extractPlaylists(res)), | ||
124 | catchError(err => this.restExtractor.handleError(err)) | ||
125 | ) | ||
126 | } | ||
127 | |||
128 | getVideoPlaylist (id: string | number) { | ||
129 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + id | ||
130 | |||
131 | return this.authHttp.get<VideoPlaylist>(url) | ||
132 | .pipe( | ||
133 | switchMap(res => this.extractPlaylist(res)), | ||
134 | catchError(err => this.restExtractor.handleError(err)) | ||
135 | ) | ||
136 | } | ||
137 | |||
138 | createVideoPlaylist (body: VideoPlaylistCreate) { | ||
139 | const data = objectToFormData(body) | ||
140 | |||
141 | return this.authHttp.post<{ videoPlaylist: { id: number } }>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data) | ||
142 | .pipe( | ||
143 | tap(res => { | ||
144 | if (!this.myAccountPlaylistCache) return | ||
145 | |||
146 | this.myAccountPlaylistCache.total++ | ||
147 | |||
148 | this.myAccountPlaylistCache.data.push({ | ||
149 | id: res.videoPlaylist.id, | ||
150 | displayName: body.displayName | ||
151 | }) | ||
152 | |||
153 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
154 | }), | ||
155 | catchError(err => this.restExtractor.handleError(err)) | ||
156 | ) | ||
157 | } | ||
158 | |||
159 | updateVideoPlaylist (videoPlaylist: VideoPlaylist, body: VideoPlaylistUpdate) { | ||
160 | const data = objectToFormData(body) | ||
161 | |||
162 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data) | ||
163 | .pipe( | ||
164 | map(this.restExtractor.extractDataBool), | ||
165 | tap(() => { | ||
166 | if (!this.myAccountPlaylistCache) return | ||
167 | |||
168 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === videoPlaylist.id) | ||
169 | playlist.displayName = body.displayName | ||
170 | |||
171 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
172 | }), | ||
173 | catchError(err => this.restExtractor.handleError(err)) | ||
174 | ) | ||
175 | } | ||
176 | |||
177 | removeVideoPlaylist (videoPlaylist: VideoPlaylist) { | ||
178 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id) | ||
179 | .pipe( | ||
180 | map(this.restExtractor.extractDataBool), | ||
181 | tap(() => { | ||
182 | if (!this.myAccountPlaylistCache) return | ||
183 | |||
184 | this.myAccountPlaylistCache.total-- | ||
185 | this.myAccountPlaylistCache.data = this.myAccountPlaylistCache.data | ||
186 | .filter(p => p.id !== videoPlaylist.id) | ||
187 | |||
188 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
189 | }), | ||
190 | catchError(err => this.restExtractor.handleError(err)) | ||
191 | ) | ||
192 | } | ||
193 | |||
194 | addVideoInPlaylist (playlistId: number, body: VideoPlaylistElementCreate) { | ||
195 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos' | ||
196 | |||
197 | return this.authHttp.post<{ videoPlaylistElement: { id: number } }>(url, body) | ||
198 | .pipe( | ||
199 | tap(res => { | ||
200 | const existsResult = this.videoExistsCache[body.videoId] | ||
201 | existsResult.push({ | ||
202 | playlistId, | ||
203 | playlistElementId: res.videoPlaylistElement.id, | ||
204 | startTimestamp: body.startTimestamp, | ||
205 | stopTimestamp: body.stopTimestamp | ||
206 | }) | ||
207 | |||
208 | this.runPlaylistCheck(body.videoId) | ||
209 | }), | ||
210 | catchError(err => this.restExtractor.handleError(err)) | ||
211 | ) | ||
212 | } | ||
213 | |||
214 | updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate, videoId: number) { | ||
215 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body) | ||
216 | .pipe( | ||
217 | map(this.restExtractor.extractDataBool), | ||
218 | tap(() => { | ||
219 | const existsResult = this.videoExistsCache[videoId] | ||
220 | const elem = existsResult.find(e => e.playlistElementId === playlistElementId) | ||
221 | |||
222 | elem.startTimestamp = body.startTimestamp | ||
223 | elem.stopTimestamp = body.stopTimestamp | ||
224 | |||
225 | this.runPlaylistCheck(videoId) | ||
226 | }), | ||
227 | catchError(err => this.restExtractor.handleError(err)) | ||
228 | ) | ||
229 | } | ||
230 | |||
231 | removeVideoFromPlaylist (playlistId: number, playlistElementId: number, videoId?: number) { | ||
232 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId) | ||
233 | .pipe( | ||
234 | map(this.restExtractor.extractDataBool), | ||
235 | tap(() => { | ||
236 | if (!videoId) return | ||
237 | |||
238 | this.videoExistsCache[videoId] = this.videoExistsCache[videoId].filter(e => e.playlistElementId !== playlistElementId) | ||
239 | this.runPlaylistCheck(videoId) | ||
240 | }), | ||
241 | catchError(err => this.restExtractor.handleError(err)) | ||
242 | ) | ||
243 | } | ||
244 | |||
245 | reorderPlaylist (playlistId: number, oldPosition: number, newPosition: number) { | ||
246 | const body: VideoPlaylistReorder = { | ||
247 | startPosition: oldPosition, | ||
248 | insertAfterPosition: newPosition | ||
249 | } | ||
250 | |||
251 | return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/reorder', body) | ||
252 | .pipe( | ||
253 | map(this.restExtractor.extractDataBool), | ||
254 | catchError(err => this.restExtractor.handleError(err)) | ||
255 | ) | ||
256 | } | ||
257 | |||
258 | getPlaylistVideos ( | ||
259 | videoPlaylistId: number | string, | ||
260 | componentPagination: ComponentPaginationLight | ||
261 | ): Observable<ResultList<VideoPlaylistElement>> { | ||
262 | const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos' | ||
263 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
264 | |||
265 | let params = new HttpParams() | ||
266 | params = this.restService.addRestGetParams(params, pagination) | ||
267 | |||
268 | return this.authHttp | ||
269 | .get<ResultList<ServerVideoPlaylistElement>>(path, { params }) | ||
270 | .pipe( | ||
271 | switchMap(res => this.extractVideoPlaylistElements(res)), | ||
272 | catchError(err => this.restExtractor.handleError(err)) | ||
273 | ) | ||
274 | } | ||
275 | |||
276 | listenToMyAccountPlaylistsChange () { | ||
277 | return this.myAccountPlaylistCacheSubject.asObservable() | ||
278 | } | ||
279 | |||
280 | listenToVideoPlaylistChange (videoId: number) { | ||
281 | if (this.videoExistsObservableCache[ videoId ]) { | ||
282 | return this.videoExistsObservableCache[ videoId ] | ||
283 | } | ||
284 | |||
285 | const obs = this.videoExistsInPlaylistObservable | ||
286 | .pipe( | ||
287 | map(existsResult => existsResult[ videoId ]), | ||
288 | filter(r => !!r), | ||
289 | tap(result => this.videoExistsCache[ videoId ] = result) | ||
290 | ) | ||
291 | |||
292 | this.videoExistsObservableCache[ videoId ] = obs | ||
293 | return obs | ||
294 | } | ||
295 | |||
296 | runPlaylistCheck (videoId: number) { | ||
297 | logger('Running playlist check.') | ||
298 | |||
299 | if (this.videoExistsCache[videoId]) { | ||
300 | logger('Found cache for %d.', videoId) | ||
301 | |||
302 | return this.videoExistsInPlaylistCacheSubject.next({ [videoId]: this.videoExistsCache[videoId] }) | ||
303 | } | ||
304 | |||
305 | logger('Fetching from network for %d.', videoId) | ||
306 | return this.videoExistsInPlaylistNotifier.next(videoId) | ||
307 | } | ||
308 | |||
309 | extractPlaylists (result: ResultList<VideoPlaylistServerModel>) { | ||
310 | return this.serverService.getServerLocale() | ||
311 | .pipe( | ||
312 | map(translations => { | ||
313 | const playlistsJSON = result.data | ||
314 | const total = result.total | ||
315 | const playlists: VideoPlaylist[] = [] | ||
316 | |||
317 | for (const playlistJSON of playlistsJSON) { | ||
318 | playlists.push(new VideoPlaylist(playlistJSON, translations)) | ||
319 | } | ||
320 | |||
321 | return { data: playlists, total } | ||
322 | }) | ||
323 | ) | ||
324 | } | ||
325 | |||
326 | extractPlaylist (playlist: VideoPlaylistServerModel) { | ||
327 | return this.serverService.getServerLocale() | ||
328 | .pipe(map(translations => new VideoPlaylist(playlist, translations))) | ||
329 | } | ||
330 | |||
331 | extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) { | ||
332 | return this.serverService.getServerLocale() | ||
333 | .pipe( | ||
334 | map(translations => { | ||
335 | const elementsJson = result.data | ||
336 | const total = result.total | ||
337 | const elements: VideoPlaylistElement[] = [] | ||
338 | |||
339 | for (const elementJson of elementsJson) { | ||
340 | elements.push(new VideoPlaylistElement(elementJson, translations)) | ||
341 | } | ||
342 | |||
343 | return { total, data: elements } | ||
344 | }) | ||
345 | ) | ||
346 | } | ||
347 | |||
348 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> { | ||
349 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' | ||
350 | |||
351 | let params = new HttpParams() | ||
352 | params = this.restService.addObjectParams(params, { videoIds }) | ||
353 | |||
354 | return this.authHttp.get<VideoExistInPlaylist>(url, { params, headers: { ignoreLoadingBar: '' } }) | ||
355 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
356 | } | ||
357 | } | ||