diff options
author | Chocobozzz <me@florianbigard.com> | 2018-06-15 16:52:15 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-06-15 18:20:56 +0200 |
commit | bbe0f0645ca958d33a3f409b15166609733b663f (patch) | |
tree | edcd5d702c73cda74a2177c4bdc08c616334337d | |
parent | 2baea0c77cc765f7cbca9c9a2f4272268892a35c (diff) | |
download | PeerTube-bbe0f0645ca958d33a3f409b15166609733b663f.tar.gz PeerTube-bbe0f0645ca958d33a3f409b15166609733b663f.tar.zst PeerTube-bbe0f0645ca958d33a3f409b15166609733b663f.zip |
Add ability to schedule video publication
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' | |||
12 | import { tap } from 'rxjs/operators' | 12 | import { tap } from 'rxjs/operators' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Subscription } from 'rxjs' | 14 | import { Subscription } from 'rxjs' |
15 | import { 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 @@ | |||
1 | import { from as observableFrom, Observable } from 'rxjs' | 1 | import { from as observableFrom, Observable } from 'rxjs' |
2 | import { concatAll, tap } from 'rxjs/operators' | 2 | import { concatAll, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit, Inject, LOCALE_ID } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { Location } from '@angular/common' | 5 | import { Location } from '@angular/common' |
6 | import { immutableAssign } from '@app/shared/misc/utils' | 6 | import { immutableAssign } from '@app/shared/misc/utils' |
@@ -12,7 +12,8 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list' | |||
12 | import { Video } from '../../shared/video/video.model' | 12 | import { Video } from '../../shared/video/video.model' |
13 | import { VideoService } from '../../shared/video/video.service' | 13 | import { VideoService } from '../../shared/video/video.service' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 14 | import { I18n } from '@ngx-translate/i18n-polyfill' |
15 | import { VideoState } from '../../../../../shared/models/videos' | 15 | import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos' |
16 | import { 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' | |||
12 | import { tap } from 'rxjs/operators' | 12 | import { tap } from 'rxjs/operators' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Subscription } from 'rxjs' | 14 | import { Subscription } from 'rxjs' |
15 | import { 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' | |||
2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
3 | import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' | 3 | import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' |
4 | import { AuthService, RedirectService, ServerService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService } from '@app/core' |
5 | import { isInSmallView } from '@app/shared/misc/utils' | ||
6 | import { is18nPath } from '../../../shared/models/i18n' | 5 | import { is18nPath } from '../../../shared/models/i18n' |
6 | import { 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 @@ | |||
1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
2 | import { Component, forwardRef, Input, OnInit } from '@angular/core' | 2 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
4 | import { isInSmallView } from '@app/shared/misc/utils' | ||
5 | import { MarkdownService } from '@app/videos/shared' | 4 | import { MarkdownService } from '@app/videos/shared' |
6 | import { Subject } from 'rxjs/Subject' | 5 | import { Subject } from 'rxjs/Subject' |
7 | import truncate from 'lodash-es/truncate' | 6 | import truncate from 'lodash-es/truncate' |
7 | import { 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 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | |||
4 | @Injectable() | ||
5 | export 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 @@ | |||
1 | import { Injectable, NgZone } from '@angular/core' | ||
2 | |||
3 | @Injectable() | ||
4 | export 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 | ||
100 | let windowInnerWidth = window.innerWidth | ||
101 | setInterval(() => windowInnerWidth = window.innerWidth, 500) | ||
102 | |||
103 | function isInSmallView () { | ||
104 | return windowInnerWidth < 600 | ||
105 | } | ||
106 | |||
107 | function isInMobileView () { | ||
108 | return windowInnerWidth < 500 | ||
109 | } | ||
110 | |||
111 | export { | 99 | export { |
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' |
44 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' | ||
45 | import { 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' | |||
2 | import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' | 2 | import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { Location } from '@angular/common' | 4 | import { Location } from '@angular/common' |
5 | import { isInMobileView } from '@app/shared/misc/utils' | ||
6 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 5 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
7 | import { NotificationsService } from 'angular2-notifications' | 6 | import { NotificationsService } from 'angular2-notifications' |
8 | import { fromEvent, Observable, Subscription } from 'rxjs' | 7 | import { fromEvent, Observable, Subscription } from 'rxjs' |
@@ -11,6 +10,7 @@ import { ComponentPagination } from '../rest/component-pagination.model' | |||
11 | import { VideoSortField } from './sort-field.type' | 10 | import { VideoSortField } from './sort-field.type' |
12 | import { Video } from './video.model' | 11 | import { Video } from './video.model' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
14 | 14 | ||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | 15 | export 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 @@ | |||
1 | import { VideoDetails } from './video-details.model' | 1 | import { VideoDetails } from './video-details.model' |
2 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | 2 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' |
3 | import { VideoUpdate } from '../../../../../shared/models/videos' | 3 | import { VideoUpdate } from '../../../../../shared/models/videos' |
4 | import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' | ||
4 | 5 | ||
5 | export class VideoEdit implements VideoUpdate { | 6 | export 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { isInMobileView } from '@app/shared/misc/utils' | ||
3 | import { Video } from './video.model' | 2 | import { Video } from './video.model' |
3 | import { 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' | |||
6 | import { ServerConfig } from '../../../../../shared/models' | 6 | import { ServerConfig } from '../../../../../shared/models' |
7 | import { Actor } from '@app/shared/actor/actor.model' | 7 | import { Actor } from '@app/shared/actor/actor.model' |
8 | import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' | 8 | import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' |
9 | import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' | ||
9 | 10 | ||
10 | export class Video implements VideoServerModel { | 11 | export 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 | ||
89 | p-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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { FormGroup, ValidatorFn } from '@angular/forms' | 2 | import { FormGroup, ValidatorFn, Validators } from '@angular/forms' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' | 4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
@@ -7,6 +7,7 @@ import { ServerService } from '../../../core/server' | |||
7 | import { VideoEdit } from '../../../shared/video/video-edit.model' | 7 | import { VideoEdit } from '../../../shared/video/video-edit.model' |
8 | import { map } from 'rxjs/operators' | 8 | import { map } from 'rxjs/operators' |
9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
10 | import { 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' | |||
4 | import { SharedModule } from '../../../shared/' | 4 | import { SharedModule } from '../../../shared/' |
5 | import { VideoEditComponent } from './video-edit.component' | 5 | import { VideoEditComponent } from './video-edit.component' |
6 | import { VideoImageComponent } from './video-image.component' | 6 | import { VideoImageComponent } from './video-image.component' |
7 | import { 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 | |||
27 | export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export 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' | |||
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' | 10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { 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' | |||
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 8 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { 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' | |||
9 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 9 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
10 | import { VideoService } from '../../shared/video/video.service' | 10 | import { VideoService } from '../../shared/video/video.service' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { 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' | |||
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 8 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { 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 | ||
146 | p-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 | ||
354 | tabset.bootstrap { | 234 | tabset.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 | ||
14 | p-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 | ||
140 | p-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 | ||
7659 | primeng@^5.2.6: | 7659 | primeng@^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 | ||
7663 | private@^0.1.6, private@^0.1.8, private@~0.1.5: | 7663 | private@^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' | |||
8 | import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
10 | import { invert } from 'lodash' | 10 | import { invert } from 'lodash' |
11 | import { RemoveOldJobsScheduler } from '../lib/schedulers/remove-old-jobs-scheduler' | ||
12 | import { 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 |
15 | let config: IConfig = require('config') | 13 | let config: IConfig = require('config') |
@@ -98,8 +96,8 @@ const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days | |||
98 | // 1 hour | 96 | // 1 hour |
99 | let SCHEDULER_INTERVALS_MS = { | 97 | let 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' | |||
5 | import { VideoPrivacy } from '../../../../shared/models/videos' | 5 | import { VideoPrivacy } from '../../../../shared/models/videos' |
6 | import { | 6 | import { |
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 @@ | |||
1 | import { VideoPrivacy } from './video-privacy.enum' | 1 | import { VideoPrivacy } from './video-privacy.enum' |
2 | import { VideoScheduleUpdate } from './video-schedule-update.model' | ||
2 | 3 | ||
3 | export interface VideoCreate { | 4 | export 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 @@ | |||
1 | import { VideoPrivacy } from './video-privacy.enum' | ||
2 | |||
3 | export 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 @@ | |||
1 | import { VideoPrivacy } from './video-privacy.enum' | 1 | import { VideoPrivacy } from './video-privacy.enum' |
2 | import { VideoScheduleUpdate } from './video-schedule-update.model' | ||
2 | 3 | ||
3 | export interface VideoUpdate { | 4 | export 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' | |||
3 | import { Avatar } from '../avatars/avatar.model' | 3 | import { Avatar } from '../avatars/avatar.model' |
4 | import { VideoChannel } from './video-channel.model' | 4 | import { VideoChannel } from './video-channel.model' |
5 | import { VideoPrivacy } from './video-privacy.enum' | 5 | import { VideoPrivacy } from './video-privacy.enum' |
6 | import { VideoScheduleUpdate } from './video-schedule-update.model' | ||
6 | 7 | ||
7 | export interface VideoConstant <T> { | 8 | export 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">"updateAt"</span>: <span class="hljs-string">"dateTime"</span>, | ||
8408 | <span class="hljs-attr">"privacy"</span>: <span class="hljs-string">"string"</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 | ||