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 = [
{
}
}
},
+
{
path: 'video-channels',
component: MyAccountVideoChannelsComponent,
}
}
},
+
+ {
+ 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,
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'
constructor (
private userSubscriptionService: UserSubscriptionService,
- private notifier: Notifier,
- private i18n: I18n
+ private notifier: Notifier
) {}
ngOnInit () {
<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>
--- /dev/null
+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')
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+@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;
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+ })
+ })
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+ }
+}
--- /dev/null
+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()
+ }
+}
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'
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: [
MyAccountServerBlocklistComponent,
MyAccountHistoryComponent,
MyAccountNotificationsComponent,
- MyAccountNotificationPreferencesComponent
+ MyAccountNotificationPreferencesComponent,
+
+ MyAccountVideoPlaylistCreateComponent,
+ MyAccountVideoPlaylistUpdateComponent,
+ MyAccountVideoPlaylistsComponent
],
exports: [
this.serverService.loadVideoLanguages()
this.serverService.loadVideoLicences()
this.serverService.loadVideoPrivacies()
+ this.serverService.loadVideoPlaylistPrivacies()
// Do not display menu on small screens
if (this.screenService.isInSmallView()) {
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)
private videoLicences: Array<VideoConstant<number>> = []
private videoLanguages: Array<VideoConstant<string>> = []
private videoPrivacies: Array<VideoConstant<VideoPrivacy>> = []
+ private videoPlaylistPrivacies: Array<VideoConstant<VideoPlaylistPrivacy>> = []
constructor (
private http: HttpClient,
}
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 () {
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>,
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 })))
})
)
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',
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'
--- /dev/null
+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: { }
+ }
+ }
+}
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
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(
VideoChangeOwnershipValidatorsService,
VideoChannelValidatorsService,
VideoCommentValidatorsService,
+ VideoPlaylistValidatorsService,
VideoValidatorsService
} from '@app/shared/forms'
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
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: [
declarations: [
LoaderComponent,
SmallLoaderComponent,
+
VideoThumbnailComponent,
VideoMiniatureComponent,
+ VideoPlaylistMiniatureComponent,
+
FeedComponent,
ButtonComponent,
DeleteButtonComponent,
TopMenuDropdownComponent,
UserNotificationsComponent,
ConfirmComponent,
- GlobalIconComponent
+
+ GlobalIconComponent,
+ ImageUploadComponent
],
exports: [
LoaderComponent,
SmallLoaderComponent,
+
VideoThumbnailComponent,
VideoMiniatureComponent,
+ VideoPlaylistMiniatureComponent,
+
FeedComponent,
ButtonComponent,
DeleteButtonComponent,
TopMenuDropdownComponent,
UserNotificationsComponent,
ConfirmComponent,
+
GlobalIconComponent,
+ ImageUploadComponent,
NumberFormatterPipe,
ObjectLengthPipe,
VideoService,
AccountService,
VideoChannelService,
+ VideoPlaylistService,
VideoCaptionService,
VideoImportService,
UserSubscriptionService,
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
+ VideoPlaylistValidatorsService,
VideoAbuseValidatorsService,
VideoChannelValidatorsService,
VideoCommentValidatorsService,
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)))
+ }
+}
@import '_mixins';
+@import '_miniature';
.videos {
text-align: center;
@import '_variables';
@import '_mixins';
+@import '_miniature';
.video-miniature {
display: inline-block;
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 {
>
<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>
@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;
}
}
- .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;
}
}
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
<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">
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'
declarations: [
VideoEditComponent,
- VideoImageComponent,
VideoCaptionAddModalComponent
],
@import '_variables';
@import '_mixins';
+@import '_miniature';
.section {
padding-top: 10px;
.section {
@include video-miniature-small-screen;
}
-}
\ No newline at end of file
+}
--- /dev/null
+@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;
+ }
+ }
+ }
+}
}
}
-@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;
- }
- }
- }
-}
"baseUrl": "src",
"paths": {
"@app/*": [ "app/*" ],
+ "@shared/*": [ "../../shared/*" ],
"video.js": [ "../node_modules/video.js/dist/alt/video.core.js" ],
"fs": [ "./shims/noop" ],
"http": [ "./shims/http" ],
"strictInjectionParameters": true,
"fullTemplateTypeCheck": true
},
+ "include": [
+ "../../shared"
+ ],
"exclude": [
+ "../../node_modules",
"../node_modules",
- "node_modules",
- "dist",
- "../server",
- "src/**/*.spec.ts"
+ "../dist",
+ "../../server",
+ "../src/**/*.spec.ts"
]
}
buildLanguages,
VIDEO_CATEGORIES,
VIDEO_IMPORT_STATES,
- VIDEO_LICENCES,
+ VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES,
VIDEO_PRIVACIES,
VIDEO_STATES
} from '../../server/initializers/constants'
.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.',
// Videos thumbnail size
const THUMBNAILS_SIZE = {
width: 223,
- height: 112
+ height: 122
}
const PREVIEWS_SIZE = {
width: 560,
.custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
body('videoChannelId')
.optional()
+ .customSanitizer(toValueOrNull)
.toInt()
] as (ValidationChain | express.Handler)[]
}
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'
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'
createdAt: Date | string
updatedAt: Date | string
- ownerAccount?: AccountSummary
+ ownerAccount: AccountSummary
videoChannel?: VideoChannelSummary
}
}
}
+function getVideoPlaylistPrivacies (url: string) {
+ const path = '/api/v1/video-playlists/privacies'
+
+ return makeGetRequest({
+ url,
+ path,
+ statusCodeExpected: 200
+ })
+}
+
// ---------------------------------------------------------------------------
export {
+ getVideoPlaylistPrivacies,
+
getVideoPlaylistsList,
getVideoChannelPlaylistsList,
getAccountPlaylistsList,