Add/update/delete/list my playlists

pull/1745/head
Chocobozzz 2019-03-06 15:36:44 +01:00 committed by Chocobozzz
parent d4c9f45b31
commit 830b4faff1
48 changed files with 1076 additions and 153 deletions

View File

@ -15,6 +15,13 @@ import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blockli
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
import {
MyAccountVideoPlaylistCreateComponent
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
import {
MyAccountVideoPlaylistUpdateComponent
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
const myAccountRoutes: Routes = [
{
@ -36,6 +43,7 @@ const myAccountRoutes: Routes = [
}
}
},
{
path: 'video-channels',
component: MyAccountVideoChannelsComponent,
@ -63,6 +71,35 @@ const myAccountRoutes: Routes = [
}
}
},
{
path: 'video-playlists',
component: MyAccountVideoPlaylistsComponent,
data: {
meta: {
title: 'Account playlists'
}
}
},
{
path: 'video-playlists/create',
component: MyAccountVideoPlaylistCreateComponent,
data: {
meta: {
title: 'Create new playlist'
}
}
},
{
path: 'video-playlists/update/:videoPlaylistId',
component: MyAccountVideoPlaylistUpdateComponent,
data: {
meta: {
title: 'Update playlist'
}
}
},
{
path: 'videos',
component: MyAccountVideosComponent,

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core'
import { Notifier } from '@app/core'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { UserSubscriptionService } from '@app/shared/user-subscription'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@ -21,8 +20,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
constructor (
private userSubscriptionService: UserSubscriptionService,
private notifier: Notifier,
private i18n: I18n
private notifier: Notifier
) {}
ngOnInit () {

View File

@ -1,7 +1,7 @@
<div class="video-channels-header">
<a class="create-button" routerLink="create">
<my-global-icon iconName="add"></my-global-icon>
<ng-container i18n>Create another video channel</ng-container>
<ng-container i18n>Create a new video channel</ng-container>
</a>
</div>

View File

@ -0,0 +1,89 @@
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { AuthService, Notifier, ServerService } from '@app/core'
import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoPlaylistValidatorsService } from '@app/shared'
import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { VideoConstant } from '@shared/models'
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
@Component({
selector: 'my-account-video-playlist-create',
templateUrl: './my-account-video-playlist-edit.component.html',
styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
})
export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylistEdit implements OnInit {
error: string
videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
constructor (
protected formValidatorService: FormValidatorService,
private authService: AuthService,
private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
private notifier: Notifier,
private router: Router,
private videoPlaylistService: VideoPlaylistService,
private serverService: ServerService,
private i18n: I18n
) {
super()
}
ngOnInit () {
this.buildForm({
'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
thumbnailfile: null
})
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
() => {
this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
this.form.patchValue({
privacy: VideoPlaylistPrivacy.PRIVATE
})
}
)
}
formValidated () {
this.error = undefined
const body = this.form.value
const videoPlaylistCreate: VideoPlaylistCreate = {
displayName: body['display-name'],
privacy: body.privacy,
description: body.description || null,
videoChannelId: body.videoChannelId || null,
thumbnailfile: body.thumbnailfile || null
}
this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe(
() => {
this.notifier.success(
this.i18n('Playlist {{playlistName}} created.', { playlistName: videoPlaylistCreate.displayName })
)
this.router.navigate([ '/my-account', 'video-playlists' ])
},
err => this.error = err.message
)
}
isCreation () {
return true
}
getFormButtonTitle () {
return this.i18n('Create')
}
}

View File

@ -0,0 +1,64 @@
<div i18n class="form-sub-title" *ngIf="isCreation() === true">Create a new playlist</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="row">
<div class="col-md-12 col-xl-6">
<div class="form-group">
<label i18n for="display-name">Display name</label>
<input
type="text" id="display-name"
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
>
<div *ngIf="formErrors['display-name']" class="form-error">
{{ formErrors['display-name'] }}
</div>
</div>
<div class="form-group">
<label i18n for="description">Description</label>
<textarea
id="description" formControlName="description"
[ngClass]="{ 'input-error': formErrors['description'] }"
></textarea>
<div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
</div>
</div>
</div>
<div class="col-md-12 col-xl-6">
<div class="form-group">
<label i18n for="privacy">Privacy</label>
<div class="peertube-select-container">
<select id="privacy" formControlName="privacy">
<option *ngFor="let privacy of videoPlaylistPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
</select>
</div>
<div *ngIf="formErrors.privacy" class="form-error">
{{ formErrors.privacy }}
</div>
</div>
<div class="form-group">
<label i18n>Channel</label>
<div class="peertube-select-container">
<select formControlName="videoChannelId">
<option></option>
<option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
</select>
</div>
</div>
<div class="form-group">
<my-image-upload
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="200px" previewHeight="110px"
></my-image-upload>
</div>
</div>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>

View File

@ -0,0 +1,27 @@
@import '_variables';
@import '_mixins';
.form-sub-title {
margin-bottom: 20px;
}
input[type=text] {
@include peertube-input-text(340px);
display: block;
}
textarea {
@include peertube-textarea(500px, 150px);
display: block;
}
.peertube-select-container {
@include peertube-select-container(340px);
}
input[type=submit] {
@include peertube-button;
@include orange-button;
}

View File

@ -0,0 +1,13 @@
import { FormReactive } from '@app/shared'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { ServerService } from '@app/core'
import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
export abstract class MyAccountVideoPlaylistEdit extends FormReactive {
// Declare it here to avoid errors in create template
videoPlaylistToUpdate: VideoPlaylist
userVideoChannels: { id: number, label: string }[] = []
abstract isCreation (): boolean
abstract getFormButtonTitle (): string
}

View File

@ -0,0 +1,132 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, Notifier, ServerService } from '@app/core'
import { Subscription } from 'rxjs'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { MyAccountVideoPlaylistEdit } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-edit'
import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { VideoPlaylistValidatorsService } from '@app/shared'
import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
import { VideoConstant } from '@shared/models'
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
@Component({
selector: 'my-account-video-playlist-update',
templateUrl: './my-account-video-playlist-edit.component.html',
styleUrls: [ './my-account-video-playlist-edit.component.scss' ]
})
export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylistEdit implements OnInit, OnDestroy {
error: string
videoPlaylistToUpdate: VideoPlaylist
videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacy>[] = []
private paramsSub: Subscription
constructor (
protected formValidatorService: FormValidatorService,
private authService: AuthService,
private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
private notifier: Notifier,
private router: Router,
private route: ActivatedRoute,
private videoPlaylistService: VideoPlaylistService,
private i18n: I18n,
private serverService: ServerService
) {
super()
}
ngOnInit () {
this.buildForm({
'display-name': this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME,
privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY,
description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION,
videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID,
thumbnailfile: null
})
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
this.paramsSub = this.route.params.subscribe(routeParams => {
const videoPlaylistId = routeParams['videoPlaylistId']
this.videoPlaylistService.getVideoPlaylist(videoPlaylistId).subscribe(
videoPlaylistToUpdate => {
this.videoPlaylistToUpdate = videoPlaylistToUpdate
this.hydrateFormFromPlaylist()
this.serverService.videoPlaylistPrivaciesLoaded.subscribe(
() => {
this.videoPlaylistPrivacies = this.serverService.getVideoPlaylistPrivacies()
.filter(p => {
// If the playlist is not private, we cannot put it in private anymore
return this.videoPlaylistToUpdate.privacy.id === VideoPlaylistPrivacy.PRIVATE ||
p.id !== VideoPlaylistPrivacy.PRIVATE
})
}
)
},
err => this.error = err.message
)
})
}
ngOnDestroy () {
if (this.paramsSub) this.paramsSub.unsubscribe()
}
formValidated () {
this.error = undefined
const body = this.form.value
const videoPlaylistUpdate: VideoPlaylistUpdate = {
displayName: body['display-name'],
privacy: body['privacy'],
description: body.description || null,
videoChannelId: body.videoChannelId || null,
thumbnailfile: body.thumbnailfile || undefined
}
this.videoPlaylistService.updateVideoPlaylist(this.videoPlaylistToUpdate, videoPlaylistUpdate).subscribe(
() => {
this.notifier.success(
this.i18n('Playlist {{videoPlaylistName}} updated.', { videoPlaylistName: videoPlaylistUpdate.displayName })
)
this.router.navigate([ '/my-account', 'video-playlists' ])
},
err => this.error = err.message
)
}
isCreation () {
return false
}
getFormButtonTitle () {
return this.i18n('Update')
}
private hydrateFormFromPlaylist () {
this.form.patchValue({
'display-name': this.videoPlaylistToUpdate.displayName,
privacy: this.videoPlaylistToUpdate.privacy.id,
description: this.videoPlaylistToUpdate.description,
videoChannelId: this.videoPlaylistToUpdate.videoChannel ? this.videoPlaylistToUpdate.videoChannel.id : null
})
fetch(this.videoPlaylistToUpdate.thumbnailUrl)
.then(response => response.blob())
.then(data => {
this.form.patchValue({
thumbnailfile: data
})
})
}
}

