diff options
author | Chocobozzz <me@florianbigard.com> | 2020-08-18 15:51:51 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-08-19 11:30:21 +0200 |
commit | e79df4eefbeba3e7ab74e17cae1369c80c94b848 (patch) | |
tree | 917e22bf85195394e0829ebd962ac5f861009bdf | |
parent | cbb513e737bfca3ad3dbd43ad9614968e6e207cf (diff) | |
download | PeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.tar.gz PeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.tar.zst PeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.zip |
Update playlist add component to accept multiple times the same video
5 files changed, 285 insertions, 127 deletions
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.ts b/client/src/app/shared/shared-forms/timestamp-input.component.ts index 8d67a96ac..0ffd03d02 100644 --- a/client/src/app/shared/shared-forms/timestamp-input.component.ts +++ b/client/src/app/shared/shared-forms/timestamp-input.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core' | 1 | import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { secondsToTime, timeToInt } from '../../../assets/player/utils' | 3 | import { secondsToTime, timeToInt } from '../../../assets/player/utils' |
4 | 4 | ||
@@ -19,6 +19,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit { | |||
19 | @Input() timestamp: number | 19 | @Input() timestamp: number |
20 | @Input() disabled = false | 20 | @Input() disabled = false |
21 | 21 | ||
22 | @Output() inputBlur = new EventEmitter() | ||
23 | |||
22 | timestampString: string | 24 | timestampString: string |
23 | 25 | ||
24 | constructor (private changeDetector: ChangeDetectorRef) {} | 26 | constructor (private changeDetector: ChangeDetectorRef) {} |
@@ -57,5 +59,7 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit { | |||
57 | 59 | ||
58 | this.propagateChange(this.timestamp) | 60 | this.propagateChange(this.timestamp) |
59 | } | 61 | } |
62 | |||
63 | this.inputBlur.emit() | ||
60 | } | 64 | } |
61 | } | 65 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html index a40e0699e..37d5017cf 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html | |||
@@ -2,58 +2,60 @@ | |||
2 | <div class="header"> | 2 | <div class="header"> |
3 | <div class="first-row"> | 3 | <div class="first-row"> |
4 | <div i18n class="title">Save to</div> | 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> | 5 | </div> |
6 | </div> | ||
12 | 7 | ||
13 | <div class="options-row" *ngIf="displayOptions"> | 8 | <div class="input-container"> |
14 | <div> | 9 | <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> |
15 | <my-peertube-checkbox | 10 | </div> |
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 | 11 | ||
28 | <div> | 12 | <div class="playlists"> |
13 | <div | ||
14 | *ngFor="let playlist of videoPlaylists" | ||
15 | class="playlist dropdown-item" [ngClass]="{ 'has-optional-row': playlist.optionalRowDisplayed }" | ||
16 | > | ||
17 | <div class="primary-row"> | ||
29 | <my-peertube-checkbox | 18 | <my-peertube-checkbox |
30 | inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" | 19 | [disabled]="isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed" [inputName]="getPrimaryInputName(playlist)" |
31 | i18n-labelText labelText="Stop at" | 20 | [ngModel]="isPrimaryCheckboxChecked(playlist)" [onPushWorkaround]="true" |
21 | (click)="toggleMainPlaylist($event, playlist)" | ||
32 | ></my-peertube-checkbox> | 22 | ></my-peertube-checkbox> |
33 | 23 | ||
34 | <my-timestamp-input | 24 | <label class="display-name" (click)="toggleMainPlaylist($event, playlist)"> |
35 | [timestamp]="timestampOptions.stopTimestamp" | 25 | {{ playlist.displayName }} |
36 | [maxTimestamp]="video.duration" | 26 | </label> |
37 | [disabled]="!timestampOptions.stopTimestampEnabled" | 27 | |
38 | [(ngModel)]="timestampOptions.stopTimestamp" | 28 | <div class="optional-row-icon" *ngIf="isPrimaryCheckboxChecked(playlist)" (click)="toggleOptionalRow(playlist)"> |
39 | ></my-timestamp-input> | 29 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> |
30 | </div> | ||
40 | </div> | 31 | </div> |
41 | </div> | ||
42 | </div> | ||
43 | 32 | ||
44 | <div class="input-container"> | 33 | <div class="optional-rows" *ngIf="playlist.optionalRowDisplayed"> |
45 | <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> | 34 | <div class="labels"> |
46 | </div> | 35 | <div i18n>Start at</div> |
36 | <div i18n>Stop at</div> | ||
37 | </div> | ||
47 | 38 | ||
48 | <div class="playlists"> | 39 | <div *ngFor="let element of buildOptionalRowElements(playlist)"> |
49 | <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> | 40 | <my-peertube-checkbox |
50 | <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> | 41 | [inputName]="getOptionalInputName(playlist, element)" |
42 | [ngModel]="element.enabled" [onPushWorkaround]="true" | ||
43 | (click)="toggleOptionalPlaylist($event, playlist, element, startAt.timestamp, stopAt.timestamp)" | ||
44 | ></my-peertube-checkbox> | ||
51 | 45 | ||
52 | <div class="display-name"> | 46 | <my-timestamp-input |
53 | {{ playlist.displayName }} | 47 | [maxTimestamp]="video.duration" |
48 | [(ngModel)]="element.startTimestamp" | ||
49 | (inputBlur)="onElementTimestampUpdate(playlist, element)" | ||
50 | #startAt | ||
51 | ></my-timestamp-input> | ||
54 | 52 | ||
55 | <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> | 53 | <my-timestamp-input |
56 | {{ formatTimestamp(playlist) }} | 54 | [maxTimestamp]="video.duration" |
55 | [(ngModel)]="element.stopTimestamp" | ||
56 | (inputBlur)="onElementTimestampUpdate(playlist, element)" | ||
57 | #stopAt | ||
58 | ></my-timestamp-input> | ||
57 | </div> | 59 | </div> |
58 | </div> | 60 | </div> |
59 | </div> | 61 | </div> |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss index cb9ab9a17..d2c8804e3 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | $optional-rows-checkbox-width: 34px; | ||
5 | $timestamp-width: 50px; | ||
6 | $timestamp-margin-right: 10px; | ||
7 | |||
4 | .header, | 8 | .header, |
5 | .dropdown-item, | 9 | .dropdown-item, |
6 | .input-container { | 10 | .input-container { |
@@ -24,31 +28,6 @@ | |||
24 | font-size: 18px; | 28 | font-size: 18px; |
25 | flex-grow: 1; | 29 | flex-grow: 1; |
26 | } | 30 | } |
27 | |||
28 | .options { | ||
29 | display: flex; | ||
30 | align-items: center; | ||
31 | font-size: 14px; | ||
32 | cursor: pointer; | ||
33 | |||
34 | my-global-icon { | ||
35 | @include apply-svg-color(#333); | ||
36 | |||
37 | width: 16px; | ||
38 | height: 23px; | ||
39 | margin-right: 3px; | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | |||
44 | .options-row { | ||
45 | margin-top: 10px; | ||
46 | padding-left: 10px; | ||
47 | |||
48 | > div { | ||
49 | display: flex; | ||
50 | align-items: center; | ||
51 | } | ||
52 | } | 31 | } |
53 | } | 32 | } |
54 | 33 | ||
@@ -58,8 +37,16 @@ | |||
58 | } | 37 | } |
59 | 38 | ||
60 | .playlist { | 39 | .playlist { |
61 | display: inline-flex; | 40 | padding: 8px 10px 8px 24px; |
62 | cursor: pointer; | 41 | |
42 | &.has-optional-row:hover { | ||
43 | background-color: inherit; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | .primary-row, | ||
48 | .optional-rows > div { | ||
49 | display: flex; | ||
63 | 50 | ||
64 | my-peertube-checkbox { | 51 | my-peertube-checkbox { |
65 | margin-right: 10px; | 52 | margin-right: 10px; |
@@ -69,11 +56,58 @@ | |||
69 | .display-name { | 56 | .display-name { |
70 | display: flex; | 57 | display: flex; |
71 | align-items: flex-end; | 58 | align-items: flex-end; |
59 | flex-grow: 1; | ||
60 | margin: 0; | ||
61 | font-weight: $font-regular; | ||
62 | cursor: pointer; | ||
63 | } | ||
64 | |||
65 | .optional-row-icon { | ||
66 | display: flex; | ||
67 | align-items: center; | ||
68 | font-size: 14px; | ||
69 | cursor: pointer; | ||
70 | |||
71 | my-global-icon { | ||
72 | @include apply-svg-color(#333); | ||
73 | |||
74 | width: 19px; | ||
75 | height: 19px; | ||
76 | margin-right: 0; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | my-timestamp-input { | ||
81 | margin-right: $timestamp-margin-right; | ||
82 | |||
83 | ::ng-deep .ui-inputtext { | ||
84 | padding: 0; | ||
85 | width: $timestamp-width; | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | |||
90 | .optional-rows { | ||
91 | > div { | ||
92 | padding: 8px 5px 5px 10px; | ||
93 | } | ||
94 | |||
95 | my-peertube-checkbox { | ||
96 | display: block; | ||
97 | width: $optional-rows-checkbox-width; | ||
98 | margin-right: 0 !important; | ||
99 | } | ||
100 | |||
101 | .labels { | ||
102 | margin-left: $optional-rows-checkbox-width; | ||
103 | font-size: 13px; | ||
104 | color: pvar(--greyForegroundColor); | ||
105 | padding-top: 5px; | ||
106 | padding-bottom: 0; | ||
72 | 107 | ||
73 | .timestamp-info { | 108 | div { |
74 | font-size: 0.9em; | 109 | margin-right: $timestamp-margin-right; |
75 | color: pvar(--greyForegroundColor); | 110 | width: $timestamp-width; |
76 | margin-left: 5px; | ||
77 | } | 111 | } |
78 | } | 112 | } |
79 | } | 113 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts index 41f16e0bf..b6a3408c7 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts | |||
@@ -4,21 +4,27 @@ import { debounceTime, filter } from 'rxjs/operators' | |||
4 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' | 4 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' |
5 | import { AuthService, DisableForReuseHook, Notifier } from '@app/core' | 5 | import { AuthService, DisableForReuseHook, Notifier } from '@app/core' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
7 | import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' | 7 | import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy, VideoPlaylistElementUpdate } from '@shared/models' |
8 | import { secondsToTime } from '../../../assets/player/utils' | 8 | import { secondsToTime } from '../../../assets/player/utils' |
9 | import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators' | 9 | import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators' |
10 | import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service' | 10 | import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service' |
11 | import { invoke, last } from 'lodash' | ||
11 | 12 | ||
12 | const logger = debug('peertube:playlists:VideoAddToPlaylistComponent') | 13 | const logger = debug('peertube:playlists:VideoAddToPlaylistComponent') |
13 | 14 | ||
15 | type PlaylistElement = { | ||
16 | enabled: boolean | ||
17 | playlistElementId?: number | ||
18 | startTimestamp?: number | ||
19 | stopTimestamp?: number | ||
20 | } | ||
21 | |||
14 | type PlaylistSummary = { | 22 | type PlaylistSummary = { |
15 | id: number | 23 | id: number |
16 | inPlaylist: boolean | ||
17 | displayName: string | 24 | displayName: string |
25 | optionalRowDisplayed: boolean | ||
18 | 26 | ||
19 | playlistElementId?: number | 27 | elements: PlaylistElement[] |
20 | startTimestamp?: number | ||
21 | stopTimestamp?: number | ||
22 | } | 28 | } |
23 | 29 | ||
24 | @Component({ | 30 | @Component({ |
@@ -33,16 +39,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
33 | @Input() lazyLoad = false | 39 | @Input() lazyLoad = false |
34 | 40 | ||
35 | isNewPlaylistBlockOpened = false | 41 | isNewPlaylistBlockOpened = false |
42 | |||
36 | videoPlaylistSearch: string | 43 | videoPlaylistSearch: string |
37 | videoPlaylistSearchChanged = new Subject<string>() | 44 | videoPlaylistSearchChanged = new Subject<string>() |
45 | |||
38 | videoPlaylists: PlaylistSummary[] = [] | 46 | videoPlaylists: PlaylistSummary[] = [] |
39 | timestampOptions: { | ||
40 | startTimestampEnabled: boolean | ||
41 | startTimestamp: number | ||
42 | stopTimestampEnabled: boolean | ||
43 | stopTimestamp: number | ||
44 | } | ||
45 | displayOptions = false | ||
46 | 47 | ||
47 | private disabled = false | 48 | private disabled = false |
48 | 49 | ||
@@ -106,7 +107,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
106 | this.videoPlaylists = [] | 107 | this.videoPlaylists = [] |
107 | this.videoPlaylistSearch = undefined | 108 | this.videoPlaylistSearch = undefined |
108 | 109 | ||
109 | this.resetOptions(true) | ||
110 | this.load() | 110 | this.load() |
111 | 111 | ||
112 | this.cd.markForCheck() | 112 | this.cd.markForCheck() |
@@ -115,7 +115,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
115 | load () { | 115 | load () { |
116 | logger('Loading component') | 116 | logger('Loading component') |
117 | 117 | ||
118 | this.listenToPlaylistChanges() | 118 | this.listenToVideoPlaylistChange() |
119 | 119 | ||
120 | this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch) | 120 | this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch) |
121 | .subscribe(playlistsResult => { | 121 | .subscribe(playlistsResult => { |
@@ -128,7 +128,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
128 | openChange (opened: boolean) { | 128 | openChange (opened: boolean) { |
129 | if (opened === false) { | 129 | if (opened === false) { |
130 | this.isNewPlaylistBlockOpened = false | 130 | this.isNewPlaylistBlockOpened = false |
131 | this.displayOptions = false | ||
132 | } | 131 | } |
133 | } | 132 | } |
134 | 133 | ||
@@ -138,17 +137,49 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
138 | this.isNewPlaylistBlockOpened = true | 137 | this.isNewPlaylistBlockOpened = true |
139 | } | 138 | } |
140 | 139 | ||
141 | togglePlaylist (event: Event, playlist: PlaylistSummary) { | 140 | toggleMainPlaylist (e: Event, playlist: PlaylistSummary) { |
142 | event.preventDefault() | 141 | e.preventDefault() |
142 | |||
143 | if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return | ||
143 | 144 | ||
144 | if (playlist.inPlaylist === true) { | 145 | if (playlist.elements.length === 0) { |
145 | this.removeVideoFromPlaylist(playlist) | 146 | const element: PlaylistElement = { |
147 | enabled: true, | ||
148 | playlistElementId: undefined, | ||
149 | startTimestamp: 0, | ||
150 | stopTimestamp: this.video.duration | ||
151 | } | ||
152 | |||
153 | this.addVideoInPlaylist(playlist, element) | ||
146 | } else { | 154 | } else { |
147 | this.addVideoInPlaylist(playlist) | 155 | this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId) |
156 | playlist.elements = [] | ||
148 | } | 157 | } |
149 | 158 | ||
150 | playlist.inPlaylist = !playlist.inPlaylist | 159 | this.cd.markForCheck() |
151 | this.resetOptions() | 160 | } |
161 | |||
162 | toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) { | ||
163 | e.preventDefault() | ||
164 | |||
165 | if (element.enabled) { | ||
166 | this.removeVideoFromPlaylist(playlist, element.playlistElementId) | ||
167 | element.enabled = false | ||
168 | |||
169 | // Hide optional rows pane when the user unchecked all the playlists | ||
170 | if (this.isPrimaryCheckboxChecked(playlist) === false) { | ||
171 | playlist.optionalRowDisplayed = false | ||
172 | } | ||
173 | } else { | ||
174 | const element: PlaylistElement = { | ||
175 | enabled: true, | ||
176 | playlistElementId: undefined, | ||
177 | startTimestamp, | ||
178 | stopTimestamp | ||
179 | } | ||
180 | |||
181 | this.addVideoInPlaylist(playlist, element) | ||
182 | } | ||
152 | 183 | ||
153 | this.cd.markForCheck() | 184 | this.cd.markForCheck() |
154 | } | 185 | } |
@@ -172,34 +203,99 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
172 | ) | 203 | ) |
173 | } | 204 | } |
174 | 205 | ||
175 | resetOptions (resetTimestamp = false) { | 206 | onVideoPlaylistSearchChanged () { |
176 | this.displayOptions = false | 207 | this.videoPlaylistSearchChanged.next() |
208 | } | ||
177 | 209 | ||
178 | this.timestampOptions = {} as any | 210 | isPrimaryCheckboxChecked (playlist: PlaylistSummary) { |
179 | this.timestampOptions.startTimestampEnabled = false | 211 | return playlist.elements.filter(e => e.enabled) |
180 | this.timestampOptions.stopTimestampEnabled = false | 212 | .length !== 0 |
213 | } | ||
181 | 214 | ||
182 | if (resetTimestamp) { | 215 | toggleOptionalRow (playlist: PlaylistSummary) { |
183 | this.timestampOptions.startTimestamp = 0 | 216 | playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed |
184 | this.timestampOptions.stopTimestamp = this.video.duration | 217 | |
185 | } | 218 | this.cd.markForCheck() |
186 | } | 219 | } |
187 | 220 | ||
188 | formatTimestamp (playlist: PlaylistSummary) { | 221 | getPrimaryInputName (playlist: PlaylistSummary) { |
189 | const start = playlist.startTimestamp ? secondsToTime(playlist.startTimestamp) : '' | 222 | return 'in-playlist-primary-' + playlist.id |
190 | const stop = playlist.stopTimestamp ? secondsToTime(playlist.stopTimestamp) : '' | 223 | } |
191 | 224 | ||
192 | return `(${start}-${stop})` | 225 | getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) { |
226 | const suffix = element | ||
227 | ? '-' + element.playlistElementId | ||
228 | : '' | ||
229 | |||
230 | return 'in-playlist-optional-' + playlist.id + suffix | ||
193 | } | 231 | } |
194 | 232 | ||
195 | onVideoPlaylistSearchChanged () { | 233 | buildOptionalRowElements (playlist: PlaylistSummary) { |
196 | this.videoPlaylistSearchChanged.next() | 234 | const elements = playlist.elements |
235 | |||
236 | const lastElement = elements.length === 0 | ||
237 | ? undefined | ||
238 | : elements[elements.length - 1] | ||
239 | |||
240 | // Build an empty last element | ||
241 | if (!lastElement || lastElement.enabled === true) { | ||
242 | elements.push({ | ||
243 | enabled: false, | ||
244 | startTimestamp: 0, | ||
245 | stopTimestamp: this.video.duration | ||
246 | }) | ||
247 | } | ||
248 | |||
249 | return elements | ||
250 | } | ||
251 | |||
252 | isPresentMultipleTimes (playlist: PlaylistSummary) { | ||
253 | return playlist.elements.filter(e => e.enabled === true).length > 1 | ||
254 | } | ||
255 | |||
256 | onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) { | ||
257 | if (!element.playlistElementId || element.enabled === false) return | ||
258 | |||
259 | const body: VideoPlaylistElementUpdate = { | ||
260 | startTimestamp: element.startTimestamp, | ||
261 | stopTimestamp: element.stopTimestamp | ||
262 | } | ||
263 | |||
264 | this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id) | ||
265 | .subscribe( | ||
266 | () => { | ||
267 | this.notifier.success($localize`Timestamps updated`) | ||
268 | }, | ||
269 | |||
270 | err => { | ||
271 | this.notifier.error(err.message) | ||
272 | }, | ||
273 | |||
274 | () => this.cd.markForCheck() | ||
275 | ) | ||
197 | } | 276 | } |
198 | 277 | ||
199 | private removeVideoFromPlaylist (playlist: PlaylistSummary) { | 278 | private isOptionalRowDisplayed (playlist: PlaylistSummary) { |
200 | if (!playlist.playlistElementId) return | 279 | const elements = playlist.elements.filter(e => e.enabled) |
280 | |||
281 | if (elements.length > 1) return true | ||
201 | 282 | ||
202 | this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, playlist.playlistElementId, this.video.id) | 283 | if (elements.length === 1) { |
284 | const element = elements[0] | ||
285 | |||
286 | if ( | ||
287 | (element.startTimestamp && element.startTimestamp !== 0) || | ||
288 | (element.stopTimestamp && element.stopTimestamp !== this.video.duration) | ||
289 | ) { | ||
290 | return true | ||
291 | } | ||
292 | } | ||
293 | |||
294 | return false | ||
295 | } | ||
296 | |||
297 | private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) { | ||
298 | this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id) | ||
203 | .subscribe( | 299 | .subscribe( |
204 | () => { | 300 | () => { |
205 | this.notifier.success($localize`Video removed from ${playlist.displayName}`) | 301 | this.notifier.success($localize`Video removed from ${playlist.displayName}`) |
@@ -213,7 +309,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
213 | ) | 309 | ) |
214 | } | 310 | } |
215 | 311 | ||
216 | private listenToPlaylistChanges () { | 312 | private listenToVideoPlaylistChange () { |
217 | this.unsubscribePlaylistChanges() | 313 | this.unsubscribePlaylistChanges() |
218 | 314 | ||
219 | this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id) | 315 | this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id) |
@@ -231,18 +327,30 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
231 | private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { | 327 | private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { |
232 | logger('Got existing results for %d.', this.video.id, existResult) | 328 | logger('Got existing results for %d.', this.video.id, existResult) |
233 | 329 | ||
330 | const oldPlaylists = this.videoPlaylists | ||
331 | |||
234 | this.videoPlaylists = [] | 332 | this.videoPlaylists = [] |
235 | for (const playlist of this.playlistsData) { | 333 | for (const playlist of this.playlistsData) { |
236 | const existingPlaylist = existResult.find(p => p.playlistId === playlist.id) | 334 | const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id) |
237 | 335 | ||
238 | this.videoPlaylists.push({ | 336 | const playlistSummary = { |
239 | id: playlist.id, | 337 | id: playlist.id, |
338 | optionalRowDisplayed: false, | ||
240 | displayName: playlist.displayName, | 339 | displayName: playlist.displayName, |
241 | inPlaylist: !!existingPlaylist, | 340 | elements: existingPlaylists.map(e => ({ |
242 | playlistElementId: existingPlaylist ? existingPlaylist.playlistElementId : undefined, | 341 | enabled: true, |
243 | startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, | 342 | playlistElementId: e.playlistElementId, |
244 | stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined | 343 | startTimestamp: e.startTimestamp || 0, |
245 | }) | 344 | stopTimestamp: e.stopTimestamp || this.video.duration |
345 | })) | ||
346 | } | ||
347 | |||
348 | const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id) | ||
349 | playlistSummary.optionalRowDisplayed = oldPlaylist | ||
350 | ? oldPlaylist.optionalRowDisplayed | ||
351 | : this.isOptionalRowDisplayed(playlistSummary) | ||
352 | |||
353 | this.videoPlaylists.push(playlistSummary) | ||
246 | } | 354 | } |
247 | 355 | ||
248 | logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists) | 356 | logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists) |
@@ -250,20 +358,22 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
250 | this.cd.markForCheck() | 358 | this.cd.markForCheck() |
251 | } | 359 | } |
252 | 360 | ||
253 | private addVideoInPlaylist (playlist: PlaylistSummary) { | 361 | private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) { |
254 | const body: VideoPlaylistElementCreate = { videoId: this.video.id } | 362 | const body: VideoPlaylistElementCreate = { videoId: this.video.id } |
255 | 363 | ||
256 | if (this.timestampOptions.startTimestampEnabled) body.startTimestamp = this.timestampOptions.startTimestamp | 364 | if (element.startTimestamp) body.startTimestamp = element.startTimestamp |
257 | if (this.timestampOptions.stopTimestampEnabled) body.stopTimestamp = this.timestampOptions.stopTimestamp | 365 | if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp |
258 | 366 | ||
259 | this.videoPlaylistService.addVideoInPlaylist(playlist.id, body) | 367 | this.videoPlaylistService.addVideoInPlaylist(playlist.id, body) |
260 | .subscribe( | 368 | .subscribe( |
261 | () => { | 369 | res => { |
262 | const message = body.startTimestamp || body.stopTimestamp | 370 | const message = body.startTimestamp || body.stopTimestamp |
263 | ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(playlist)}` | 371 | ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}` |
264 | : $localize`Video added in ${playlist.displayName}` | 372 | : $localize`Video added in ${playlist.displayName}` |
265 | 373 | ||
266 | this.notifier.success(message) | 374 | this.notifier.success(message) |
375 | |||
376 | if (element) element.playlistElementId = res.videoPlaylistElement.id | ||
267 | }, | 377 | }, |
268 | 378 | ||
269 | err => { | 379 | err => { |
@@ -273,4 +383,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
273 | () => this.cd.markForCheck() | 383 | () => this.cd.markForCheck() |
274 | ) | 384 | ) |
275 | } | 385 | } |
386 | |||
387 | private formatTimestamp (element: PlaylistElement) { | ||
388 | const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : '' | ||
389 | const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : '' | ||
390 | |||
391 | return `(${start}-${stop})` | ||
392 | } | ||
276 | } | 393 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index dc1b56129..1b87e0b2a 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { uniq } from 'lodash-es' | 2 | import { uniq } from 'lodash-es' |
3 | import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' | 3 | import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' |
4 | import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' | 4 | import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators' |
5 | import { HttpClient, HttpParams } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { Injectable, NgZone } from '@angular/core' | 6 | import { Injectable, NgZone } from '@angular/core' |
7 | import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' | 7 | import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' |
@@ -53,6 +53,7 @@ export class VideoPlaylistService { | |||
53 | ) { | 53 | ) { |
54 | this.videoExistsInPlaylistObservable = merge( | 54 | this.videoExistsInPlaylistObservable = merge( |
55 | this.videoExistsInPlaylistNotifier.pipe( | 55 | this.videoExistsInPlaylistNotifier.pipe( |
56 | distinctUntilChanged(), | ||
56 | // We leave Angular zone so Protractor does not get stuck | 57 | // We leave Angular zone so Protractor does not get stuck |
57 | bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), | 58 | bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), |
58 | filter(videoIds => videoIds.length !== 0), | 59 | filter(videoIds => videoIds.length !== 0), |