Add ability to schedule video publication
This commit is contained in:
parent
2baea0c77c
commit
bbe0f0645c
|
@ -85,7 +85,7 @@
|
|||
"ngx-pipes": "^2.1.7",
|
||||
"node-sass": "^4.1.1",
|
||||
"npm-font-source-sans-pro": "^1.0.2",
|
||||
"primeng": "^5.2.6",
|
||||
"primeng": "^6.0.0-rc.1",
|
||||
"protractor": "^5.3.2",
|
||||
"purify-css": "^1.2.5",
|
||||
"purifycss-webpack": "^0.7.0",
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AccountService } from '@app/shared/account/account.service'
|
|||
import { tap } from 'rxjs/operators'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
@ -37,6 +38,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
|||
protected notificationsService: NotificationsService,
|
||||
protected confirmService: ConfirmService,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
protected i18n: I18n,
|
||||
private accountService: AccountService,
|
||||
private videoService: VideoService
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div class="video-info">
|
||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
|
||||
<div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
|
||||
<div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Display only once -->
|
||||
|
@ -28,9 +28,9 @@
|
|||
Cancel
|
||||
</span>
|
||||
|
||||
<span i18n class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
||||
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
||||
<span class="icon icon-delete-white"></span>
|
||||
Delete
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
|
||||
color: #000;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
font-size: 16px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { from as observableFrom, Observable } from 'rxjs'
|
||||
import { concatAll, tap } from 'rxjs/operators'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, Inject, LOCALE_ID } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Location } from '@angular/common'
|
||||
import { immutableAssign } from '@app/shared/misc/utils'
|
||||
|
@ -12,7 +12,8 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
|||
import { Video } from '../../shared/video/video.model'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { VideoState } from '../../../../../shared/models/videos'
|
||||
import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
@ -39,8 +40,10 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
|||
protected notificationsService: NotificationsService,
|
||||
protected confirmService: ConfirmService,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
protected i18n: I18n,
|
||||
private videoService: VideoService
|
||||
private videoService: VideoService,
|
||||
@Inject(LOCALE_ID) private localeId: string
|
||||
) {
|
||||
super()
|
||||
|
||||
|
@ -131,12 +134,22 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
|||
}
|
||||
|
||||
getStateLabel (video: Video) {
|
||||
if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
|
||||
let suffix: string
|
||||
|
||||
if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding')
|
||||
if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode')
|
||||
if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) {
|
||||
suffix = this.i18n('Published')
|
||||
} else if (video.scheduledUpdate) {
|
||||
const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
|
||||
suffix = this.i18n('Publication scheduled on ') + updateAt
|
||||
} else if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) {
|
||||
suffix = this.i18n('Waiting transcoding')
|
||||
} else if (video.state.id === VideoState.TO_TRANSCODE) {
|
||||
suffix = this.i18n('To transcode')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.i18n('Unknown state')
|
||||
return ' - ' + suffix
|
||||
}
|
||||
|
||||
protected buildVideoHeight () {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
|||
import { tap } from 'rxjs/operators'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-videos',
|
||||
|
@ -37,6 +38,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
|||
protected notificationsService: NotificationsService,
|
||||
protected confirmService: ConfirmService,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
protected i18n: I18n,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private videoService: VideoService
|
||||
|
|
|
@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'
|
|||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
|
||||
import { AuthService, RedirectService, ServerService } from '@app/core'
|
||||
import { isInSmallView } from '@app/shared/misc/utils'
|
||||
import { is18nPath } from '../../../shared/models/i18n'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
|
@ -33,7 +33,8 @@ export class AppComponent implements OnInit {
|
|||
private authService: AuthService,
|
||||
private serverService: ServerService,
|
||||
private domSanitizer: DomSanitizer,
|
||||
private redirectService: RedirectService
|
||||
private redirectService: RedirectService,
|
||||
private screenService: ScreenService
|
||||
) { }
|
||||
|
||||
get serverVersion () {
|
||||
|
@ -75,14 +76,14 @@ export class AppComponent implements OnInit {
|
|||
this.serverService.loadVideoPrivacies()
|
||||
|
||||
// Do not display menu on small screens
|
||||
if (isInSmallView()) {
|
||||
if (this.screenService.isInSmallView()) {
|
||||
this.isMenuDisplayed = false
|
||||
}
|
||||
|
||||
this.router.events.subscribe(
|
||||
e => {
|
||||
// User clicked on a link in the menu, change the page
|
||||
if (e instanceof GuardsCheckStart && isInSmallView()) {
|
||||
if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
|
||||
this.isMenuDisplayed = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ export class ServerService {
|
|||
)
|
||||
.subscribe(({ data, translations }) => {
|
||||
Object.keys(data)
|
||||
.map(dataKey => parseInt(dataKey, 10))
|
||||
.forEach(dataKey => {
|
||||
const label = data[ dataKey ]
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export class VideoValidatorsService {
|
|||
readonly VIDEO_DESCRIPTION: BuildFormValidator
|
||||
readonly VIDEO_TAGS: BuildFormValidator
|
||||
readonly VIDEO_SUPPORT: BuildFormValidator
|
||||
readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
|
||||
|
||||
constructor (private i18n: I18n) {
|
||||
|
||||
|
@ -84,5 +85,12 @@ export class VideoValidatorsService {
|
|||
'maxlength': this.i18n('Video support cannot be more than 500 characters long.')
|
||||
}
|
||||
}
|
||||
|
||||
this.VIDEO_SCHEDULE_PUBLICATION_AT = {
|
||||
VALIDATORS: [ ],
|
||||
MESSAGES: {
|
||||
'required': this.i18n('A date is required to schedule video update.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { isInSmallView } from '@app/shared/misc/utils'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
import truncate from 'lodash-es/truncate'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-markdown-textarea',
|
||||
|
@ -35,7 +35,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
|||
|
||||
private contentChanged = new Subject<string>()
|
||||
|
||||
constructor (private markdownService: MarkdownService) {}
|
||||
constructor (
|
||||
private screenService: ScreenService,
|
||||
private markdownService: MarkdownService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.contentChanged
|
||||
|
@ -76,7 +79,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
|||
}
|
||||
|
||||
arePreviewsDisplayed () {
|
||||
return isInSmallView() === false
|
||||
return this.screenService.isInSmallView() === false
|
||||
}
|
||||
|
||||
private updatePreviews () {
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class I18nPrimengCalendarService {
|
||||
private readonly calendarLocale: any = {}
|
||||
|
||||
constructor (private i18n: I18n) {
|
||||
this.calendarLocale = {
|
||||
firstDayOfWeek: 0,
|
||||
dayNames: [
|
||||
this.i18n('Sunday'),
|
||||
this.i18n('Monday'),
|
||||
this.i18n('Tuesday'),
|
||||
this.i18n('Wednesday'),
|
||||
this.i18n('Thursday'),
|
||||
this.i18n('Friday'),
|
||||
this.i18n('Saturday')
|
||||
],
|
||||
|
||||
dayNamesShort: [
|
||||
this.i18n({ value: 'Sun', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Mon', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Tue', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Wed', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Thu', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Fri', description: 'Day name short' }),
|
||||
this.i18n({ value: 'Sat', description: 'Day name short' })
|
||||
],
|
||||
|
||||
dayNamesMin: [
|
||||
this.i18n({ value: 'Su', description: 'Day name min' }),
|
||||
this.i18n({ value: 'Mo', description: 'Day name min' }),
|
||||
this.i18n({ value: 'Tu', description: 'Day name min' }),
|
||||
this.i18n({ value: 'We', description: 'Day name min' }),
|
||||
this.i18n({ value: 'Th', description: 'Day name min' }),
|
||||
this.i18n({ value: 'Fr', description: 'Day name min' }),
|
||||
this.i18n({ value: 'Sa', description: 'Day name min' })
|
||||
],
|
||||
|
||||
monthNames: [
|
||||
this.i18n('January'),
|
||||
this.i18n('February'),
|
||||
this.i18n('March'),
|
||||
this.i18n('April'),
|
||||
this.i18n('May'),
|
||||
this.i18n('June'),
|
||||
this.i18n('July'),
|
||||
this.i18n('August'),
|
||||
this.i18n('September'),
|
||||
this.i18n('October'),
|
||||
this.i18n('November'),
|
||||
this.i18n('December')
|
||||
],
|
||||
|
||||
monthNamesShort: [
|
||||
this.i18n({ value: 'Jan', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Feb', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Mar', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Apr', description: 'Month name short' }),
|
||||
this.i18n({ value: 'May', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Jun', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Jul', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Aug', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Sep', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Oct', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Nov', description: 'Month name short' }),
|
||||
this.i18n({ value: 'Dec', description: 'Month name short' })
|
||||
],
|
||||
|
||||
today: this.i18n('Today'),
|
||||
|
||||
clear: this.i18n('Clear')
|
||||
}
|
||||
}
|
||||
|
||||
getCalendarLocale () {
|
||||
return this.calendarLocale
|
||||
}
|
||||
|
||||
getTimezone () {
|
||||
const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
return `${timezone} - ${gmt}`
|
||||
}
|
||||
|
||||
getDateFormat () {
|
||||
return this.i18n({
|
||||
value: 'yy-mm-dd ',
|
||||
description: 'Date format in this locale.'
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable, NgZone } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class ScreenService {
|
||||
private windowInnerWidth: number
|
||||
|
||||
constructor (private zone: NgZone) {
|
||||
this.windowInnerWidth = window.innerWidth
|
||||
|
||||
// Try to cache a little bit window.innerWidth
|
||||
this.zone.runOutsideAngular(() => {
|
||||
setInterval(() => this.windowInnerWidth = window.innerWidth, 500)
|
||||
})
|
||||
}
|
||||
|
||||
isInSmallView () {
|
||||
return this.windowInnerWidth < 600
|
||||
}
|
||||
|
||||
isInMobileView () {
|
||||
return this.windowInnerWidth < 500
|
||||
}
|
||||
}
|
|
@ -96,26 +96,12 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
|
|||
})
|
||||
}
|
||||
|
||||
// Try to cache a little bit window.innerWidth
|
||||
let windowInnerWidth = window.innerWidth
|
||||
setInterval(() => windowInnerWidth = window.innerWidth, 500)
|
||||
|
||||
function isInSmallView () {
|
||||
return windowInnerWidth < 600
|
||||
}
|
||||
|
||||
function isInMobileView () {
|
||||
return windowInnerWidth < 500
|
||||
}
|
||||
|
||||
export {
|
||||
objectToUrlEncoded,
|
||||
getParameterByName,
|
||||
populateAsyncUserVideoChannels,
|
||||
getAbsoluteAPIUrl,
|
||||
dateToHuman,
|
||||
isInSmallView,
|
||||
isInMobileView,
|
||||
immutableAssign,
|
||||
objectToFormData,
|
||||
lineFeedToHtml
|
||||
|
|
|
@ -41,6 +41,8 @@ import {
|
|||
ResetPasswordValidatorsService,
|
||||
UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService
|
||||
} from '@app/shared/forms'
|
||||
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -128,6 +130,9 @@ import {
|
|||
VideoCommentValidatorsService,
|
||||
VideoValidatorsService,
|
||||
|
||||
I18nPrimengCalendarService,
|
||||
ScreenService,
|
||||
|
||||
I18n
|
||||
]
|
||||
})
|
||||
|
|
|
@ -2,7 +2,6 @@ import { debounceTime } from 'rxjs/operators'
|
|||
import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Location } from '@angular/common'
|
||||
import { isInMobileView } from '@app/shared/misc/utils'
|
||||
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { fromEvent, Observable, Subscription } from 'rxjs'
|
||||
|
@ -11,6 +10,7 @@ import { ComponentPagination } from '../rest/component-pagination.model'
|
|||
import { VideoSortField } from './sort-field.type'
|
||||
import { Video } from './video.model'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
||||
private static LINES_PER_PAGE = 4
|
||||
|
@ -41,6 +41,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
|||
protected abstract authService: AuthService
|
||||
protected abstract router: Router
|
||||
protected abstract route: ActivatedRoute
|
||||
protected abstract screenService: ScreenService
|
||||
protected abstract i18n: I18n
|
||||
protected abstract location: Location
|
||||
protected abstract currentRoute: string
|
||||
|
@ -199,7 +200,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private calcPageSizes () {
|
||||
if (isInMobileView() || this.baseVideoWidth === -1) {
|
||||
if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
|
||||
this.pagination.itemsPerPage = 5
|
||||
|
||||
// Video takes all the width
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { VideoDetails } from './video-details.model'
|
||||
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
|
||||
import { VideoUpdate } from '../../../../../shared/models/videos'
|
||||
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
|
||||
|
||||
export class VideoEdit implements VideoUpdate {
|
||||
static readonly SPECIAL_SCHEDULED_PRIVACY = -1
|
||||
|
||||
category: number
|
||||
licence: number
|
||||
language: string
|
||||
|
@ -21,6 +24,7 @@ export class VideoEdit implements VideoUpdate {
|
|||
previewUrl: string
|
||||
uuid?: string
|
||||
id?: number
|
||||
scheduleUpdate?: VideoScheduleUpdate
|
||||
|
||||
constructor (videoDetails?: VideoDetails) {
|
||||
if (videoDetails) {
|
||||
|
@ -40,6 +44,8 @@ export class VideoEdit implements VideoUpdate {
|
|||
this.support = videoDetails.support
|
||||
this.thumbnailUrl = videoDetails.thumbnailUrl
|
||||
this.previewUrl = videoDetails.previewUrl
|
||||
|
||||
this.scheduleUpdate = videoDetails.scheduledUpdate
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,10 +53,22 @@ export class VideoEdit implements VideoUpdate {
|
|||
Object.keys(values).forEach((key) => {
|
||||
this[ key ] = values[ key ]
|
||||
})
|
||||
|
||||
// If schedule publication, the video is private and will be changed to public privacy
|
||||
if (values['schedulePublicationAt']) {
|
||||
const updateAt = (values['schedulePublicationAt'] as Date)
|
||||
updateAt.setSeconds(0)
|
||||
|
||||
this.privacy = VideoPrivacy.PRIVATE
|
||||
this.scheduleUpdate = {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return {
|
||||
toFormPatch () {
|
||||
const json = {
|
||||
category: this.category,
|
||||
licence: this.licence,
|
||||
language: this.language,
|
||||
|
@ -64,5 +82,15 @@ export class VideoEdit implements VideoUpdate {
|
|||
channelId: this.channelId,
|
||||
privacy: this.privacy
|
||||
}
|
||||
|
||||
// Special case if we scheduled an update
|
||||
if (this.scheduleUpdate) {
|
||||
Object.assign(json, {
|
||||
privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
|
||||
schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString())
|
||||
})
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { isInMobileView } from '@app/shared/misc/utils'
|
||||
import { Video } from './video.model'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-thumbnail',
|
||||
|
@ -11,10 +11,12 @@ export class VideoThumbnailComponent {
|
|||
@Input() video: Video
|
||||
@Input() nsfw = false
|
||||
|
||||
constructor (private screenService: ScreenService) {}
|
||||
|
||||
getImageUrl () {
|
||||
if (!this.video) return ''
|
||||
|
||||
if (isInMobileView()) {
|
||||
if (this.screenService.isInMobileView()) {
|
||||
return this.video.previewUrl
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { getAbsoluteAPIUrl } from '../misc/utils'
|
|||
import { ServerConfig } from '../../../../../shared/models'
|
||||
import { Actor } from '@app/shared/actor/actor.model'
|
||||
import { peertubeTranslate } from '@app/shared/i18n/i18n-utils'
|
||||
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
|
||||
|
||||
export class Video implements VideoServerModel {
|
||||
by: string
|
||||
|
@ -38,6 +39,7 @@ export class Video implements VideoServerModel {
|
|||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoState>
|
||||
scheduledUpdate?: VideoScheduleUpdate
|
||||
|
||||
account: {
|
||||
id: number
|
||||
|
@ -109,6 +111,7 @@ export class Video implements VideoServerModel {
|
|||
this.language.label = peertubeTranslate(this.language.label, translations)
|
||||
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
|
||||
|
||||
this.scheduledUpdate = hash.scheduledUpdate
|
||||
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,8 @@ export class VideoService {
|
|||
waitTranscoding: video.waitTranscoding,
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
thumbnailfile: video.thumbnailfile,
|
||||
previewfile: video.previewfile
|
||||
previewfile: video.previewfile,
|
||||
scheduleUpdate: video.scheduleUpdate || undefined
|
||||
}
|
||||
|
||||
const data = objectToFormData(body)
|
||||
|
|
|
@ -88,6 +88,7 @@
|
|||
<select id="privacy" formControlName="privacy">
|
||||
<option></option>
|
||||
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
|
||||
<option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -96,11 +97,27 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="schedulePublicationEnabled" class="form-group">
|
||||
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
|
||||
<p-calendar
|
||||
id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
|
||||
[locale]="calendarLocale" [minDate]="minScheduledDate" [showTime]="true" [hideOnDateTimeSelect]="true"
|
||||
>
|
||||
</p-calendar>
|
||||
|
||||
<div *ngIf="formErrors.schedulePublicationAt" class="form-error">
|
||||
{{ formErrors.schedulePublicationAt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-checkbox">
|
||||
<input type="checkbox" id="nsfw" formControlName="nsfw" />
|
||||
<label for="nsfw"></label>
|
||||
<label i18n for="nsfw">This video contains mature or explicit content</label>
|
||||
<my-help tooltipPlacement="top" helpType="custom" i18n-customHtml customHtml="Some instances do not list NSFW videos by default."></my-help>
|
||||
<my-help
|
||||
tooltipPlacement="top" helpType="custom" i18n-customHtml
|
||||
customHtml="Some instances do not list videos containing mature or explicit content by default."
|
||||
></my-help>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-checkbox">
|
||||
|
|
|
@ -44,18 +44,6 @@
|
|||
font-size: 15px;
|
||||
}
|
||||
|
||||
.root-tabset /deep/ > .nav {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.nav-link {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 30px !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-settings .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
@ -98,7 +86,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
p-calendar {
|
||||
display: block;
|
||||
|
||||
/deep/ {
|
||||
input,
|
||||
.ui-calendar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
@include peertube-input-text(100%);
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ {
|
||||
.root-tabset > .nav {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.nav-link {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 30px !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ng2-tag-input {
|
||||
border: none !important;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormGroup, ValidatorFn } from '@angular/forms'
|
||||
import { FormGroup, ValidatorFn, Validators } from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
|
@ -7,6 +7,7 @@ import { ServerService } from '../../../core/server'
|
|||
import { VideoEdit } from '../../../shared/video/video-edit.model'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-edit',
|
||||
|
@ -20,16 +21,26 @@ export class VideoEditComponent implements OnInit {
|
|||
@Input() validationMessages: FormReactiveValidationMessages = {}
|
||||
@Input() videoPrivacies = []
|
||||
@Input() userVideoChannels: { id: number, label: string, support: string }[] = []
|
||||
@Input() schedulePublicationPossible = true
|
||||
|
||||
// So that it can be accessed in the template
|
||||
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
|
||||
|
||||
videoCategories = []
|
||||
videoLicences = []
|
||||
videoLanguages = []
|
||||
video: VideoEdit
|
||||
|
||||
tagValidators: ValidatorFn[]
|
||||
tagValidatorsMessages: { [ name: string ]: string }
|
||||
|
||||
schedulePublicationEnabled = false
|
||||
|
||||
error: string = null
|
||||
calendarLocale: any = {}
|
||||
minScheduledDate = new Date()
|
||||
|
||||
calendarTimezone: string
|
||||
calendarDateFormat: string
|
||||
|
||||
constructor (
|
||||
private formValidatorService: FormValidatorService,
|
||||
|
@ -37,10 +48,15 @@ export class VideoEditComponent implements OnInit {
|
|||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private serverService: ServerService
|
||||
private serverService: ServerService,
|
||||
private i18nPrimengCalendarService: I18nPrimengCalendarService
|
||||
) {
|
||||
this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
|
||||
this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
|
||||
|
||||
this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
|
||||
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
|
||||
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
|
||||
}
|
||||
|
||||
updateForm () {
|
||||
|
@ -64,7 +80,8 @@ export class VideoEditComponent implements OnInit {
|
|||
tags: null,
|
||||
thumbnailfile: null,
|
||||
previewfile: null,
|
||||
support: this.videoValidatorsService.VIDEO_SUPPORT
|
||||
support: this.videoValidatorsService.VIDEO_SUPPORT,
|
||||
schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
|
||||
}
|
||||
|
||||
this.formValidatorService.updateForm(
|
||||
|
@ -75,6 +92,52 @@ export class VideoEditComponent implements OnInit {
|
|||
defaultValues
|
||||
)
|
||||
|
||||
this.trackChannelChange()
|
||||
this.trackPrivacyChange()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.updateForm()
|
||||
|
||||
this.videoCategories = this.serverService.getVideoCategories()
|
||||
this.videoLicences = this.serverService.getVideoLicences()
|
||||
this.videoLanguages = this.serverService.getVideoLanguages()
|
||||
|
||||
setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
|
||||
}
|
||||
|
||||
private trackPrivacyChange () {
|
||||
// We will update the "support" field depending on the channel
|
||||
this.form.controls[ 'privacy' ]
|
||||
.valueChanges
|
||||
.pipe(map(res => parseInt(res.toString(), 10)))
|
||||
.subscribe(
|
||||
newPrivacyId => {
|
||||
this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
|
||||
|
||||
// Value changed
|
||||
const scheduleControl = this.form.get('schedulePublicationAt')
|
||||
const waitTranscodingControl = this.form.get('waitTranscoding')
|
||||
|
||||
if (this.schedulePublicationEnabled) {
|
||||
scheduleControl.setValidators([ Validators.required ])
|
||||
|
||||
waitTranscodingControl.disable()
|
||||
waitTranscodingControl.setValue(false)
|
||||
} else {
|
||||
scheduleControl.clearValidators()
|
||||
|
||||
waitTranscodingControl.enable()
|
||||
waitTranscodingControl.setValue(true)
|
||||
}
|
||||
|
||||
scheduleControl.updateValueAndValidity()
|
||||
waitTranscodingControl.updateValueAndValidity()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private trackChannelChange () {
|
||||
// We will update the "support" field depending on the channel
|
||||
this.form.controls[ 'channelId' ]
|
||||
.valueChanges
|
||||
|
@ -108,14 +171,6 @@ export class VideoEditComponent implements OnInit {
|
|||
)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.updateForm()
|
||||
|
||||
this.videoCategories = this.serverService.getVideoCategories()
|
||||
this.videoLicences = this.serverService.getVideoLicences()
|
||||
this.videoLanguages = this.serverService.getVideoLanguages()
|
||||
}
|
||||
|
||||
private updateSupportField (support: string) {
|
||||
return this.form.patchValue({ support: support || '' })
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ 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'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
TagInputModule,
|
||||
CalendarModule,
|
||||
|
||||
SharedModule
|
||||
],
|
||||
|
@ -20,6 +22,7 @@ import { VideoImageComponent } from './video-image.component'
|
|||
exports: [
|
||||
TagInputModule,
|
||||
TabsModule,
|
||||
CalendarModule,
|
||||
|
||||
VideoEditComponent
|
||||
],
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<div class="peertube-select-container">
|
||||
<select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
|
||||
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
|
||||
<option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,9 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
|
|||
export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
@ViewChild('videofileInput') videofileInput
|
||||
|
||||
// So that it can be accessed in the template
|
||||
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
|
||||
|
||||
isUploadingVideo = false
|
||||
isUpdatingVideo = false
|
||||
videoUploaded = false
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<form novalidate [formGroup]="form">
|
||||
|
||||
<my-video-edit
|
||||
[form]="form" [formErrors]="formErrors"
|
||||
[form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
|
||||
[validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
|
||||
></my-video-edit>
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
isUpdatingVideo = false
|
||||
videoPrivacies = []
|
||||
userVideoChannels = []
|
||||
schedulePublicationPossible = false
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
|
@ -70,13 +71,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
this.userVideoChannels = videoChannels
|
||||
|
||||
// We cannot set private a video that was not private
|
||||
if (video.privacy.id !== VideoPrivacy.PRIVATE) {
|
||||
const newVideoPrivacies = []
|
||||
for (const p of this.videoPrivacies) {
|
||||
if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p)
|
||||
}
|
||||
|
||||
this.videoPrivacies = newVideoPrivacies
|
||||
if (this.video.privacy !== VideoPrivacy.PRIVATE) {
|
||||
this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE)
|
||||
} else { // We can schedule video publication only if it it is private
|
||||
this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
this.hydrateFormFromVideo()
|
||||
|
@ -123,7 +121,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
private hydrateFormFromVideo () {
|
||||
this.form.patchValue(this.video.toJSON())
|
||||
this.form.patchValue(this.video.toFormPatch())
|
||||
|
||||
const objects = [
|
||||
{
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
<div id="video-element-wrapper">
|
||||
</div>
|
||||
|
||||
<div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
|
||||
<div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()">
|
||||
The video is being transcoded, it may not work properly.
|
||||
</div>
|
||||
|
||||
<div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
|
||||
This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
|
||||
</div>
|
||||
|
||||
<!-- Video information -->
|
||||
<div *ngIf="video" class="margin-content video-bottom">
|
||||
<div class="video-info">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
#warning-transcoding {
|
||||
.alert {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -280,6 +280,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
return this.video && this.video.state.id === VideoState.TO_TRANSCODE
|
||||
}
|
||||
|
||||
hasVideoScheduledPublication () {
|
||||
return this.video && this.video.scheduledUpdate !== undefined
|
||||
}
|
||||
|
||||
private updateVideoDescription (description: string) {
|
||||
this.video.description = description
|
||||
this.setVideoDescriptionHTML()
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
|
|||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-local',
|
||||
|
@ -28,6 +29,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
|
|||
protected authService: AuthService,
|
||||
protected location: Location,
|
||||
protected i18n: I18n,
|
||||
protected screenService: ScreenService,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
super()
|
||||
|
|
|
@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
|||
import { VideoSortField } from '../../shared/video/sort-field.type'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-recently-added',
|
||||
|
@ -26,6 +27,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
|
|||
protected notificationsService: NotificationsService,
|
||||
protected authService: AuthService,
|
||||
protected i18n: I18n,
|
||||
protected screenService: ScreenService,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
super()
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AuthService } from '../../core/auth'
|
|||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-search',
|
||||
|
@ -32,6 +33,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
|
|||
protected authService: AuthService,
|
||||
protected location: Location,
|
||||
protected i18n: I18n,
|
||||
protected screenService: ScreenService,
|
||||
private videoService: VideoService,
|
||||
private redirectService: RedirectService
|
||||
) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
|||
import { VideoSortField } from '../../shared/video/sort-field.type'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-trending',
|
||||
|
@ -25,6 +26,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
protected notificationsService: NotificationsService,
|
||||
protected authService: AuthService,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
protected i18n: I18n,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
|
|
|
@ -6,14 +6,14 @@ $icon-font-path: '../../node_modules/bootstrap-sass/assets/fonts/bootstrap/';
|
|||
|
||||
@import '_fonts';
|
||||
|
||||
@import '~primeng/resources/themes/bootstrap/theme.css';
|
||||
@import '~primeng/resources/primeng.css';
|
||||
@import '~video.js/dist/video-js.css';
|
||||
|
||||
$assets-path: '../assets/';
|
||||
@import './player/player';
|
||||
@import './loading-bar';
|
||||
|
||||
@import './primeng-custom';
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -142,126 +142,6 @@ label {
|
|||
to { transform: scale(1) rotate(360deg);}
|
||||
}
|
||||
|
||||
// ngprime data table customizations
|
||||
p-table {
|
||||
font-size: 15px !important;
|
||||
|
||||
td {
|
||||
border: 1px solid #E5E5E5 !important;
|
||||
padding-left: 15px !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff !important;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.ui-table-tbody {
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
&:not(:hover) {
|
||||
.action-cell * {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child td {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expander {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #f0f0f0 !important;
|
||||
text-align: left !important;
|
||||
padding: 5px 0 5px 15px !important;
|
||||
font-weight: $font-semibold !important;
|
||||
color: #000 !important;
|
||||
|
||||
&.ui-sortable-column:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
border: 1px solid #f0f0f0 !important;
|
||||
border-width: 0 1px !important;
|
||||
|
||||
&:first-child {
|
||||
border-width: 0 1px 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-state-highlight {
|
||||
background-color: #fff !important;
|
||||
|
||||
.fa {
|
||||
@extend .glyphicon;
|
||||
font-size: 11px;
|
||||
|
||||
&.fa-sort-asc {
|
||||
@extend .glyphicon-triangle-top;
|
||||
}
|
||||
|
||||
&.fa-sort-desc {
|
||||
@extend .glyphicon-triangle-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
width: 250px !important;
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
|
||||
my-edit-button + my-delete-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
p-paginator {
|
||||
.ui-paginator-bottom {
|
||||
position: relative;
|
||||
border: none !important;
|
||||
border: 1px solid #f0f0f0 !important;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
color: #000 !important;
|
||||
font-weight: $font-semibold !important;
|
||||
margin-right: 20px !important;
|
||||
outline: 0 !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 5px 2px !important;
|
||||
|
||||
&.ui-state-active {
|
||||
&, &:hover, &:active, &:focus {
|
||||
color: #fff !important;
|
||||
background-color: $orange-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap customizations
|
||||
.dropdown-menu {
|
||||
border-radius: 3px;
|
||||
|
@ -352,6 +232,8 @@ tabset:not(.bootstrap) {
|
|||
}
|
||||
|
||||
tabset.bootstrap {
|
||||
margin-left: 0;
|
||||
|
||||
.nav-item .nav-link {
|
||||
&, & a {
|
||||
color: #000;
|
||||
|
|
|
@ -281,6 +281,12 @@
|
|||
cursor: pointer;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&[disabled] + label,
|
||||
&[disabled] + label + label{
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
@import '~primeng/resources/primeng.css';
|
||||
@import '~primeng/resources/themes/bootstrap/theme.css';
|
||||
|
||||
@mixin glyphicon-light {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
text-decoration: none !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
// data table customizations
|
||||
p-table {
|
||||
font-size: 15px !important;
|
||||
|
||||
td {
|
||||
border: 1px solid #E5E5E5 !important;
|
||||
padding-left: 15px !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff !important;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.ui-table-tbody {
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
&:not(:hover) {
|
||||
.action-cell * {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child td {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expander {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #f0f0f0 !important;
|
||||
text-align: left !important;
|
||||
padding: 5px 0 5px 15px !important;
|
||||
font-weight: $font-semibold !important;
|
||||
color: #000 !important;
|
||||
|
||||
&.ui-sortable-column:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
border: 1px solid #f0f0f0 !important;
|
||||
border-width: 0 1px !important;
|
||||
|
||||
&:first-child {
|
||||
border-width: 0 1px 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-state-highlight {
|
||||
background-color: #fff !important;
|
||||
|
||||
.pi {
|
||||
@extend .glyphicon;
|
||||
|
||||
color: #000;
|
||||
font-size: 11px;
|
||||
|
||||
&.pi-sort-up {
|
||||
@extend .glyphicon-triangle-top;
|
||||
}
|
||||
|
||||
&.pi-sort-down {
|
||||
@extend .glyphicon-triangle-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
width: 250px !important;
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
|
||||
my-edit-button + my-delete-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
p-paginator {
|
||||
.ui-paginator-bottom {
|
||||
position: relative;
|
||||
border: 1px solid #f0f0f0 !important;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.ui-paginator-pages {
|
||||
height: auto !important;
|
||||
|
||||
a {
|
||||
color: #000 !important;
|
||||
font-weight: $font-semibold !important;
|
||||
margin-right: 20px !important;
|
||||
outline: 0 !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 5px 2px !important;
|
||||
height: auto !important;
|
||||
|
||||
&.ui-state-active {
|
||||
&, &:hover, &:active, &:focus {
|
||||
color: #fff !important;
|
||||
background-color: $orange-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrimeNG calendar tweaks
|
||||
p-calendar .ui-datepicker {
|
||||
a {
|
||||
@include disable-default-a-behaviour;
|
||||
}
|
||||
|
||||
.ui-datepicker-header {
|
||||
|
||||
.ui-datepicker-year {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ui-datepicker-next {
|
||||
@extend .glyphicon-chevron-right;
|
||||
@include glyphicon-light;
|
||||
}
|
||||
|
||||
.ui-datepicker-prev {
|
||||
@extend .glyphicon-chevron-left;
|
||||
@include glyphicon-light;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-timepicker {
|
||||
|
||||
.pi.pi-chevron-up {
|
||||
@extend .glyphicon-chevron-up;
|
||||
@include glyphicon-light;
|
||||
}
|
||||
|
||||
.pi.pi-chevron-down {
|
||||
@extend .glyphicon-chevron-down;
|
||||
@include glyphicon-light;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7656,9 +7656,9 @@ pretty-error@^2.0.2:
|
|||
renderkid "^2.0.1"
|
||||
utila "~0.4"
|
||||
|
||||
primeng@^5.2.6:
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/primeng/-/primeng-5.2.7.tgz#9dcf461b6a82ea46de85751dc235ea82303e64b1"
|
||||
primeng@^6.0.0-rc.1:
|
||||
version "6.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/primeng/-/primeng-6.0.0-rc.1.tgz#038e5657a5395e08a5c1fd9312b12cac1a44b527"
|
||||
|
||||
private@^0.1.6, private@^0.1.8, private@~0.1.5:
|
||||
version "0.1.8"
|
||||
|
|
|
@ -8,8 +8,6 @@ import { VideoPrivacy } from '../../shared/models/videos'
|
|||
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
|
||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||
import { invert } from 'lodash'
|
||||
import { RemoveOldJobsScheduler } from '../lib/schedulers/remove-old-jobs-scheduler'
|
||||
import { UpdateVideosScheduler } from '../lib/schedulers/update-videos-scheduler'
|
||||
|
||||
// Use a variable to reload the configuration if we need
|
||||
let config: IConfig = require('config')
|
||||
|
@ -98,8 +96,8 @@ const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
|
|||
// 1 hour
|
||||
let SCHEDULER_INTERVALS_MS = {
|
||||
badActorFollow: 60000 * 60, // 1 hour
|
||||
removeOldJobs: 60000 * 60, // 1 jour
|
||||
updateVideos: 60000 * 1, // 1 minute
|
||||
removeOldJobs: 60000 * 60, // 1 hour
|
||||
updateVideos: 60000 // 1 minute
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -33,7 +33,9 @@ export class UpdateVideosScheduler extends AbstractScheduler {
|
|||
}
|
||||
}
|
||||
|
||||
private updateVideos () {
|
||||
private async updateVideos () {
|
||||
if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
|
||||
|
||||
|
|
|
@ -223,7 +223,7 @@ const videosUpdateValidator = [
|
|||
|
||||
if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
|
||||
return res.status(409)
|
||||
.json({ error: 'Cannot set "private" a video that was not private anymore.' })
|
||||
.json({ error: 'Cannot set "private" a video that was not private.' })
|
||||
.end()
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
|
|||
@AllowNull(true)
|
||||
@Default(null)
|
||||
@Column
|
||||
privacy: VideoPrivacy
|
||||
privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
@ -45,6 +45,21 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
|
|||
})
|
||||
Video: VideoModel
|
||||
|
||||
static areVideosToUpdate () {
|
||||
const query = {
|
||||
logging: false,
|
||||
attributes: [ 'id' ],
|
||||
where: {
|
||||
updateAt: {
|
||||
[Sequelize.Op.lte]: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ScheduleVideoUpdateModel.findOne(query)
|
||||
.then(res => !!res)
|
||||
}
|
||||
|
||||
static listVideosToUpdate (t: Transaction) {
|
||||
const query = {
|
||||
where: {
|
||||
|
@ -68,4 +83,10 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
|
|||
return ScheduleVideoUpdateModel.findAll(query)
|
||||
}
|
||||
|
||||
toFormattedJSON () {
|
||||
return {
|
||||
updateAt: this.updateAt,
|
||||
privacy: this.privacy || undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,7 +97,8 @@ export enum ScopeNames {
|
|||
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
||||
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
|
||||
WITH_TAGS = 'WITH_TAGS',
|
||||
WITH_FILES = 'WITH_FILES'
|
||||
WITH_FILES = 'WITH_FILES',
|
||||
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
|
||||
}
|
||||
|
||||
@Scopes({
|
||||
|
@ -286,6 +287,14 @@ export enum ScopeNames {
|
|||
required: true
|
||||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_SCHEDULED_UPDATE]: {
|
||||
include: [
|
||||
{
|
||||
model: () => ScheduleVideoUpdateModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
@Table({
|
||||
|
@ -843,7 +852,7 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
|
||||
return VideoModel
|
||||
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
|
||||
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
|
||||
.findById(id, options)
|
||||
}
|
||||
|
||||
|
@ -869,7 +878,7 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
|
||||
return VideoModel
|
||||
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
|
||||
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
|
||||
.findOne(options)
|
||||
}
|
||||
|
||||
|
@ -1022,9 +1031,9 @@ export class VideoModel extends Model<VideoModel> {
|
|||
|
||||
toFormattedJSON (options?: {
|
||||
additionalAttributes: {
|
||||
state: boolean,
|
||||
waitTranscoding: boolean,
|
||||
scheduledUpdate: boolean
|
||||
state?: boolean,
|
||||
waitTranscoding?: boolean,
|
||||
scheduledUpdate?: boolean
|
||||
}
|
||||
}): Video {
|
||||
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
|
||||
|
@ -1084,18 +1093,18 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
|
||||
if (options) {
|
||||
if (options.additionalAttributes.state) {
|
||||
if (options.additionalAttributes.state === true) {
|
||||
videoObject.state = {
|
||||
id: this.state,
|
||||
label: VideoModel.getStateLabel(this.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.waitTranscoding) {
|
||||
if (options.additionalAttributes.waitTranscoding === true) {
|
||||
videoObject.waitTranscoding = this.waitTranscoding
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.scheduledUpdate && this.ScheduleVideoUpdate) {
|
||||
if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
|
||||
videoObject.scheduledUpdate = {
|
||||
updateAt: this.ScheduleVideoUpdate.updateAt,
|
||||
privacy: this.ScheduleVideoUpdate.privacy || undefined
|
||||
|
@ -1107,7 +1116,11 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
|
||||
toFormattedDetailsJSON (): VideoDetails {
|
||||
const formattedJson = this.toFormattedJSON()
|
||||
const formattedJson = this.toFormattedJSON({
|
||||
additionalAttributes: {
|
||||
scheduledUpdate: true
|
||||
}
|
||||
})
|
||||
|
||||
const detailsJson = {
|
||||
support: this.support,
|
||||
|
|
|
@ -291,6 +291,23 @@ describe('Test videos API validator', function () {
|
|||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (miss updateAt)', async function () {
|
||||
const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC })
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
|
||||
const fields = immutableAssign(baseCorrectParams, {
|
||||
'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC,
|
||||
'scheduleUpdate[updateAt]': 'toto'
|
||||
})
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail without an input file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {}
|
||||
|
@ -494,6 +511,18 @@ describe('Test videos API validator', function () {
|
|||
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (miss updateAt)', async function () {
|
||||
const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
|
||||
const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
|
|
|
@ -5,11 +5,14 @@ import 'mocha'
|
|||
import { VideoPrivacy } from '../../../../shared/models/videos'
|
||||
import {
|
||||
doubleFollow,
|
||||
flushAndRunMultipleServers, getMyVideos,
|
||||
flushAndRunMultipleServers,
|
||||
getMyVideos,
|
||||
getVideosList,
|
||||
getVideoWithToken,
|
||||
killallServers,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers, updateVideo,
|
||||
setAccessTokensToServers,
|
||||
updateVideo,
|
||||
uploadVideo,
|
||||
wait
|
||||
} from '../../utils'
|
||||
|
@ -69,17 +72,22 @@ describe('Test video update scheduler', function () {
|
|||
const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
|
||||
expect(res.body.total).to.equal(1)
|
||||
|
||||
const video = res.body.data[0]
|
||||
expect(video.name).to.equal('video 1')
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
|
||||
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
|
||||
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
const videoFromList = res.body.data[0]
|
||||
const res2 = await getVideoWithToken(servers[0].url, servers[0].accessToken, videoFromList.uuid)
|
||||
const videoFromGet = res2.body
|
||||
|
||||
for (const video of [ videoFromList, videoFromGet ]) {
|
||||
expect(video.name).to.equal('video 1')
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
|
||||
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
|
||||
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should wait some seconds and have the video in public privacy', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await wait(10000)
|
||||
await wait(15000)
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
|
@ -144,7 +152,7 @@ describe('Test video update scheduler', function () {
|
|||
it('Should wait some seconds and have the updated video in public privacy', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await wait(10000)
|
||||
await wait(15000)
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { VideoPrivacy } from './video-privacy.enum'
|
||||
import { VideoScheduleUpdate } from './video-schedule-update.model'
|
||||
|
||||
export interface VideoCreate {
|
||||
category?: number
|
||||
|
@ -13,8 +14,5 @@ export interface VideoCreate {
|
|||
tags?: string[]
|
||||
commentsEnabled?: boolean
|
||||
privacy: VideoPrivacy
|
||||
scheduleUpdate?: {
|
||||
updateAt: Date
|
||||
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
|
||||
}
|
||||
scheduleUpdate?: VideoScheduleUpdate
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { VideoPrivacy } from './video-privacy.enum'
|
||||
|
||||
export interface VideoScheduleUpdate {
|
||||
updateAt: Date | string
|
||||
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { VideoPrivacy } from './video-privacy.enum'
|
||||
import { VideoScheduleUpdate } from './video-schedule-update.model'
|
||||
|
||||
export interface VideoUpdate {
|
||||
name?: string
|
||||
|
@ -15,8 +16,5 @@ export interface VideoUpdate {
|
|||
channelId?: number
|
||||
thumbnailfile?: Blob
|
||||
previewfile?: Blob
|
||||
scheduleUpdate?: {
|
||||
updateAt: Date
|
||||
privacy?: VideoPrivacy
|
||||
}
|
||||
scheduleUpdate?: VideoScheduleUpdate
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Account } from '../actors'
|
|||
import { Avatar } from '../avatars/avatar.model'
|
||||
import { VideoChannel } from './video-channel.model'
|
||||
import { VideoPrivacy } from './video-privacy.enum'
|
||||
import { VideoScheduleUpdate } from './video-schedule-update.model'
|
||||
|
||||
export interface VideoConstant <T> {
|
||||
id: T
|
||||
|
@ -43,10 +44,7 @@ export interface Video {
|
|||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoState>
|
||||
scheduledUpdate?: {
|
||||
updateAt: Date | string
|
||||
privacy?: VideoPrivacy
|
||||
}
|
||||
scheduledUpdate?: VideoScheduleUpdate
|
||||
|
||||
account: {
|
||||
id: number
|
||||
|
|
|
@ -264,6 +264,7 @@
|
|||
<a href="#definition-GetMeVideoRating"> GetMeVideoRating </a>
|
||||
<a href="#definition-RegisterUser"> RegisterUser </a>
|
||||
<a href="#definition-VideoChannelInput"> VideoChannelInput </a>
|
||||
<a href="#definition-ScheduleVideoUpdate"> ScheduleVideoUpdate </a>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="docs" class="row collapse expanded drawer" data-drawer>
|
||||
|
@ -3530,6 +3531,19 @@
|
|||
<p>Video privacy</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">scheduleUpdate</div>
|
||||
<div class="prop-subtitle"> in formData </div>
|
||||
<div class="prop-subtitle">
|
||||
<span class="json-property-type">[object Object]</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-value">
|
||||
<p class="no-description">(no description)</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="doc-examples"></div>
|
||||
|
@ -4161,12 +4175,7 @@
|
|||
<span class="json-property-required"></span>
|
||||
<div class="prop-subtitle"> in formData </div>
|
||||
<div class="prop-subtitle">
|
||||
<span class="json-property-type">string</span>
|
||||
<span class="json-property-enum" title="Possible values">
|
||||
<span class="json-property-enum-item">Public</span>,
|
||||
<span class="json-property-enum-item">Unlisted</span>,
|
||||
<span class="json-property-enum-item">Private</span>
|
||||
</span>
|
||||
<span class="json-property-type">[object Object]</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4174,6 +4183,19 @@
|
|||
<p>Video privacy</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">scheduleUpdate</div>
|
||||
<div class="prop-subtitle"> in formData </div>
|
||||
<div class="prop-subtitle">
|
||||
<span class="json-property-type">[object Object]</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-value">
|
||||
<p class="no-description">(no description)</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="doc-examples"></div>
|
||||
|
@ -8339,6 +8361,52 @@
|
|||
<span class="hljs-attr">"name"</span>: <span class="hljs-string">"string"</span>,
|
||||
<span class="hljs-attr">"description"</span>: <span class="hljs-string">"string"</span>
|
||||
}
|
||||
</code></pre>
|
||||
<!-- </div> -->
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="definition-ScheduleVideoUpdate" class="definition panel" data-traverse-target="definition-ScheduleVideoUpdate">
|
||||
<h2 class="panel-title">
|
||||
<a name="/definitions/ScheduleVideoUpdate"></a>ScheduleVideoUpdate:
|
||||
<!-- <span class="json-property-type"><span class="json-property-type">object</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
|
||||
|
||||
</span> -->
|
||||
</h2>
|
||||
<div class="doc-row">
|
||||
<div class="doc-copy">
|
||||
<section class="json-schema-properties">
|
||||
<dl>
|
||||
<dt data-property-name="updateAt" class="has-description">
|
||||
<span class="json-property-name">updateAt:</span>
|
||||
<span class="json-property-type">dateTime</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</dt>
|
||||
<dd>
|
||||
<p>When to update the video</p>
|
||||
</dd>
|
||||
<dt data-property-name="privacy">
|
||||
<span class="json-property-name">privacy:</span>
|
||||
<span class="json-property-type">
|
||||
<span class="">
|
||||
<a class="json-schema-ref" href="#/definitions/VideoPrivacy">VideoPrivacy</a>
|
||||
</span>
|
||||
</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</dt>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
<div class="doc-examples">
|
||||
<section>
|
||||
<h5>Example</h5>
|
||||
<!-- <div class="hljs"> --><pre><code class="hljs lang-json">{
|
||||
<span class="hljs-attr">"updateAt"</span>: <span class="hljs-string">"dateTime"</span>,
|
||||
<span class="hljs-attr">"privacy"</span>: <span class="hljs-string">"string"</span>
|
||||
}
|
||||
</code></pre>
|
||||
<!-- </div> -->
|
||||
</section>
|
||||
|
|
|
@ -711,6 +711,12 @@ paths:
|
|||
type: string
|
||||
enum: [Public, Unlisted]
|
||||
description: 'Video privacy'
|
||||
- name: scheduleUpdate
|
||||
in: formData
|
||||
required: false
|
||||
description: 'Schedule an update at a specific datetime'
|
||||
type:
|
||||
$ref: '#/definitions/ScheduleVideoUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
|
@ -864,9 +870,15 @@ paths:
|
|||
- name: privacy
|
||||
in: formData
|
||||
required: true
|
||||
type: string
|
||||
enum: [Public, Unlisted, Private]
|
||||
type:
|
||||
$ref: '#/definitions/VideoPrivacy'
|
||||
description: 'Video privacy'
|
||||
- name: scheduleUpdate
|
||||
in: formData
|
||||
required: false
|
||||
description: 'Schedule an update at a specific datetime'
|
||||
type:
|
||||
$ref: '#/definitions/ScheduleVideoUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
|
@ -1709,3 +1721,12 @@ definitions:
|
|||
type: string
|
||||
description:
|
||||
type: string
|
||||
ScheduleVideoUpdate:
|
||||
properties:
|
||||
updateAt:
|
||||
type: dateTime
|
||||
description: 'When to update the video'
|
||||
required: true
|
||||
privacy:
|
||||
$ref: '#/definitions/VideoPrivacy'
|
||||
required: false
|
Loading…
Reference in New Issue