View File

@ -0,0 +1,20 @@
<div class="video-playlists-header">
<a class="create-button" routerLink="create">
<my-global-icon iconName="add"></my-global-icon>
<ng-container i18n>Create a new playlist</ng-container>
</a>
</div>
<div class="video-playlists">
<div *ngFor="let playlist of videoPlaylists" class="video-playlist">
<div class="miniature-wrapper">
<my-video-playlist-miniature [playlist]="playlist"></my-video-playlist-miniature>
</div>
<div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
<my-delete-button (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
<my-edit-button [routerLink]="[ 'update', playlist.uuid ]"></my-edit-button>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
@import '_variables';
@import '_mixins';
.create-button {
@include create-button;
}
/deep/ .action-button {
&.action-button-delete {
margin-right: 10px;
}
}
.video-playlist {
@include row-blocks;
.miniature-wrapper {
flex-grow: 1;
/deep/ .miniature {
display: flex;
.miniature-bottom {
margin-left: 10px;
}
}
}
.video-playlist-buttons {
min-width: 190px;
}
}
.video-playlists-header {
text-align: right;
margin: 20px 0 50px;
}
@media screen and (max-width: 800px) {
.video-playlists-header {
text-align: center;
}
.video-playlist {
.video-playlist-buttons {
margin-top: 10px;
}
}
}

View File

@ -0,0 +1,85 @@
import { Component, OnInit } from '@angular/core'
import { Notifier } from '@app/core'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { User } from '@app/shared'
import { flatMap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { VideoPlaylistType } from '@shared/models'
@Component({
selector: 'my-account-video-playlists',
templateUrl: './my-account-video-playlists.component.html',
styleUrls: [ './my-account-video-playlists.component.scss' ]
})
export class MyAccountVideoPlaylistsComponent implements OnInit {
videoPlaylists: VideoPlaylist[] = []
pagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 10,
totalItems: null
}
private user: User
constructor (
private authService: AuthService,
private notifier: Notifier,
private confirmService: ConfirmService,
private videoPlaylistService: VideoPlaylistService,
private i18n: I18n
) {}
ngOnInit () {
this.user = this.authService.getUser()
this.loadVideoPlaylists()
}
async deleteVideoPlaylist (videoPlaylist: VideoPlaylist) {
const res = await this.confirmService.confirm(
this.i18n(
'Do you really want to delete {{playlistDisplayName}}?',
{ playlistDisplayName: videoPlaylist.displayName }
),
this.i18n('Delete')
)
if (res === false) return
this.videoPlaylistService.removeVideoPlaylist(videoPlaylist)
.subscribe(
() => {
this.videoPlaylists = this.videoPlaylists
.filter(p => p.id !== videoPlaylist.id)
this.notifier.success(
this.i18n('Playlist {{playlistDisplayName}} deleted.', { playlistDisplayName: videoPlaylist.displayName })
)
},
error => this.notifier.error(error.message)
)
}
isRegularPlaylist (playlist: VideoPlaylist) {
return playlist.type.id === VideoPlaylistType.REGULAR
}
private loadVideoPlaylists () {
this.authService.userInformationLoaded
.pipe(flatMap(() => this.videoPlaylistService.listAccountPlaylists(this.user.account)))
.subscribe(res => this.videoPlaylists = res.data)
}
private ofNearOfBottom () {
// Last page
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
this.pagination.currentPage += 1
this.loadVideoPlaylists()
}
}

View File

@ -27,6 +27,10 @@ export class MyAccountComponent {
label: this.i18n('My videos'),
routerLink: '/my-account/videos'
},
{
label: this.i18n('My playlists'),
routerLink: '/my-account/video-playlists'
},
{
label: this.i18n('My subscriptions'),
routerLink: '/my-account/subscriptions'

View File

@ -25,6 +25,13 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
import {
MyAccountVideoPlaylistCreateComponent
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
import {
MyAccountVideoPlaylistUpdateComponent
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
@NgModule({
imports: [
@ -57,7 +64,11 @@ import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-a
MyAccountServerBlocklistComponent,
MyAccountHistoryComponent,
MyAccountNotificationsComponent,
MyAccountNotificationPreferencesComponent
MyAccountNotificationPreferencesComponent,
MyAccountVideoPlaylistCreateComponent,
MyAccountVideoPlaylistUpdateComponent,
MyAccountVideoPlaylistsComponent
],
exports: [

View File

@ -74,6 +74,7 @@ export class AppComponent implements OnInit {
this.serverService.loadVideoLanguages()
this.serverService.loadVideoLicences()
this.serverService.loadVideoPrivacies()
this.serverService.loadVideoPlaylistPrivacies()
// Do not display menu on small screens
if (this.screenService.isInSmallView()) {

View File

@ -9,17 +9,20 @@ import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos
import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
import { sortBy } from '@app/shared/misc/utils'
import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
@Injectable()
export class ServerService {
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
configLoaded = new ReplaySubject<boolean>(1)
videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
videoPlaylistPrivaciesLoaded = new ReplaySubject<boolean>(1)
videoCategoriesLoaded = new ReplaySubject<boolean>(1)
videoLicencesLoaded = new ReplaySubject<boolean>(1)
videoLanguagesLoaded = new ReplaySubject<boolean>(1)
@ -101,6 +104,7 @@ export class ServerService {
private videoLicences: Array<VideoConstant<number>> = []
private videoLanguages: Array<VideoConstant<string>> = []
private videoPrivacies: Array<VideoConstant<VideoPrivacy>> = []
private videoPlaylistPrivacies: Array<VideoConstant<VideoPlaylistPrivacy>> = []
constructor (
private http: HttpClient,
@ -121,19 +125,28 @@ export class ServerService {
}
loadVideoCategories () {
return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded, true)
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'categories', this.videoCategories, this.videoCategoriesLoaded, true)
}
loadVideoLicences () {
return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded)
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'licences', this.videoLicences, this.videoLicencesLoaded)
}
loadVideoLanguages () {
return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded, true)
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'languages', this.videoLanguages, this.videoLanguagesLoaded, true)
}
loadVideoPrivacies () {
return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
return this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
}
loadVideoPlaylistPrivacies () {
return this.loadAttributeEnum(
ServerService.BASE_VIDEO_PLAYLIST_URL,
'privacies',
this.videoPlaylistPrivacies,
this.videoPlaylistPrivaciesLoaded
)
}
getConfig () {
@ -156,7 +169,12 @@ export class ServerService {
return this.videoPrivacies
}
private loadVideoAttributeEnum (
getVideoPlaylistPrivacies () {
return this.videoPlaylistPrivacies
}
private loadAttributeEnum (
baseUrl: string,
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: VideoConstant<string | number>[],
notifier: ReplaySubject<boolean>,
@ -165,7 +183,7 @@ export class ServerService {
this.localeObservable
.pipe(
switchMap(translations => {
return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName)
return this.http.get<{ [id: string]: string }>(baseUrl + attributeName)
.pipe(map(data => ({ data, translations })))
})
)

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { GlobalIconName } from '@app/shared/icons/global-icon.component'
import { GlobalIconName } from '@app/shared/images/global-icon.component'
@Component({
selector: 'my-button',

View File

@ -10,6 +10,7 @@ export * from './video-blacklist-validators.service'
export * from './video-channel-validators.service'
export * from './video-comment-validators.service'
export * from './video-validators.service'
export * from './video-playlist-validators.service'
export * from './video-captions-validators.service'
export * from './video-change-ownership-validators.service'
export * from './video-accept-ownership-validators.service'

View File

@ -0,0 +1,52 @@
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Validators } from '@angular/forms'
import { Injectable } from '@angular/core'
import { BuildFormValidator } from '@app/shared'
@Injectable()
export class VideoPlaylistValidatorsService {
readonly VIDEO_PLAYLIST_DISPLAY_NAME: BuildFormValidator
readonly VIDEO_PLAYLIST_PRIVACY: BuildFormValidator
readonly VIDEO_PLAYLIST_DESCRIPTION: BuildFormValidator
readonly VIDEO_PLAYLIST_CHANNEL_ID: BuildFormValidator
constructor (private i18n: I18n) {
this.VIDEO_PLAYLIST_DISPLAY_NAME = {
VALIDATORS: [
Validators.required,
Validators.minLength(1),
Validators.maxLength(120)
],
MESSAGES: {
'required': this.i18n('Display name is required.'),
'minlength': this.i18n('Display name must be at least 1 character long.'),
'maxlength': this.i18n('Display name cannot be more than 120 characters long.')
}
}
this.VIDEO_PLAYLIST_PRIVACY = {
VALIDATORS: [
Validators.required
],
MESSAGES: {
'required': this.i18n('Privacy is required.')
}
}
this.VIDEO_PLAYLIST_DESCRIPTION = {
VALIDATORS: [
Validators.minLength(3),
Validators.maxLength(1000)
],
MESSAGES: {
'minlength': i18n('Description must be at least 3 characters long.'),
'maxlength': i18n('Description cannot be more than 1000 characters long.')
}
}
this.VIDEO_PLAYLIST_CHANNEL_ID = {
VALIDATORS: [ ],
MESSAGES: { }
}
}
}

View File

@ -4,18 +4,18 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
import { ServerService } from '@app/core'
@Component({
selector: 'my-video-image',
styleUrls: [ './video-image.component.scss' ],
templateUrl: './video-image.component.html',
selector: 'my-image-upload',
styleUrls: [ './image-upload.component.scss' ],
templateUrl: './image-upload.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => VideoImageComponent),
useExisting: forwardRef(() => ImageUploadComponent),
multi: true
}
]
})
export class VideoImageComponent implements ControlValueAccessor {
export class ImageUploadComponent implements ControlValueAccessor {
@Input() inputLabel: string
@Input() inputName: string
@Input() previewWidth: string

View File

@ -17,7 +17,7 @@ function getParameterByName (name: string, url: string) {
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support: string }[]) {
function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) {
return new Promise(res => {
authService.userInformationLoaded
.subscribe(

View File

@ -45,6 +45,7 @@ import {
VideoChangeOwnershipValidatorsService,
VideoChannelValidatorsService,
VideoCommentValidatorsService,
VideoPlaylistValidatorsService,
VideoValidatorsService
} from '@app/shared/forms'
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
@ -68,8 +69,11 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications
import { InstanceService } from '@app/shared/instance/instance.service'
import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
import { GlobalIconComponent } from '@app/shared/icons/global-icon.component'
import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { ImageUploadComponent } from '@app/shared/images/image-upload.component'
import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
@NgModule({
imports: [
@ -92,8 +96,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
declarations: [
LoaderComponent,
SmallLoaderComponent,
VideoThumbnailComponent,
VideoMiniatureComponent,
VideoPlaylistMiniatureComponent,
FeedComponent,
ButtonComponent,
DeleteButtonComponent,
@ -116,7 +123,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
TopMenuDropdownComponent,
UserNotificationsComponent,
ConfirmComponent,
GlobalIconComponent
GlobalIconComponent,
ImageUploadComponent
],
exports: [
@ -138,8 +147,11 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
LoaderComponent,
SmallLoaderComponent,
VideoThumbnailComponent,
VideoMiniatureComponent,
VideoPlaylistMiniatureComponent,
FeedComponent,
ButtonComponent,
DeleteButtonComponent,
@ -159,7 +171,9 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
TopMenuDropdownComponent,
UserNotificationsComponent,
ConfirmComponent,
GlobalIconComponent,
ImageUploadComponent,
NumberFormatterPipe,
ObjectLengthPipe,
@ -177,6 +191,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
VideoService,
AccountService,
VideoChannelService,
VideoPlaylistService,
VideoCaptionService,
VideoImportService,
UserSubscriptionService,
@ -186,6 +201,7 @@ import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
VideoPlaylistValidatorsService,
VideoAbuseValidatorsService,
VideoChannelValidatorsService,
VideoCommentValidatorsService,

View File

@ -0,0 +1,22 @@
<div class="miniature">
<a
[routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName"
class="miniature-thumbnail"
>
<img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
<div class="miniature-playlist-info-overlay">
<ng-container i18n>{playlist.videosLength, plural, =0 {No videos} =1 {1 video} other {{{playlist.videosLength}} videos}}</ng-container>
</div>
<div class="play-overlay">
<div class="icon"></div>
</div>
</a>
<div class="miniature-bottom">
<a tabindex="-1" class="miniature-name" [routerLink]="[ '/videos/watch' ]" [attr.title]="playlist.displayName">
{{ playlist.displayName }}
</a>
</div>
</div>

View File

@ -0,0 +1,34 @@
@import '_variables';
@import '_mixins';
@import '_miniature';
.miniature {
display: inline-block;
.miniature-thumbnail {
@include miniature-thumbnail;
.miniature-playlist-info-overlay {
@include static-thumbnail-overlay;
position: absolute;
right: 0;
bottom: 0;
height: $video-thumbnail-height;
padding: 0 10px;
display: flex;
align-items: center;
font-size: 15px;
}
}
.miniature-bottom {
width: 200px;
margin-top: 2px;
line-height: normal;
.miniature-name {
@include miniature-name;
}
}
}

View File

@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
@Component({
selector: 'my-video-playlist-miniature',
styleUrls: [ './video-playlist-miniature.component.scss' ],
templateUrl: './video-playlist-miniature.component.html'
})
export class VideoPlaylistMiniatureComponent {
@Input() playlist: VideoPlaylist
}

View File

@ -0,0 +1,74 @@
import {
VideoChannelSummary,
VideoConstant,
VideoPlaylist as ServerVideoPlaylist,
VideoPlaylistPrivacy,
VideoPlaylistType
} from '../../../../../shared/models/videos'
import { AccountSummary, peertubeTranslate } from '@shared/models'
import { Actor } from '@app/shared/actor/actor.model'
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
export class VideoPlaylist implements ServerVideoPlaylist {
id: number
uuid: string
isLocal: boolean
displayName: string
description: string
privacy: VideoConstant<VideoPlaylistPrivacy>
thumbnailPath: string
videosLength: number
type: VideoConstant<VideoPlaylistType>
createdAt: Date | string
updatedAt: Date | string
ownerAccount: AccountSummary
videoChannel?: VideoChannelSummary
thumbnailUrl: string
ownerBy: string
ownerAvatarUrl: string
videoChannelBy?: string
videoChannelAvatarUrl?: string
constructor (hash: ServerVideoPlaylist, translations: {}) {
const absoluteAPIUrl = getAbsoluteAPIUrl()
this.id = hash.id
this.uuid = hash.uuid
this.isLocal = hash.isLocal
this.displayName = hash.displayName
this.description = hash.description
this.privacy = hash.privacy
this.thumbnailPath = hash.thumbnailPath
this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
this.videosLength = hash.videosLength
this.type = hash.type
this.createdAt = new Date(hash.createdAt)
this.updatedAt = new Date(hash.updatedAt)
this.ownerAccount = hash.ownerAccount
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount)
if (hash.videoChannel) {
this.videoChannel = hash.videoChannel
this.videoChannelBy = Actor.CREATE_BY_STRING(hash.videoChannel.name, hash.videoChannel.host)
this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.videoChannel)
}
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
}
}

View File

@ -0,0 +1,108 @@
import { catchError, map, switchMap } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { RestExtractor } from '../rest/rest-extractor.service'
import { HttpClient } from '@angular/common/http'
import { ResultList } from '../../../../../shared'
import { environment } from '../../../environments/environment'
import { VideoPlaylist as VideoPlaylistServerModel } from '@shared/models/videos/playlist/video-playlist.model'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model'
import { objectToFormData } from '@app/shared/misc/utils'
import { ServerService } from '@app/core'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { AccountService } from '@app/shared/account/account.service'
import { Account } from '@app/shared/account/account.model'
@Injectable()
export class VideoPlaylistService {
static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
constructor (
private authHttp: HttpClient,
private serverService: ServerService,
private restExtractor: RestExtractor
) { }
listChannelPlaylists (videoChannel: VideoChannel): Observable<ResultList<VideoPlaylist>> {
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists'
return this.authHttp.get<ResultList<VideoPlaylist>>(url)
.pipe(
switchMap(res => this.extractPlaylists(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
listAccountPlaylists (account: Account): Observable<ResultList<VideoPlaylist>> {
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists'
return this.authHttp.get<ResultList<VideoPlaylist>>(url)
.pipe(
switchMap(res => this.extractPlaylists(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
getVideoPlaylist (id: string | number) {
const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + id
return this.authHttp.get<VideoPlaylist>(url)
.pipe(
switchMap(res => this.extractPlaylist(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
createVideoPlaylist (body: VideoPlaylistCreate) {
const data = objectToFormData(body)
return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err))
)
}
updateVideoPlaylist (videoPlaylist: VideoPlaylist, body: VideoPlaylistUpdate) {
const data = objectToFormData(body)
return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err))
)
}
removeVideoPlaylist (videoPlaylist: VideoPlaylist) {
return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err))
)
}
extractPlaylists (result: ResultList<VideoPlaylistServerModel>) {
return this.serverService.localeObservable
.pipe(
map(translations => {
const playlistsJSON = result.data
const total = result.total
const playlists: VideoPlaylist[] = []
for (const playlistJSON of playlistsJSON) {
playlists.push(new VideoPlaylist(playlistJSON, translations))
}
return { data: playlists, total }
})
)
}
extractPlaylist (playlist: VideoPlaylistServerModel) {
return this.serverService.localeObservable
.pipe(map(translations => new VideoPlaylist(playlist, translations)))
}
}

View File

@ -1,4 +1,5 @@
@import '_mixins';
@import '_miniature';
.videos {
text-align: center;

View File

@ -1,5 +1,6 @@
@import '_variables';
@import '_mixins';
@import '_miniature';
.video-miniature {
display: inline-block;
@ -14,26 +15,7 @@
line-height: normal;
.video-miniature-name {
@include ellipsis-multiline(
$font-size: 1rem,
$line-height: 1,
$lines-to-show: 2
);
transition: color 0.2s;
font-size: 16px;
font-weight: $font-semibold;
color: var(--mainForegroundColor);
margin-top: 5px;
margin-bottom: 5px;
&:hover {
text-decoration: none;
}
&.blur-filter {
filter: blur(3px);
padding-left: 4px;
}
@include miniature-name;
}
.video-miniature-created-at-views {

View File

@ -4,9 +4,11 @@
>
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
<div class="video-thumbnail-overlay">{{ video.durationLabel }}</div>
<div class="video-thumbnail-duration-overlay">{{ video.durationLabel }}</div>
<div class="play-overlay"></div>
<div class="play-overlay">
<div class="icon"></div>
</div>
<div class="progress-bar" *ngIf="video.userHistory?.currentTime">
<div [ngStyle]="{ 'width.%': getProgressPercent() }"></div>

View File

@ -1,66 +1,9 @@
@import '_variables';
@import '_mixins';
$play-overlay-transition: 0.2s ease;
$play-overlay-height: 26px;
$play-overlay-width: 18px;
@import '_miniature';
.video-thumbnail {
@include disable-outline;
display: inline-block;
position: relative;
border-radius: 3px;
overflow: hidden;
width: $video-thumbnail-width;
height: $video-thumbnail-height;
background-color: #ececec;
transition: filter $play-overlay-transition;
&:hover {
text-decoration: none !important;
filter: brightness(85%);
.play-overlay {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
&.focus-visible {
box-shadow: 0 0 0 2px var(--mainColor);
}
img {
width: $video-thumbnail-width;
height: $video-thumbnail-height;
&.blur-filter {
filter: blur(5px);
transform : scale(1.03);
}
}
.play-overlay {
width: 0;
height: 0;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.5);
transition: all $play-overlay-transition;
border-top: ($play-overlay-height / 2) solid transparent;
border-bottom: ($play-overlay-height / 2) solid transparent;
border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
opacity: 0;
}
@include miniature-thumbnail;
.progress-bar {
height: 3px;
@ -75,16 +18,15 @@ $play-overlay-width: 18px;
}
}
.video-thumbnail-overlay {
.video-thumbnail-duration-overlay {
@include static-thumbnail-overlay;
position: absolute;
right: 5px;
bottom: 5px;
display: inline-block;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 0 5px;
border-radius: 3px;
font-size: 12px;
font-weight: $font-bold;
border-radius: 3px;
padding: 0 5px;
}
}

View File

@ -117,9 +117,8 @@ export class Video implements VideoServerModel {
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
this.scheduledUpdate = hash.scheduledUpdate
this.originallyPublishedAt = hash.originallyPublishedAt ?
new Date(hash.originallyPublishedAt.toString())
: null
this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
this.blacklisted = hash.blacklisted

View File

@ -188,17 +188,17 @@
<div class="row advanced-settings">
<div class="col-md-12 col-xl-8">
<div class="form-group">
<my-video-image
<my-image-upload
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="200px" previewHeight="110px"
></my-video-image>
></my-image-upload>
</div>
<div class="form-group">
<my-video-image
<my-image-upload
i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
previewWidth="360px" previewHeight="200px"
></my-video-image>
></my-image-upload>
</div>
<div class="form-group">

View File

@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'
import { TagInputModule } from 'ngx-chips'
import { SharedModule } from '../../../shared/'
import { VideoEditComponent } from './video-edit.component'
import { VideoImageComponent } from './video-image.component'
import { CalendarModule } from 'primeng/components/calendar/calendar'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
@ -16,7 +15,6 @@ import { VideoCaptionAddModalComponent } from './video-caption-add-modal.compone
declarations: [
VideoEditComponent,
VideoImageComponent,
VideoCaptionAddModalComponent
],

View File

@ -1,5 +1,6 @@
@import '_variables';
@import '_mixins';
@import '_miniature';
.section {
padding-top: 10px;
@ -50,4 +51,4 @@
.section {
@include video-miniature-small-screen;
}
}
}

View File

@ -0,0 +1,133 @@
@import '_variables';
@import '_mixins';
@mixin miniature-name {
@include ellipsis-multiline(
$font-size: 1rem,
$line-height: 1,
$lines-to-show: 2
);
transition: color 0.2s;
font-size: 16px;
font-weight: $font-semibold;
color: var(--mainForegroundColor);
margin-top: 5px;
margin-bottom: 5px;
&:hover {
text-decoration: none;
}
&.blur-filter {
filter: blur(3px);
padding-left: 4px;
}
}
$play-overlay-transition: 0.2s ease;
$play-overlay-height: 26px;
$play-overlay-width: 18px;
@mixin miniature-thumbnail {
@include disable-outline;
display: inline-block;
position: relative;
border-radius: 3px;
overflow: hidden;
width: $video-thumbnail-width;
height: $video-thumbnail-height;
background-color: #ececec;
transition: filter $play-overlay-transition;
.play-overlay {
position: absolute;
right: 0;
bottom: 0;
width: $video-thumbnail-width;
height: $video-thumbnail-height;
opacity: 0;
background-color: rgba(0, 0, 0, 0.7);
&, .icon {
transition: all $play-overlay-transition;
}
.icon {
width: 0;
height: 0;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.5);
border-top: ($play-overlay-height / 2) solid transparent;
border-bottom: ($play-overlay-height / 2) solid transparent;
border-left: $play-overlay-width solid rgba(255, 255, 255, 0.95);
}
}
&:hover {
text-decoration: none !important;
.play-overlay {
opacity: 1;
.icon {
transform: translate(-50%, -50%) scale(1);
}
}
}
&.focus-visible {
box-shadow: 0 0 0 2px var(--mainColor);
}
img {
width: $video-thumbnail-width;
height: $video-thumbnail-height;
&.blur-filter {
filter: blur(5px);
transform : scale(1.03);
}
}
}
@mixin static-thumbnail-overlay {
display: inline-block;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
}
@mixin video-miniature-small-screen {
text-align: center;
/deep/ .video-miniature {
padding-right: 0;
height: auto;
width: 100%;
margin-bottom: 20px;
.video-miniature-information {
width: 100% !important;
span {
width: 100%;
}
}
.video-thumbnail {
width: 100%;
height: auto;
img {
width: 100%;
height: auto;
}
}
}
}

View File

@ -516,31 +516,3 @@
}
}
@mixin video-miniature-small-screen {
text-align: center;
/deep/ .video-miniature {
padding-right: 0;
height: auto;
width: 100%;
margin-bottom: 20px;
.video-miniature-information {
width: 100% !important;
span {
width: 100%;
}
}
.video-thumbnail {
width: 100%;
height: auto;
img {
width: 100%;
height: auto;
}
}
}
}

View File

@ -28,6 +28,7 @@
"baseUrl": "src",
"paths": {
"@app/*": [ "app/*" ],
"@shared/*": [ "../../shared/*" ],
"video.js": [ "../node_modules/video.js/dist/alt/video.core.js" ],
"fs": [ "./shims/noop" ],
"http": [ "./shims/http" ],
@ -41,11 +42,14 @@
"strictInjectionParameters": true,
"fullTemplateTypeCheck": true
},
"include": [
"../../shared"
],
"exclude": [
"../../node_modules",
"../node_modules",
"node_modules",
"dist",
"../server",
"src/**/*.spec.ts"
"../dist",
"../../server",
"../src/**/*.spec.ts"
]
}

View File

@ -5,7 +5,7 @@ import {
buildLanguages,
VIDEO_CATEGORIES,
VIDEO_IMPORT_STATES,
VIDEO_LICENCES,
VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES,
VIDEO_PRIVACIES,
VIDEO_STATES
} from '../../server/initializers/constants'
@ -46,6 +46,8 @@ values(VIDEO_CATEGORIES)
.concat(values(VIDEO_PRIVACIES))
.concat(values(VIDEO_STATES))
.concat(values(VIDEO_IMPORT_STATES))
.concat(values(VIDEO_PLAYLIST_PRIVACIES))
.concat(values(VIDEO_PLAYLIST_TYPES))
.concat([
'This video does not exist.',
'We cannot fetch the video. Please try again later.',

View File

@ -642,7 +642,7 @@ let STATIC_MAX_AGE = '2h'
// Videos thumbnail size
const THUMBNAILS_SIZE = {
width: 223,
height: 112
height: 122
}
const PREVIEWS_SIZE = {
width: 560,

View File

@ -344,6 +344,7 @@ function getCommonPlaylistEditAttributes () {
.custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
body('videoChannelId')
.optional()
.customSanitizer(toValueOrNull)
.toInt()
] as (ValidationChain | express.Handler)[]
}

View File

@ -11,6 +11,13 @@ export * from './blacklist/video-blacklist-update.model'
export * from './channel/video-channel-create.model'
export * from './channel/video-channel-update.model'
export * from './channel/video-channel.model'
export * from './playlist/video-playlist-create.model'
export * from './playlist/video-playlist-element-create.model'
export * from './playlist/video-playlist-element-update.model'
export * from './playlist/video-playlist-privacy.model'
export * from './playlist/video-playlist-type.model'
export * from './playlist/video-playlist-update.model'
export * from './playlist/video-playlist.model'
export * from './video-change-ownership.model'
export * from './video-change-ownership-create.model'
export * from './video-create.model'
@ -27,4 +34,4 @@ export * from './caption/video-caption-update.model'
export * from './import/video-import-create.model'
export * from './import/video-import-state.enum'
export * from './import/video-import.model'
export { VideoConstant } from './video-constant.model'
export * from './video-constant.model'

View File

@ -21,6 +21,6 @@ export interface VideoPlaylist {
createdAt: Date | string
updatedAt: Date | string
ownerAccount?: AccountSummary
ownerAccount: AccountSummary
videoChannel?: VideoChannelSummary
}

View File

@ -265,9 +265,21 @@ async function checkPlaylistFilesWereRemoved (
}
}
function getVideoPlaylistPrivacies (url: string) {
const path = '/api/v1/video-playlists/privacies'
return makeGetRequest({
url,
path,
statusCodeExpected: 200
})
}
// ---------------------------------------------------------------------------
export {
getVideoPlaylistPrivacies,
getVideoPlaylistsList,
getVideoChannelPlaylistsList,
getAccountPlaylistsList,