aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/package.json2
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html6
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss1
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts29
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts2
-rw-r--r--client/src/app/app.component.ts9
-rw-r--r--client/src/app/core/server/server.service.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/video-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts9
-rw-r--r--client/src/app/shared/i18n/i18n-primeng-calendar.ts94
-rw-r--r--client/src/app/shared/misc/screen.service.ts23
-rw-r--r--client/src/app/shared/misc/utils.ts14
-rw-r--r--client/src/app/shared/shared.module.ts5
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts5
-rw-r--r--client/src/app/shared/video/video-edit.model.ts32
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts6
-rw-r--r--client/src/app/shared/video/video.model.ts3
-rw-r--r--client/src/app/shared/video/video.service.ts3
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html19
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss40
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts79
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html1
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts14
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html6
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-search.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts2
-rw-r--r--client/src/sass/application.scss126
-rw-r--r--client/src/sass/include/_mixins.scss6
-rw-r--r--client/src/sass/primeng-custom.scss174
-rw-r--r--client/yarn.lock6
-rw-r--r--server/initializers/constants.ts6
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts4
-rw-r--r--server/middlewares/validators/videos.ts2
-rw-r--r--server/models/video/schedule-video-update.ts23
-rw-r--r--server/models/video/video.ts33
-rw-r--r--server/tests/api/check-params/videos.ts29
-rw-r--r--server/tests/api/videos/video-schedule-update.ts26
-rw-r--r--shared/models/videos/video-create.model.ts6
-rw-r--r--shared/models/videos/video-schedule-update.model.ts6
-rw-r--r--shared/models/videos/video-update.model.ts6
-rw-r--r--shared/models/videos/video.model.ts6
-rw-r--r--support/doc/api/html/index.html80
-rw-r--r--support/doc/api/openapi.yaml25
51 files changed, 752 insertions, 247 deletions
diff --git a/client/package.json b/client/package.json
index 22f48fe7e..ee7743ff2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -85,7 +85,7 @@
85 "ngx-pipes": "^2.1.7", 85 "ngx-pipes": "^2.1.7",
86 "node-sass": "^4.1.1", 86 "node-sass": "^4.1.1",
87 "npm-font-source-sans-pro": "^1.0.2", 87 "npm-font-source-sans-pro": "^1.0.2",
88 "primeng": "^5.2.6", 88 "primeng": "^6.0.0-rc.1",
89 "protractor": "^5.3.2", 89 "protractor": "^5.3.2",
90 "purify-css": "^1.2.5", 90 "purify-css": "^1.2.5",
91 "purifycss-webpack": "^0.7.0", 91 "purifycss-webpack": "^0.7.0",
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 5e3dbb6b3..d4fcd7acf 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -12,6 +12,7 @@ import { AccountService } from '@app/shared/account/account.service'
12import { tap } from 'rxjs/operators' 12import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 14import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service'
15 16
16@Component({ 17@Component({
17 selector: 'my-account-videos', 18 selector: 'my-account-videos',
@@ -37,6 +38,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
37 protected notificationsService: NotificationsService, 38 protected notificationsService: NotificationsService,
38 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
39 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService,
40 protected i18n: I18n, 42 protected i18n: I18n,
41 private accountService: AccountService, 43 private accountService: AccountService,
42 private videoService: VideoService 44 private videoService: VideoService
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index eb24de7a7..7ac6371db 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -18,7 +18,7 @@
18 <div class="video-info"> 18 <div class="video-info">
19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> 19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
21 <div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div> 21 <div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
22 </div> 22 </div>
23 23
24 <!-- Display only once --> 24 <!-- Display only once -->
@@ -28,9 +28,9 @@
28 Cancel 28 Cancel
29 </span> 29 </span>
30 30
31 <span i18n class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> 31 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
32 <span class="icon icon-delete-white"></span> 32 <span class="icon icon-delete-white"></span>
33 Delete 33 <ng-container i18n>Delete</ng-container>
34 </span> 34 </span>
35 </div> 35 </div>
36 </div> 36 </div>
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index f276ea389..1f22aec71 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -75,6 +75,7 @@
75 75
76 color: #000; 76 color: #000;
77 display: block; 77 display: block;
78 width: fit-content;
78 font-size: 16px; 79 font-size: 16px;
79 font-weight: $font-semibold; 80 font-weight: $font-semibold;
80 } 81 }
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index afc01073c..e698b75ec 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -1,6 +1,6 @@
1import { from as observableFrom, Observable } from 'rxjs' 1import { from as observableFrom, Observable } from 'rxjs'
2import { concatAll, tap } from 'rxjs/operators' 2import { concatAll, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit, Inject, LOCALE_ID } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { Location } from '@angular/common' 5import { Location } from '@angular/common'
6import { immutableAssign } from '@app/shared/misc/utils' 6import { immutableAssign } from '@app/shared/misc/utils'
@@ -12,7 +12,8 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
12import { Video } from '../../shared/video/video.model' 12import { Video } from '../../shared/video/video.model'
13import { VideoService } from '../../shared/video/video.service' 13import { VideoService } from '../../shared/video/video.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { VideoState } from '../../../../../shared/models/videos' 15import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos'
16import { ScreenService } from '@app/shared/misc/screen.service'
16 17
17@Component({ 18@Component({
18 selector: 'my-account-videos', 19 selector: 'my-account-videos',
@@ -39,8 +40,10 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
39 protected notificationsService: NotificationsService, 40 protected notificationsService: NotificationsService,
40 protected confirmService: ConfirmService, 41 protected confirmService: ConfirmService,
41 protected location: Location, 42 protected location: Location,
43 protected screenService: ScreenService,
42 protected i18n: I18n, 44 protected i18n: I18n,
43 private videoService: VideoService 45 private videoService: VideoService,
46 @Inject(LOCALE_ID) private localeId: string
44 ) { 47 ) {
45 super() 48 super()
46 49
@@ -131,12 +134,22 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
131 } 134 }
132 135
133 getStateLabel (video: Video) { 136 getStateLabel (video: Video) {
134 if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published') 137 let suffix: string
135 138
136 if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding') 139 if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) {
137 if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode') 140 suffix = this.i18n('Published')
141 } else if (video.scheduledUpdate) {
142 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
143 suffix = this.i18n('Publication scheduled on ') + updateAt
144 } else if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) {
145 suffix = this.i18n('Waiting transcoding')
146 } else if (video.state.id === VideoState.TO_TRANSCODE) {
147 suffix = this.i18n('To transcode')
148 } else {
149 return ''
150 }
138 151
139 return this.i18n('Unknown state') 152 return ' - ' + suffix
140 } 153 }
141 154
142 protected buildVideoHeight () { 155 protected buildVideoHeight () {
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 2d3f66994..800d97b7f 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -12,6 +12,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
12import { tap } from 'rxjs/operators' 12import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 14import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-channel-videos', 18 selector: 'my-video-channel-videos',
@@ -37,6 +38,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
37 protected notificationsService: NotificationsService, 38 protected notificationsService: NotificationsService,
38 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
39 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService,
40 protected i18n: I18n, 42 protected i18n: I18n,
41 private videoChannelService: VideoChannelService, 43 private videoChannelService: VideoChannelService,
42 private videoService: VideoService 44 private videoService: VideoService
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 0bfe9f916..494cd9ea6 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' 3import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
4import { AuthService, RedirectService, ServerService } from '@app/core' 4import { AuthService, RedirectService, ServerService } from '@app/core'
5import { isInSmallView } from '@app/shared/misc/utils'
6import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service'
7 7
8@Component({ 8@Component({
9 selector: 'my-app', 9 selector: 'my-app',
@@ -33,7 +33,8 @@ export class AppComponent implements OnInit {
33 private authService: AuthService, 33 private authService: AuthService,
34 private serverService: ServerService, 34 private serverService: ServerService,
35 private domSanitizer: DomSanitizer, 35 private domSanitizer: DomSanitizer,
36 private redirectService: RedirectService 36 private redirectService: RedirectService,
37 private screenService: ScreenService
37 ) { } 38 ) { }
38 39
39 get serverVersion () { 40 get serverVersion () {
@@ -75,14 +76,14 @@ export class AppComponent implements OnInit {
75 this.serverService.loadVideoPrivacies() 76 this.serverService.loadVideoPrivacies()
76 77
77 // Do not display menu on small screens 78 // Do not display menu on small screens
78 if (isInSmallView()) { 79 if (this.screenService.isInSmallView()) {
79 this.isMenuDisplayed = false 80 this.isMenuDisplayed = false
80 } 81 }
81 82
82 this.router.events.subscribe( 83 this.router.events.subscribe(
83 e => { 84 e => {
84 // User clicked on a link in the menu, change the page 85 // User clicked on a link in the menu, change the page
85 if (e instanceof GuardsCheckStart && isInSmallView()) { 86 if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
86 this.isMenuDisplayed = false 87 this.isMenuDisplayed = false
87 } 88 }
88 } 89 }
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 74363e6a1..6323a7edf 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -141,6 +141,7 @@ export class ServerService {
141 ) 141 )
142 .subscribe(({ data, translations }) => { 142 .subscribe(({ data, translations }) => {
143 Object.keys(data) 143 Object.keys(data)
144 .map(dataKey => parseInt(dataKey, 10))
144 .forEach(dataKey => { 145 .forEach(dataKey => {
145 const label = data[ dataKey ] 146 const label = data[ dataKey ]
146 147
diff --git a/client/src/app/shared/forms/form-validators/video-validators.service.ts b/client/src/app/shared/forms/form-validators/video-validators.service.ts
index 76fc5cf04..396be6f3b 100644
--- a/client/src/app/shared/forms/form-validators/video-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-validators.service.ts
@@ -15,6 +15,7 @@ export class VideoValidatorsService {
15 readonly VIDEO_DESCRIPTION: BuildFormValidator 15 readonly VIDEO_DESCRIPTION: BuildFormValidator
16 readonly VIDEO_TAGS: BuildFormValidator 16 readonly VIDEO_TAGS: BuildFormValidator
17 readonly VIDEO_SUPPORT: BuildFormValidator 17 readonly VIDEO_SUPPORT: BuildFormValidator
18 readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
18 19
19 constructor (private i18n: I18n) { 20 constructor (private i18n: I18n) {
20 21
@@ -84,5 +85,12 @@ export class VideoValidatorsService {
84 'maxlength': this.i18n('Video support cannot be more than 500 characters long.') 85 'maxlength': this.i18n('Video support cannot be more than 500 characters long.')
85 } 86 }
86 } 87 }
88
89 this.VIDEO_SCHEDULE_PUBLICATION_AT = {
90 VALIDATORS: [ ],
91 MESSAGES: {
92 'required': this.i18n('A date is required to schedule video update.')
93 }
94 }
87 } 95 }
88} 96}
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts
index 8b932cd15..db6f9e5c8 100644
--- a/client/src/app/shared/forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/forms/markdown-textarea.component.ts
@@ -1,10 +1,10 @@
1import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 1import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
2import { Component, forwardRef, Input, OnInit } from '@angular/core' 2import { Component, forwardRef, Input, OnInit } from '@angular/core'
3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
4import { isInSmallView } from '@app/shared/misc/utils'
5import { MarkdownService } from '@app/videos/shared' 4import { MarkdownService } from '@app/videos/shared'
6import { Subject } from 'rxjs/Subject' 5import { Subject } from 'rxjs/Subject'
7import truncate from 'lodash-es/truncate' 6import truncate from 'lodash-es/truncate'
7import { ScreenService } from '@app/shared/misc/screen.service'
8 8
9@Component({ 9@Component({
10 selector: 'my-markdown-textarea', 10 selector: 'my-markdown-textarea',
@@ -35,7 +35,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
35 35
36 private contentChanged = new Subject<string>() 36 private contentChanged = new Subject<string>()
37 37
38 constructor (private markdownService: MarkdownService) {} 38 constructor (
39 private screenService: ScreenService,
40 private markdownService: MarkdownService
41) {}
39 42
40 ngOnInit () { 43 ngOnInit () {
41 this.contentChanged 44 this.contentChanged
@@ -76,7 +79,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
76 } 79 }
77 80
78 arePreviewsDisplayed () { 81 arePreviewsDisplayed () {
79 return isInSmallView() === false 82 return this.screenService.isInSmallView() === false
80 } 83 }
81 84
82 private updatePreviews () { 85 private updatePreviews () {
diff --git a/client/src/app/shared/i18n/i18n-primeng-calendar.ts b/client/src/app/shared/i18n/i18n-primeng-calendar.ts
new file mode 100644
index 000000000..b05852ff8
--- /dev/null
+++ b/client/src/app/shared/i18n/i18n-primeng-calendar.ts
@@ -0,0 +1,94 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Injectable } from '@angular/core'
3
4@Injectable()
5export class I18nPrimengCalendarService {
6 private readonly calendarLocale: any = {}
7
8 constructor (private i18n: I18n) {
9 this.calendarLocale = {
10 firstDayOfWeek: 0,
11 dayNames: [
12 this.i18n('Sunday'),
13 this.i18n('Monday'),
14 this.i18n('Tuesday'),
15 this.i18n('Wednesday'),
16 this.i18n('Thursday'),
17 this.i18n('Friday'),
18 this.i18n('Saturday')
19 ],
20
21 dayNamesShort: [
22 this.i18n({ value: 'Sun', description: 'Day name short' }),
23 this.i18n({ value: 'Mon', description: 'Day name short' }),
24 this.i18n({ value: 'Tue', description: 'Day name short' }),
25 this.i18n({ value: 'Wed', description: 'Day name short' }),
26 this.i18n({ value: 'Thu', description: 'Day name short' }),
27 this.i18n({ value: 'Fri', description: 'Day name short' }),
28 this.i18n({ value: 'Sat', description: 'Day name short' })
29 ],
30
31 dayNamesMin: [
32 this.i18n({ value: 'Su', description: 'Day name min' }),
33 this.i18n({ value: 'Mo', description: 'Day name min' }),
34 this.i18n({ value: 'Tu', description: 'Day name min' }),
35 this.i18n({ value: 'We', description: 'Day name min' }),
36 this.i18n({ value: 'Th', description: 'Day name min' }),
37 this.i18n({ value: 'Fr', description: 'Day name min' }),
38 this.i18n({ value: 'Sa', description: 'Day name min' })
39 ],
40
41 monthNames: [
42 this.i18n('January'),
43 this.i18n('February'),
44 this.i18n('March'),
45 this.i18n('April'),
46 this.i18n('May'),
47 this.i18n('June'),
48 this.i18n('July'),
49 this.i18n('August'),
50 this.i18n('September'),
51 this.i18n('October'),
52 this.i18n('November'),
53 this.i18n('December')
54 ],
55
56 monthNamesShort: [
57 this.i18n({ value: 'Jan', description: 'Month name short' }),
58 this.i18n({ value: 'Feb', description: 'Month name short' }),
59 this.i18n({ value: 'Mar', description: 'Month name short' }),
60 this.i18n({ value: 'Apr', description: 'Month name short' }),
61 this.i18n({ value: 'May', description: 'Month name short' }),
62 this.i18n({ value: 'Jun', description: 'Month name short' }),
63 this.i18n({ value: 'Jul', description: 'Month name short' }),
64 this.i18n({ value: 'Aug', description: 'Month name short' }),
65 this.i18n({ value: 'Sep', description: 'Month name short' }),
66 this.i18n({ value: 'Oct', description: 'Month name short' }),
67 this.i18n({ value: 'Nov', description: 'Month name short' }),
68 this.i18n({ value: 'Dec', description: 'Month name short' })
69 ],
70
71 today: this.i18n('Today'),
72
73 clear: this.i18n('Clear')
74 }
75 }
76
77 getCalendarLocale () {
78 return this.calendarLocale
79 }
80
81 getTimezone () {
82 const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
83 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
84
85 return `${timezone} - ${gmt}`
86 }
87
88 getDateFormat () {
89 return this.i18n({
90 value: 'yy-mm-dd ',
91 description: 'Date format in this locale.'
92 })
93 }
94}
diff --git a/client/src/app/shared/misc/screen.service.ts b/client/src/app/shared/misc/screen.service.ts
new file mode 100644
index 000000000..5b17a914a
--- /dev/null
+++ b/client/src/app/shared/misc/screen.service.ts
@@ -0,0 +1,23 @@
1import { Injectable, NgZone } from '@angular/core'
2
3@Injectable()
4export class ScreenService {
5 private windowInnerWidth: number
6
7 constructor (private zone: NgZone) {
8 this.windowInnerWidth = window.innerWidth
9
10 // Try to cache a little bit window.innerWidth
11 this.zone.runOutsideAngular(() => {
12 setInterval(() => this.windowInnerWidth = window.innerWidth, 500)
13 })
14 }
15
16 isInSmallView () {
17 return this.windowInnerWidth < 600
18 }
19
20 isInMobileView () {
21 return this.windowInnerWidth < 500
22 }
23}
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 2219ac802..53aff1b24 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -96,26 +96,12 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
96 }) 96 })
97} 97}
98 98
99// Try to cache a little bit window.innerWidth
100let windowInnerWidth = window.innerWidth
101setInterval(() => windowInnerWidth = window.innerWidth, 500)
102
103function isInSmallView () {
104 return windowInnerWidth < 600
105}
106
107function isInMobileView () {
108 return windowInnerWidth < 500
109}
110
111export { 99export {
112 objectToUrlEncoded, 100 objectToUrlEncoded,
113 getParameterByName, 101 getParameterByName,
114 populateAsyncUserVideoChannels, 102 populateAsyncUserVideoChannels,
115 getAbsoluteAPIUrl, 103 getAbsoluteAPIUrl,
116 dateToHuman, 104 dateToHuman,
117 isInSmallView,
118 isInMobileView,
119 immutableAssign, 105 immutableAssign,
120 objectToFormData, 106 objectToFormData,
121 lineFeedToHtml 107 lineFeedToHtml
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b85445ef5..97e49e7ab 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -41,6 +41,8 @@ import {
41 ResetPasswordValidatorsService, 41 ResetPasswordValidatorsService,
42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService 42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService
43} from '@app/shared/forms' 43} from '@app/shared/forms'
44import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
45import { ScreenService } from '@app/shared/misc/screen.service'
44 46
45@NgModule({ 47@NgModule({
46 imports: [ 48 imports: [
@@ -128,6 +130,9 @@ import {
128 VideoCommentValidatorsService, 130 VideoCommentValidatorsService,
129 VideoValidatorsService, 131 VideoValidatorsService,
130 132
133 I18nPrimengCalendarService,
134 ScreenService,
135
131 I18n 136 I18n
132 ] 137 ]
133}) 138})
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 1c84573da..a468d3231 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -2,7 +2,6 @@ import { debounceTime } from 'rxjs/operators'
2import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' 2import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { isInMobileView } from '@app/shared/misc/utils'
6import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 5import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
7import { NotificationsService } from 'angular2-notifications' 6import { NotificationsService } from 'angular2-notifications'
8import { fromEvent, Observable, Subscription } from 'rxjs' 7import { fromEvent, Observable, Subscription } from 'rxjs'
@@ -11,6 +10,7 @@ import { ComponentPagination } from '../rest/component-pagination.model'
11import { VideoSortField } from './sort-field.type' 10import { VideoSortField } from './sort-field.type'
12import { Video } from './video.model' 11import { Video } from './video.model'
13import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
13import { ScreenService } from '@app/shared/misc/screen.service'
14 14
15export abstract class AbstractVideoList implements OnInit, OnDestroy { 15export abstract class AbstractVideoList implements OnInit, OnDestroy {
16 private static LINES_PER_PAGE = 4 16 private static LINES_PER_PAGE = 4
@@ -41,6 +41,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
41 protected abstract authService: AuthService 41 protected abstract authService: AuthService
42 protected abstract router: Router 42 protected abstract router: Router
43 protected abstract route: ActivatedRoute 43 protected abstract route: ActivatedRoute
44 protected abstract screenService: ScreenService
44 protected abstract i18n: I18n 45 protected abstract i18n: I18n
45 protected abstract location: Location 46 protected abstract location: Location
46 protected abstract currentRoute: string 47 protected abstract currentRoute: string
@@ -199,7 +200,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
199 } 200 }
200 201
201 private calcPageSizes () { 202 private calcPageSizes () {
202 if (isInMobileView() || this.baseVideoWidth === -1) { 203 if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
203 this.pagination.itemsPerPage = 5 204 this.pagination.itemsPerPage = 5
204 205
205 // Video takes all the width 206 // Video takes all the width
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index f045a3acd..78aed4f9f 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -1,8 +1,11 @@
1import { VideoDetails } from './video-details.model' 1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
3import { VideoUpdate } from '../../../../../shared/models/videos' 3import { VideoUpdate } from '../../../../../shared/models/videos'
4import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
4 5
5export class VideoEdit implements VideoUpdate { 6export class VideoEdit implements VideoUpdate {
7 static readonly SPECIAL_SCHEDULED_PRIVACY = -1
8
6 category: number 9 category: number
7 licence: number 10 licence: number
8 language: string 11 language: string
@@ -21,6 +24,7 @@ export class VideoEdit implements VideoUpdate {
21 previewUrl: string 24 previewUrl: string
22 uuid?: string 25 uuid?: string
23 id?: number 26 id?: number
27 scheduleUpdate?: VideoScheduleUpdate
24 28
25 constructor (videoDetails?: VideoDetails) { 29 constructor (videoDetails?: VideoDetails) {
26 if (videoDetails) { 30 if (videoDetails) {
@@ -40,6 +44,8 @@ export class VideoEdit implements VideoUpdate {
40 this.support = videoDetails.support 44 this.support = videoDetails.support
41 this.thumbnailUrl = videoDetails.thumbnailUrl 45 this.thumbnailUrl = videoDetails.thumbnailUrl
42 this.previewUrl = videoDetails.previewUrl 46 this.previewUrl = videoDetails.previewUrl
47
48 this.scheduleUpdate = videoDetails.scheduledUpdate
43 } 49 }
44 } 50 }
45 51
@@ -47,10 +53,22 @@ export class VideoEdit implements VideoUpdate {
47 Object.keys(values).forEach((key) => { 53 Object.keys(values).forEach((key) => {
48 this[ key ] = values[ key ] 54 this[ key ] = values[ key ]
49 }) 55 })
56
57 // If schedule publication, the video is private and will be changed to public privacy
58 if (values['schedulePublicationAt']) {
59 const updateAt = (values['schedulePublicationAt'] as Date)
60 updateAt.setSeconds(0)
61
62 this.privacy = VideoPrivacy.PRIVATE
63 this.scheduleUpdate = {
64 updateAt: updateAt.toISOString(),
65 privacy: VideoPrivacy.PUBLIC
66 }
67 }
50 } 68 }
51 69
52 toJSON () { 70 toFormPatch () {
53 return { 71 const json = {
54 category: this.category, 72 category: this.category,
55 licence: this.licence, 73 licence: this.licence,
56 language: this.language, 74 language: this.language,
@@ -64,5 +82,15 @@ export class VideoEdit implements VideoUpdate {
64 channelId: this.channelId, 82 channelId: this.channelId,
65 privacy: this.privacy 83 privacy: this.privacy
66 } 84 }
85
86 // Special case if we scheduled an update
87 if (this.scheduleUpdate) {
88 Object.assign(json, {
89 privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
90 schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString())
91 })
92 }
93
94 return json
67 } 95 }
68} 96}
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts
index e52f7dfb0..86d8f6f74 100644
--- a/client/src/app/shared/video/video-thumbnail.component.ts
+++ b/client/src/app/shared/video/video-thumbnail.component.ts
@@ -1,6 +1,6 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { isInMobileView } from '@app/shared/misc/utils'
3import { Video } from './video.model' 2import { Video } from './video.model'
3import { ScreenService } from '@app/shared/misc/screen.service'
4 4
5@Component({ 5@Component({
6 selector: 'my-video-thumbnail', 6 selector: 'my-video-thumbnail',
@@ -11,10 +11,12 @@ export class VideoThumbnailComponent {
11 @Input() video: Video 11 @Input() video: Video
12 @Input() nsfw = false 12 @Input() nsfw = false
13 13
14 constructor (private screenService: ScreenService) {}
15
14 getImageUrl () { 16 getImageUrl () {
15 if (!this.video) return '' 17 if (!this.video) return ''
16 18
17 if (isInMobileView()) { 19 if (this.screenService.isInMobileView()) {
18 return this.video.previewUrl 20 return this.video.previewUrl
19 } 21 }
20 22
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 48a4b4260..7f421dbbb 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -6,6 +6,7 @@ import { getAbsoluteAPIUrl } from '../misc/utils'
6import { ServerConfig } from '../../../../../shared/models' 6import { ServerConfig } from '../../../../../shared/models'
7import { Actor } from '@app/shared/actor/actor.model' 7import { Actor } from '@app/shared/actor/actor.model'
8import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' 8import { peertubeTranslate } from '@app/shared/i18n/i18n-utils'
9import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
9 10
10export class Video implements VideoServerModel { 11export class Video implements VideoServerModel {
11 by: string 12 by: string
@@ -38,6 +39,7 @@ export class Video implements VideoServerModel {
38 39
39 waitTranscoding?: boolean 40 waitTranscoding?: boolean
40 state?: VideoConstant<VideoState> 41 state?: VideoConstant<VideoState>
42 scheduledUpdate?: VideoScheduleUpdate
41 43
42 account: { 44 account: {
43 id: number 45 id: number
@@ -109,6 +111,7 @@ export class Video implements VideoServerModel {
109 this.language.label = peertubeTranslate(this.language.label, translations) 111 this.language.label = peertubeTranslate(this.language.label, translations)
110 this.privacy.label = peertubeTranslate(this.privacy.label, translations) 112 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
111 113
114 this.scheduledUpdate = hash.scheduledUpdate
112 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) 115 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
113 } 116 }
114 117
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index d63915ad2..3af90e7ad 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -83,7 +83,8 @@ export class VideoService {
83 waitTranscoding: video.waitTranscoding, 83 waitTranscoding: video.waitTranscoding,
84 commentsEnabled: video.commentsEnabled, 84 commentsEnabled: video.commentsEnabled,
85 thumbnailfile: video.thumbnailfile, 85 thumbnailfile: video.thumbnailfile,
86 previewfile: video.previewfile 86 previewfile: video.previewfile,
87 scheduleUpdate: video.scheduleUpdate || undefined
87 } 88 }
88 89
89 const data = objectToFormData(body) 90 const data = objectToFormData(body)
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 379cf7948..447c5ab9b 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -88,6 +88,7 @@
88 <select id="privacy" formControlName="privacy"> 88 <select id="privacy" formControlName="privacy">
89 <option></option> 89 <option></option>
90 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 90 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
91 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
91 </select> 92 </select>
92 </div> 93 </div>
93 94
@@ -96,11 +97,27 @@
96 </div> 97 </div>
97 </div> 98 </div>
98 99
100 <div *ngIf="schedulePublicationEnabled" class="form-group">
101 <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
102 <p-calendar
103 id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
104 [locale]="calendarLocale" [minDate]="minScheduledDate" [showTime]="true" [hideOnDateTimeSelect]="true"
105 >
106 </p-calendar>
107
108 <div *ngIf="formErrors.schedulePublicationAt" class="form-error">
109 {{ formErrors.schedulePublicationAt }}
110 </div>
111 </div>
112
99 <div class="form-group form-group-checkbox"> 113 <div class="form-group form-group-checkbox">
100 <input type="checkbox" id="nsfw" formControlName="nsfw" /> 114 <input type="checkbox" id="nsfw" formControlName="nsfw" />
101 <label for="nsfw"></label> 115 <label for="nsfw"></label>
102 <label i18n for="nsfw">This video contains mature or explicit content</label> 116 <label i18n for="nsfw">This video contains mature or explicit content</label>
103 <my-help tooltipPlacement="top" helpType="custom" i18n-customHtml customHtml="Some instances do not list NSFW videos by default."></my-help> 117 <my-help
118 tooltipPlacement="top" helpType="custom" i18n-customHtml
119 customHtml="Some instances do not list videos containing mature or explicit content by default."
120 ></my-help>
104 </div> 121 </div>
105 122
106 <div class="form-group form-group-checkbox"> 123 <div class="form-group form-group-checkbox">
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index 1295cf098..061eca4a7 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
@@ -44,18 +44,6 @@
44 font-size: 15px; 44 font-size: 15px;
45 } 45 }
46 46
47 .root-tabset /deep/ > .nav {
48 margin-left: 15px;
49 margin-bottom: 15px;
50
51 .nav-link {
52 display: flex !important;
53 align-items: center;
54 height: 30px !important;
55 padding: 0 15px !important;
56 }
57 }
58
59 .advanced-settings .form-group { 47 .advanced-settings .form-group {
60 margin-bottom: 20px; 48 margin-bottom: 20px;
61 } 49 }
@@ -98,7 +86,35 @@
98 } 86 }
99} 87}
100 88
89p-calendar {
90 display: block;
91
92 /deep/ {
93 input,
94 .ui-calendar {
95 width: 100%;
96 }
97
98 input {
99 @include peertube-input-text(100%);
100 color: #000;
101 }
102 }
103}
104
101/deep/ { 105/deep/ {
106 .root-tabset > .nav {
107 margin-left: 15px;
108 margin-bottom: 15px;
109
110 .nav-link {
111 display: flex !important;
112 align-items: center;
113 height: 30px !important;
114 padding: 0 15px !important;
115 }
116 }
117
102 .ng2-tag-input { 118 .ng2-tag-input {
103 border: none !important; 119 border: none !important;
104 } 120 }
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index ee4fd5dc1..24418fc4f 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { FormGroup, ValidatorFn } from '@angular/forms' 2import { FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
@@ -7,6 +7,7 @@ import { ServerService } from '../../../core/server'
7import { VideoEdit } from '../../../shared/video/video-edit.model' 7import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-edit', 13 selector: 'my-video-edit',
@@ -20,16 +21,26 @@ export class VideoEditComponent implements OnInit {
20 @Input() validationMessages: FormReactiveValidationMessages = {} 21 @Input() validationMessages: FormReactiveValidationMessages = {}
21 @Input() videoPrivacies = [] 22 @Input() videoPrivacies = []
22 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 23 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
24 @Input() schedulePublicationPossible = true
25
26 // So that it can be accessed in the template
27 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
23 28
24 videoCategories = [] 29 videoCategories = []
25 videoLicences = [] 30 videoLicences = []
26 videoLanguages = [] 31 videoLanguages = []
27 video: VideoEdit
28 32
29 tagValidators: ValidatorFn[] 33 tagValidators: ValidatorFn[]
30 tagValidatorsMessages: { [ name: string ]: string } 34 tagValidatorsMessages: { [ name: string ]: string }
31 35
36 schedulePublicationEnabled = false
37
32 error: string = null 38 error: string = null
39 calendarLocale: any = {}
40 minScheduledDate = new Date()
41
42 calendarTimezone: string
43 calendarDateFormat: string
33 44
34 constructor ( 45 constructor (
35 private formValidatorService: FormValidatorService, 46 private formValidatorService: FormValidatorService,
@@ -37,10 +48,15 @@ export class VideoEditComponent implements OnInit {
37 private route: ActivatedRoute, 48 private route: ActivatedRoute,
38 private router: Router, 49 private router: Router,
39 private notificationsService: NotificationsService, 50 private notificationsService: NotificationsService,
40 private serverService: ServerService 51 private serverService: ServerService,
52 private i18nPrimengCalendarService: I18nPrimengCalendarService
41 ) { 53 ) {
42 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS 54 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
43 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES 55 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
56
57 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
58 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
59 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
44 } 60 }
45 61
46 updateForm () { 62 updateForm () {
@@ -64,7 +80,8 @@ export class VideoEditComponent implements OnInit {
64 tags: null, 80 tags: null,
65 thumbnailfile: null, 81 thumbnailfile: null,
66 previewfile: null, 82 previewfile: null,
67 support: this.videoValidatorsService.VIDEO_SUPPORT 83 support: this.videoValidatorsService.VIDEO_SUPPORT,
84 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
68 } 85 }
69 86
70 this.formValidatorService.updateForm( 87 this.formValidatorService.updateForm(
@@ -75,6 +92,52 @@ export class VideoEditComponent implements OnInit {
75 defaultValues 92 defaultValues
76 ) 93 )
77 94
95 this.trackChannelChange()
96 this.trackPrivacyChange()
97 }
98
99 ngOnInit () {
100 this.updateForm()
101
102 this.videoCategories = this.serverService.getVideoCategories()
103 this.videoLicences = this.serverService.getVideoLicences()
104 this.videoLanguages = this.serverService.getVideoLanguages()
105
106 setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
107 }
108
109 private trackPrivacyChange () {
110 // We will update the "support" field depending on the channel
111 this.form.controls[ 'privacy' ]
112 .valueChanges
113 .pipe(map(res => parseInt(res.toString(), 10)))
114 .subscribe(
115 newPrivacyId => {
116 this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
117
118 // Value changed
119 const scheduleControl = this.form.get('schedulePublicationAt')
120 const waitTranscodingControl = this.form.get('waitTranscoding')
121
122 if (this.schedulePublicationEnabled) {
123 scheduleControl.setValidators([ Validators.required ])
124
125 waitTranscodingControl.disable()
126 waitTranscodingControl.setValue(false)
127 } else {
128 scheduleControl.clearValidators()
129
130 waitTranscodingControl.enable()
131 waitTranscodingControl.setValue(true)
132 }
133
134 scheduleControl.updateValueAndValidity()
135 waitTranscodingControl.updateValueAndValidity()
136 }
137 )
138 }
139
140 private trackChannelChange () {
78 // We will update the "support" field depending on the channel 141 // We will update the "support" field depending on the channel
79 this.form.controls[ 'channelId' ] 142 this.form.controls[ 'channelId' ]
80 .valueChanges 143 .valueChanges
@@ -108,14 +171,6 @@ export class VideoEditComponent implements OnInit {
108 ) 171 )
109 } 172 }
110 173
111 ngOnInit () {
112 this.updateForm()
113
114 this.videoCategories = this.serverService.getVideoCategories()
115 this.videoLicences = this.serverService.getVideoLicences()
116 this.videoLanguages = this.serverService.getVideoLanguages()
117 }
118
119 private updateSupportField (support: string) { 174 private updateSupportField (support: string) {
120 return this.form.patchValue({ support: support || '' }) 175 return this.form.patchValue({ support: support || '' })
121 } 176 }
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
index 76eba9c19..6bf3e34b1 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
@@ -4,10 +4,12 @@ import { TagInputModule } from 'ngx-chips'
4import { SharedModule } from '../../../shared/' 4import { SharedModule } from '../../../shared/'
5import { VideoEditComponent } from './video-edit.component' 5import { VideoEditComponent } from './video-edit.component'
6import { VideoImageComponent } from './video-image.component' 6import { VideoImageComponent } from './video-image.component'
7import { CalendarModule } from 'primeng/components/calendar/calendar'
7 8
8@NgModule({ 9@NgModule({
9 imports: [ 10 imports: [
10 TagInputModule, 11 TagInputModule,
12 CalendarModule,
11 13
12 SharedModule 14 SharedModule
13 ], 15 ],
@@ -20,6 +22,7 @@ import { VideoImageComponent } from './video-image.component'
20 exports: [ 22 exports: [
21 TagInputModule, 23 TagInputModule,
22 TabsModule, 24 TabsModule,
25 CalendarModule,
23 26
24 VideoEditComponent 27 VideoEditComponent
25 ], 28 ],
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index f00cfe016..07034e4e1 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -27,6 +27,7 @@
27 <div class="peertube-select-container"> 27 <div class="peertube-select-container">
28 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId"> 28 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
29 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 29 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
30 <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
30 </select> 31 </select>
31 </div> 32 </div>
32 </div> 33 </div>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 85afd0caa..3ddeda109 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -27,6 +27,9 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
27export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
28 @ViewChild('videofileInput') videofileInput 28 @ViewChild('videofileInput') videofileInput
29 29
30 // So that it can be accessed in the template
31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
32
30 isUploadingVideo = false 33 isUploadingVideo = false
31 isUpdatingVideo = false 34 isUpdatingVideo = false
32 videoUploaded = false 35 videoUploaded = false
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index 73b2bc08f..5cb16c8ab 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -6,7 +6,7 @@
6 <form novalidate [formGroup]="form"> 6 <form novalidate [formGroup]="form">
7 7
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 ></my-video-edit> 11 ></my-video-edit>
12 12
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 0266164af..c4e6f44de 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -24,6 +24,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
24 isUpdatingVideo = false 24 isUpdatingVideo = false
25 videoPrivacies = [] 25 videoPrivacies = []
26 userVideoChannels = [] 26 userVideoChannels = []
27 schedulePublicationPossible = false
27 28
28 constructor ( 29 constructor (
29 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
@@ -70,13 +71,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
70 this.userVideoChannels = videoChannels 71 this.userVideoChannels = videoChannels
71 72
72 // We cannot set private a video that was not private 73 // We cannot set private a video that was not private
73 if (video.privacy.id !== VideoPrivacy.PRIVATE) { 74 if (this.video.privacy !== VideoPrivacy.PRIVATE) {
74 const newVideoPrivacies = [] 75 this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE)
75 for (const p of this.videoPrivacies) { 76 } else { // We can schedule video publication only if it it is private
76 if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p) 77 this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
77 }
78
79 this.videoPrivacies = newVideoPrivacies
80 } 78 }
81 79
82 this.hydrateFormFromVideo() 80 this.hydrateFormFromVideo()
@@ -123,7 +121,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
123 } 121 }
124 122
125 private hydrateFormFromVideo () { 123 private hydrateFormFromVideo () {
126 this.form.patchValue(this.video.toJSON()) 124 this.form.patchValue(this.video.toFormPatch())
127 125
128 const objects = [ 126 const objects = [
129 { 127 {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 8bd5c00ff..208375e33 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -3,10 +3,14 @@
3 <div id="video-element-wrapper"> 3 <div id="video-element-wrapper">
4 </div> 4 </div>
5 5
6 <div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()"> 6 <div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()">
7 The video is being transcoded, it may not work properly. 7 The video is being transcoded, it may not work properly.
8 </div> 8 </div>
9 9
10 <div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
11 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
12 </div>
13
10 <!-- Video information --> 14 <!-- Video information -->
11 <div *ngIf="video" class="margin-content video-bottom"> 15 <div *ngIf="video" class="margin-content video-bottom">
12 <div class="video-info"> 16 <div class="video-info">
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index ae8bdccaf..71770c93b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -28,7 +28,7 @@
28 } 28 }
29} 29}
30 30
31#warning-transcoding { 31.alert {
32 text-align: center; 32 text-align: center;
33} 33}
34 34
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index a760c03e8..72e96ca93 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -280,6 +280,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
280 return this.video && this.video.state.id === VideoState.TO_TRANSCODE 280 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
281 } 281 }
282 282
283 hasVideoScheduledPublication () {
284 return this.video && this.video.scheduledUpdate !== undefined
285 }
286
283 private updateVideoDescription (description: string) { 287 private updateVideoDescription (description: string) {
284 this.video.description = description 288 this.video.description = description
285 this.setVideoDescriptionHTML() 289 this.setVideoDescriptionHTML()
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 2fd82a940..dbe1d937d 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -9,6 +9,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-videos-local', 15 selector: 'my-videos-local',
@@ -28,6 +29,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
28 protected authService: AuthService, 29 protected authService: AuthService,
29 protected location: Location, 30 protected location: Location,
30 protected i18n: I18n, 31 protected i18n: I18n,
32 protected screenService: ScreenService,
31 private videoService: VideoService 33 private videoService: VideoService
32 ) { 34 ) {
33 super() 35 super()
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index 8183357f8..004a49168 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -26,6 +27,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
26 protected notificationsService: NotificationsService, 27 protected notificationsService: NotificationsService,
27 protected authService: AuthService, 28 protected authService: AuthService,
28 protected i18n: I18n, 29 protected i18n: I18n,
30 protected screenService: ScreenService,
29 private videoService: VideoService 31 private videoService: VideoService
30 ) { 32 ) {
31 super() 33 super()
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
index b6434f347..33ed3f00e 100644
--- a/client/src/app/videos/video-list/video-search.component.ts
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -9,6 +9,7 @@ import { AuthService } from '../../core/auth'
9import { AbstractVideoList } from '../../shared/video/abstract-video-list' 9import { AbstractVideoList } from '../../shared/video/abstract-video-list'
10import { VideoService } from '../../shared/video/video.service' 10import { VideoService } from '../../shared/video/video.service'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-videos-search', 15 selector: 'my-videos-search',
@@ -32,6 +33,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
32 protected authService: AuthService, 33 protected authService: AuthService,
33 protected location: Location, 34 protected location: Location,
34 protected i18n: I18n, 35 protected i18n: I18n,
36 protected screenService: ScreenService,
35 private videoService: VideoService, 37 private videoService: VideoService,
36 private redirectService: RedirectService 38 private redirectService: RedirectService
37 ) { 39 ) {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index e56b749d1..f2174aa14 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -25,6 +26,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
25 protected notificationsService: NotificationsService, 26 protected notificationsService: NotificationsService,
26 protected authService: AuthService, 27 protected authService: AuthService,
27 protected location: Location, 28 protected location: Location,
29 protected screenService: ScreenService,
28 protected i18n: I18n, 30 protected i18n: I18n,
29 private videoService: VideoService 31 private videoService: VideoService
30 ) { 32 ) {
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 4006c9128..dae0c52c2 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -6,14 +6,14 @@ $icon-font-path: '../../node_modules/bootstrap-sass/assets/fonts/bootstrap/';
6 6
7@import '_fonts'; 7@import '_fonts';
8 8
9@import '~primeng/resources/themes/bootstrap/theme.css';
10@import '~primeng/resources/primeng.css';
11@import '~video.js/dist/video-js.css'; 9@import '~video.js/dist/video-js.css';
12 10
13$assets-path: '../assets/'; 11$assets-path: '../assets/';
14@import './player/player'; 12@import './player/player';
15@import './loading-bar'; 13@import './loading-bar';
16 14
15@import './primeng-custom';
16
17[hidden] { 17[hidden] {
18 display: none !important; 18 display: none !important;
19} 19}
@@ -142,126 +142,6 @@ label {
142 to { transform: scale(1) rotate(360deg);} 142 to { transform: scale(1) rotate(360deg);}
143} 143}
144 144
145// ngprime data table customizations
146p-table {
147 font-size: 15px !important;
148
149 td {
150 border: 1px solid #E5E5E5 !important;
151 padding-left: 15px !important;
152 overflow: hidden !important;
153 text-overflow: ellipsis !important;
154 white-space: nowrap !important;
155 }
156
157 tr {
158 background-color: #fff !important;
159 height: 46px;
160 }
161
162 .ui-table-tbody {
163 tr {
164 &:hover {
165 background-color: #f0f0f0 !important;
166 }
167
168 &:not(:hover) {
169 .action-cell * {
170 display: none !important;
171 }
172 }
173
174 &:first-child td {
175 border-top: none !important;
176 }
177
178 &:last-child td {
179 border-bottom: none !important;
180 }
181 }
182
183 .expander {
184 cursor: pointer;
185 position: relative;
186 top: 1px;
187 }
188 }
189
190 th {
191 border: none !important;
192 border-bottom: 1px solid #f0f0f0 !important;
193 text-align: left !important;
194 padding: 5px 0 5px 15px !important;
195 font-weight: $font-semibold !important;
196 color: #000 !important;
197
198 &.ui-sortable-column:hover {
199 background-color: #f0f0f0 !important;
200 border: 1px solid #f0f0f0 !important;
201 border-width: 0 1px !important;
202
203 &:first-child {
204 border-width: 0 1px 0 0 !important;
205 }
206 }
207
208 &.ui-state-highlight {
209 background-color: #fff !important;
210
211 .fa {
212 @extend .glyphicon;
213 font-size: 11px;
214
215 &.fa-sort-asc {
216 @extend .glyphicon-triangle-top;
217 }
218
219 &.fa-sort-desc {
220 @extend .glyphicon-triangle-bottom;
221 }
222 }
223 }
224 }
225
226 .action-cell {
227 width: 250px !important;
228 padding: 0 !important;
229 text-align: center;
230
231 my-edit-button + my-delete-button {
232 margin-left: 5px;
233 }
234 }
235
236 p-paginator {
237 .ui-paginator-bottom {
238 position: relative;
239 border: none !important;
240 border: 1px solid #f0f0f0 !important;
241 height: 40px;
242 display: flex;
243 justify-content: center;
244 align-items: center;
245
246 a {
247 color: #000 !important;
248 font-weight: $font-semibold !important;
249 margin-right: 20px !important;
250 outline: 0 !important;
251 border-radius: 3px !important;
252 padding: 5px 2px !important;
253
254 &.ui-state-active {
255 &, &:hover, &:active, &:focus {
256 color: #fff !important;
257 background-color: $orange-color !important;
258 }
259 }
260 }
261 }
262 }
263}
264
265// Bootstrap customizations 145// Bootstrap customizations
266.dropdown-menu { 146.dropdown-menu {
267 border-radius: 3px; 147 border-radius: 3px;
@@ -352,6 +232,8 @@ tabset:not(.bootstrap) {
352} 232}
353 233
354tabset.bootstrap { 234tabset.bootstrap {
235 margin-left: 0;
236
355 .nav-item .nav-link { 237 .nav-item .nav-link {
356 &, & a { 238 &, & a {
357 color: #000; 239 color: #000;
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 748c98afa..3904751c2 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -281,6 +281,12 @@
281 cursor: pointer; 281 cursor: pointer;
282 display: inline; 282 display: inline;
283 } 283 }
284
285 &[disabled] + label,
286 &[disabled] + label + label{
287 opacity: 0.5;
288 cursor: default;
289 }
284} 290}
285 291
286 292
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
new file mode 100644
index 000000000..b28b20e0f
--- /dev/null
+++ b/client/src/sass/primeng-custom.scss
@@ -0,0 +1,174 @@
1@import '_variables';
2@import '_mixins';
3
4@import '~primeng/resources/primeng.css';
5@import '~primeng/resources/themes/bootstrap/theme.css';
6
7@mixin glyphicon-light {
8 font-family: 'Glyphicons Halflings';
9 text-decoration: none !important;
10 color: #000 !important;
11}
12
13// data table customizations
14p-table {
15 font-size: 15px !important;
16
17 td {
18 border: 1px solid #E5E5E5 !important;
19 padding-left: 15px !important;
20 overflow: hidden !important;
21 text-overflow: ellipsis !important;
22 white-space: nowrap !important;
23 }
24
25 tr {
26 background-color: #fff !important;
27 height: 46px;
28 }
29
30 .ui-table-tbody {
31 tr {
32 &:hover {
33 background-color: #f0f0f0 !important;
34 }
35
36 &:not(:hover) {
37 .action-cell * {
38 display: none !important;
39 }
40 }
41
42 &:first-child td {
43 border-top: none !important;
44 }
45
46 &:last-child td {
47 border-bottom: none !important;
48 }
49 }
50
51 .expander {
52 cursor: pointer;
53 position: relative;
54 top: 1px;
55 }
56 }
57
58 th {
59 border: none !important;
60 border-bottom: 1px solid #f0f0f0 !important;
61 text-align: left !important;
62 padding: 5px 0 5px 15px !important;
63 font-weight: $font-semibold !important;
64 color: #000 !important;
65
66 &.ui-sortable-column:hover {
67 background-color: #f0f0f0 !important;
68 border: 1px solid #f0f0f0 !important;
69 border-width: 0 1px !important;
70
71 &:first-child {
72 border-width: 0 1px 0 0 !important;
73 }
74 }
75
76 &.ui-state-highlight {
77 background-color: #fff !important;
78
79 .pi {
80 @extend .glyphicon;
81
82 color: #000;
83 font-size: 11px;
84
85 &.pi-sort-up {
86 @extend .glyphicon-triangle-top;
87 }
88
89 &.pi-sort-down {
90 @extend .glyphicon-triangle-bottom;
91 }
92 }
93 }
94 }
95
96 .action-cell {
97 width: 250px !important;
98 padding: 0 !important;
99 text-align: center;
100
101 my-edit-button + my-delete-button {
102 margin-left: 5px;
103 }
104 }
105
106 p-paginator {
107 .ui-paginator-bottom {
108 position: relative;
109 border: 1px solid #f0f0f0 !important;
110 height: 40px;
111 display: flex;
112 justify-content: center;
113 align-items: center;
114
115 .ui-paginator-pages {
116 height: auto !important;
117
118 a {
119 color: #000 !important;
120 font-weight: $font-semibold !important;
121 margin-right: 20px !important;
122 outline: 0 !important;
123 border-radius: 3px !important;
124 padding: 5px 2px !important;
125 height: auto !important;
126
127 &.ui-state-active {
128 &, &:hover, &:active, &:focus {
129 color: #fff !important;
130 background-color: $orange-color !important;
131 }
132 }
133 }
134 }
135 }
136 }
137}
138
139// PrimeNG calendar tweaks
140p-calendar .ui-datepicker {
141 a {
142 @include disable-default-a-behaviour;
143 }
144
145 .ui-datepicker-header {
146
147 .ui-datepicker-year {
148 margin-left: 5px;
149 }
150
151 .ui-datepicker-next {
152 @extend .glyphicon-chevron-right;
153 @include glyphicon-light;
154 }
155
156 .ui-datepicker-prev {
157 @extend .glyphicon-chevron-left;
158 @include glyphicon-light;
159 }
160 }
161
162 .ui-timepicker {
163
164 .pi.pi-chevron-up {
165 @extend .glyphicon-chevron-up;
166 @include glyphicon-light;
167 }
168
169 .pi.pi-chevron-down {
170 @extend .glyphicon-chevron-down;
171 @include glyphicon-light;
172 }
173 }
174} \ No newline at end of file
diff --git a/client/yarn.lock b/client/yarn.lock
index e2d0da541..b9b13c18c 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -7656,9 +7656,9 @@ pretty-error@^2.0.2:
7656 renderkid "^2.0.1" 7656 renderkid "^2.0.1"
7657 utila "~0.4" 7657 utila "~0.4"
7658 7658
7659primeng@^5.2.6: 7659primeng@^6.0.0-rc.1:
7660 version "5.2.7" 7660 version "6.0.0-rc.1"
7661 resolved "https://registry.yarnpkg.com/primeng/-/primeng-5.2.7.tgz#9dcf461b6a82ea46de85751dc235ea82303e64b1" 7661 resolved "https://registry.yarnpkg.com/primeng/-/primeng-6.0.0-rc.1.tgz#038e5657a5395e08a5c1fd9312b12cac1a44b527"
7662 7662
7663private@^0.1.6, private@^0.1.8, private@~0.1.5: 7663private@^0.1.6, private@^0.1.8, private@~0.1.5:
7664 version "0.1.8" 7664 version "0.1.8"
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 164378505..53902071c 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -8,8 +8,6 @@ import { VideoPrivacy } from '../../shared/models/videos'
8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
10import { invert } from 'lodash' 10import { invert } from 'lodash'
11import { RemoveOldJobsScheduler } from '../lib/schedulers/remove-old-jobs-scheduler'
12import { UpdateVideosScheduler } from '../lib/schedulers/update-videos-scheduler'
13 11
14// Use a variable to reload the configuration if we need 12// Use a variable to reload the configuration if we need
15let config: IConfig = require('config') 13let config: IConfig = require('config')
@@ -98,8 +96,8 @@ const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
98// 1 hour 96// 1 hour
99let SCHEDULER_INTERVALS_MS = { 97let SCHEDULER_INTERVALS_MS = {
100 badActorFollow: 60000 * 60, // 1 hour 98 badActorFollow: 60000 * 60, // 1 hour
101 removeOldJobs: 60000 * 60, // 1 jour 99 removeOldJobs: 60000 * 60, // 1 hour
102 updateVideos: 60000 * 1, // 1 minute 100 updateVideos: 60000 // 1 minute
103} 101}
104 102
105// --------------------------------------------------------------------------- 103// ---------------------------------------------------------------------------
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index d123c3ceb..a964648fd 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -33,7 +33,9 @@ export class UpdateVideosScheduler extends AbstractScheduler {
33 } 33 }
34 } 34 }
35 35
36 private updateVideos () { 36 private async updateVideos () {
37 if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined
38
37 return sequelizeTypescript.transaction(async t => { 39 return sequelizeTypescript.transaction(async t => {
38 const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) 40 const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
39 41
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index 9fe5a253b..da17b4a68 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -223,7 +223,7 @@ const videosUpdateValidator = [
223 223
224 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) { 224 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
225 return res.status(409) 225 return res.status(409)
226 .json({ error: 'Cannot set "private" a video that was not private anymore.' }) 226 .json({ error: 'Cannot set "private" a video that was not private.' })
227 .end() 227 .end()
228 } 228 }
229 229
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index d4e37beb5..3cf5f6c99 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -25,7 +25,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
25 @AllowNull(true) 25 @AllowNull(true)
26 @Default(null) 26 @Default(null)
27 @Column 27 @Column
28 privacy: VideoPrivacy 28 privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
29 29
30 @CreatedAt 30 @CreatedAt
31 createdAt: Date 31 createdAt: Date
@@ -45,6 +45,21 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
45 }) 45 })
46 Video: VideoModel 46 Video: VideoModel
47 47
48 static areVideosToUpdate () {
49 const query = {
50 logging: false,
51 attributes: [ 'id' ],
52 where: {
53 updateAt: {
54 [Sequelize.Op.lte]: new Date()
55 }
56 }
57 }
58
59 return ScheduleVideoUpdateModel.findOne(query)
60 .then(res => !!res)
61 }
62
48 static listVideosToUpdate (t: Transaction) { 63 static listVideosToUpdate (t: Transaction) {
49 const query = { 64 const query = {
50 where: { 65 where: {
@@ -68,4 +83,10 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
68 return ScheduleVideoUpdateModel.findAll(query) 83 return ScheduleVideoUpdateModel.findAll(query)
69 } 84 }
70 85
86 toFormattedJSON () {
87 return {
88 updateAt: this.updateAt,
89 privacy: this.privacy || undefined
90 }
91 }
71} 92}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 440f4d171..0041e4d38 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -97,7 +97,8 @@ export enum ScopeNames {
97 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 97 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
98 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 98 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
99 WITH_TAGS = 'WITH_TAGS', 99 WITH_TAGS = 'WITH_TAGS',
100 WITH_FILES = 'WITH_FILES' 100 WITH_FILES = 'WITH_FILES',
101 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
101} 102}
102 103
103@Scopes({ 104@Scopes({
@@ -286,6 +287,14 @@ export enum ScopeNames {
286 required: true 287 required: true
287 } 288 }
288 ] 289 ]
290 },
291 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
292 include: [
293 {
294 model: () => ScheduleVideoUpdateModel.unscoped(),
295 required: false
296 }
297 ]
289 } 298 }
290}) 299})
291@Table({ 300@Table({
@@ -843,7 +852,7 @@ export class VideoModel extends Model<VideoModel> {
843 } 852 }
844 853
845 return VideoModel 854 return VideoModel
846 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ]) 855 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
847 .findById(id, options) 856 .findById(id, options)
848 } 857 }
849 858
@@ -869,7 +878,7 @@ export class VideoModel extends Model<VideoModel> {
869 } 878 }
870 879
871 return VideoModel 880 return VideoModel
872 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ]) 881 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
873 .findOne(options) 882 .findOne(options)
874 } 883 }
875 884
@@ -1022,9 +1031,9 @@ export class VideoModel extends Model<VideoModel> {
1022 1031
1023 toFormattedJSON (options?: { 1032 toFormattedJSON (options?: {
1024 additionalAttributes: { 1033 additionalAttributes: {
1025 state: boolean, 1034 state?: boolean,
1026 waitTranscoding: boolean, 1035 waitTranscoding?: boolean,
1027 scheduledUpdate: boolean 1036 scheduledUpdate?: boolean
1028 } 1037 }
1029 }): Video { 1038 }): Video {
1030 const formattedAccount = this.VideoChannel.Account.toFormattedJSON() 1039 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
@@ -1084,18 +1093,18 @@ export class VideoModel extends Model<VideoModel> {
1084 } 1093 }
1085 1094
1086 if (options) { 1095 if (options) {
1087 if (options.additionalAttributes.state) { 1096 if (options.additionalAttributes.state === true) {
1088 videoObject.state = { 1097 videoObject.state = {
1089 id: this.state, 1098 id: this.state,
1090 label: VideoModel.getStateLabel(this.state) 1099 label: VideoModel.getStateLabel(this.state)
1091 } 1100 }
1092 } 1101 }
1093 1102
1094 if (options.additionalAttributes.waitTranscoding) { 1103 if (options.additionalAttributes.waitTranscoding === true) {
1095 videoObject.waitTranscoding = this.waitTranscoding 1104 videoObject.waitTranscoding = this.waitTranscoding
1096 } 1105 }
1097 1106
1098 if (options.additionalAttributes.scheduledUpdate && this.ScheduleVideoUpdate) { 1107 if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
1099 videoObject.scheduledUpdate = { 1108 videoObject.scheduledUpdate = {
1100 updateAt: this.ScheduleVideoUpdate.updateAt, 1109 updateAt: this.ScheduleVideoUpdate.updateAt,
1101 privacy: this.ScheduleVideoUpdate.privacy || undefined 1110 privacy: this.ScheduleVideoUpdate.privacy || undefined
@@ -1107,7 +1116,11 @@ export class VideoModel extends Model<VideoModel> {
1107 } 1116 }
1108 1117
1109 toFormattedDetailsJSON (): VideoDetails { 1118 toFormattedDetailsJSON (): VideoDetails {
1110 const formattedJson = this.toFormattedJSON() 1119 const formattedJson = this.toFormattedJSON({
1120 additionalAttributes: {
1121 scheduledUpdate: true
1122 }
1123 })
1111 1124
1112 const detailsJson = { 1125 const detailsJson = {
1113 support: this.support, 1126 support: this.support,
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 04bed3b44..abbea6ba3 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -291,6 +291,23 @@ describe('Test videos API validator', function () {
291 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 291 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
292 }) 292 })
293 293
294 it('Should fail with a bad schedule update (miss updateAt)', async function () {
295 const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC })
296 const attaches = baseCorrectAttaches
297
298 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
299 })
300
301 it('Should fail with a bad schedule update (wrong updateAt)', async function () {
302 const fields = immutableAssign(baseCorrectParams, {
303 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC,
304 'scheduleUpdate[updateAt]': 'toto'
305 })
306 const attaches = baseCorrectAttaches
307
308 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
309 })
310
294 it('Should fail without an input file', async function () { 311 it('Should fail without an input file', async function () {
295 const fields = baseCorrectParams 312 const fields = baseCorrectParams
296 const attaches = {} 313 const attaches = {}
@@ -494,6 +511,18 @@ describe('Test videos API validator', function () {
494 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) 511 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
495 }) 512 })
496 513
514 it('Should fail with a bad schedule update (miss updateAt)', async function () {
515 const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
516
517 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
518 })
519
520 it('Should fail with a bad schedule update (wrong updateAt)', async function () {
521 const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } })
522
523 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
524 })
525
497 it('Should fail with an incorrect thumbnail file', async function () { 526 it('Should fail with an incorrect thumbnail file', async function () {
498 const fields = baseCorrectParams 527 const fields = baseCorrectParams
499 const attaches = { 528 const attaches = {
diff --git a/server/tests/api/videos/video-schedule-update.ts b/server/tests/api/videos/video-schedule-update.ts
index 8b87ea855..a260fa4da 100644
--- a/server/tests/api/videos/video-schedule-update.ts
+++ b/server/tests/api/videos/video-schedule-update.ts
@@ -5,11 +5,14 @@ import 'mocha'
5import { VideoPrivacy } from '../../../../shared/models/videos' 5import { VideoPrivacy } from '../../../../shared/models/videos'
6import { 6import {
7 doubleFollow, 7 doubleFollow,
8 flushAndRunMultipleServers, getMyVideos, 8 flushAndRunMultipleServers,
9 getMyVideos,
9 getVideosList, 10 getVideosList,
11 getVideoWithToken,
10 killallServers, 12 killallServers,
11 ServerInfo, 13 ServerInfo,
12 setAccessTokensToServers, updateVideo, 14 setAccessTokensToServers,
15 updateVideo,
13 uploadVideo, 16 uploadVideo,
14 wait 17 wait
15} from '../../utils' 18} from '../../utils'
@@ -69,17 +72,22 @@ describe('Test video update scheduler', function () {
69 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) 72 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
70 expect(res.body.total).to.equal(1) 73 expect(res.body.total).to.equal(1)
71 74
72 const video = res.body.data[0] 75 const videoFromList = res.body.data[0]
73 expect(video.name).to.equal('video 1') 76 const res2 = await getVideoWithToken(servers[0].url, servers[0].accessToken, videoFromList.uuid)
74 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE) 77 const videoFromGet = res2.body
75 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date()) 78
76 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC) 79 for (const video of [ videoFromList, videoFromGet ]) {
80 expect(video.name).to.equal('video 1')
81 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
82 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
83 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
84 }
77 }) 85 })
78 86
79 it('Should wait some seconds and have the video in public privacy', async function () { 87 it('Should wait some seconds and have the video in public privacy', async function () {
80 this.timeout(20000) 88 this.timeout(20000)
81 89
82 await wait(10000) 90 await wait(15000)
83 await waitJobs(servers) 91 await waitJobs(servers)
84 92
85 for (const server of servers) { 93 for (const server of servers) {
@@ -144,7 +152,7 @@ describe('Test video update scheduler', function () {
144 it('Should wait some seconds and have the updated video in public privacy', async function () { 152 it('Should wait some seconds and have the updated video in public privacy', async function () {
145 this.timeout(20000) 153 this.timeout(20000)
146 154
147 await wait(10000) 155 await wait(15000)
148 await waitJobs(servers) 156 await waitJobs(servers)
149 157
150 for (const server of servers) { 158 for (const server of servers) {
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts
index 531eafe54..190d63783 100644
--- a/shared/models/videos/video-create.model.ts
+++ b/shared/models/videos/video-create.model.ts
@@ -1,4 +1,5 @@
1import { VideoPrivacy } from './video-privacy.enum' 1import { VideoPrivacy } from './video-privacy.enum'
2import { VideoScheduleUpdate } from './video-schedule-update.model'
2 3
3export interface VideoCreate { 4export interface VideoCreate {
4 category?: number 5 category?: number
@@ -13,8 +14,5 @@ export interface VideoCreate {
13 tags?: string[] 14 tags?: string[]
14 commentsEnabled?: boolean 15 commentsEnabled?: boolean
15 privacy: VideoPrivacy 16 privacy: VideoPrivacy
16 scheduleUpdate?: { 17 scheduleUpdate?: VideoScheduleUpdate
17 updateAt: Date
18 privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
19 }
20} 18}
diff --git a/shared/models/videos/video-schedule-update.model.ts b/shared/models/videos/video-schedule-update.model.ts
new file mode 100644
index 000000000..b865c1614
--- /dev/null
+++ b/shared/models/videos/video-schedule-update.model.ts
@@ -0,0 +1,6 @@
1import { VideoPrivacy } from './video-privacy.enum'
2
3export interface VideoScheduleUpdate {
4 updateAt: Date | string
5 privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE
6}
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts
index fc0df6810..ed141a824 100644
--- a/shared/models/videos/video-update.model.ts
+++ b/shared/models/videos/video-update.model.ts
@@ -1,4 +1,5 @@
1import { VideoPrivacy } from './video-privacy.enum' 1import { VideoPrivacy } from './video-privacy.enum'
2import { VideoScheduleUpdate } from './video-schedule-update.model'
2 3
3export interface VideoUpdate { 4export interface VideoUpdate {
4 name?: string 5 name?: string
@@ -15,8 +16,5 @@ export interface VideoUpdate {
15 channelId?: number 16 channelId?: number
16 thumbnailfile?: Blob 17 thumbnailfile?: Blob
17 previewfile?: Blob 18 previewfile?: Blob
18 scheduleUpdate?: { 19 scheduleUpdate?: VideoScheduleUpdate
19 updateAt: Date
20 privacy?: VideoPrivacy
21 }
22} 20}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 676354ce3..f88f381cb 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -3,6 +3,7 @@ import { Account } from '../actors'
3import { Avatar } from '../avatars/avatar.model' 3import { Avatar } from '../avatars/avatar.model'
4import { VideoChannel } from './video-channel.model' 4import { VideoChannel } from './video-channel.model'
5import { VideoPrivacy } from './video-privacy.enum' 5import { VideoPrivacy } from './video-privacy.enum'
6import { VideoScheduleUpdate } from './video-schedule-update.model'
6 7
7export interface VideoConstant <T> { 8export interface VideoConstant <T> {
8 id: T 9 id: T
@@ -43,10 +44,7 @@ export interface Video {
43 44
44 waitTranscoding?: boolean 45 waitTranscoding?: boolean
45 state?: VideoConstant<VideoState> 46 state?: VideoConstant<VideoState>
46 scheduledUpdate?: { 47 scheduledUpdate?: VideoScheduleUpdate
47 updateAt: Date | string
48 privacy?: VideoPrivacy
49 }
50 48
51 account: { 49 account: {
52 id: number 50 id: number
diff --git a/support/doc/api/html/index.html b/support/doc/api/html/index.html
index e1bf61b06..24017e674 100644
--- a/support/doc/api/html/index.html
+++ b/support/doc/api/html/index.html
@@ -264,6 +264,7 @@
264 <a href="#definition-GetMeVideoRating"> GetMeVideoRating </a> 264 <a href="#definition-GetMeVideoRating"> GetMeVideoRating </a>
265 <a href="#definition-RegisterUser"> RegisterUser </a> 265 <a href="#definition-RegisterUser"> RegisterUser </a>
266 <a href="#definition-VideoChannelInput"> VideoChannelInput </a> 266 <a href="#definition-VideoChannelInput"> VideoChannelInput </a>
267 <a href="#definition-ScheduleVideoUpdate"> ScheduleVideoUpdate </a>
267 </nav> 268 </nav>
268 </div> 269 </div>
269 <div id="docs" class="row collapse expanded drawer" data-drawer> 270 <div id="docs" class="row collapse expanded drawer" data-drawer>
@@ -3530,6 +3531,19 @@
3530 <p>Video privacy</p> 3531 <p>Video privacy</p>
3531 </div> 3532 </div>
3532 </div> 3533 </div>
3534 <div class="prop-row prop-group">
3535 <div class="prop-name">
3536 <div class="prop-title">scheduleUpdate</div>
3537 <div class="prop-subtitle"> in formData </div>
3538 <div class="prop-subtitle">
3539 <span class="json-property-type">[object Object]</span>
3540 <span class="json-property-range" title="Value limits"></span>
3541 </div>
3542 </div>
3543 <div class="prop-value">
3544 <p class="no-description">(no description)</p>
3545 </div>
3546 </div>
3533 </section> 3547 </section>
3534 </div> 3548 </div>
3535 <div class="doc-examples"></div> 3549 <div class="doc-examples"></div>
@@ -4161,12 +4175,7 @@
4161 <span class="json-property-required"></span> 4175 <span class="json-property-required"></span>
4162 <div class="prop-subtitle"> in formData </div> 4176 <div class="prop-subtitle"> in formData </div>
4163 <div class="prop-subtitle"> 4177 <div class="prop-subtitle">
4164 <span class="json-property-type">string</span> 4178 <span class="json-property-type">[object Object]</span>
4165 <span class="json-property-enum" title="Possible values">
4166 <span class="json-property-enum-item">Public</span>,
4167 <span class="json-property-enum-item">Unlisted</span>,
4168 <span class="json-property-enum-item">Private</span>
4169 </span>
4170 <span class="json-property-range" title="Value limits"></span> 4179 <span class="json-property-range" title="Value limits"></span>
4171 </div> 4180 </div>
4172 </div> 4181 </div>
@@ -4174,6 +4183,19 @@
4174 <p>Video privacy</p> 4183 <p>Video privacy</p>
4175 </div> 4184 </div>
4176 </div> 4185 </div>
4186 <div class="prop-row prop-group">
4187 <div class="prop-name">
4188 <div class="prop-title">scheduleUpdate</div>
4189 <div class="prop-subtitle"> in formData </div>
4190 <div class="prop-subtitle">
4191 <span class="json-property-type">[object Object]</span>
4192 <span class="json-property-range" title="Value limits"></span>
4193 </div>
4194 </div>
4195 <div class="prop-value">
4196 <p class="no-description">(no description)</p>
4197 </div>
4198 </div>
4177 </section> 4199 </section>
4178 </div> 4200 </div>
4179 <div class="doc-examples"></div> 4201 <div class="doc-examples"></div>
@@ -8345,6 +8367,52 @@
8345 </div> 8367 </div>
8346 </div> 8368 </div>
8347 </div> 8369 </div>
8370 <div id="definition-ScheduleVideoUpdate" class="definition panel" data-traverse-target="definition-ScheduleVideoUpdate">
8371 <h2 class="panel-title">
8372 <a name="/definitions/ScheduleVideoUpdate"></a>ScheduleVideoUpdate:
8373 <!-- <span class="json-property-type"><span class="json-property-type">object</span>
8374 <span class="json-property-range" title="Value limits"></span>
8375
8376
8377 </span> -->
8378 </h2>
8379 <div class="doc-row">
8380 <div class="doc-copy">
8381 <section class="json-schema-properties">
8382 <dl>
8383 <dt data-property-name="updateAt" class="has-description">
8384 <span class="json-property-name">updateAt:</span>
8385 <span class="json-property-type">dateTime</span>
8386 <span class="json-property-range" title="Value limits"></span>
8387 </dt>
8388 <dd>
8389 <p>When to update the video</p>
8390 </dd>
8391 <dt data-property-name="privacy">
8392 <span class="json-property-name">privacy:</span>
8393 <span class="json-property-type">
8394 <span class="">
8395 <a class="json-schema-ref" href="#/definitions/VideoPrivacy">VideoPrivacy</a>
8396 </span>
8397 </span>
8398 <span class="json-property-range" title="Value limits"></span>
8399 </dt>
8400 </dl>
8401 </section>
8402 </div>
8403 <div class="doc-examples">
8404 <section>
8405 <h5>Example</h5>
8406 <!-- <div class="hljs"> --><pre><code class="hljs lang-json">{
8407 <span class="hljs-attr">&quot;updateAt&quot;</span>: <span class="hljs-string">&quot;dateTime&quot;</span>,
8408 <span class="hljs-attr">&quot;privacy&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>
8409}
8410</code></pre>
8411 <!-- </div> -->
8412 </section>
8413 </div>
8414 </div>
8415 </div>
8348 <div class="doc-row no-margin"> 8416 <div class="doc-row no-margin">
8349 <div class="doc-copy doc-separator"> 8417 <div class="doc-copy doc-separator">
8350 <a class="powered-by" href="https://sourcey.com/spectacle">Documentation by 8418 <a class="powered-by" href="https://sourcey.com/spectacle">Documentation by
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index be40af570..4b7bc23b4 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -711,6 +711,12 @@ paths:
711 type: string 711 type: string
712 enum: [Public, Unlisted] 712 enum: [Public, Unlisted]
713 description: 'Video privacy' 713 description: 'Video privacy'
714 - name: scheduleUpdate
715 in: formData
716 required: false
717 description: 'Schedule an update at a specific datetime'
718 type:
719 $ref: '#/definitions/ScheduleVideoUpdate'
714 responses: 720 responses:
715 '200': 721 '200':
716 description: successful operation 722 description: successful operation
@@ -864,9 +870,15 @@ paths:
864 - name: privacy 870 - name: privacy
865 in: formData 871 in: formData
866 required: true 872 required: true
867 type: string 873 type:
868 enum: [Public, Unlisted, Private] 874 $ref: '#/definitions/VideoPrivacy'
869 description: 'Video privacy' 875 description: 'Video privacy'
876 - name: scheduleUpdate
877 in: formData
878 required: false
879 description: 'Schedule an update at a specific datetime'
880 type:
881 $ref: '#/definitions/ScheduleVideoUpdate'
870 responses: 882 responses:
871 '200': 883 '200':
872 description: successful operation 884 description: successful operation
@@ -1709,3 +1721,12 @@ definitions:
1709 type: string 1721 type: string
1710 description: 1722 description:
1711 type: string 1723 type: string
1724 ScheduleVideoUpdate:
1725 properties:
1726 updateAt:
1727 type: dateTime
1728 description: 'When to update the video'
1729 required: true
1730 privacy:
1731 $ref: '#/definitions/VideoPrivacy'
1732 required: false \ No newline at end of file