aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html6
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss1
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts29
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts2
-rw-r--r--client/src/app/app.component.ts9
-rw-r--r--client/src/app/core/server/server.service.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/video-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts9
-rw-r--r--client/src/app/shared/i18n/i18n-primeng-calendar.ts94
-rw-r--r--client/src/app/shared/misc/screen.service.ts23
-rw-r--r--client/src/app/shared/misc/utils.ts14
-rw-r--r--client/src/app/shared/shared.module.ts5
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts5
-rw-r--r--client/src/app/shared/video/video-edit.model.ts32
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts6
-rw-r--r--client/src/app/shared/video/video.model.ts3
-rw-r--r--client/src/app/shared/video/video.service.ts3
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html19
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss40
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts79
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html1
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts14
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html6
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-search.component.ts2
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts2
-rw-r--r--client/src/sass/application.scss126
-rw-r--r--client/src/sass/include/_mixins.scss6
-rw-r--r--client/src/sass/primeng-custom.scss174
36 files changed, 542 insertions, 197 deletions
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 5e3dbb6b3..d4fcd7acf 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -12,6 +12,7 @@ import { AccountService } from '@app/shared/account/account.service'
12import { tap } from 'rxjs/operators' 12import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 14import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service'
15 16
16@Component({ 17@Component({
17 selector: 'my-account-videos', 18 selector: 'my-account-videos',
@@ -37,6 +38,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
37 protected notificationsService: NotificationsService, 38 protected notificationsService: NotificationsService,
38 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
39 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService,
40 protected i18n: I18n, 42 protected i18n: I18n,
41 private accountService: AccountService, 43 private accountService: AccountService,
42 private videoService: VideoService 44 private videoService: VideoService
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index eb24de7a7..7ac6371db 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -18,7 +18,7 @@
18 <div class="video-info"> 18 <div class="video-info">
19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> 19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 20 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
21 <div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div> 21 <div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
22 </div> 22 </div>
23 23
24 <!-- Display only once --> 24 <!-- Display only once -->
@@ -28,9 +28,9 @@
28 Cancel 28 Cancel
29 </span> 29 </span>
30 30
31 <span i18n class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> 31 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
32 <span class="icon icon-delete-white"></span> 32 <span class="icon icon-delete-white"></span>
33 Delete 33 <ng-container i18n>Delete</ng-container>
34 </span> 34 </span>
35 </div> 35 </div>
36 </div> 36 </div>
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index f276ea389..1f22aec71 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -75,6 +75,7 @@
75 75
76 color: #000; 76 color: #000;
77 display: block; 77 display: block;
78 width: fit-content;
78 font-size: 16px; 79 font-size: 16px;
79 font-weight: $font-semibold; 80 font-weight: $font-semibold;
80 } 81 }
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index afc01073c..e698b75ec 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -1,6 +1,6 @@
1import { from as observableFrom, Observable } from 'rxjs' 1import { from as observableFrom, Observable } from 'rxjs'
2import { concatAll, tap } from 'rxjs/operators' 2import { concatAll, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit, Inject, LOCALE_ID } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { Location } from '@angular/common' 5import { Location } from '@angular/common'
6import { immutableAssign } from '@app/shared/misc/utils' 6import { immutableAssign } from '@app/shared/misc/utils'
@@ -12,7 +12,8 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
12import { Video } from '../../shared/video/video.model' 12import { Video } from '../../shared/video/video.model'
13import { VideoService } from '../../shared/video/video.service' 13import { VideoService } from '../../shared/video/video.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { VideoState } from '../../../../../shared/models/videos' 15import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos'
16import { ScreenService } from '@app/shared/misc/screen.service'
16 17
17@Component({ 18@Component({
18 selector: 'my-account-videos', 19 selector: 'my-account-videos',
@@ -39,8 +40,10 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
39 protected notificationsService: NotificationsService, 40 protected notificationsService: NotificationsService,
40 protected confirmService: ConfirmService, 41 protected confirmService: ConfirmService,
41 protected location: Location, 42 protected location: Location,
43 protected screenService: ScreenService,
42 protected i18n: I18n, 44 protected i18n: I18n,
43 private videoService: VideoService 45 private videoService: VideoService,
46 @Inject(LOCALE_ID) private localeId: string
44 ) { 47 ) {
45 super() 48 super()
46 49
@@ -131,12 +134,22 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
131 } 134 }
132 135
133 getStateLabel (video: Video) { 136 getStateLabel (video: Video) {
134 if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published') 137 let suffix: string
135 138
136 if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding') 139 if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) {
137 if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode') 140 suffix = this.i18n('Published')
141 } else if (video.scheduledUpdate) {
142 const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
143 suffix = this.i18n('Publication scheduled on ') + updateAt
144 } else if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) {
145 suffix = this.i18n('Waiting transcoding')
146 } else if (video.state.id === VideoState.TO_TRANSCODE) {
147 suffix = this.i18n('To transcode')
148 } else {
149 return ''
150 }
138 151
139 return this.i18n('Unknown state') 152 return ' - ' + suffix
140 } 153 }
141 154
142 protected buildVideoHeight () { 155 protected buildVideoHeight () {
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 2d3f66994..800d97b7f 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -12,6 +12,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
12import { tap } from 'rxjs/operators' 12import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 14import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service'
15 16
16@Component({ 17@Component({
17 selector: 'my-video-channel-videos', 18 selector: 'my-video-channel-videos',
@@ -37,6 +38,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
37 protected notificationsService: NotificationsService, 38 protected notificationsService: NotificationsService,
38 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
39 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService,
40 protected i18n: I18n, 42 protected i18n: I18n,
41 private videoChannelService: VideoChannelService, 43 private videoChannelService: VideoChannelService,
42 private videoService: VideoService 44 private videoService: VideoService
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 0bfe9f916..494cd9ea6 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' 3import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
4import { AuthService, RedirectService, ServerService } from '@app/core' 4import { AuthService, RedirectService, ServerService } from '@app/core'
5import { isInSmallView } from '@app/shared/misc/utils'
6import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service'
7 7
8@Component({ 8@Component({
9 selector: 'my-app', 9 selector: 'my-app',
@@ -33,7 +33,8 @@ export class AppComponent implements OnInit {
33 private authService: AuthService, 33 private authService: AuthService,
34 private serverService: ServerService, 34 private serverService: ServerService,
35 private domSanitizer: DomSanitizer, 35 private domSanitizer: DomSanitizer,
36 private redirectService: RedirectService 36 private redirectService: RedirectService,
37 private screenService: ScreenService
37 ) { } 38 ) { }
38 39
39 get serverVersion () { 40 get serverVersion () {
@@ -75,14 +76,14 @@ export class AppComponent implements OnInit {
75 this.serverService.loadVideoPrivacies() 76 this.serverService.loadVideoPrivacies()
76 77
77 // Do not display menu on small screens 78 // Do not display menu on small screens
78 if (isInSmallView()) { 79 if (this.screenService.isInSmallView()) {
79 this.isMenuDisplayed = false 80 this.isMenuDisplayed = false
80 } 81 }
81 82
82 this.router.events.subscribe( 83 this.router.events.subscribe(
83 e => { 84 e => {
84 // User clicked on a link in the menu, change the page 85 // User clicked on a link in the menu, change the page
85 if (e instanceof GuardsCheckStart && isInSmallView()) { 86 if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
86 this.isMenuDisplayed = false 87 this.isMenuDisplayed = false
87 } 88 }
88 } 89 }
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 74363e6a1..6323a7edf 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -141,6 +141,7 @@ export class ServerService {
141 ) 141 )
142 .subscribe(({ data, translations }) => { 142 .subscribe(({ data, translations }) => {
143 Object.keys(data) 143 Object.keys(data)
144 .map(dataKey => parseInt(dataKey, 10))
144 .forEach(dataKey => { 145 .forEach(dataKey => {
145 const label = data[ dataKey ] 146 const label = data[ dataKey ]
146 147
diff --git a/client/src/app/shared/forms/form-validators/video-validators.service.ts b/client/src/app/shared/forms/form-validators/video-validators.service.ts
index 76fc5cf04..396be6f3b 100644
--- a/client/src/app/shared/forms/form-validators/video-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-validators.service.ts
@@ -15,6 +15,7 @@ export class VideoValidatorsService {
15 readonly VIDEO_DESCRIPTION: BuildFormValidator 15 readonly VIDEO_DESCRIPTION: BuildFormValidator
16 readonly VIDEO_TAGS: BuildFormValidator 16 readonly VIDEO_TAGS: BuildFormValidator
17 readonly VIDEO_SUPPORT: BuildFormValidator 17 readonly VIDEO_SUPPORT: BuildFormValidator
18 readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
18 19
19 constructor (private i18n: I18n) { 20 constructor (private i18n: I18n) {
20 21
@@ -84,5 +85,12 @@ export class VideoValidatorsService {
84 'maxlength': this.i18n('Video support cannot be more than 500 characters long.') 85 'maxlength': this.i18n('Video support cannot be more than 500 characters long.')
85 } 86 }
86 } 87 }
88
89 this.VIDEO_SCHEDULE_PUBLICATION_AT = {
90 VALIDATORS: [ ],
91 MESSAGES: {
92 'required': this.i18n('A date is required to schedule video update.')
93 }
94 }
87 } 95 }
88} 96}
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts
index 8b932cd15..db6f9e5c8 100644
--- a/client/src/app/shared/forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/forms/markdown-textarea.component.ts
@@ -1,10 +1,10 @@
1import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 1import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
2import { Component, forwardRef, Input, OnInit } from '@angular/core' 2import { Component, forwardRef, Input, OnInit } from '@angular/core'
3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
4import { isInSmallView } from '@app/shared/misc/utils'
5import { MarkdownService } from '@app/videos/shared' 4import { MarkdownService } from '@app/videos/shared'
6import { Subject } from 'rxjs/Subject' 5import { Subject } from 'rxjs/Subject'
7import truncate from 'lodash-es/truncate' 6import truncate from 'lodash-es/truncate'
7import { ScreenService } from '@app/shared/misc/screen.service'
8 8
9@Component({ 9@Component({
10 selector: 'my-markdown-textarea', 10 selector: 'my-markdown-textarea',
@@ -35,7 +35,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
35 35
36 private contentChanged = new Subject<string>() 36 private contentChanged = new Subject<string>()
37 37
38 constructor (private markdownService: MarkdownService) {} 38 constructor (
39 private screenService: ScreenService,
40 private markdownService: MarkdownService
41) {}
39 42
40 ngOnInit () { 43 ngOnInit () {
41 this.contentChanged 44 this.contentChanged
@@ -76,7 +79,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
76 } 79 }
77 80
78 arePreviewsDisplayed () { 81 arePreviewsDisplayed () {
79 return isInSmallView() === false 82 return this.screenService.isInSmallView() === false
80 } 83 }
81 84
82 private updatePreviews () { 85 private updatePreviews () {
diff --git a/client/src/app/shared/i18n/i18n-primeng-calendar.ts b/client/src/app/shared/i18n/i18n-primeng-calendar.ts
new file mode 100644
index 000000000..b05852ff8
--- /dev/null
+++ b/client/src/app/shared/i18n/i18n-primeng-calendar.ts
@@ -0,0 +1,94 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Injectable } from '@angular/core'
3
4@Injectable()
5export class I18nPrimengCalendarService {
6 private readonly calendarLocale: any = {}
7
8 constructor (private i18n: I18n) {
9 this.calendarLocale = {
10 firstDayOfWeek: 0,
11 dayNames: [
12 this.i18n('Sunday'),
13 this.i18n('Monday'),
14 this.i18n('Tuesday'),
15 this.i18n('Wednesday'),
16 this.i18n('Thursday'),
17 this.i18n('Friday'),
18 this.i18n('Saturday')
19 ],
20
21 dayNamesShort: [
22 this.i18n({ value: 'Sun', description: 'Day name short' }),
23 this.i18n({ value: 'Mon', description: 'Day name short' }),
24 this.i18n({ value: 'Tue', description: 'Day name short' }),
25 this.i18n({ value: 'Wed', description: 'Day name short' }),
26 this.i18n({ value: 'Thu', description: 'Day name short' }),
27 this.i18n({ value: 'Fri', description: 'Day name short' }),
28 this.i18n({ value: 'Sat', description: 'Day name short' })
29 ],
30
31 dayNamesMin: [
32 this.i18n({ value: 'Su', description: 'Day name min' }),
33 this.i18n({ value: 'Mo', description: 'Day name min' }),
34 this.i18n({ value: 'Tu', description: 'Day name min' }),
35 this.i18n({ value: 'We', description: 'Day name min' }),
36 this.i18n({ value: 'Th', description: 'Day name min' }),
37 this.i18n({ value: 'Fr', description: 'Day name min' }),
38 this.i18n({ value: 'Sa', description: 'Day name min' })
39 ],
40
41 monthNames: [
42 this.i18n('January'),
43 this.i18n('February'),
44 this.i18n('March'),
45 this.i18n('April'),
46 this.i18n('May'),
47 this.i18n('June'),
48 this.i18n('July'),
49 this.i18n('August'),
50 this.i18n('September'),
51 this.i18n('October'),
52 this.i18n('November'),
53 this.i18n('December')
54 ],
55
56 monthNamesShort: [
57 this.i18n({ value: 'Jan', description: 'Month name short' }),
58 this.i18n({ value: 'Feb', description: 'Month name short' }),
59 this.i18n({ value: 'Mar', description: 'Month name short' }),
60 this.i18n({ value: 'Apr', description: 'Month name short' }),
61 this.i18n({ value: 'May', description: 'Month name short' }),
62 this.i18n({ value: 'Jun', description: 'Month name short' }),
63 this.i18n({ value: 'Jul', description: 'Month name short' }),
64 this.i18n({ value: 'Aug', description: 'Month name short' }),
65 this.i18n({ value: 'Sep', description: 'Month name short' }),
66 this.i18n({ value: 'Oct', description: 'Month name short' }),
67 this.i18n({ value: 'Nov', description: 'Month name short' }),
68 this.i18n({ value: 'Dec', description: 'Month name short' })
69 ],
70
71 today: this.i18n('Today'),
72
73 clear: this.i18n('Clear')
74 }
75 }
76
77 getCalendarLocale () {
78 return this.calendarLocale
79 }
80
81 getTimezone () {
82 const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
83 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
84
85 return `${timezone} - ${gmt}`
86 }
87
88 getDateFormat () {
89 return this.i18n({
90 value: 'yy-mm-dd ',
91 description: 'Date format in this locale.'
92 })
93 }
94}
diff --git a/client/src/app/shared/misc/screen.service.ts b/client/src/app/shared/misc/screen.service.ts
new file mode 100644
index 000000000..5b17a914a
--- /dev/null
+++ b/client/src/app/shared/misc/screen.service.ts
@@ -0,0 +1,23 @@
1import { Injectable, NgZone } from '@angular/core'
2
3@Injectable()
4export class ScreenService {
5 private windowInnerWidth: number
6
7 constructor (private zone: NgZone) {
8 this.windowInnerWidth = window.innerWidth
9
10 // Try to cache a little bit window.innerWidth
11 this.zone.runOutsideAngular(() => {
12 setInterval(() => this.windowInnerWidth = window.innerWidth, 500)
13 })
14 }
15
16 isInSmallView () {
17 return this.windowInnerWidth < 600
18 }
19
20 isInMobileView () {
21 return this.windowInnerWidth < 500
22 }
23}
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 2219ac802..53aff1b24 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -96,26 +96,12 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
96 }) 96 })
97} 97}
98 98
99// Try to cache a little bit window.innerWidth
100let windowInnerWidth = window.innerWidth
101setInterval(() => windowInnerWidth = window.innerWidth, 500)
102
103function isInSmallView () {
104 return windowInnerWidth < 600
105}
106
107function isInMobileView () {
108 return windowInnerWidth < 500
109}
110
111export { 99export {
112 objectToUrlEncoded, 100 objectToUrlEncoded,
113 getParameterByName, 101 getParameterByName,
114 populateAsyncUserVideoChannels, 102 populateAsyncUserVideoChannels,
115 getAbsoluteAPIUrl, 103 getAbsoluteAPIUrl,
116 dateToHuman, 104 dateToHuman,
117 isInSmallView,
118 isInMobileView,
119 immutableAssign, 105 immutableAssign,
120 objectToFormData, 106 objectToFormData,
121 lineFeedToHtml 107 lineFeedToHtml
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b85445ef5..97e49e7ab 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -41,6 +41,8 @@ import {
41 ResetPasswordValidatorsService, 41 ResetPasswordValidatorsService,
42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService 42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService
43} from '@app/shared/forms' 43} from '@app/shared/forms'
44import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
45import { ScreenService } from '@app/shared/misc/screen.service'
44 46
45@NgModule({ 47@NgModule({
46 imports: [ 48 imports: [
@@ -128,6 +130,9 @@ import {
128 VideoCommentValidatorsService, 130 VideoCommentValidatorsService,
129 VideoValidatorsService, 131 VideoValidatorsService,
130 132
133 I18nPrimengCalendarService,
134 ScreenService,
135
131 I18n 136 I18n
132 ] 137 ]
133}) 138})
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 1c84573da..a468d3231 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -2,7 +2,6 @@ import { debounceTime } from 'rxjs/operators'
2import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' 2import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { isInMobileView } from '@app/shared/misc/utils'
6import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 5import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
7import { NotificationsService } from 'angular2-notifications' 6import { NotificationsService } from 'angular2-notifications'
8import { fromEvent, Observable, Subscription } from 'rxjs' 7import { fromEvent, Observable, Subscription } from 'rxjs'
@@ -11,6 +10,7 @@ import { ComponentPagination } from '../rest/component-pagination.model'
11import { VideoSortField } from './sort-field.type' 10import { VideoSortField } from './sort-field.type'
12import { Video } from './video.model' 11import { Video } from './video.model'
13import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
13import { ScreenService } from '@app/shared/misc/screen.service'
14 14
15export abstract class AbstractVideoList implements OnInit, OnDestroy { 15export abstract class AbstractVideoList implements OnInit, OnDestroy {
16 private static LINES_PER_PAGE = 4 16 private static LINES_PER_PAGE = 4
@@ -41,6 +41,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
41 protected abstract authService: AuthService 41 protected abstract authService: AuthService
42 protected abstract router: Router 42 protected abstract router: Router
43 protected abstract route: ActivatedRoute 43 protected abstract route: ActivatedRoute
44 protected abstract screenService: ScreenService
44 protected abstract i18n: I18n 45 protected abstract i18n: I18n
45 protected abstract location: Location 46 protected abstract location: Location
46 protected abstract currentRoute: string 47 protected abstract currentRoute: string
@@ -199,7 +200,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
199 } 200 }
200 201
201 private calcPageSizes () { 202 private calcPageSizes () {
202 if (isInMobileView() || this.baseVideoWidth === -1) { 203 if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
203 this.pagination.itemsPerPage = 5 204 this.pagination.itemsPerPage = 5
204 205
205 // Video takes all the width 206 // Video takes all the width
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index f045a3acd..78aed4f9f 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -1,8 +1,11 @@
1import { VideoDetails } from './video-details.model' 1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
3import { VideoUpdate } from '../../../../../shared/models/videos' 3import { VideoUpdate } from '../../../../../shared/models/videos'
4import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
4 5
5export class VideoEdit implements VideoUpdate { 6export class VideoEdit implements VideoUpdate {
7 static readonly SPECIAL_SCHEDULED_PRIVACY = -1
8
6 category: number 9 category: number
7 licence: number 10 licence: number
8 language: string 11 language: string
@@ -21,6 +24,7 @@ export class VideoEdit implements VideoUpdate {
21 previewUrl: string 24 previewUrl: string
22 uuid?: string 25 uuid?: string
23 id?: number 26 id?: number
27 scheduleUpdate?: VideoScheduleUpdate
24 28
25 constructor (videoDetails?: VideoDetails) { 29 constructor (videoDetails?: VideoDetails) {
26 if (videoDetails) { 30 if (videoDetails) {
@@ -40,6 +44,8 @@ export class VideoEdit implements VideoUpdate {
40 this.support = videoDetails.support 44 this.support = videoDetails.support
41 this.thumbnailUrl = videoDetails.thumbnailUrl 45 this.thumbnailUrl = videoDetails.thumbnailUrl
42 this.previewUrl = videoDetails.previewUrl 46 this.previewUrl = videoDetails.previewUrl
47
48 this.scheduleUpdate = videoDetails.scheduledUpdate
43 } 49 }
44 } 50 }
45 51
@@ -47,10 +53,22 @@ export class VideoEdit implements VideoUpdate {
47 Object.keys(values).forEach((key) => { 53 Object.keys(values).forEach((key) => {
48 this[ key ] = values[ key ] 54 this[ key ] = values[ key ]
49 }) 55 })
56
57 // If schedule publication, the video is private and will be changed to public privacy
58 if (values['schedulePublicationAt']) {
59 const updateAt = (values['schedulePublicationAt'] as Date)
60 updateAt.setSeconds(0)
61
62 this.privacy = VideoPrivacy.PRIVATE
63 this.scheduleUpdate = {
64 updateAt: updateAt.toISOString(),
65 privacy: VideoPrivacy.PUBLIC
66 }
67 }
50 } 68 }
51 69
52 toJSON () { 70 toFormPatch () {
53 return { 71 const json = {
54 category: this.category, 72 category: this.category,
55 licence: this.licence, 73 licence: this.licence,
56 language: this.language, 74 language: this.language,
@@ -64,5 +82,15 @@ export class VideoEdit implements VideoUpdate {
64 channelId: this.channelId, 82 channelId: this.channelId,
65 privacy: this.privacy 83 privacy: this.privacy
66 } 84 }
85
86 // Special case if we scheduled an update
87 if (this.scheduleUpdate) {
88 Object.assign(json, {
89 privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
90 schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString())
91 })
92 }
93
94 return json
67 } 95 }
68} 96}
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts
index e52f7dfb0..86d8f6f74 100644
--- a/client/src/app/shared/video/video-thumbnail.component.ts
+++ b/client/src/app/shared/video/video-thumbnail.component.ts
@@ -1,6 +1,6 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { isInMobileView } from '@app/shared/misc/utils'
3import { Video } from './video.model' 2import { Video } from './video.model'
3import { ScreenService } from '@app/shared/misc/screen.service'
4 4
5@Component({ 5@Component({
6 selector: 'my-video-thumbnail', 6 selector: 'my-video-thumbnail',
@@ -11,10 +11,12 @@ export class VideoThumbnailComponent {
11 @Input() video: Video 11 @Input() video: Video
12 @Input() nsfw = false 12 @Input() nsfw = false
13 13
14 constructor (private screenService: ScreenService) {}
15
14 getImageUrl () { 16 getImageUrl () {
15 if (!this.video) return '' 17 if (!this.video) return ''
16 18
17 if (isInMobileView()) { 19 if (this.screenService.isInMobileView()) {
18 return this.video.previewUrl 20 return this.video.previewUrl
19 } 21 }
20 22
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 48a4b4260..7f421dbbb 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -6,6 +6,7 @@ import { getAbsoluteAPIUrl } from '../misc/utils'
6import { ServerConfig } from '../../../../../shared/models' 6import { ServerConfig } from '../../../../../shared/models'
7import { Actor } from '@app/shared/actor/actor.model' 7import { Actor } from '@app/shared/actor/actor.model'
8import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' 8import { peertubeTranslate } from '@app/shared/i18n/i18n-utils'
9import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
9 10
10export class Video implements VideoServerModel { 11export class Video implements VideoServerModel {
11 by: string 12 by: string
@@ -38,6 +39,7 @@ export class Video implements VideoServerModel {
38 39
39 waitTranscoding?: boolean 40 waitTranscoding?: boolean
40 state?: VideoConstant<VideoState> 41 state?: VideoConstant<VideoState>
42 scheduledUpdate?: VideoScheduleUpdate
41 43
42 account: { 44 account: {
43 id: number 45 id: number
@@ -109,6 +111,7 @@ export class Video implements VideoServerModel {
109 this.language.label = peertubeTranslate(this.language.label, translations) 111 this.language.label = peertubeTranslate(this.language.label, translations)
110 this.privacy.label = peertubeTranslate(this.privacy.label, translations) 112 this.privacy.label = peertubeTranslate(this.privacy.label, translations)
111 113
114 this.scheduledUpdate = hash.scheduledUpdate
112 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) 115 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
113 } 116 }
114 117
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index d63915ad2..3af90e7ad 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -83,7 +83,8 @@ export class VideoService {
83 waitTranscoding: video.waitTranscoding, 83 waitTranscoding: video.waitTranscoding,
84 commentsEnabled: video.commentsEnabled, 84 commentsEnabled: video.commentsEnabled,
85 thumbnailfile: video.thumbnailfile, 85 thumbnailfile: video.thumbnailfile,
86 previewfile: video.previewfile 86 previewfile: video.previewfile,
87 scheduleUpdate: video.scheduleUpdate || undefined
87 } 88 }
88 89
89 const data = objectToFormData(body) 90 const data = objectToFormData(body)
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 379cf7948..447c5ab9b 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -88,6 +88,7 @@
88 <select id="privacy" formControlName="privacy"> 88 <select id="privacy" formControlName="privacy">
89 <option></option> 89 <option></option>
90 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 90 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
91 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
91 </select> 92 </select>
92 </div> 93 </div>
93 94
@@ -96,11 +97,27 @@
96 </div> 97 </div>
97 </div> 98 </div>
98 99
100 <div *ngIf="schedulePublicationEnabled" class="form-group">
101 <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
102 <p-calendar
103 id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
104 [locale]="calendarLocale" [minDate]="minScheduledDate" [showTime]="true" [hideOnDateTimeSelect]="true"
105 >
106 </p-calendar>
107
108 <div *ngIf="formErrors.schedulePublicationAt" class="form-error">
109 {{ formErrors.schedulePublicationAt }}
110 </div>
111 </div>
112
99 <div class="form-group form-group-checkbox"> 113 <div class="form-group form-group-checkbox">
100 <input type="checkbox" id="nsfw" formControlName="nsfw" /> 114 <input type="checkbox" id="nsfw" formControlName="nsfw" />
101 <label for="nsfw"></label> 115 <label for="nsfw"></label>
102 <label i18n for="nsfw">This video contains mature or explicit content</label> 116 <label i18n for="nsfw">This video contains mature or explicit content</label>
103 <my-help tooltipPlacement="top" helpType="custom" i18n-customHtml customHtml="Some instances do not list NSFW videos by default."></my-help> 117 <my-help
118 tooltipPlacement="top" helpType="custom" i18n-customHtml
119 customHtml="Some instances do not list videos containing mature or explicit content by default."
120 ></my-help>
104 </div> 121 </div>
105 122
106 <div class="form-group form-group-checkbox"> 123 <div class="form-group form-group-checkbox">
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index 1295cf098..061eca4a7 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
@@ -44,18 +44,6 @@
44 font-size: 15px; 44 font-size: 15px;
45 } 45 }
46 46
47 .root-tabset /deep/ > .nav {
48 margin-left: 15px;
49 margin-bottom: 15px;
50
51 .nav-link {
52 display: flex !important;
53 align-items: center;
54 height: 30px !important;
55 padding: 0 15px !important;
56 }
57 }
58
59 .advanced-settings .form-group { 47 .advanced-settings .form-group {
60 margin-bottom: 20px; 48 margin-bottom: 20px;
61 } 49 }
@@ -98,7 +86,35 @@
98 } 86 }
99} 87}
100 88
89p-calendar {
90 display: block;
91
92 /deep/ {
93 input,
94 .ui-calendar {
95 width: 100%;
96 }
97
98 input {
99 @include peertube-input-text(100%);
100 color: #000;
101 }
102 }
103}
104
101/deep/ { 105/deep/ {
106 .root-tabset > .nav {
107 margin-left: 15px;
108 margin-bottom: 15px;
109
110 .nav-link {
111 display: flex !important;
112 align-items: center;
113 height: 30px !important;
114 padding: 0 15px !important;
115 }
116 }
117
102 .ng2-tag-input { 118 .ng2-tag-input {
103 border: none !important; 119 border: none !important;
104 } 120 }
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index ee4fd5dc1..24418fc4f 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { FormGroup, ValidatorFn } from '@angular/forms' 2import { FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
@@ -7,6 +7,7 @@ import { ServerService } from '../../../core/server'
7import { VideoEdit } from '../../../shared/video/video-edit.model' 7import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-edit', 13 selector: 'my-video-edit',
@@ -20,16 +21,26 @@ export class VideoEditComponent implements OnInit {
20 @Input() validationMessages: FormReactiveValidationMessages = {} 21 @Input() validationMessages: FormReactiveValidationMessages = {}
21 @Input() videoPrivacies = [] 22 @Input() videoPrivacies = []
22 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 23 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
24 @Input() schedulePublicationPossible = true
25
26 // So that it can be accessed in the template
27 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
23 28
24 videoCategories = [] 29 videoCategories = []
25 videoLicences = [] 30 videoLicences = []
26 videoLanguages = [] 31 videoLanguages = []
27 video: VideoEdit
28 32
29 tagValidators: ValidatorFn[] 33 tagValidators: ValidatorFn[]
30 tagValidatorsMessages: { [ name: string ]: string } 34 tagValidatorsMessages: { [ name: string ]: string }
31 35
36 schedulePublicationEnabled = false
37
32 error: string = null 38 error: string = null
39 calendarLocale: any = {}
40 minScheduledDate = new Date()
41
42 calendarTimezone: string
43 calendarDateFormat: string
33 44
34 constructor ( 45 constructor (
35 private formValidatorService: FormValidatorService, 46 private formValidatorService: FormValidatorService,
@@ -37,10 +48,15 @@ export class VideoEditComponent implements OnInit {
37 private route: ActivatedRoute, 48 private route: ActivatedRoute,
38 private router: Router, 49 private router: Router,
39 private notificationsService: NotificationsService, 50 private notificationsService: NotificationsService,
40 private serverService: ServerService 51 private serverService: ServerService,
52 private i18nPrimengCalendarService: I18nPrimengCalendarService
41 ) { 53 ) {
42 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS 54 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
43 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES 55 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
56
57 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
58 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
59 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
44 } 60 }
45 61
46 updateForm () { 62 updateForm () {
@@ -64,7 +80,8 @@ export class VideoEditComponent implements OnInit {
64 tags: null, 80 tags: null,
65 thumbnailfile: null, 81 thumbnailfile: null,
66 previewfile: null, 82 previewfile: null,
67 support: this.videoValidatorsService.VIDEO_SUPPORT 83 support: this.videoValidatorsService.VIDEO_SUPPORT,
84 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
68 } 85 }
69 86
70 this.formValidatorService.updateForm( 87 this.formValidatorService.updateForm(
@@ -75,6 +92,52 @@ export class VideoEditComponent implements OnInit {
75 defaultValues 92 defaultValues
76 ) 93 )
77 94
95 this.trackChannelChange()
96 this.trackPrivacyChange()
97 }
98
99 ngOnInit () {
100 this.updateForm()
101
102 this.videoCategories = this.serverService.getVideoCategories()
103 this.videoLicences = this.serverService.getVideoLicences()
104 this.videoLanguages = this.serverService.getVideoLanguages()
105
106 setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
107 }
108
109 private trackPrivacyChange () {
110 // We will update the "support" field depending on the channel
111 this.form.controls[ 'privacy' ]
112 .valueChanges
113 .pipe(map(res => parseInt(res.toString(), 10)))
114 .subscribe(
115 newPrivacyId => {
116 this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
117
118 // Value changed
119 const scheduleControl = this.form.get('schedulePublicationAt')
120 const waitTranscodingControl = this.form.get('waitTranscoding')
121
122 if (this.schedulePublicationEnabled) {
123 scheduleControl.setValidators([ Validators.required ])
124
125 waitTranscodingControl.disable()
126 waitTranscodingControl.setValue(false)
127 } else {
128 scheduleControl.clearValidators()
129
130 waitTranscodingControl.enable()
131 waitTranscodingControl.setValue(true)
132 }
133
134 scheduleControl.updateValueAndValidity()
135 waitTranscodingControl.updateValueAndValidity()
136 }
137 )
138 }
139
140 private trackChannelChange () {
78 // We will update the "support" field depending on the channel 141 // We will update the "support" field depending on the channel
79 this.form.controls[ 'channelId' ] 142 this.form.controls[ 'channelId' ]
80 .valueChanges 143 .valueChanges
@@ -108,14 +171,6 @@ export class VideoEditComponent implements OnInit {
108 ) 171 )
109 } 172 }
110 173
111 ngOnInit () {
112 this.updateForm()
113
114 this.videoCategories = this.serverService.getVideoCategories()
115 this.videoLicences = this.serverService.getVideoLicences()
116 this.videoLanguages = this.serverService.getVideoLanguages()
117 }
118
119 private updateSupportField (support: string) { 174 private updateSupportField (support: string) {
120 return this.form.patchValue({ support: support || '' }) 175 return this.form.patchValue({ support: support || '' })
121 } 176 }
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
index 76eba9c19..6bf3e34b1 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
@@ -4,10 +4,12 @@ import { TagInputModule } from 'ngx-chips'
4import { SharedModule } from '../../../shared/' 4import { SharedModule } from '../../../shared/'
5import { VideoEditComponent } from './video-edit.component' 5import { VideoEditComponent } from './video-edit.component'
6import { VideoImageComponent } from './video-image.component' 6import { VideoImageComponent } from './video-image.component'
7import { CalendarModule } from 'primeng/components/calendar/calendar'
7 8
8@NgModule({ 9@NgModule({
9 imports: [ 10 imports: [
10 TagInputModule, 11 TagInputModule,
12 CalendarModule,
11 13
12 SharedModule 14 SharedModule
13 ], 15 ],
@@ -20,6 +22,7 @@ import { VideoImageComponent } from './video-image.component'
20 exports: [ 22 exports: [
21 TagInputModule, 23 TagInputModule,
22 TabsModule, 24 TabsModule,
25 CalendarModule,
23 26
24 VideoEditComponent 27 VideoEditComponent
25 ], 28 ],
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index f00cfe016..07034e4e1 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -27,6 +27,7 @@
27 <div class="peertube-select-container"> 27 <div class="peertube-select-container">
28 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId"> 28 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
29 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> 29 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
30 <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
30 </select> 31 </select>
31 </div> 32 </div>
32 </div> 33 </div>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 85afd0caa..3ddeda109 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -27,6 +27,9 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
27export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
28 @ViewChild('videofileInput') videofileInput 28 @ViewChild('videofileInput') videofileInput
29 29
30 // So that it can be accessed in the template
31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
32
30 isUploadingVideo = false 33 isUploadingVideo = false
31 isUpdatingVideo = false 34 isUpdatingVideo = false
32 videoUploaded = false 35 videoUploaded = false
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index 73b2bc08f..5cb16c8ab 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -6,7 +6,7 @@
6 <form novalidate [formGroup]="form"> 6 <form novalidate [formGroup]="form">
7 7
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 ></my-video-edit> 11 ></my-video-edit>
12 12
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 0266164af..c4e6f44de 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -24,6 +24,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
24 isUpdatingVideo = false 24 isUpdatingVideo = false
25 videoPrivacies = [] 25 videoPrivacies = []
26 userVideoChannels = [] 26 userVideoChannels = []
27 schedulePublicationPossible = false
27 28
28 constructor ( 29 constructor (
29 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
@@ -70,13 +71,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
70 this.userVideoChannels = videoChannels 71 this.userVideoChannels = videoChannels
71 72
72 // We cannot set private a video that was not private 73 // We cannot set private a video that was not private
73 if (video.privacy.id !== VideoPrivacy.PRIVATE) { 74 if (this.video.privacy !== VideoPrivacy.PRIVATE) {
74 const newVideoPrivacies = [] 75 this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE)
75 for (const p of this.videoPrivacies) { 76 } else { // We can schedule video publication only if it it is private
76 if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p) 77 this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
77 }
78
79 this.videoPrivacies = newVideoPrivacies
80 } 78 }
81 79
82 this.hydrateFormFromVideo() 80 this.hydrateFormFromVideo()
@@ -123,7 +121,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
123 } 121 }
124 122
125 private hydrateFormFromVideo () { 123 private hydrateFormFromVideo () {
126 this.form.patchValue(this.video.toJSON()) 124 this.form.patchValue(this.video.toFormPatch())
127 125
128 const objects = [ 126 const objects = [
129 { 127 {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 8bd5c00ff..208375e33 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -3,10 +3,14 @@
3 <div id="video-element-wrapper"> 3 <div id="video-element-wrapper">
4 </div> 4 </div>
5 5
6 <div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()"> 6 <div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()">
7 The video is being transcoded, it may not work properly. 7 The video is being transcoded, it may not work properly.
8 </div> 8 </div>
9 9
10 <div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
11 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
12 </div>
13
10 <!-- Video information --> 14 <!-- Video information -->
11 <div *ngIf="video" class="margin-content video-bottom"> 15 <div *ngIf="video" class="margin-content video-bottom">
12 <div class="video-info"> 16 <div class="video-info">
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index ae8bdccaf..71770c93b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -28,7 +28,7 @@
28 } 28 }
29} 29}
30 30
31#warning-transcoding { 31.alert {
32 text-align: center; 32 text-align: center;
33} 33}
34 34
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index a760c03e8..72e96ca93 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -280,6 +280,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
280 return this.video && this.video.state.id === VideoState.TO_TRANSCODE 280 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
281 } 281 }
282 282
283 hasVideoScheduledPublication () {
284 return this.video && this.video.scheduledUpdate !== undefined
285 }
286
283 private updateVideoDescription (description: string) { 287 private updateVideoDescription (description: string) {
284 this.video.description = description 288 this.video.description = description
285 this.setVideoDescriptionHTML() 289 this.setVideoDescriptionHTML()
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 2fd82a940..dbe1d937d 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -9,6 +9,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-videos-local', 15 selector: 'my-videos-local',
@@ -28,6 +29,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
28 protected authService: AuthService, 29 protected authService: AuthService,
29 protected location: Location, 30 protected location: Location,
30 protected i18n: I18n, 31 protected i18n: I18n,
32 protected screenService: ScreenService,
31 private videoService: VideoService 33 private videoService: VideoService
32 ) { 34 ) {
33 super() 35 super()
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index 8183357f8..004a49168 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -26,6 +27,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
26 protected notificationsService: NotificationsService, 27 protected notificationsService: NotificationsService,
27 protected authService: AuthService, 28 protected authService: AuthService,
28 protected i18n: I18n, 29 protected i18n: I18n,
30 protected screenService: ScreenService,
29 private videoService: VideoService 31 private videoService: VideoService
30 ) { 32 ) {
31 super() 33 super()
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
index b6434f347..33ed3f00e 100644
--- a/client/src/app/videos/video-list/video-search.component.ts
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -9,6 +9,7 @@ import { AuthService } from '../../core/auth'
9import { AbstractVideoList } from '../../shared/video/abstract-video-list' 9import { AbstractVideoList } from '../../shared/video/abstract-video-list'
10import { VideoService } from '../../shared/video/video.service' 10import { VideoService } from '../../shared/video/video.service'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service'
12 13
13@Component({ 14@Component({
14 selector: 'my-videos-search', 15 selector: 'my-videos-search',
@@ -32,6 +33,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
32 protected authService: AuthService, 33 protected authService: AuthService,
33 protected location: Location, 34 protected location: Location,
34 protected i18n: I18n, 35 protected i18n: I18n,
36 protected screenService: ScreenService,
35 private videoService: VideoService, 37 private videoService: VideoService,
36 private redirectService: RedirectService 38 private redirectService: RedirectService
37 ) { 39 ) {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index e56b749d1..f2174aa14 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -25,6 +26,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
25 protected notificationsService: NotificationsService, 26 protected notificationsService: NotificationsService,
26 protected authService: AuthService, 27 protected authService: AuthService,
27 protected location: Location, 28 protected location: Location,
29 protected screenService: ScreenService,
28 protected i18n: I18n, 30 protected i18n: I18n,
29 private videoService: VideoService 31 private videoService: VideoService
30 ) { 32 ) {
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 4006c9128..dae0c52c2 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -6,14 +6,14 @@ $icon-font-path: '../../node_modules/bootstrap-sass/assets/fonts/bootstrap/';
6 6
7@import '_fonts'; 7@import '_fonts';
8 8
9@import '~primeng/resources/themes/bootstrap/theme.css';
10@import '~primeng/resources/primeng.css';
11@import '~video.js/dist/video-js.css'; 9@import '~video.js/dist/video-js.css';
12 10
13$assets-path: '../assets/'; 11$assets-path: '../assets/';
14@import './player/player'; 12@import './player/player';
15@import './loading-bar'; 13@import './loading-bar';
16 14
15@import './primeng-custom';
16
17[hidden] { 17[hidden] {
18 display: none !important; 18 display: none !important;
19} 19}
@@ -142,126 +142,6 @@ label {
142 to { transform: scale(1) rotate(360deg);} 142 to { transform: scale(1) rotate(360deg);}
143} 143}
144 144
145// ngprime data table customizations
146p-table {
147 font-size: 15px !important;
148
149 td {
150 border: 1px solid #E5E5E5 !important;
151 padding-left: 15px !important;
152 overflow: hidden !important;
153 text-overflow: ellipsis !important;
154 white-space: nowrap !important;
155 }
156
157 tr {
158 background-color: #fff !important;
159 height: 46px;
160 }
161
162 .ui-table-tbody {
163 tr {
164 &:hover {
165 background-color: #f0f0f0 !important;
166 }
167
168 &:not(:hover) {
169 .action-cell * {
170 display: none !important;
171 }
172 }
173
174 &:first-child td {
175 border-top: none !important;
176 }
177
178 &:last-child td {
179 border-bottom: none !important;
180 }
181 }
182
183 .expander {
184 cursor: pointer;
185 position: relative;
186 top: 1px;
187 }
188 }
189
190 th {
191 border: none !important;
192 border-bottom: 1px solid #f0f0f0 !important;
193 text-align: left !important;
194 padding: 5px 0 5px 15px !important;
195 font-weight: $font-semibold !important;
196 color: #000 !important;
197
198 &.ui-sortable-column:hover {
199 background-color: #f0f0f0 !important;
200 border: 1px solid #f0f0f0 !important;
201 border-width: 0 1px !important;
202
203 &:first-child {
204 border-width: 0 1px 0 0 !important;
205 }
206 }
207
208 &.ui-state-highlight {
209 background-color: #fff !important;
210
211 .fa {
212 @extend .glyphicon;
213 font-size: 11px;
214
215 &.fa-sort-asc {
216 @extend .glyphicon-triangle-top;
217 }
218
219 &.fa-sort-desc {
220 @extend .glyphicon-triangle-bottom;
221 }
222 }
223 }
224 }
225
226 .action-cell {
227 width: 250px !important;
228 padding: 0 !important;
229 text-align: center;
230
231 my-edit-button + my-delete-button {
232 margin-left: 5px;
233 }
234 }
235
236 p-paginator {
237 .ui-paginator-bottom {
238 position: relative;
239 border: none !important;
240 border: 1px solid #f0f0f0 !important;
241 height: 40px;
242 display: flex;
243 justify-content: center;
244 align-items: center;
245
246 a {
247 color: #000 !important;
248 font-weight: $font-semibold !important;
249 margin-right: 20px !important;
250 outline: 0 !important;
251 border-radius: 3px !important;
252 padding: 5px 2px !important;
253
254 &.ui-state-active {
255 &, &:hover, &:active, &:focus {
256 color: #fff !important;
257 background-color: $orange-color !important;
258 }
259 }
260 }
261 }
262 }
263}
264
265// Bootstrap customizations 145// Bootstrap customizations
266.dropdown-menu { 146.dropdown-menu {
267 border-radius: 3px; 147 border-radius: 3px;
@@ -352,6 +232,8 @@ tabset:not(.bootstrap) {
352} 232}
353 233
354tabset.bootstrap { 234tabset.bootstrap {
235 margin-left: 0;
236
355 .nav-item .nav-link { 237 .nav-item .nav-link {
356 &, & a { 238 &, & a {
357 color: #000; 239 color: #000;
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 748c98afa..3904751c2 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -281,6 +281,12 @@
281 cursor: pointer; 281 cursor: pointer;
282 display: inline; 282 display: inline;
283 } 283 }
284
285 &[disabled] + label,
286 &[disabled] + label + label{
287 opacity: 0.5;
288 cursor: default;
289 }
284} 290}
285 291
286 292
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
new file mode 100644
index 000000000..b28b20e0f
--- /dev/null
+++ b/client/src/sass/primeng-custom.scss
@@ -0,0 +1,174 @@
1@import '_variables';
2@import '_mixins';
3
4@import '~primeng/resources/primeng.css';
5@import '~primeng/resources/themes/bootstrap/theme.css';
6
7@mixin glyphicon-light {
8 font-family: 'Glyphicons Halflings';
9 text-decoration: none !important;
10 color: #000 !important;
11}
12
13// data table customizations
14p-table {
15 font-size: 15px !important;
16
17 td {
18 border: 1px solid #E5E5E5 !important;
19 padding-left: 15px !important;
20 overflow: hidden !important;
21 text-overflow: ellipsis !important;
22 white-space: nowrap !important;
23 }
24
25 tr {
26 background-color: #fff !important;
27 height: 46px;
28 }
29
30 .ui-table-tbody {
31 tr {
32 &:hover {
33 background-color: #f0f0f0 !important;
34 }
35
36 &:not(:hover) {
37 .action-cell * {
38 display: none !important;
39 }
40 }
41
42 &:first-child td {
43 border-top: none !important;
44 }
45
46 &:last-child td {
47 border-bottom: none !important;
48 }
49 }
50
51 .expander {
52 cursor: pointer;
53 position: relative;
54 top: 1px;
55 }
56 }
57
58 th {
59 border: none !important;
60 border-bottom: 1px solid #f0f0f0 !important;
61 text-align: left !important;
62 padding: 5px 0 5px 15px !important;
63 font-weight: $font-semibold !important;
64 color: #000 !important;
65
66 &.ui-sortable-column:hover {
67 background-color: #f0f0f0 !important;
68 border: 1px solid #f0f0f0 !important;
69 border-width: 0 1px !important;
70
71 &:first-child {
72 border-width: 0 1px 0 0 !important;
73 }
74 }
75
76 &.ui-state-highlight {
77 background-color: #fff !important;
78
79 .pi {
80 @extend .glyphicon;
81
82 color: #000;
83 font-size: 11px;
84
85 &.pi-sort-up {
86 @extend .glyphicon-triangle-top;
87 }
88
89 &.pi-sort-down {
90 @extend .glyphicon-triangle-bottom;
91 }
92 }
93 }
94 }
95
96 .action-cell {
97 width: 250px !important;
98 padding: 0 !important;
99 text-align: center;
100
101 my-edit-button + my-delete-button {
102 margin-left: 5px;
103 }
104 }
105
106 p-paginator {
107 .ui-paginator-bottom {
108 position: relative;
109 border: 1px solid #f0f0f0 !important;
110 height: 40px;
111 display: flex;
112 justify-content: center;
113 align-items: center;
114
115 .ui-paginator-pages {
116 height: auto !important;
117
118 a {
119 color: #000 !important;
120 font-weight: $font-semibold !important;
121 margin-right: 20px !important;
122 outline: 0 !important;
123 border-radius: 3px !important;
124 padding: 5px 2px !important;
125 height: auto !important;
126
127 &.ui-state-active {
128 &, &:hover, &:active, &:focus {
129 color: #fff !important;
130 background-color: $orange-color !important;
131 }
132 }
133 }
134 }
135 }
136 }
137}
138
139// PrimeNG calendar tweaks
140p-calendar .ui-datepicker {
141 a {
142 @include disable-default-a-behaviour;
143 }
144
145 .ui-datepicker-header {
146
147 .ui-datepicker-year {
148 margin-left: 5px;
149 }
150
151 .ui-datepicker-next {
152 @extend .glyphicon-chevron-right;
153 @include glyphicon-light;
154 }
155
156 .ui-datepicker-prev {
157 @extend .glyphicon-chevron-left;
158 @include glyphicon-light;
159 }
160 }
161
162 .ui-timepicker {
163
164 .pi.pi-chevron-up {
165 @extend .glyphicon-chevron-up;
166 @include glyphicon-light;
167 }
168
169 .pi.pi-chevron-down {
170 @extend .glyphicon-chevron-down;
171 @include glyphicon-light;
172 }
173 }
174} \ No newline at end of file