Update playlist add component to accept multiple times the same video

pull/3108/head
Chocobozzz 2020-08-18 15:51:51 +02:00 committed by Chocobozzz
parent cbb513e737
commit e79df4eefb
5 changed files with 292 additions and 134 deletions

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'
import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { secondsToTime, timeToInt } from '../../../assets/player/utils'
@ -19,6 +19,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
@Input() timestamp: number
@Input() disabled = false
@Output() inputBlur = new EventEmitter()
timestampString: string
constructor (private changeDetector: ChangeDetectorRef) {}
@ -57,5 +59,7 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
this.propagateChange(this.timestamp)
}
this.inputBlur.emit()
}
}

View File

@ -2,42 +2,6 @@
<div class="header">
<div class="first-row">
<div i18n class="title">Save to</div>
<div class="options" (click)="displayOptions = !displayOptions">
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<span i18n>Options</span>
</div>
</div>
<div class="options-row" *ngIf="displayOptions">
<div>
<my-peertube-checkbox
inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
i18n-labelText labelText="Start at"
></my-peertube-checkbox>
<my-timestamp-input
[timestamp]="timestampOptions.startTimestamp"
[maxTimestamp]="video.duration"
[disabled]="!timestampOptions.startTimestampEnabled"
[(ngModel)]="timestampOptions.startTimestamp"
></my-timestamp-input>
</div>
<div>
<my-peertube-checkbox
inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
i18n-labelText labelText="Stop at"
></my-peertube-checkbox>
<my-timestamp-input
[timestamp]="timestampOptions.stopTimestamp"
[maxTimestamp]="video.duration"
[disabled]="!timestampOptions.stopTimestampEnabled"
[(ngModel)]="timestampOptions.stopTimestamp"
></my-timestamp-input>
</div>
</div>
</div>
@ -46,14 +10,52 @@
</div>
<div class="playlists">
<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
<my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox>
<div
*ngFor="let playlist of videoPlaylists"
class="playlist dropdown-item" [ngClass]="{ 'has-optional-row': playlist.optionalRowDisplayed }"
>
<div class="primary-row">
<my-peertube-checkbox
[disabled]="isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed" [inputName]="getPrimaryInputName(playlist)"
[ngModel]="isPrimaryCheckboxChecked(playlist)" [onPushWorkaround]="true"
(click)="toggleMainPlaylist($event, playlist)"
></my-peertube-checkbox>
<div class="display-name">
{{ playlist.displayName }}
<label class="display-name" (click)="toggleMainPlaylist($event, playlist)">
{{ playlist.displayName }}
</label>
<div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
{{ formatTimestamp(playlist) }}
<div class="optional-row-icon" *ngIf="isPrimaryCheckboxChecked(playlist)" (click)="toggleOptionalRow(playlist)">
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
</div>
</div>
<div class="optional-rows" *ngIf="playlist.optionalRowDisplayed">
<div class="labels">
<div i18n>Start at</div>
<div i18n>Stop at</div>
</div>
<div *ngFor="let element of buildOptionalRowElements(playlist)">
<my-peertube-checkbox
[inputName]="getOptionalInputName(playlist, element)"
[ngModel]="element.enabled" [onPushWorkaround]="true"
(click)="toggleOptionalPlaylist($event, playlist, element, startAt.timestamp, stopAt.timestamp)"
></my-peertube-checkbox>
<my-timestamp-input
[maxTimestamp]="video.duration"
[(ngModel)]="element.startTimestamp"
(inputBlur)="onElementTimestampUpdate(playlist, element)"
#startAt
></my-timestamp-input>
<my-timestamp-input
[maxTimestamp]="video.duration"
[(ngModel)]="element.stopTimestamp"
(inputBlur)="onElementTimestampUpdate(playlist, element)"
#stopAt
></my-timestamp-input>
</div>
</div>
</div>

View File

@ -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;
}
}
}

View File

@ -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<string>()
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})`
}
}

View File

@ -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),