+
-
- {{ playlist.displayName }}
+
-
- {{ formatTimestamp(playlist) }}
+
+
+
+
+
+
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 @@
@import '_variables';
@import '_mixins';
+$optional-rows-checkbox-width: 34px;
+$timestamp-width: 50px;
+$timestamp-margin-right: 10px;
+
.header,
.dropdown-item,
.input-container {
@@ -24,31 +28,6 @@
font-size: 18px;
flex-grow: 1;
}
-
- .options {
- display: flex;
- align-items: center;
- font-size: 14px;
- cursor: pointer;
-
- my-global-icon {
- @include apply-svg-color(#333);
-
- width: 16px;
- height: 23px;
- margin-right: 3px;
- }
- }
- }
-
- .options-row {
- margin-top: 10px;
- padding-left: 10px;
-
- > div {
- display: flex;
- align-items: center;
- }
}
}
@@ -58,8 +37,16 @@
}
.playlist {
- display: inline-flex;
- cursor: pointer;
+ padding: 8px 10px 8px 24px;
+
+ &.has-optional-row:hover {
+ background-color: inherit;
+ }
+}
+
+.primary-row,
+.optional-rows > div {
+ display: flex;
my-peertube-checkbox {
margin-right: 10px;
@@ -69,11 +56,58 @@
.display-name {
display: flex;
align-items: flex-end;
+ flex-grow: 1;
+ margin: 0;
+ font-weight: $font-regular;
+ cursor: pointer;
+ }
- .timestamp-info {
- font-size: 0.9em;
- color: pvar(--greyForegroundColor);
- margin-left: 5px;
+ .optional-row-icon {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ cursor: pointer;
+
+ my-global-icon {
+ @include apply-svg-color(#333);
+
+ width: 19px;
+ height: 19px;
+ margin-right: 0;
+ }
+ }
+
+ my-timestamp-input {
+ margin-right: $timestamp-margin-right;
+
+ ::ng-deep .ui-inputtext {
+ padding: 0;
+ width: $timestamp-width;
+ }
+ }
+}
+
+.optional-rows {
+ > div {
+ padding: 8px 5px 5px 10px;
+ }
+
+ my-peertube-checkbox {
+ display: block;
+ width: $optional-rows-checkbox-width;
+ margin-right: 0 !important;
+ }
+
+ .labels {
+ margin-left: $optional-rows-checkbox-width;
+ font-size: 13px;
+ color: pvar(--greyForegroundColor);
+ padding-top: 5px;
+ padding-bottom: 0;
+
+ div {
+ margin-right: $timestamp-margin-right;
+ width: $timestamp-width;
}
}
}
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,23 +4,29 @@ import { debounceTime, filter } from 'rxjs/operators'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
-import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models'
+import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy, VideoPlaylistElementUpdate } from '@shared/models'
import { secondsToTime } from '../../../assets/player/utils'
import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
+import { invoke, last } from 'lodash'
const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
-type PlaylistSummary = {
- id: number
- inPlaylist: boolean
- displayName: string
-
+type PlaylistElement = {
+ enabled: boolean
playlistElementId?: number
startTimestamp?: number
stopTimestamp?: number
}
+type PlaylistSummary = {
+ id: number
+ displayName: string
+ optionalRowDisplayed: boolean
+
+ elements: PlaylistElement[]
+}
+
@Component({
selector: 'my-video-add-to-playlist',
styleUrls: [ './video-add-to-playlist.component.scss' ],
@@ -33,16 +39,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
@Input() lazyLoad = false
isNewPlaylistBlockOpened = false
+
videoPlaylistSearch: string
videoPlaylistSearchChanged = new Subject
()
+
videoPlaylists: PlaylistSummary[] = []
- timestampOptions: {
- startTimestampEnabled: boolean
- startTimestamp: number
- stopTimestampEnabled: boolean
- stopTimestamp: number
- }
- displayOptions = false
private disabled = false
@@ -106,7 +107,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
this.videoPlaylists = []
this.videoPlaylistSearch = undefined
- this.resetOptions(true)
this.load()
this.cd.markForCheck()
@@ -115,7 +115,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
load () {
logger('Loading component')
- this.listenToPlaylistChanges()
+ this.listenToVideoPlaylistChange()
this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
.subscribe(playlistsResult => {
@@ -128,7 +128,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
openChange (opened: boolean) {
if (opened === false) {
this.isNewPlaylistBlockOpened = false
- this.displayOptions = false
}
}
@@ -138,17 +137,49 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
this.isNewPlaylistBlockOpened = true
}
- togglePlaylist (event: Event, playlist: PlaylistSummary) {
- event.preventDefault()
+ toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
+ e.preventDefault()
- if (playlist.inPlaylist === true) {
- this.removeVideoFromPlaylist(playlist)
+ if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
+
+ if (playlist.elements.length === 0) {
+ const element: PlaylistElement = {
+ enabled: true,
+ playlistElementId: undefined,
+ startTimestamp: 0,
+ stopTimestamp: this.video.duration
+ }
+
+ this.addVideoInPlaylist(playlist, element)
} else {
- this.addVideoInPlaylist(playlist)
+ this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
+ playlist.elements = []
}
- playlist.inPlaylist = !playlist.inPlaylist
- this.resetOptions()
+ this.cd.markForCheck()
+ }
+
+ toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) {
+ e.preventDefault()
+
+ if (element.enabled) {
+ this.removeVideoFromPlaylist(playlist, element.playlistElementId)
+ element.enabled = false
+
+ // Hide optional rows pane when the user unchecked all the playlists
+ if (this.isPrimaryCheckboxChecked(playlist) === false) {
+ playlist.optionalRowDisplayed = false
+ }
+ } else {
+ const element: PlaylistElement = {
+ enabled: true,
+ playlistElementId: undefined,
+ startTimestamp,
+ stopTimestamp
+ }
+
+ this.addVideoInPlaylist(playlist, element)
+ }
this.cd.markForCheck()
}
@@ -172,34 +203,99 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
)
}
- resetOptions (resetTimestamp = false) {
- this.displayOptions = false
-
- this.timestampOptions = {} as any
- this.timestampOptions.startTimestampEnabled = false
- this.timestampOptions.stopTimestampEnabled = false
-
- if (resetTimestamp) {
- this.timestampOptions.startTimestamp = 0
- this.timestampOptions.stopTimestamp = this.video.duration
- }
- }
-
- formatTimestamp (playlist: PlaylistSummary) {
- const start = playlist.startTimestamp ? secondsToTime(playlist.startTimestamp) : ''
- const stop = playlist.stopTimestamp ? secondsToTime(playlist.stopTimestamp) : ''
-
- return `(${start}-${stop})`
- }
-
onVideoPlaylistSearchChanged () {
this.videoPlaylistSearchChanged.next()
}
- private removeVideoFromPlaylist (playlist: PlaylistSummary) {
- if (!playlist.playlistElementId) return
+ isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
+ return playlist.elements.filter(e => e.enabled)
+ .length !== 0
+ }
- this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, playlist.playlistElementId, this.video.id)
+ toggleOptionalRow (playlist: PlaylistSummary) {
+ playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
+
+ this.cd.markForCheck()
+ }
+
+ getPrimaryInputName (playlist: PlaylistSummary) {
+ return 'in-playlist-primary-' + playlist.id
+ }
+
+ getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
+ const suffix = element
+ ? '-' + element.playlistElementId
+ : ''
+
+ return 'in-playlist-optional-' + playlist.id + suffix
+ }
+
+ buildOptionalRowElements (playlist: PlaylistSummary) {
+ const elements = playlist.elements
+
+ const lastElement = elements.length === 0
+ ? undefined
+ : elements[elements.length - 1]
+
+ // Build an empty last element
+ if (!lastElement || lastElement.enabled === true) {
+ elements.push({
+ enabled: false,
+ startTimestamp: 0,
+ stopTimestamp: this.video.duration
+ })
+ }
+
+ return elements
+ }
+
+ isPresentMultipleTimes (playlist: PlaylistSummary) {
+ return playlist.elements.filter(e => e.enabled === true).length > 1
+ }
+
+ onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) {
+ if (!element.playlistElementId || element.enabled === false) return
+
+ const body: VideoPlaylistElementUpdate = {
+ startTimestamp: element.startTimestamp,
+ stopTimestamp: element.stopTimestamp
+ }
+
+ this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id)
+ .subscribe(
+ () => {
+ this.notifier.success($localize`Timestamps updated`)
+ },
+
+ err => {
+ this.notifier.error(err.message)
+ },
+
+ () => this.cd.markForCheck()
+ )
+ }
+
+ private isOptionalRowDisplayed (playlist: PlaylistSummary) {
+ const elements = playlist.elements.filter(e => e.enabled)
+
+ if (elements.length > 1) return true
+
+ if (elements.length === 1) {
+ const element = elements[0]
+
+ if (
+ (element.startTimestamp && element.startTimestamp !== 0) ||
+ (element.stopTimestamp && element.stopTimestamp !== this.video.duration)
+ ) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) {
+ this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id)
.subscribe(
() => {
this.notifier.success($localize`Video removed from ${playlist.displayName}`)
@@ -213,7 +309,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
)
}
- private listenToPlaylistChanges () {
+ private listenToVideoPlaylistChange () {
this.unsubscribePlaylistChanges()
this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
@@ -231,18 +327,30 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
logger('Got existing results for %d.', this.video.id, existResult)
+ const oldPlaylists = this.videoPlaylists
+
this.videoPlaylists = []
for (const playlist of this.playlistsData) {
- const existingPlaylist = existResult.find(p => p.playlistId === playlist.id)
+ const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
- this.videoPlaylists.push({
+ const playlistSummary = {
id: playlist.id,
+ optionalRowDisplayed: false,
displayName: playlist.displayName,
- inPlaylist: !!existingPlaylist,
- playlistElementId: existingPlaylist ? existingPlaylist.playlistElementId : undefined,
- startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined,
- stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined
- })
+ elements: existingPlaylists.map(e => ({
+ enabled: true,
+ playlistElementId: e.playlistElementId,
+ startTimestamp: e.startTimestamp || 0,
+ stopTimestamp: e.stopTimestamp || this.video.duration
+ }))
+ }
+
+ const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id)
+ playlistSummary.optionalRowDisplayed = oldPlaylist
+ ? oldPlaylist.optionalRowDisplayed
+ : this.isOptionalRowDisplayed(playlistSummary)
+
+ this.videoPlaylists.push(playlistSummary)
}
logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
@@ -250,20 +358,22 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
this.cd.markForCheck()
}
- private addVideoInPlaylist (playlist: PlaylistSummary) {
+ private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
const body: VideoPlaylistElementCreate = { videoId: this.video.id }
- if (this.timestampOptions.startTimestampEnabled) body.startTimestamp = this.timestampOptions.startTimestamp
- if (this.timestampOptions.stopTimestampEnabled) body.stopTimestamp = this.timestampOptions.stopTimestamp
+ if (element.startTimestamp) body.startTimestamp = element.startTimestamp
+ if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
.subscribe(
- () => {
+ res => {
const message = body.startTimestamp || body.stopTimestamp
- ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(playlist)}`
+ ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}`
: $localize`Video added in ${playlist.displayName}`
this.notifier.success(message)
+
+ if (element) element.playlistElementId = res.videoPlaylistElement.id
},
err => {
@@ -273,4 +383,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
() => this.cd.markForCheck()
)
}
+
+ private formatTimestamp (element: PlaylistElement) {
+ const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : ''
+ const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : ''
+
+ return `(${start}-${stop})`
+ }
}
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 @@
import * as debug from 'debug'
import { uniq } from 'lodash-es'
import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs'
-import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators'
+import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable, NgZone } from '@angular/core'
import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
@@ -53,6 +53,7 @@ export class VideoPlaylistService {
) {
this.videoExistsInPlaylistObservable = merge(
this.videoExistsInPlaylistNotifier.pipe(
+ distinctUntilChanged(),
// We leave Angular zone so Protractor does not get stuck
bufferTime(500, leaveZone(this.ngZone, asyncScheduler)),
filter(videoIds => videoIds.length !== 0),