"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",
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',
protected notificationsService: NotificationsService,
protected confirmService: ConfirmService,
protected location: Location,
+ protected screenService: ScreenService,
protected i18n: I18n,
private accountService: AccountService,
private videoService: VideoService
<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 -->
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>
color: #000;
display: block;
+ width: fit-content;
font-size: 16px;
font-weight: $font-semibold;
}
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'
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',
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()
}
getStateLabel (video: Video) {
- if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
-
- 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')
+ let suffix: string
+
+ 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 () {
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',
protected notificationsService: NotificationsService,
protected confirmService: ConfirmService,
protected location: Location,
+ protected screenService: ScreenService,
protected i18n: I18n,
private videoChannelService: VideoChannelService,
private videoService: VideoService
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',
private authService: AuthService,
private serverService: ServerService,
private domSanitizer: DomSanitizer,
- private redirectService: RedirectService
+ private redirectService: RedirectService,
+ private screenService: ScreenService
) { }
get serverVersion () {
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
}
}
)
.subscribe(({ data, translations }) => {
Object.keys(data)
+ .map(dataKey => parseInt(dataKey, 10))
.forEach(dataKey => {
const label = data[ dataKey ]
readonly VIDEO_DESCRIPTION: BuildFormValidator
readonly VIDEO_TAGS: BuildFormValidator
readonly VIDEO_SUPPORT: BuildFormValidator
+ readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
constructor (private i18n: I18n) {
'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.')
+ }
+ }
}
}
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',
private contentChanged = new Subject<string>()
- constructor (private markdownService: MarkdownService) {}
+ constructor (
+ private screenService: ScreenService,
+ private markdownService: MarkdownService
+) {}
ngOnInit () {
this.contentChanged
}
arePreviewsDisplayed () {
- return isInSmallView() === false
+ return this.screenService.isInSmallView() === false
}
private updatePreviews () {
--- /dev/null
+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.'
+ })
+ }
+}
--- /dev/null
+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
+ }
+}
})
}
-// 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
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: [
VideoCommentValidatorsService,
VideoValidatorsService,
+ I18nPrimengCalendarService,
+ ScreenService,
+
I18n
]
})
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'
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
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
}
private calcPageSizes () {
- if (isInMobileView() || this.baseVideoWidth === -1) {
+ if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
this.pagination.itemsPerPage = 5
// Video takes all the width
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
previewUrl: string
uuid?: string
id?: number
+ scheduleUpdate?: VideoScheduleUpdate
constructor (videoDetails?: VideoDetails) {
if (videoDetails) {
this.support = videoDetails.support
this.thumbnailUrl = videoDetails.thumbnailUrl
this.previewUrl = videoDetails.previewUrl
+
+ this.scheduleUpdate = videoDetails.scheduledUpdate
}
}
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,
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
}
}
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',
@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
}
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
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
+ scheduledUpdate?: VideoScheduleUpdate
account: {
id: number
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)
}
waitTranscoding: video.waitTranscoding,
commentsEnabled: video.commentsEnabled,
thumbnailfile: video.thumbnailfile,
- previewfile: video.previewfile
+ previewfile: video.previewfile,
+ scheduleUpdate: video.scheduleUpdate || undefined
}
const data = objectToFormData(body)
<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>
</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">
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;
}
}
}
+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;
}
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'
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',
@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,
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 () {
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(
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
)
}
- 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 || '' })
}
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
],
exports: [
TagInputModule,
TabsModule,
+ CalendarModule,
VideoEditComponent
],
<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>
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
<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>
isUpdatingVideo = false
videoPrivacies = []
userVideoChannels = []
+ schedulePublicationPossible = false
constructor (
protected formValidatorService: FormValidatorService,
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()
}
private hydrateFormFromVideo () {
- this.form.patchValue(this.video.toJSON())
+ this.form.patchValue(this.video.toFormPatch())
const objects = [
{
<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">
}
}
-#warning-transcoding {
+.alert {
text-align: center;
}
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()
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',
protected authService: AuthService,
protected location: Location,
protected i18n: I18n,
+ protected screenService: ScreenService,
private videoService: VideoService
) {
super()
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',
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected i18n: I18n,
+ protected screenService: ScreenService,
private videoService: VideoService
) {
super()
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',
protected authService: AuthService,
protected location: Location,
protected i18n: I18n,
+ protected screenService: ScreenService,
private videoService: VideoService,
private redirectService: RedirectService
) {
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',
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
+ protected screenService: ScreenService,
protected i18n: I18n,
private videoService: VideoService
) {
@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;
}
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;
}
tabset.bootstrap {
+ margin-left: 0;
+
.nav-item .nav-link {
&, & a {
color: #000;
cursor: pointer;
display: inline;
}
+
+ &[disabled] + label,
+ &[disabled] + label + label{
+ opacity: 0.5;
+ cursor: default;
+ }
}
--- /dev/null
+@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;
+ }
+ }
+}
\ No newline at end of file
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"
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')
// 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
}
// ---------------------------------------------------------------------------
}
}
- private updateVideos () {
+ private async updateVideos () {
+ if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined
+
return sequelizeTypescript.transaction(async t => {
const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
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()
}
@AllowNull(true)
@Default(null)
@Column
- privacy: VideoPrivacy
+ privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
@CreatedAt
createdAt: Date
})
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: {
return ScheduleVideoUpdateModel.findAll(query)
}
+ toFormattedJSON () {
+ return {
+ updateAt: this.updateAt,
+ privacy: this.privacy || undefined
+ }
+ }
}
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({
required: true
}
]
+ },
+ [ScopeNames.WITH_SCHEDULED_UPDATE]: {
+ include: [
+ {
+ model: () => ScheduleVideoUpdateModel.unscoped(),
+ required: false
+ }
+ ]
}
})
@Table({
}
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)
}
}
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)
}
toFormattedJSON (options?: {
additionalAttributes: {
- state: boolean,
- waitTranscoding: boolean,
- scheduledUpdate: boolean
+ state?: boolean,
+ waitTranscoding?: boolean,
+ scheduledUpdate?: boolean
}
}): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
}
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
}
toFormattedDetailsJSON (): VideoDetails {
- const formattedJson = this.toFormattedJSON()
+ const formattedJson = this.toFormattedJSON({
+ additionalAttributes: {
+ scheduledUpdate: true
+ }
+ })
const detailsJson = {
support: this.support,
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 = {}
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 = {
import { VideoPrivacy } from '../../../../shared/models/videos'
import {
doubleFollow,
- flushAndRunMultipleServers, getMyVideos,
+ flushAndRunMultipleServers,
+ getMyVideos,
getVideosList,
+ getVideoWithToken,
killallServers,
ServerInfo,
- setAccessTokensToServers, updateVideo,
+ setAccessTokensToServers,
+ updateVideo,
uploadVideo,
wait
} from '../../utils'
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) {
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) {
import { VideoPrivacy } from './video-privacy.enum'
+import { VideoScheduleUpdate } from './video-schedule-update.model'
export interface VideoCreate {
category?: number
tags?: string[]
commentsEnabled?: boolean
privacy: VideoPrivacy
- scheduleUpdate?: {
- updateAt: Date
- privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
- }
+ scheduleUpdate?: VideoScheduleUpdate
}
--- /dev/null
+import { VideoPrivacy } from './video-privacy.enum'
+
+export interface VideoScheduleUpdate {
+ updateAt: Date | string
+ privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE
+}
import { VideoPrivacy } from './video-privacy.enum'
+import { VideoScheduleUpdate } from './video-schedule-update.model'
export interface VideoUpdate {
name?: string
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob
- scheduleUpdate?: {
- updateAt: Date
- privacy?: VideoPrivacy
- }
+ scheduleUpdate?: VideoScheduleUpdate
}
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
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
- scheduledUpdate?: {
- updateAt: Date | string
- privacy?: VideoPrivacy
- }
+ scheduledUpdate?: VideoScheduleUpdate
account: {
id: number
<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>
<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>
<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>
<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>
<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>
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
- 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
type: string
description:
type: string
+ ScheduleVideoUpdate:
+ properties:
+ updateAt:
+ type: dateTime
+ description: 'When to update the video'
+ required: true
+ privacy:
+ $ref: '#/definitions/VideoPrivacy'
+ required: false
\ No newline at end of file