aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.ts17
-rw-r--r--client/src/app/+about/about-routing.module.ts6
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts6
-rw-r--r--client/src/app/+admin/config/config.routes.ts2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts45
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts4
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts8
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html33
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss16
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts3
-rw-r--r--client/src/app/+admin/plugins/plugins.routes.ts6
-rw-r--r--client/src/app/+admin/system/system.routes.ts8
-rw-r--r--client/src/app/+admin/users/user-edit/user-create.component.ts27
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts6
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts17
-rw-r--r--client/src/app/+admin/users/users.routes.ts6
-rw-r--r--client/src/app/+login/login-routing.module.ts2
-rw-r--r--client/src/app/+login/login.component.ts8
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts17
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts14
-rw-r--r--client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts6
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts28
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts10
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts10
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts10
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts20
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss17
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts20
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts6
-rw-r--r--client/src/app/+page-not-found/page-not-found-routing.module.ts2
-rw-r--r--client/src/app/+reset-password/reset-password-routing.module.ts2
-rw-r--r--client/src/app/+reset-password/reset-password.component.ts10
-rw-r--r--client/src/app/+search/search-routing.module.ts2
-rw-r--r--client/src/app/+signup/+register/register-routing.module.ts2
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.ts10
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts22
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts6
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts4
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts6
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts10
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html15
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss3
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts89
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html3
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html3
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html3
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts8
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts6
-rw-r--r--client/src/app/+videos/+video-watch/video-watch-playlist.component.html7
-rw-r--r--client/src/app/+videos/+video-watch/video-watch-playlist.component.ts49
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html3
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts32
-rw-r--r--client/src/app/+videos/videos-routing.module.ts16
-rw-r--r--client/src/app/app.component.ts2
-rw-r--r--client/src/app/core/auth/auth-user.model.ts6
-rw-r--r--client/src/app/core/menu/menu.service.ts19
-rw-r--r--client/src/app/core/plugins/plugin.service.ts92
-rw-r--r--client/src/app/core/wrappers/screen.service.ts2
-rw-r--r--client/src/app/helpers/utils.ts36
-rw-r--r--client/src/app/shared/form-validators/abuse-validators.ts29
-rw-r--r--client/src/app/shared/form-validators/batch-domains-validators.ts60
-rw-r--r--client/src/app/shared/form-validators/custom-config-validators.ts80
-rw-r--r--client/src/app/shared/form-validators/form-validator.model.ts14
-rw-r--r--client/src/app/shared/form-validators/host.ts (renamed from client/src/app/shared/shared-forms/form-validators/host.ts)0
-rw-r--r--client/src/app/shared/form-validators/index.ts17
-rw-r--r--client/src/app/shared/form-validators/instance-validators.ts49
-rw-r--r--client/src/app/shared/form-validators/login-validators.ts20
-rw-r--r--client/src/app/shared/form-validators/reset-password-validators.ts11
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts144
-rw-r--r--client/src/app/shared/form-validators/video-block-validators.ts10
-rw-r--r--client/src/app/shared/form-validators/video-captions-validators.ts16
-rw-r--r--client/src/app/shared/form-validators/video-channel-validators.ts52
-rw-r--r--client/src/app/shared/form-validators/video-comment-validators.ts11
-rw-r--r--client/src/app/shared/form-validators/video-ownership-change-validators.ts25
-rw-r--r--client/src/app/shared/form-validators/video-playlist-validators.ts54
-rw-r--r--client/src/app/shared/form-validators/video-validators.ts101
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts6
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts8
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.html37
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.scss24
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.ts15
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.ts3
-rw-r--r--client/src/app/shared/shared-forms/form-validator.service.ts (renamed from client/src/app/shared/shared-forms/form-validators/form-validator.service.ts)16
-rw-r--r--client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts39
-rw-r--r--client/src/app/shared/shared-forms/form-validators/batch-domains-validators.service.ts68
-rw-r--r--client/src/app/shared/shared-forms/form-validators/custom-config-validators.service.ts97
-rw-r--r--client/src/app/shared/shared-forms/form-validators/index.ts17
-rw-r--r--client/src/app/shared/shared-forms/form-validators/instance-validators.service.ts61
-rw-r--r--client/src/app/shared/shared-forms/form-validators/login-validators.service.ts29
-rw-r--r--client/src/app/shared/shared-forms/form-validators/reset-password-validators.service.ts19
-rw-r--r--client/src/app/shared/shared-forms/form-validators/user-validators.service.ts166
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-accept-ownership-validators.service.ts17
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-block-validators.service.ts18
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-captions-validators.service.ts26
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-change-ownership-validators.service.ts26
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-channel-validators.service.ts63
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-comment-validators.service.ts19
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-playlist-validators.service.ts65
-rw-r--r--client/src/app/shared/shared-forms/form-validators/video-validators.service.ts122
-rw-r--r--client/src/app/shared/shared-forms/index.ts2
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts52
-rw-r--r--client/src/app/shared/shared-forms/timestamp-input.component.ts6
-rw-r--r--client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts6
-rw-r--r--client/src/app/shared/shared-main/video/video-edit.model.ts6
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts1
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.ts10
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/account-report.component.ts6
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts6
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts6
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts8
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts6
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts5
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.ts46
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts8
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html84
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss100
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts235
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss9
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts18
123 files changed, 1627 insertions, 1508 deletions
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
index 215e281bb..11e442f6b 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.ts
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
@@ -1,6 +1,12 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { FormReactive, FormValidatorService, InstanceValidatorsService } from '@app/shared/shared-forms' 3import {
4 BODY_VALIDATOR,
5 FROM_EMAIL_VALIDATOR,
6 FROM_NAME_VALIDATOR,
7 SUBJECT_VALIDATOR
8} from '@app/shared/form-validators/instance-validators'
9import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { InstanceService } from '@app/shared/shared-instance' 10import { InstanceService } from '@app/shared/shared-instance'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 11import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 12import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -22,7 +28,6 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
22 constructor ( 28 constructor (
23 protected formValidatorService: FormValidatorService, 29 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal, 30 private modalService: NgbModal,
25 private instanceValidatorsService: InstanceValidatorsService,
26 private instanceService: InstanceService, 31 private instanceService: InstanceService,
27 private serverService: ServerService, 32 private serverService: ServerService,
28 private notifier: Notifier 33 private notifier: Notifier
@@ -40,10 +45,10 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
40 .subscribe(config => this.serverConfig = config) 45 .subscribe(config => this.serverConfig = config)
41 46
42 this.buildForm({ 47 this.buildForm({
43 fromName: this.instanceValidatorsService.FROM_NAME, 48 fromName: FROM_NAME_VALIDATOR,
44 fromEmail: this.instanceValidatorsService.FROM_EMAIL, 49 fromEmail: FROM_EMAIL_VALIDATOR,
45 subject: this.instanceValidatorsService.SUBJECT, 50 subject: SUBJECT_VALIDATOR,
46 body: this.instanceValidatorsService.BODY 51 body: BODY_VALIDATOR
47 }) 52 })
48 } 53 }
49 54
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts
index 91ccb846f..828b2884c 100644
--- a/client/src/app/+about/about-routing.module.ts
+++ b/client/src/app/+about/about-routing.module.ts
@@ -23,7 +23,7 @@ const aboutRoutes: Routes = [
23 component: AboutInstanceComponent, 23 component: AboutInstanceComponent,
24 data: { 24 data: {
25 meta: { 25 meta: {
26 title: 'About this instance' 26 title: $localize`About this instance`
27 } 27 }
28 }, 28 },
29 resolve: { 29 resolve: {
@@ -35,7 +35,7 @@ const aboutRoutes: Routes = [
35 component: AboutPeertubeComponent, 35 component: AboutPeertubeComponent,
36 data: { 36 data: {
37 meta: { 37 meta: {
38 title: 'About PeerTube' 38 title: $localize`About PeerTube`
39 } 39 }
40 } 40 }
41 }, 41 },
@@ -44,7 +44,7 @@ const aboutRoutes: Routes = [
44 component: AboutFollowsComponent, 44 component: AboutFollowsComponent,
45 data: { 45 data: {
46 meta: { 46 meta: {
47 title: 'About follows' 47 title: $localize`About follows`
48 } 48 }
49 } 49 }
50 } 50 }
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 45b24eb55..d2ca784b0 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -26,7 +26,7 @@ const accountsRoutes: Routes = [
26 component: AccountVideosComponent, 26 component: AccountVideosComponent,
27 data: { 27 data: {
28 meta: { 28 meta: {
29 title: 'Account videos' 29 title: $localize`Account videos`
30 }, 30 },
31 reuse: { 31 reuse: {
32 enabled: true, 32 enabled: true,
@@ -39,7 +39,7 @@ const accountsRoutes: Routes = [
39 component: AccountVideoChannelsComponent, 39 component: AccountVideoChannelsComponent,
40 data: { 40 data: {
41 meta: { 41 meta: {
42 title: 'Account video channels' 42 title: $localize`Account video channels`
43 } 43 }
44 } 44 }
45 }, 45 },
@@ -48,7 +48,7 @@ const accountsRoutes: Routes = [
48 component: AccountAboutComponent, 48 component: AccountAboutComponent,
49 data: { 49 data: {
50 meta: { 50 meta: {
51 title: 'About account' 51 title: $localize`About account`
52 } 52 }
53 } 53 }
54 } 54 }
diff --git a/client/src/app/+admin/config/config.routes.ts b/client/src/app/+admin/config/config.routes.ts
index 7c1a1a166..1b76b29cc 100644
--- a/client/src/app/+admin/config/config.routes.ts
+++ b/client/src/app/+admin/config/config.routes.ts
@@ -23,7 +23,7 @@ export const ConfigRoutes: Routes = [
23 component: EditCustomConfigComponent, 23 component: EditCustomConfigComponent,
24 data: { 24 data: {
25 meta: { 25 meta: {
26 title: 'Edit custom configuration' 26 title: $localize`Edit custom configuration`
27 } 27 }
28 } 28 }
29 } 29 }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 3a60b144f..78e9dd5e5 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -5,12 +5,19 @@ import { ConfigService } from '@app/+admin/config/shared/config.service'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { ServerService } from '@app/core/server/server.service' 6import { ServerService } from '@app/core/server/server.service'
7import { 7import {
8 CustomConfigValidatorsService, 8 ADMIN_EMAIL_VALIDATOR,
9 FormReactive, 9 CACHE_CAPTIONS_SIZE_VALIDATOR,
10 FormValidatorService, 10 CACHE_PREVIEWS_SIZE_VALIDATOR,
11 SelectOptionsItem, 11 INDEX_URL_VALIDATOR,
12 UserValidatorsService 12 INSTANCE_NAME_VALIDATOR,
13} from '@app/shared/shared-forms' 13 INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
14 SEARCH_INDEX_URL_VALIDATOR,
15 SERVICES_TWITTER_USERNAME_VALIDATOR,
16 SIGNUP_LIMIT_VALIDATOR,
17 TRANSCODING_THREADS_VALIDATOR
18} from '@app/shared/form-validators/custom-config-validators'
19import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
20import { FormReactive, FormValidatorService, SelectOptionsItem } from '@app/shared/shared-forms'
14import { NgbNav } from '@ng-bootstrap/ng-bootstrap' 21import { NgbNav } from '@ng-bootstrap/ng-bootstrap'
15import { CustomConfig, ServerConfig } from '@shared/models' 22import { CustomConfig, ServerConfig } from '@shared/models'
16 23
@@ -37,8 +44,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
37 constructor ( 44 constructor (
38 private viewportScroller: ViewportScroller, 45 private viewportScroller: ViewportScroller,
39 protected formValidatorService: FormValidatorService, 46 protected formValidatorService: FormValidatorService,
40 private customConfigValidatorsService: CustomConfigValidatorsService,
41 private userValidatorsService: UserValidatorsService,
42 private notifier: Notifier, 47 private notifier: Notifier,
43 private configService: ConfigService, 48 private configService: ConfigService,
44 private serverService: ServerService 49 private serverService: ServerService
@@ -110,8 +115,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
110 115
111 const formGroupData: { [key in keyof CustomConfig ]: any } = { 116 const formGroupData: { [key in keyof CustomConfig ]: any } = {
112 instance: { 117 instance: {
113 name: this.customConfigValidatorsService.INSTANCE_NAME, 118 name: INSTANCE_NAME_VALIDATOR,
114 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 119 shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
115 description: null, 120 description: null,
116 121
117 isNSFW: false, 122 isNSFW: false,
@@ -143,21 +148,21 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
143 }, 148 },
144 services: { 149 services: {
145 twitter: { 150 twitter: {
146 username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 151 username: SERVICES_TWITTER_USERNAME_VALIDATOR,
147 whitelisted: null 152 whitelisted: null
148 } 153 }
149 }, 154 },
150 cache: { 155 cache: {
151 previews: { 156 previews: {
152 size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE 157 size: CACHE_PREVIEWS_SIZE_VALIDATOR
153 }, 158 },
154 captions: { 159 captions: {
155 size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE 160 size: CACHE_CAPTIONS_SIZE_VALIDATOR
156 } 161 }
157 }, 162 },
158 signup: { 163 signup: {
159 enabled: null, 164 enabled: null,
160 limit: this.customConfigValidatorsService.SIGNUP_LIMIT, 165 limit: SIGNUP_LIMIT_VALIDATOR,
161 requiresEmailVerification: null 166 requiresEmailVerification: null
162 }, 167 },
163 import: { 168 import: {
@@ -171,18 +176,18 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
171 } 176 }
172 }, 177 },
173 admin: { 178 admin: {
174 email: this.customConfigValidatorsService.ADMIN_EMAIL 179 email: ADMIN_EMAIL_VALIDATOR
175 }, 180 },
176 contactForm: { 181 contactForm: {
177 enabled: null 182 enabled: null
178 }, 183 },
179 user: { 184 user: {
180 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 185 videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
181 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY 186 videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR
182 }, 187 },
183 transcoding: { 188 transcoding: {
184 enabled: null, 189 enabled: null,
185 threads: this.customConfigValidatorsService.TRANSCODING_THREADS, 190 threads: TRANSCODING_THREADS_VALIDATOR,
186 allowAdditionalExtensions: null, 191 allowAdditionalExtensions: null,
187 allowAudioFiles: null, 192 allowAudioFiles: null,
188 resolutions: {}, 193 resolutions: {},
@@ -213,7 +218,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
213 }, 218 },
214 autoFollowIndex: { 219 autoFollowIndex: {
215 enabled: null, 220 enabled: null,
216 indexUrl: this.customConfigValidatorsService.INDEX_URL 221 indexUrl: INDEX_URL_VALIDATOR
217 } 222 }
218 } 223 }
219 }, 224 },
@@ -230,7 +235,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
230 }, 235 },
231 searchIndex: { 236 searchIndex: {
232 enabled: null, 237 enabled: null,
233 url: this.customConfigValidatorsService.SEARCH_INDEX_URL, 238 url: SEARCH_INDEX_URL_VALIDATOR,
234 disableLocalSearch: null, 239 disableLocalSearch: null,
235 isDefaultSearch: null 240 isDefaultSearch: null
236 } 241 }
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts
index 817074536..cd70daf77 100644
--- a/client/src/app/+admin/follows/follows.routes.ts
+++ b/client/src/app/+admin/follows/follows.routes.ts
@@ -25,7 +25,7 @@ export const FollowsRoutes: Routes = [
25 component: FollowingListComponent, 25 component: FollowingListComponent,
26 data: { 26 data: {
27 meta: { 27 meta: {
28 title: 'Following list' 28 title: $localize`Following list`
29 } 29 }
30 } 30 }
31 }, 31 },
@@ -34,7 +34,7 @@ export const FollowsRoutes: Routes = [
34 component: FollowersListComponent, 34 component: FollowersListComponent,
35 data: { 35 data: {
36 meta: { 36 meta: {
37 title: 'Followers list' 37 title: $localize`Followers list`
38 } 38 }
39 } 39 }
40 }, 40 },
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 8a31a54dc..b60dd5334 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -33,7 +33,7 @@ export const ModerationRoutes: Routes = [
33 data: { 33 data: {
34 userRight: UserRight.MANAGE_ABUSES, 34 userRight: UserRight.MANAGE_ABUSES,
35 meta: { 35 meta: {
36 title: 'Reports' 36 title: $localize`Reports`
37 } 37 }
38 } 38 }
39 }, 39 },
@@ -64,7 +64,7 @@ export const ModerationRoutes: Routes = [
64 data: { 64 data: {
65 userRight: UserRight.MANAGE_VIDEO_BLACKLIST, 65 userRight: UserRight.MANAGE_VIDEO_BLACKLIST,
66 meta: { 66 meta: {
67 title: 'Videos blocked' 67 title: $localize`Videos blocked`
68 } 68 }
69 } 69 }
70 }, 70 },
@@ -75,7 +75,7 @@ export const ModerationRoutes: Routes = [
75 data: { 75 data: {
76 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST, 76 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
77 meta: { 77 meta: {
78 title: 'Muted accounts' 78 title: $localize`Muted accounts`
79 } 79 }
80 } 80 }
81 }, 81 },
@@ -86,7 +86,7 @@ export const ModerationRoutes: Routes = [
86 data: { 86 data: {
87 userRight: UserRight.MANAGE_SERVERS_BLOCKLIST, 87 userRight: UserRight.MANAGE_SERVERS_BLOCKLIST,
88 meta: { 88 meta: {
89 title: 'Muted instances' 89 title: $localize`Muted instances`
90 } 90 }
91 } 91 }
92 } 92 }
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
index f3fc429ff..cb2894568 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
@@ -7,38 +7,7 @@
7 7
8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
9 <div class="form-group" *ngFor="let setting of registeredSettings"> 9 <div class="form-group" *ngFor="let setting of registeredSettings">
10 <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label> 10 <my-dynamic-form-field [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
11
12 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
13
14 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
15
16 <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
17
18 <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
19
20 <my-markdown-textarea
21 *ngIf="setting.type === 'markdown-text'"
22 markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
23 [classes]="{ 'input-error': formErrors['settings.name'] }"
24 ></my-markdown-textarea>
25
26 <my-markdown-textarea
27 *ngIf="setting.type === 'markdown-enhanced'"
28 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
29 [classes]="{ 'input-error': formErrors['settings.name'] }"
30 ></my-markdown-textarea>
31
32 <my-peertube-checkbox
33 *ngIf="setting.type === 'input-checkbox'"
34 [id]="setting.name"
35 [formControlName]="setting.name"
36 [labelInnerHTML]="setting.label"
37 ></my-peertube-checkbox>
38
39 <div *ngIf="formErrors[setting.name]" class="form-error">
40 {{ formErrors[setting.name] }}
41 </div>
42 </div> 11 </div>
43 12
44 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid"> 13 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
index cc35aec57..5ab6e5f1b 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
@@ -5,22 +5,6 @@ h2 {
5 margin-bottom: 20px; 5 margin-bottom: 20px;
6} 6}
7 7
8input:not([type=submit]) {
9 @include peertube-input-text(340px);
10
11 display: block;
12}
13
14textarea {
15 @include peertube-textarea(340px, 200px);
16
17 display: block;
18}
19
20.peertube-select-container {
21 @include peertube-select-container(340px);
22}
23
24input[type=submit], button { 8input[type=submit], button {
25 @include peertube-button; 9 @include peertube-button;
26 @include orange-button; 10 @include orange-button;
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
index a33f01691..1acaf9674 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
@@ -3,7 +3,8 @@ import { map, switchMap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { BuildFormArgument, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { BuildFormArgument } from '@app/shared/form-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models' 8import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models'
8import { PluginApiService } from '../shared/plugin-api.service' 9import { PluginApiService } from '../shared/plugin-api.service'
9 10
diff --git a/client/src/app/+admin/plugins/plugins.routes.ts b/client/src/app/+admin/plugins/plugins.routes.ts
index 4bef27be3..4cf55dda2 100644
--- a/client/src/app/+admin/plugins/plugins.routes.ts
+++ b/client/src/app/+admin/plugins/plugins.routes.ts
@@ -25,7 +25,7 @@ export const PluginsRoutes: Routes = [
25 component: PluginListInstalledComponent, 25 component: PluginListInstalledComponent,
26 data: { 26 data: {
27 meta: { 27 meta: {
28 title: 'List installed plugins' 28 title: $localize`List installed plugins`
29 } 29 }
30 } 30 }
31 }, 31 },
@@ -34,7 +34,7 @@ export const PluginsRoutes: Routes = [
34 component: PluginSearchComponent, 34 component: PluginSearchComponent,
35 data: { 35 data: {
36 meta: { 36 meta: {
37 title: 'Search plugins' 37 title: $localize`Search plugins`
38 } 38 }
39 } 39 }
40 }, 40 },
@@ -43,7 +43,7 @@ export const PluginsRoutes: Routes = [
43 component: PluginShowInstalledComponent, 43 component: PluginShowInstalledComponent,
44 data: { 44 data: {
45 meta: { 45 meta: {
46 title: 'Show plugin' 46 title: $localize`Show plugin`
47 } 47 }
48 } 48 }
49 } 49 }
diff --git a/client/src/app/+admin/system/system.routes.ts b/client/src/app/+admin/system/system.routes.ts
index 0e8d98519..72ab6705a 100644
--- a/client/src/app/+admin/system/system.routes.ts
+++ b/client/src/app/+admin/system/system.routes.ts
@@ -10,8 +10,6 @@ export const SystemRoutes: Routes = [
10 { 10 {
11 path: 'system', 11 path: 'system',
12 component: SystemComponent, 12 component: SystemComponent,
13 data: {
14 },
15 children: [ 13 children: [
16 { 14 {
17 path: '', 15 path: '',
@@ -25,7 +23,7 @@ export const SystemRoutes: Routes = [
25 data: { 23 data: {
26 meta: { 24 meta: {
27 userRight: UserRight.MANAGE_JOBS, 25 userRight: UserRight.MANAGE_JOBS,
28 title: 'Jobs' 26 title: $localize`Jobs`
29 } 27 }
30 } 28 }
31 }, 29 },
@@ -36,7 +34,7 @@ export const SystemRoutes: Routes = [
36 data: { 34 data: {
37 meta: { 35 meta: {
38 userRight: UserRight.MANAGE_LOGS, 36 userRight: UserRight.MANAGE_LOGS,
39 title: 'Logs' 37 title: $localize`Logs`
40 } 38 }
41 } 39 }
42 }, 40 },
@@ -47,7 +45,7 @@ export const SystemRoutes: Routes = [
47 data: { 45 data: {
48 meta: { 46 meta: {
49 userRight: UserRight.MANAGE_DEBUG, 47 userRight: UserRight.MANAGE_DEBUG,
50 title: 'Debug' 48 title: $localize`Debug`
51 } 49 }
52 } 50 }
53 } 51 }
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts
index 36d71a927..d0aac1cb9 100644
--- a/client/src/app/+admin/users/user-edit/user-create.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-create.component.ts
@@ -2,7 +2,17 @@ import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { ConfigService } from '@app/+admin/config/shared/config.service' 3import { ConfigService } from '@app/+admin/config/shared/config.service'
4import { AuthService, Notifier, ScreenService, ServerService, UserService } from '@app/core' 4import { AuthService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
5import { FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 5import {
6 USER_CHANNEL_NAME_VALIDATOR,
7 USER_EMAIL_VALIDATOR,
8 USER_PASSWORD_OPTIONAL_VALIDATOR,
9 USER_PASSWORD_VALIDATOR,
10 USER_ROLE_VALIDATOR,
11 USER_USERNAME_VALIDATOR,
12 USER_VIDEO_QUOTA_DAILY_VALIDATOR,
13 USER_VIDEO_QUOTA_VALIDATOR
14} from '@app/shared/form-validators/user-validators'
15import { FormValidatorService } from '@app/shared/shared-forms'
6import { UserCreate, UserRole } from '@shared/models' 16import { UserCreate, UserRole } from '@shared/models'
7import { UserEdit } from './user-edit' 17import { UserEdit } from './user-edit'
8 18
@@ -20,7 +30,6 @@ export class UserCreateComponent extends UserEdit implements OnInit {
20 protected configService: ConfigService, 30 protected configService: ConfigService,
21 protected screenService: ScreenService, 31 protected screenService: ScreenService,
22 protected auth: AuthService, 32 protected auth: AuthService,
23 private userValidatorsService: UserValidatorsService,
24 private route: ActivatedRoute, 33 private route: ActivatedRoute,
25 private router: Router, 34 private router: Router,
26 private notifier: Notifier, 35 private notifier: Notifier,
@@ -41,13 +50,13 @@ export class UserCreateComponent extends UserEdit implements OnInit {
41 } 50 }
42 51
43 this.buildForm({ 52 this.buildForm({
44 username: this.userValidatorsService.USER_USERNAME, 53 username: USER_USERNAME_VALIDATOR,
45 channelName: this.userValidatorsService.USER_CHANNEL_NAME, 54 channelName: USER_CHANNEL_NAME_VALIDATOR,
46 email: this.userValidatorsService.USER_EMAIL, 55 email: USER_EMAIL_VALIDATOR,
47 password: this.isPasswordOptional() ? this.userValidatorsService.USER_PASSWORD_OPTIONAL : this.userValidatorsService.USER_PASSWORD, 56 password: this.isPasswordOptional() ? USER_PASSWORD_OPTIONAL_VALIDATOR : USER_PASSWORD_VALIDATOR,
48 role: this.userValidatorsService.USER_ROLE, 57 role: USER_ROLE_VALIDATOR,
49 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 58 videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
50 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 59 videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR,
51 byPassAutoBlock: null 60 byPassAutoBlock: null
52 }, defaultValues) 61 }, defaultValues)
53 } 62 }
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts
index 25f13495a..05d52b17f 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-password.component.ts
@@ -1,6 +1,7 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Notifier, UserService } from '@app/core' 2import { Notifier, UserService } from '@app/core'
3import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 3import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { UserUpdate } from '@shared/models' 5import { UserUpdate } from '@shared/models'
5 6
6@Component({ 7@Component({
@@ -17,7 +18,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
17 18
18 constructor ( 19 constructor (
19 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
20 private userValidatorsService: UserValidatorsService,
21 private notifier: Notifier, 21 private notifier: Notifier,
22 private userService: UserService 22 private userService: UserService
23 ) { 23 ) {
@@ -26,7 +26,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
26 26
27 ngOnInit () { 27 ngOnInit () {
28 this.buildForm({ 28 this.buildForm({
29 password: this.userValidatorsService.USER_PASSWORD 29 password: USER_PASSWORD_VALIDATOR
30 }) 30 })
31 } 31 }
32 32
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index 55bc7290e..e16f66a2b 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -3,7 +3,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
5import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' 5import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core'
6import { FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 6import {
7 USER_EMAIL_VALIDATOR,
8 USER_ROLE_VALIDATOR,
9 USER_VIDEO_QUOTA_DAILY_VALIDATOR,
10 USER_VIDEO_QUOTA_VALIDATOR
11} from '@app/shared/form-validators/user-validators'
12import { FormValidatorService } from '@app/shared/shared-forms'
7import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' 13import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
8import { UserEdit } from './user-edit' 14import { UserEdit } from './user-edit'
9 15
@@ -23,7 +29,6 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
23 protected configService: ConfigService, 29 protected configService: ConfigService,
24 protected screenService: ScreenService, 30 protected screenService: ScreenService,
25 protected auth: AuthService, 31 protected auth: AuthService,
26 private userValidatorsService: UserValidatorsService,
27 private route: ActivatedRoute, 32 private route: ActivatedRoute,
28 private router: Router, 33 private router: Router,
29 private notifier: Notifier, 34 private notifier: Notifier,
@@ -44,10 +49,10 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
44 } 49 }
45 50
46 this.buildForm({ 51 this.buildForm({
47 email: this.userValidatorsService.USER_EMAIL, 52 email: USER_EMAIL_VALIDATOR,
48 role: this.userValidatorsService.USER_ROLE, 53 role: USER_ROLE_VALIDATOR,
49 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 54 videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
50 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 55 videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR,
51 byPassAutoBlock: null 56 byPassAutoBlock: null
52 }, defaultValues) 57 }, defaultValues)
53 58
diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts
index 6560f0260..5183498d6 100644
--- a/client/src/app/+admin/users/users.routes.ts
+++ b/client/src/app/+admin/users/users.routes.ts
@@ -24,7 +24,7 @@ export const UsersRoutes: Routes = [
24 component: UserListComponent, 24 component: UserListComponent,
25 data: { 25 data: {
26 meta: { 26 meta: {
27 title: 'Users list' 27 title: $localize`Users list`
28 } 28 }
29 } 29 }
30 }, 30 },
@@ -33,7 +33,7 @@ export const UsersRoutes: Routes = [
33 component: UserCreateComponent, 33 component: UserCreateComponent,
34 data: { 34 data: {
35 meta: { 35 meta: {
36 title: 'Create a user' 36 title: $localize`Create a user`
37 } 37 }
38 }, 38 },
39 resolve: { 39 resolve: {
@@ -45,7 +45,7 @@ export const UsersRoutes: Routes = [
45 component: UserUpdateComponent, 45 component: UserUpdateComponent,
46 data: { 46 data: {
47 meta: { 47 meta: {
48 title: 'Update a user' 48 title: $localize`Update a user`
49 } 49 }
50 } 50 }
51 } 51 }
diff --git a/client/src/app/+login/login-routing.module.ts b/client/src/app/+login/login-routing.module.ts
index aad55eac8..258ddc5c1 100644
--- a/client/src/app/+login/login-routing.module.ts
+++ b/client/src/app/+login/login-routing.module.ts
@@ -11,7 +11,7 @@ const loginRoutes: Routes = [
11 canActivate: [ MetaGuard ], 11 canActivate: [ MetaGuard ],
12 data: { 12 data: {
13 meta: { 13 meta: {
14 title: 'Login' 14 title: $localize`Login`
15 } 15 }
16 }, 16 },
17 resolve: { 17 resolve: {
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index e9336172e..351750453 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -3,7 +3,8 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { FormReactive, FormValidatorService, LoginValidatorsService } from '@app/shared/shared-forms' 6import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
8import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 9import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
9 10
@@ -31,7 +32,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
31 protected formValidatorService: FormValidatorService, 32 protected formValidatorService: FormValidatorService,
32 private route: ActivatedRoute, 33 private route: ActivatedRoute,
33 private modalService: NgbModal, 34 private modalService: NgbModal,
34 private loginValidatorsService: LoginValidatorsService,
35 private authService: AuthService, 35 private authService: AuthService,
36 private userService: UserService, 36 private userService: UserService,
37 private redirectService: RedirectService, 37 private redirectService: RedirectService,
@@ -65,8 +65,8 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
65 } 65 }
66 66
67 this.buildForm({ 67 this.buildForm({
68 username: this.loginValidatorsService.LOGIN_USERNAME, 68 username: LOGIN_USERNAME_VALIDATOR,
69 password: this.loginValidatorsService.LOGIN_PASSWORD 69 password: LOGIN_PASSWORD_VALIDATOR
70 }) 70 })
71 } 71 }
72 72
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts
index 5c438c3bf..e2ea87fb8 100644
--- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts
+++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-create.component.ts
@@ -1,7 +1,13 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { FormValidatorService, VideoChannelValidatorsService } from '@app/shared/shared-forms' 4import {
5 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
6 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
7 VIDEO_CHANNEL_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
5import { VideoChannelService } from '@app/shared/shared-main' 11import { VideoChannelService } from '@app/shared/shared-main'
6import { VideoChannelCreate } from '@shared/models' 12import { VideoChannelCreate } from '@shared/models'
7import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' 13import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
@@ -17,7 +23,6 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
17 constructor ( 23 constructor (
18 protected formValidatorService: FormValidatorService, 24 protected formValidatorService: FormValidatorService,
19 private authService: AuthService, 25 private authService: AuthService,
20 private videoChannelValidatorsService: VideoChannelValidatorsService,
21 private notifier: Notifier, 26 private notifier: Notifier,
22 private router: Router, 27 private router: Router,
23 private videoChannelService: VideoChannelService 28 private videoChannelService: VideoChannelService
@@ -31,10 +36,10 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
31 36
32 ngOnInit () { 37 ngOnInit () {
33 this.buildForm({ 38 this.buildForm({
34 name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, 39 name: VIDEO_CHANNEL_NAME_VALIDATOR,
35 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, 40 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
36 description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, 41 description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
37 support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT 42 support: VIDEO_CHANNEL_SUPPORT_VALIDATOR
38 }) 43 })
39 } 44 }
40 45
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts
index 485521dcc..01659b8da 100644
--- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts
+++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channel-update.component.ts
@@ -2,7 +2,12 @@ import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core' 2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core' 4import { AuthService, Notifier, ServerService } from '@app/core'
5import { FormValidatorService, VideoChannelValidatorsService } from '@app/shared/shared-forms' 5import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
7import { ServerConfig, VideoChannelUpdate } from '@shared/models' 12import { ServerConfig, VideoChannelUpdate } from '@shared/models'
8import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' 13import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
@@ -23,7 +28,6 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
23 constructor ( 28 constructor (
24 protected formValidatorService: FormValidatorService, 29 protected formValidatorService: FormValidatorService,
25 private authService: AuthService, 30 private authService: AuthService,
26 private videoChannelValidatorsService: VideoChannelValidatorsService,
27 private notifier: Notifier, 31 private notifier: Notifier,
28 private router: Router, 32 private router: Router,
29 private route: ActivatedRoute, 33 private route: ActivatedRoute,
@@ -39,9 +43,9 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
39 .subscribe(config => this.serverConfig = config) 43 .subscribe(config => this.serverConfig = config)
40 44
41 this.buildForm({ 45 this.buildForm({
42 'display-name': this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, 46 'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
43 description: this.videoChannelValidatorsService.VIDEO_CHANNEL_DESCRIPTION, 47 description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
44 support: this.videoChannelValidatorsService.VIDEO_CHANNEL_SUPPORT, 48 support: VIDEO_CHANNEL_SUPPORT_VALIDATOR,
45 bulkVideosSupportUpdate: null 49 bulkVideosSupportUpdate: null
46 }) 50 })
47 51
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts
index 94037e18f..3aa3e360f 100644
--- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts
+++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels-routing.module.ts
@@ -10,7 +10,7 @@ const myAccountVideoChannelsRoutes: Routes = [
10 component: MyAccountVideoChannelsComponent, 10 component: MyAccountVideoChannelsComponent,
11 data: { 11 data: {
12 meta: { 12 meta: {
13 title: 'Account video channels' 13 title: $localize`Account video channels`
14 } 14 }
15 } 15 }
16 }, 16 },
@@ -19,7 +19,7 @@ const myAccountVideoChannelsRoutes: Routes = [
19 component: MyAccountVideoChannelCreateComponent, 19 component: MyAccountVideoChannelCreateComponent,
20 data: { 20 data: {
21 meta: { 21 meta: {
22 title: 'Create new video channel' 22 title: $localize`Create new video channel`
23 } 23 }
24 } 24 }
25 }, 25 },
@@ -28,7 +28,7 @@ const myAccountVideoChannelsRoutes: Routes = [
28 component: MyAccountVideoChannelUpdateComponent, 28 component: MyAccountVideoChannelUpdateComponent,
29 data: { 29 data: {
30 meta: { 30 meta: {
31 title: 'Update video channel' 31 title: $localize`Update video channel`
32 } 32 }
33 } 33 }
34 } 34 }
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
index 3bfffe2da..4c4436755 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
@@ -1,6 +1,7 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, Notifier } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, VideoAcceptOwnershipValidatorsService } from '@app/shared/shared-forms' 3import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' 5import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { VideoChangeOwnership, VideoChannel } from '@shared/models' 7import { VideoChangeOwnership, VideoChannel } from '@shared/models'
@@ -23,7 +24,6 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
23 24
24 constructor ( 25 constructor (
25 protected formValidatorService: FormValidatorService, 26 protected formValidatorService: FormValidatorService,
26 private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService,
27 private videoOwnershipService: VideoOwnershipService, 27 private videoOwnershipService: VideoOwnershipService,
28 private notifier: Notifier, 28 private notifier: Notifier,
29 private authService: AuthService, 29 private authService: AuthService,
@@ -40,7 +40,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
40 .subscribe(videoChannels => this.videoChannels = videoChannels.data) 40 .subscribe(videoChannels => this.videoChannels = videoChannels.data)
41 41
42 this.buildForm({ 42 this.buildForm({
43 channel: this.videoChangeOwnershipValidatorsService.CHANNEL 43 channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR
44 }) 44 })
45 } 45 }
46 46
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 48237e133..0bcb38ef5 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -34,7 +34,7 @@ const myAccountRoutes: Routes = [
34 component: MyAccountSettingsComponent, 34 component: MyAccountSettingsComponent,
35 data: { 35 data: {
36 meta: { 36 meta: {
37 title: 'Account settings' 37 title: $localize`Account settings`
38 } 38 }
39 } 39 }
40 }, 40 },
@@ -52,7 +52,7 @@ const myAccountRoutes: Routes = [
52 component: MyAccountVideoPlaylistsComponent, 52 component: MyAccountVideoPlaylistsComponent,
53 data: { 53 data: {
54 meta: { 54 meta: {
55 title: 'Account playlists' 55 title: $localize`Account playlists`
56 } 56 }
57 } 57 }
58 }, 58 },
@@ -61,7 +61,7 @@ const myAccountRoutes: Routes = [
61 component: MyAccountVideoPlaylistCreateComponent, 61 component: MyAccountVideoPlaylistCreateComponent,
62 data: { 62 data: {
63 meta: { 63 meta: {
64 title: 'Create new playlist' 64 title: $localize`Create new playlist`
65 } 65 }
66 } 66 }
67 }, 67 },
@@ -70,7 +70,7 @@ const myAccountRoutes: Routes = [
70 component: MyAccountVideoPlaylistElementsComponent, 70 component: MyAccountVideoPlaylistElementsComponent,
71 data: { 71 data: {
72 meta: { 72 meta: {
73 title: 'Playlist elements' 73 title: $localize`Playlist elements`
74 } 74 }
75 } 75 }
76 }, 76 },
@@ -79,7 +79,7 @@ const myAccountRoutes: Routes = [
79 component: MyAccountVideoPlaylistUpdateComponent, 79 component: MyAccountVideoPlaylistUpdateComponent,
80 data: { 80 data: {
81 meta: { 81 meta: {
82 title: 'Update playlist' 82 title: $localize`Update playlist`
83 } 83 }
84 } 84 }
85 }, 85 },
@@ -89,7 +89,7 @@ const myAccountRoutes: Routes = [
89 component: MyAccountVideosComponent, 89 component: MyAccountVideosComponent,
90 data: { 90 data: {
91 meta: { 91 meta: {
92 title: 'Account videos' 92 title: $localize`Account videos`
93 }, 93 },
94 reuse: { 94 reuse: {
95 enabled: true, 95 enabled: true,
@@ -102,7 +102,7 @@ const myAccountRoutes: Routes = [
102 component: MyAccountVideoImportsComponent, 102 component: MyAccountVideoImportsComponent,
103 data: { 103 data: {
104 meta: { 104 meta: {
105 title: 'Account video imports' 105 title: $localize`Account video imports`
106 } 106 }
107 } 107 }
108 }, 108 },
@@ -111,7 +111,7 @@ const myAccountRoutes: Routes = [
111 component: MyAccountSubscriptionsComponent, 111 component: MyAccountSubscriptionsComponent,
112 data: { 112 data: {
113 meta: { 113 meta: {
114 title: 'Account subscriptions' 114 title: $localize`Account subscriptions`
115 } 115 }
116 } 116 }
117 }, 117 },
@@ -120,7 +120,7 @@ const myAccountRoutes: Routes = [
120 component: MyAccountOwnershipComponent, 120 component: MyAccountOwnershipComponent,
121 data: { 121 data: {
122 meta: { 122 meta: {
123 title: 'Ownership changes' 123 title: $localize`Ownership changes`
124 } 124 }
125 } 125 }
126 }, 126 },
@@ -129,7 +129,7 @@ const myAccountRoutes: Routes = [
129 component: MyAccountBlocklistComponent, 129 component: MyAccountBlocklistComponent,
130 data: { 130 data: {
131 meta: { 131 meta: {
132 title: 'Muted accounts' 132 title: $localize`Muted accounts`
133 } 133 }
134 } 134 }
135 }, 135 },
@@ -138,7 +138,7 @@ const myAccountRoutes: Routes = [
138 component: MyAccountServerBlocklistComponent, 138 component: MyAccountServerBlocklistComponent,
139 data: { 139 data: {
140 meta: { 140 meta: {
141 title: 'Muted servers' 141 title: $localize`Muted servers`
142 } 142 }
143 } 143 }
144 }, 144 },
@@ -147,7 +147,7 @@ const myAccountRoutes: Routes = [
147 component: MyAccountHistoryComponent, 147 component: MyAccountHistoryComponent,
148 data: { 148 data: {
149 meta: { 149 meta: {
150 title: 'Videos history' 150 title: $localize`Videos history`
151 }, 151 },
152 reuse: { 152 reuse: {
153 enabled: true, 153 enabled: true,
@@ -160,7 +160,7 @@ const myAccountRoutes: Routes = [
160 component: MyAccountNotificationsComponent, 160 component: MyAccountNotificationsComponent,
161 data: { 161 data: {
162 meta: { 162 meta: {
163 title: 'Notifications' 163 title: $localize`Notifications`
164 } 164 }
165 } 165 }
166 }, 166 },
@@ -169,7 +169,7 @@ const myAccountRoutes: Routes = [
169 component: MyAccountAbusesListComponent, 169 component: MyAccountAbusesListComponent,
170 data: { 170 data: {
171 meta: { 171 meta: {
172 title: 'My abuse reports' 172 title: $localize`My abuse reports`
173 } 173 }
174 } 174 }
175 } 175 }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
index 396936ef3..b2b7849c2 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
@@ -2,7 +2,8 @@ import { forkJoin } from 'rxjs'
2import { tap } from 'rxjs/operators' 2import { tap } from 'rxjs/operators'
3import { Component, OnInit } from '@angular/core' 3import { Component, OnInit } from '@angular/core'
4import { AuthService, ServerService, UserService } from '@app/core' 4import { AuthService, ServerService, UserService } from '@app/core'
5import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 5import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
6import { User } from '@shared/models' 7import { User } from '@shared/models'
7 8
8@Component({ 9@Component({
@@ -17,18 +18,17 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
17 18
18 constructor ( 19 constructor (
19 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
20 private userValidatorsService: UserValidatorsService,
21 private authService: AuthService, 21 private authService: AuthService,
22 private userService: UserService, 22 private userService: UserService,
23 private serverService: ServerService 23 private serverService: ServerService
24 ) { 24 ) {
25 super() 25 super()
26 } 26 }
27 27
28 ngOnInit () { 28 ngOnInit () {
29 this.buildForm({ 29 this.buildForm({
30 'new-email': this.userValidatorsService.USER_EMAIL, 30 'new-email': USER_EMAIL_VALIDATOR,
31 'password': this.userValidatorsService.USER_PASSWORD 31 'password': USER_PASSWORD_VALIDATOR
32 }) 32 })
33 33
34 this.user = this.authService.getUser() 34 this.user = this.authService.getUser()
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
index 91fe4ec72..ba68bab32 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
@@ -1,7 +1,8 @@
1import { filter } from 'rxjs/operators' 1import { filter } from 'rxjs/operators'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { AuthService, Notifier, UserService } from '@app/core' 3import { AuthService, Notifier, UserService } from '@app/core'
4import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 4import { USER_CONFIRM_PASSWORD_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { User } from '@shared/models' 6import { User } from '@shared/models'
6 7
7@Component({ 8@Component({
@@ -15,7 +16,6 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
15 16
16 constructor ( 17 constructor (
17 protected formValidatorService: FormValidatorService, 18 protected formValidatorService: FormValidatorService,
18 private userValidatorsService: UserValidatorsService,
19 private notifier: Notifier, 19 private notifier: Notifier,
20 private authService: AuthService, 20 private authService: AuthService,
21 private userService: UserService 21 private userService: UserService
@@ -25,9 +25,9 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
25 25
26 ngOnInit () { 26 ngOnInit () {
27 this.buildForm({ 27 this.buildForm({
28 'current-password': this.userValidatorsService.USER_PASSWORD, 28 'current-password': USER_PASSWORD_VALIDATOR,
29 'new-password': this.userValidatorsService.USER_PASSWORD, 29 'new-password': USER_PASSWORD_VALIDATOR,
30 'new-confirmed-password': this.userValidatorsService.USER_CONFIRM_PASSWORD 30 'new-confirmed-password': USER_CONFIRM_PASSWORD_VALIDATOR
31 }) 31 })
32 32
33 this.user = this.authService.getUser() 33 this.user = this.authService.getUser()
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
index ed0984bf7..000a2c0ac 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
@@ -1,7 +1,8 @@
1import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
2import { Component, Input, OnInit } from '@angular/core' 2import { Component, Input, OnInit } from '@angular/core'
3import { Notifier, User, UserService } from '@app/core' 3import { Notifier, User, UserService } from '@app/core'
4import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 4import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5 6
6@Component({ 7@Component({
7 selector: 'my-account-profile', 8 selector: 'my-account-profile',
@@ -16,17 +17,16 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
16 17
17 constructor ( 18 constructor (
18 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
19 private userValidatorsService: UserValidatorsService,
20 private notifier: Notifier, 20 private notifier: Notifier,
21 private userService: UserService 21 private userService: UserService
22 ) { 22 ) {
23 super() 23 super()
24 } 24 }
25 25
26 ngOnInit () { 26 ngOnInit () {
27 this.buildForm({ 27 this.buildForm({
28 'display-name': this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED, 28 'display-name': USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
29 description: this.userValidatorsService.USER_DESCRIPTION 29 description: USER_DESCRIPTION_VALIDATOR
30 }) 30 })
31 31
32 this.userInformationLoaded.subscribe(() => { 32 this.userInformationLoaded.subscribe(() => {
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
index 5427dc3a0..7a80aaa92 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component.ts
@@ -2,7 +2,14 @@ import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { populateAsyncUserVideoChannels } from '@app/helpers' 4import { populateAsyncUserVideoChannels } from '@app/helpers'
5import { FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/shared-forms' 5import {
6 setPlaylistChannelValidator,
7 VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
8 VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
9 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
10 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
11} from '@app/shared/form-validators/video-playlist-validators'
12import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoPlaylistService } from '@app/shared/shared-video-playlist' 13import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
7import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' 14import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
8import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' 15import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
@@ -19,7 +26,6 @@ export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylis
19 constructor ( 26 constructor (
20 protected formValidatorService: FormValidatorService, 27 protected formValidatorService: FormValidatorService,
21 private authService: AuthService, 28 private authService: AuthService,
22 private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
23 private notifier: Notifier, 29 private notifier: Notifier,
24 private router: Router, 30 private router: Router,
25 private videoPlaylistService: VideoPlaylistService, 31 private videoPlaylistService: VideoPlaylistService,
@@ -30,15 +36,15 @@ export class MyAccountVideoPlaylistCreateComponent extends MyAccountVideoPlaylis
30 36
31 ngOnInit () { 37 ngOnInit () {
32 this.buildForm({ 38 this.buildForm({
33 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME, 39 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
34 privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY, 40 privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR,
35 description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION, 41 description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
36 videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID, 42 videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
37 thumbnailfile: null 43 thumbnailfile: null
38 }) 44 })
39 45
40 this.form.get('privacy').valueChanges.subscribe(privacy => { 46 this.form.get('privacy').valueChanges.subscribe(privacy => {
41 this.videoPlaylistValidatorsService.setChannelValidator(this.form.get('videoChannelId'), privacy) 47 setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
42 }) 48 })
43 49
44 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) 50 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss
index 3204167ff..de7e1993f 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.scss
@@ -65,12 +65,19 @@
65 padding-top: 20px; 65 padding-top: 20px;
66 margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); 66 margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1);
67 } 67 }
68}
69 68
70@media not all and (hover: hover) and (pointer: fine) { 69 .playlist-elements {
71 .video { 70 padding: 0 !important;
72 .more { 71 }
73 opacity: 1; 72
73 ::ng-deep my-video-playlist-element-miniature {
74
75 .video {
76 padding: 5px !important;
77 }
78
79 .position {
80 margin-right: 5px !important;
74 } 81 }
75 } 82 }
76} 83}
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
index 149d0d94f..fefc6d607 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component.ts
@@ -4,7 +4,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { populateAsyncUserVideoChannels } from '@app/helpers' 6import { populateAsyncUserVideoChannels } from '@app/helpers'
7import { FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/shared-forms' 7import {
8 setPlaylistChannelValidator,
9 VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
10 VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
11 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
12 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
13} from '@app/shared/form-validators/video-playlist-validators'
14import { FormValidatorService } from '@app/shared/shared-forms'
8import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 15import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
9import { VideoPlaylistUpdate } from '@shared/models' 16import { VideoPlaylistUpdate } from '@shared/models'
10import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit' 17import { MyAccountVideoPlaylistEdit } from './my-account-video-playlist-edit'
@@ -23,7 +30,6 @@ export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylis
23 constructor ( 30 constructor (
24 protected formValidatorService: FormValidatorService, 31 protected formValidatorService: FormValidatorService,
25 private authService: AuthService, 32 private authService: AuthService,
26 private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
27 private notifier: Notifier, 33 private notifier: Notifier,
28 private router: Router, 34 private router: Router,
29 private route: ActivatedRoute, 35 private route: ActivatedRoute,
@@ -35,15 +41,15 @@ export class MyAccountVideoPlaylistUpdateComponent extends MyAccountVideoPlaylis
35 41
36 ngOnInit () { 42 ngOnInit () {
37 this.buildForm({ 43 this.buildForm({
38 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME, 44 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
39 privacy: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_PRIVACY, 45 privacy: VIDEO_PLAYLIST_PRIVACY_VALIDATOR,
40 description: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DESCRIPTION, 46 description: VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR,
41 videoChannelId: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_CHANNEL_ID, 47 videoChannelId: VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR,
42 thumbnailfile: null 48 thumbnailfile: null
43 }) 49 })
44 50
45 this.form.get('privacy').valueChanges.subscribe(privacy => { 51 this.form.get('privacy').valueChanges.subscribe(privacy => {
46 this.videoPlaylistValidatorsService.setChannelValidator(this.form.get('videoChannelId'), privacy) 52 setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy)
47 }) 53 })
48 54
49 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) 55 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
index edd691694..84237dee1 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
@@ -1,6 +1,7 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { Notifier, UserService } from '@app/core' 2import { Notifier, UserService } from '@app/core'
3import { FormReactive, FormValidatorService, VideoChangeOwnershipValidatorsService } from '@app/shared/shared-forms' 3import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { Video, VideoOwnershipService } from '@app/shared/shared-main' 5import { Video, VideoOwnershipService } from '@app/shared/shared-main'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6 7
@@ -20,7 +21,6 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
20 21
21 constructor ( 22 constructor (
22 protected formValidatorService: FormValidatorService, 23 protected formValidatorService: FormValidatorService,
23 private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService,
24 private videoOwnershipService: VideoOwnershipService, 24 private videoOwnershipService: VideoOwnershipService,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private userService: UserService, 26 private userService: UserService,
@@ -31,7 +31,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
31 31
32 ngOnInit () { 32 ngOnInit () {
33 this.buildForm({ 33 this.buildForm({
34 username: this.videoChangeOwnershipValidatorsService.USERNAME 34 username: OWNERSHIP_CHANGE_USERNAME_VALIDATOR
35 }) 35 })
36 this.usernamePropositions = [] 36 this.usernamePropositions = []
37 } 37 }
diff --git a/client/src/app/+page-not-found/page-not-found-routing.module.ts b/client/src/app/+page-not-found/page-not-found-routing.module.ts
index e3407099d..11399fbfd 100644
--- a/client/src/app/+page-not-found/page-not-found-routing.module.ts
+++ b/client/src/app/+page-not-found/page-not-found-routing.module.ts
@@ -10,7 +10,7 @@ const pageNotFoundRoutes: Routes = [
10 canActivate: [ MetaGuard ], 10 canActivate: [ MetaGuard ],
11 data: { 11 data: {
12 meta: { 12 meta: {
13 title: 'Not found' 13 title: $localize`Not found`
14 } 14 }
15 } 15 }
16 } 16 }
diff --git a/client/src/app/+reset-password/reset-password-routing.module.ts b/client/src/app/+reset-password/reset-password-routing.module.ts
index 31bc08709..7f1ba2f68 100644
--- a/client/src/app/+reset-password/reset-password-routing.module.ts
+++ b/client/src/app/+reset-password/reset-password-routing.module.ts
@@ -10,7 +10,7 @@ const resetPasswordRoutes: Routes = [
10 canActivate: [ MetaGuard ], 10 canActivate: [ MetaGuard ],
11 data: { 11 data: {
12 meta: { 12 meta: {
13 title: 'Reset password' 13 title: `Reset password`
14 } 14 }
15 } 15 }
16 } 16 }
diff --git a/client/src/app/+reset-password/reset-password.component.ts b/client/src/app/+reset-password/reset-password.component.ts
index 16e4f4090..ce9144170 100644
--- a/client/src/app/+reset-password/reset-password.component.ts
+++ b/client/src/app/+reset-password/reset-password.component.ts
@@ -1,7 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Notifier, UserService } from '@app/core' 3import { Notifier, UserService } from '@app/core'
4import { FormReactive, FormValidatorService, ResetPasswordValidatorsService, UserValidatorsService } from '@app/shared/shared-forms' 4import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators'
5import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5 7
6@Component({ 8@Component({
7 selector: 'my-login', 9 selector: 'my-login',
@@ -15,8 +17,6 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
15 17
16 constructor ( 18 constructor (
17 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
18 private resetPasswordValidatorsService: ResetPasswordValidatorsService,
19 private userValidatorsService: UserValidatorsService,
20 private userService: UserService, 20 private userService: UserService,
21 private notifier: Notifier, 21 private notifier: Notifier,
22 private router: Router, 22 private router: Router,
@@ -27,8 +27,8 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
27 27
28 ngOnInit () { 28 ngOnInit () {
29 this.buildForm({ 29 this.buildForm({
30 password: this.userValidatorsService.USER_PASSWORD, 30 password: USER_PASSWORD_VALIDATOR,
31 'password-confirm': this.resetPasswordValidatorsService.RESET_PASSWORD_CONFIRM 31 'password-confirm': RESET_PASSWORD_CONFIRM_VALIDATOR
32 }) 32 })
33 33
34 this.userId = this.route.snapshot.queryParams['userId'] 34 this.userId = this.route.snapshot.queryParams['userId']
diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts
index 14a0d0a13..e5d7d1ede 100644
--- a/client/src/app/+search/search-routing.module.ts
+++ b/client/src/app/+search/search-routing.module.ts
@@ -12,7 +12,7 @@ const searchRoutes: Routes = [
12 canActivate: [ MetaGuard ], 12 canActivate: [ MetaGuard ],
13 data: { 13 data: {
14 meta: { 14 meta: {
15 title: 'Search' 15 title: $localize`Search`
16 } 16 }
17 } 17 }
18 }, 18 },
diff --git a/client/src/app/+signup/+register/register-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts
index 0deed8a9b..61a2fa42d 100644
--- a/client/src/app/+signup/+register/register-routing.module.ts
+++ b/client/src/app/+signup/+register/register-routing.module.ts
@@ -11,7 +11,7 @@ const registerRoutes: Routes = [
11 canActivate: [ MetaGuard, UnloggedGuard ], 11 canActivate: [ MetaGuard, UnloggedGuard ],
12 data: { 12 data: {
13 meta: { 13 meta: {
14 title: 'Register' 14 title: $localize`Register`
15 } 15 }
16 }, 16 },
17 resolve: { 17 resolve: {
diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/register-step-channel.component.ts
index 8a0120840..d965a7865 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.ts
+++ b/client/src/app/+signup/+register/register-step-channel.component.ts
@@ -3,7 +3,8 @@ import { pairwise } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { UserService } from '@app/core' 5import { UserService } from '@app/core'
6import { FormReactive, FormValidatorService, VideoChannelValidatorsService } from '@app/shared/shared-forms' 6import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7 8
8@Component({ 9@Component({
9 selector: 'my-register-step-channel', 10 selector: 'my-register-step-channel',
@@ -16,8 +17,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
16 17
17 constructor ( 18 constructor (
18 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
19 private userService: UserService, 20 private userService: UserService
20 private videoChannelValidatorsService: VideoChannelValidatorsService
21 ) { 21 ) {
22 super() 22 super()
23 } 23 }
@@ -28,8 +28,8 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
28 28
29 ngOnInit () { 29 ngOnInit () {
30 this.buildForm({ 30 this.buildForm({
31 displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME, 31 displayName: VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
32 name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME 32 name: VIDEO_CHANNEL_NAME_VALIDATOR
33 }) 33 })
34 34
35 setTimeout(() => this.formBuilt.emit(this.form)) 35 setTimeout(() => this.formBuilt.emit(this.form))
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts
index 3d9ab8b6b..65536568b 100644
--- a/client/src/app/+signup/+register/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/register-step-user.component.ts
@@ -3,7 +3,14 @@ import { pairwise } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { UserService } from '@app/core' 5import { UserService } from '@app/core'
6import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 6import {
7 USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
8 USER_EMAIL_VALIDATOR,
9 USER_PASSWORD_VALIDATOR,
10 USER_TERMS_VALIDATOR,
11 USER_USERNAME_VALIDATOR
12} from '@app/shared/form-validators/user-validators'
13import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7 14
8@Component({ 15@Component({
9 selector: 'my-register-step-user', 16 selector: 'my-register-step-user',
@@ -19,8 +26,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
19 26
20 constructor ( 27 constructor (
21 protected formValidatorService: FormValidatorService, 28 protected formValidatorService: FormValidatorService,
22 private userService: UserService, 29 private userService: UserService
23 private userValidatorsService: UserValidatorsService
24 ) { 30 ) {
25 super() 31 super()
26 } 32 }
@@ -31,11 +37,11 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
31 37
32 ngOnInit () { 38 ngOnInit () {
33 this.buildForm({ 39 this.buildForm({
34 displayName: this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED, 40 displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
35 username: this.userValidatorsService.USER_USERNAME, 41 username: USER_USERNAME_VALIDATOR,
36 password: this.userValidatorsService.USER_PASSWORD, 42 password: USER_PASSWORD_VALIDATOR,
37 email: this.userValidatorsService.USER_EMAIL, 43 email: USER_EMAIL_VALIDATOR,
38 terms: this.userValidatorsService.USER_TERMS 44 terms: USER_TERMS_VALIDATOR
39 }) 45 })
40 46
41 setTimeout(() => this.formBuilt.emit(this.form)) 47 setTimeout(() => this.formBuilt.emit(this.form))
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
index b26581d2b..830dd9962 100644
--- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Notifier, RedirectService, ServerService, UserService } from '@app/core' 2import { Notifier, RedirectService, ServerService, UserService } from '@app/core'
3import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 3import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { ServerConfig } from '@shared/models' 5import { ServerConfig } from '@shared/models'
5 6
6@Component({ 7@Component({
@@ -14,7 +15,6 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
14 15
15 constructor ( 16 constructor (
16 protected formValidatorService: FormValidatorService, 17 protected formValidatorService: FormValidatorService,
17 private userValidatorsService: UserValidatorsService,
18 private userService: UserService, 18 private userService: UserService,
19 private serverService: ServerService, 19 private serverService: ServerService,
20 private notifier: Notifier, 20 private notifier: Notifier,
@@ -33,7 +33,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
33 .subscribe(config => this.serverConfig = config) 33 .subscribe(config => this.serverConfig = config)
34 34
35 this.buildForm({ 35 this.buildForm({
36 'verify-email-email': this.userValidatorsService.USER_EMAIL 36 'verify-email-email': USER_EMAIL_VALIDATOR
37 }) 37 })
38 } 38 }
39 39
diff --git a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
index 16d5fe0d0..c9ac67e4c 100644
--- a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
@@ -14,7 +14,7 @@ const verifyAccountRoutes: Routes = [
14 component: VerifyAccountEmailComponent, 14 component: VerifyAccountEmailComponent,
15 data: { 15 data: {
16 meta: { 16 meta: {
17 title: 'Verify account email' 17 title: $localize`Verify account email`
18 } 18 }
19 } 19 }
20 }, 20 },
@@ -23,7 +23,7 @@ const verifyAccountRoutes: Routes = [
23 component: VerifyAccountAskSendEmailComponent, 23 component: VerifyAccountAskSendEmailComponent,
24 data: { 24 data: {
25 meta: { 25 meta: {
26 title: 'Verify account ask send email' 26 title: $localize`Verify account ask send email`
27 } 27 }
28 } 28 }
29 } 29 }
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index e79e6a680..f8c32f14e 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -22,7 +22,7 @@ const videoChannelsRoutes: Routes = [
22 component: VideoChannelVideosComponent, 22 component: VideoChannelVideosComponent,
23 data: { 23 data: {
24 meta: { 24 meta: {
25 title: 'Video channel videos' 25 title: $localize`Video channel videos`
26 }, 26 },
27 reuse: { 27 reuse: {
28 enabled: true, 28 enabled: true,
@@ -35,7 +35,7 @@ const videoChannelsRoutes: Routes = [
35 component: VideoChannelPlaylistsComponent, 35 component: VideoChannelPlaylistsComponent,
36 data: { 36 data: {
37 meta: { 37 meta: {
38 title: 'Video channel playlists' 38 title: $localize`Video channel playlists`
39 } 39 }
40 } 40 }
41 }, 41 },
@@ -44,7 +44,7 @@ const videoChannelsRoutes: Routes = [
44 component: VideoChannelAboutComponent, 44 component: VideoChannelAboutComponent,
45 data: { 45 data: {
46 meta: { 46 meta: {
47 title: 'About video channel' 47 title: $localize`About video channel`
48 } 48 }
49 } 49 }
50 } 50 }
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts
index a90d04ce8..e48d16527 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -1,6 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { FormReactive, FormValidatorService, VideoCaptionsValidatorsService } from '@app/shared/shared-forms' 3import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { VideoCaptionEdit } from '@app/shared/shared-main' 5import { VideoCaptionEdit } from '@app/shared/shared-main'
5import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
6import { ServerConfig, VideoConstant } from '@shared/models' 7import { ServerConfig, VideoConstant } from '@shared/models'
@@ -27,8 +28,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
27 constructor ( 28 constructor (
28 protected formValidatorService: FormValidatorService, 29 protected formValidatorService: FormValidatorService,
29 private modalService: NgbModal, 30 private modalService: NgbModal,
30 private serverService: ServerService, 31 private serverService: ServerService
31 private videoCaptionsValidatorsService: VideoCaptionsValidatorsService
32 ) { 32 ) {
33 super() 33 super()
34 } 34 }
@@ -46,8 +46,8 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
46 .subscribe(languages => this.videoCaptionLanguages = languages) 46 .subscribe(languages => this.videoCaptionLanguages = languages)
47 47
48 this.buildForm({ 48 this.buildForm({
49 language: this.videoCaptionsValidatorsService.VIDEO_CAPTION_LANGUAGE, 49 language: VIDEO_CAPTION_LANGUAGE_VALIDATOR,
50 captionfile: this.videoCaptionsValidatorsService.VIDEO_CAPTION_FILE 50 captionfile: VIDEO_CAPTION_FILE_VALIDATOR
51 }) 51 })
52 } 52 }
53 53
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 ae3413e79..842997b20 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
@@ -265,6 +265,21 @@
265 </ng-template> 265 </ng-template>
266 </ng-container> 266 </ng-container>
267 267
268 <ng-container ngbNavItem *ngIf="pluginFields.length !== 0">
269 <a ngbNavLink i18n>Plugin settings</a>
270
271 <ng-template ngbNavContent>
272 <div class="row plugin-settings">
273
274 <div class="col-md-12 col-xl-8">
275 <div *ngFor="let pluginSetting of pluginFields" class="form-group">
276 <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
277 </div>
278 </div>
279
280 </div>
281 </ng-template>
282 </ng-container>
268 </div> 283 </div>
269 284
270 <div [ngbNavOutlet]="nav"></div> 285 <div [ngbNavOutlet]="nav"></div>
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 9caf009c5..3082a4f72 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
@@ -7,7 +7,8 @@
7@import 'variables'; 7@import 'variables';
8@import 'mixins'; 8@import 'mixins';
9 9
10label { 10label,
11my-dynamic-form-field ::ng-deep label {
11 font-weight: $font-regular; 12 font-weight: $font-regular;
12 font-size: 100%; 13 font-size: 100%;
13} 14}
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 050b6d931..f04111e69 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,13 +1,27 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
2import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
3import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
5import { ServerService } from '@app/core' 5import { HooksService, PluginService, ServerService } from '@app/core'
6import { removeElementFromArray } from '@app/helpers' 6import { removeElementFromArray } from '@app/helpers'
7import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem, VideoValidatorsService } from '@app/shared/shared-forms' 7import {
8 VIDEO_CATEGORY_VALIDATOR,
9 VIDEO_CHANNEL_VALIDATOR,
10 VIDEO_DESCRIPTION_VALIDATOR,
11 VIDEO_LANGUAGE_VALIDATOR,
12 VIDEO_LICENCE_VALIDATOR,
13 VIDEO_NAME_VALIDATOR,
14 VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
15 VIDEO_PRIVACY_VALIDATOR,
16 VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR,
17 VIDEO_SUPPORT_VALIDATOR,
18 VIDEO_TAGS_ARRAY_VALIDATOR
19} from '@app/shared/form-validators/video-validators'
20import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms'
8import { InstanceService } from '@app/shared/shared-instance' 21import { InstanceService } from '@app/shared/shared-instance'
9import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 22import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
10import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' 23import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
24import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
11import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 25import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
12import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 26import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
13 27
@@ -26,9 +40,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
26 @Input() schedulePublicationPossible = true 40 @Input() schedulePublicationPossible = true
27 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] 41 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
28 @Input() waitTranscodingEnabled = true 42 @Input() waitTranscodingEnabled = true
43 @Input() type: 'import-url' | 'import-torrent' | 'upload' | 'update'
29 44
30 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent 45 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
31 46
47 @Output() pluginFieldsAdded = new EventEmitter<void>()
48
32 // So that it can be accessed in the template 49 // So that it can be accessed in the template
33 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 50 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
34 51
@@ -40,6 +57,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
40 tagValidators: ValidatorFn[] 57 tagValidators: ValidatorFn[]
41 tagValidatorsMessages: { [ name: string ]: string } 58 tagValidatorsMessages: { [ name: string ]: string }
42 59
60 pluginDataFormGroup: FormGroup
61
43 schedulePublicationEnabled = false 62 schedulePublicationEnabled = false
44 63
45 calendarLocale: any = {} 64 calendarLocale: any = {}
@@ -51,18 +70,24 @@ export class VideoEditComponent implements OnInit, OnDestroy {
51 70
52 serverConfig: ServerConfig 71 serverConfig: ServerConfig
53 72
73 pluginFields: {
74 commonOptions: RegisterClientFormFieldOptions
75 videoFormOptions: RegisterClientVideoFieldOptions
76 }[] = []
77
54 private schedulerInterval: any 78 private schedulerInterval: any
55 private firstPatchDone = false 79 private firstPatchDone = false
56 private initialVideoCaptions: string[] = [] 80 private initialVideoCaptions: string[] = []
57 81
58 constructor ( 82 constructor (
59 private formValidatorService: FormValidatorService, 83 private formValidatorService: FormValidatorService,
60 private videoValidatorsService: VideoValidatorsService,
61 private videoService: VideoService, 84 private videoService: VideoService,
62 private serverService: ServerService, 85 private serverService: ServerService,
86 private pluginService: PluginService,
63 private instanceService: InstanceService, 87 private instanceService: InstanceService,
64 private i18nPrimengCalendarService: I18nPrimengCalendarService, 88 private i18nPrimengCalendarService: I18nPrimengCalendarService,
65 private ngZone: NgZone 89 private ngZone: NgZone,
90 private hooks: HooksService
66 ) { 91 ) {
67 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() 92 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
68 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() 93 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
@@ -84,22 +109,22 @@ export class VideoEditComponent implements OnInit, OnDestroy {
84 tags: [] 109 tags: []
85 } 110 }
86 const obj: any = { 111 const obj: any = {
87 name: this.videoValidatorsService.VIDEO_NAME, 112 name: VIDEO_NAME_VALIDATOR,
88 privacy: this.videoValidatorsService.VIDEO_PRIVACY, 113 privacy: VIDEO_PRIVACY_VALIDATOR,
89 channelId: this.videoValidatorsService.VIDEO_CHANNEL, 114 channelId: VIDEO_CHANNEL_VALIDATOR,
90 nsfw: null, 115 nsfw: null,
91 commentsEnabled: null, 116 commentsEnabled: null,
92 downloadEnabled: null, 117 downloadEnabled: null,
93 waitTranscoding: null, 118 waitTranscoding: null,
94 category: this.videoValidatorsService.VIDEO_CATEGORY, 119 category: VIDEO_CATEGORY_VALIDATOR,
95 licence: this.videoValidatorsService.VIDEO_LICENCE, 120 licence: VIDEO_LICENCE_VALIDATOR,
96 language: this.videoValidatorsService.VIDEO_LANGUAGE, 121 language: VIDEO_LANGUAGE_VALIDATOR,
97 description: this.videoValidatorsService.VIDEO_DESCRIPTION, 122 description: VIDEO_DESCRIPTION_VALIDATOR,
98 tags: this.videoValidatorsService.VIDEO_TAGS_ARRAY, 123 tags: VIDEO_TAGS_ARRAY_VALIDATOR,
99 previewfile: null, 124 previewfile: null,
100 support: this.videoValidatorsService.VIDEO_SUPPORT, 125 support: VIDEO_SUPPORT_VALIDATOR,
101 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, 126 schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR,
102 originallyPublishedAt: this.videoValidatorsService.VIDEO_ORIGINALLY_PUBLISHED_AT 127 originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR
103 } 128 }
104 129
105 this.formValidatorService.updateForm( 130 this.formValidatorService.updateForm(
@@ -124,19 +149,26 @@ export class VideoEditComponent implements OnInit, OnDestroy {
124 ngOnInit () { 149 ngOnInit () {
125 this.updateForm() 150 this.updateForm()
126 151
152 this.pluginService.ensurePluginsAreLoaded('video-edit')
153 .then(() => this.updatePluginFields())
154
127 this.serverService.getVideoCategories() 155 this.serverService.getVideoCategories()
128 .subscribe(res => this.videoCategories = res) 156 .subscribe(res => this.videoCategories = res)
157
129 this.serverService.getVideoLicences() 158 this.serverService.getVideoLicences()
130 .subscribe(res => this.videoLicences = res) 159 .subscribe(res => this.videoLicences = res)
160
131 forkJoin([ 161 forkJoin([
132 this.instanceService.getAbout(), 162 this.instanceService.getAbout(),
133 this.serverService.getVideoLanguages() 163 this.serverService.getVideoLanguages()
134 ]).pipe(map(([ about, languages ]) => ({ about, languages }))) 164 ]).pipe(map(([ about, languages ]) => ({ about, languages })))
135 .subscribe(res => { 165 .subscribe(res => {
136 this.videoLanguages = res.languages 166 this.videoLanguages = res.languages
137 .map(l => res.about.instance.languages.includes(l.id) 167 .map(l => {
138 ? { ...l, group: $localize`Instance languages`, groupOrder: 0 } 168 return res.about.instance.languages.includes(l.id)
139 : { ...l, group: $localize`All languages`, groupOrder: 1 }) 169 ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
170 : { ...l, group: $localize`All languages`, groupOrder: 1 }
171 })
140 .sort((a, b) => a.groupOrder - b.groupOrder) 172 .sort((a, b) => a.groupOrder - b.groupOrder)
141 }) 173 })
142 174
@@ -161,6 +193,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
161 this.ngZone.runOutsideAngular(() => { 193 this.ngZone.runOutsideAngular(() => {
162 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute 194 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
163 }) 195 })
196
197 this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type })
164 } 198 }
165 199
166 ngOnDestroy () { 200 ngOnDestroy () {
@@ -211,6 +245,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
211 }) 245 })
212 } 246 }
213 247
248 private updatePluginFields () {
249 this.pluginFields = this.pluginService.getRegisteredVideoFormFields(this.type)
250
251 if (this.pluginFields.length === 0) return
252
253 const obj: any = {}
254
255 for (const setting of this.pluginFields) {
256 obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default)
257 }
258
259 this.pluginDataFormGroup = new FormGroup(obj)
260 this.form.addControl('pluginData', this.pluginDataFormGroup)
261
262 this.pluginFieldsAdded.emit()
263 }
264
214 private trackPrivacyChange () { 265 private trackPrivacyChange () {
215 // We will update the schedule input and the wait transcoding checkbox validators 266 // We will update the schedule input and the wait transcoding checkbox validators
216 this.form.controls[ 'privacy' ] 267 this.form.controls[ 'privacy' ]
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
index 8db37a293..785514c76 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -14,7 +14,7 @@
14 <my-help> 14 <my-help>
15 <ng-template ptTemplate="customHtml"> 15 <ng-template ptTemplate="customHtml">
16 <ng-container i18n> 16 <ng-container i18n>
17 You can import any torrent file that points to a mp4 file. 17 You can import any torrent file that points to a media file.
18 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance. 18 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
19 </ng-container> 19 </ng-container>
20 </ng-template> 20 </ng-template>
@@ -58,6 +58,7 @@
58 <my-video-edit 58 <my-video-edit
59 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 59 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
60 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 60 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
61 type="import-torrent"
61 ></my-video-edit> 62 ></my-video-edit>
62 63
63 <div class="submit-container"> 64 <div class="submit-container">
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
index 9b5cc3361..3e4eb5fbc 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
@@ -9,7 +9,7 @@
9 <ng-template ptTemplate="customHtml"> 9 <ng-template ptTemplate="customHtml">
10 <ng-container i18n> 10 <ng-container i18n>
11 You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a> 11 You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a>
12 or URL that points to a raw MP4 file. 12 or URL that points to a media file.
13 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance. 13 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
14 </ng-container> 14 </ng-container>
15 </ng-template> 15 </ng-template>
@@ -54,6 +54,7 @@
54 <my-video-edit 54 <my-video-edit
55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
56 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 56 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
57 type="import-url"
57 ></my-video-edit> 58 ></my-video-edit>
58 59
59 <div class="submit-container"> 60 <div class="submit-container">
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index ed697c25b..677fa1197 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -69,6 +69,7 @@
69 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 69 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
70 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 70 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
71 [waitTranscodingEnabled]="waitTranscodingEnabled" 71 [waitTranscodingEnabled]="waitTranscodingEnabled"
72 type="upload"
72 ></my-video-edit> 73 ></my-video-edit>
73 74
74 <div class="submit-container"> 75 <div class="submit-container">
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 6c1239395..b37596399 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -10,11 +10,12 @@
10 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 10 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" 12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
13 ></my-video-edit> 14 ></my-video-edit>
14 15
15 <div class="submit-container"> 16 <div class="submit-container">
16 <my-button className="orange-button" i18n-label label="Update" icon="circle-tick" 17 <my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
17 (click)="update()" (keydown.enter)="update()" 18 (click)="update()" (keydown.enter)="update()"
18 [disabled]="!form.valid || isUpdatingVideo === true" 19 [disabled]="!form.valid || isUpdatingVideo === true"
19 ></my-button> 20 ></my-button>
20 </div> 21 </div>
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 2e1d0f89d..20438a2d3 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -126,6 +126,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
126 ) 126 )
127 } 127 }
128 128
129 hydratePluginFieldsFromVideo () {
130 if (!this.video.pluginData) return
131
132 this.form.patchValue({
133 pluginData: this.video.pluginData
134 })
135 }
136
129 private hydrateFormFromVideo () { 137 private hydrateFormFromVideo () {
130 this.form.patchValue(this.video.toFormPatch()) 138 this.form.patchValue(this.video.toFormPatch())
131 139
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
index fa20ec3b9..c1d0032cc 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comment-add.component.ts
@@ -2,7 +2,8 @@ import { Observable } from 'rxjs'
2import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { Notifier, User } from '@app/core' 4import { Notifier, User } from '@app/core'
5import { FormReactive, FormValidatorService, VideoCommentValidatorsService } from '@app/shared/shared-forms' 5import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
6import { Video } from '@app/shared/shared-main' 7import { Video } from '@app/shared/shared-main'
7import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 9import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -33,7 +34,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
33 34
34 constructor ( 35 constructor (
35 protected formValidatorService: FormValidatorService, 36 protected formValidatorService: FormValidatorService,
36 private videoCommentValidatorsService: VideoCommentValidatorsService,
37 private notifier: Notifier, 37 private notifier: Notifier,
38 private videoCommentService: VideoCommentService, 38 private videoCommentService: VideoCommentService,
39 private modalService: NgbModal, 39 private modalService: NgbModal,
@@ -50,7 +50,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
50 50
51 ngOnInit () { 51 ngOnInit () {
52 this.buildForm({ 52 this.buildForm({
53 text: this.videoCommentValidatorsService.VIDEO_COMMENT_TEXT 53 text: VIDEO_COMMENT_TEXT_VALIDATOR
54 }) 54 })
55 55
56 if (this.user) { 56 if (this.user) {
diff --git a/client/src/app/+videos/+video-watch/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/video-watch-playlist.component.html
index 246ef83cf..c270142a3 100644
--- a/client/src/app/+videos/+video-watch/video-watch-playlist.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch-playlist.component.html
@@ -1,4 +1,7 @@
1<div *ngIf="playlist && video" class="playlist" myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()"> 1<div
2 *ngIf="playlist && currentPlaylistPosition" class="playlist"
3 myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()"
4>
2 <div class="playlist-info"> 5 <div class="playlist-info">
3 <div class="playlist-display-name"> 6 <div class="playlist-display-name">
4 {{ playlist.displayName }} 7 {{ playlist.displayName }}
@@ -36,7 +39,7 @@
36 </div> 39 </div>
37 </div> 40 </div>
38 41
39 <div *ngFor="let playlistElement of playlistElements"> 42 <div *ngFor="let playlistElement of playlistElements" [ngClass]="'element-' + playlistElement.position">
40 <my-video-playlist-element-miniature 43 <my-video-playlist-element-miniature
41 [playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)" 44 [playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
42 [playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position" 45 [playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position"
diff --git a/client/src/app/+videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/video-watch-playlist.component.ts
index c60ca4671..d76d0bbd2 100644
--- a/client/src/app/+videos/+video-watch/video-watch-playlist.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch-playlist.component.ts
@@ -1,9 +1,10 @@
1import { Component, Input } from '@angular/core' 1
2import { Component, EventEmitter, Input, Output } from '@angular/core'
2import { Router } from '@angular/router' 3import { Router } from '@angular/router'
3import { AuthService, ComponentPagination, LocalStorageService, Notifier, SessionStorageService, UserService } from '@app/core' 4import { AuthService, ComponentPagination, LocalStorageService, Notifier, SessionStorageService, UserService } from '@app/core'
4import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' 5import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist'
5import { peertubeLocalStorage, peertubeSessionStorage } from '@root-helpers/peertube-web-storage' 6import { peertubeLocalStorage, peertubeSessionStorage } from '@root-helpers/peertube-web-storage'
6import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' 7import { VideoPlaylistPrivacy } from '@shared/models'
7 8
8@Component({ 9@Component({
9 selector: 'my-video-watch-playlist', 10 selector: 'my-video-watch-playlist',
@@ -14,9 +15,10 @@ export class VideoWatchPlaylistComponent {
14 static LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'auto_play_video_playlist' 15 static LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'auto_play_video_playlist'
15 static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'loop_playlist' 16 static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'loop_playlist'
16 17
17 @Input() video: VideoDetails
18 @Input() playlist: VideoPlaylist 18 @Input() playlist: VideoPlaylist
19 19
20 @Output() videoFound = new EventEmitter<string>()
21
20 playlistElements: VideoPlaylistElement[] = [] 22 playlistElements: VideoPlaylistElement[] = []
21 playlistPagination: ComponentPagination = { 23 playlistPagination: ComponentPagination = {
22 currentPage: 1, 24 currentPage: 1,
@@ -29,7 +31,8 @@ export class VideoWatchPlaylistComponent {
29 loopPlaylist: boolean 31 loopPlaylist: boolean
30 loopPlaylistSwitchText = '' 32 loopPlaylistSwitchText = ''
31 noPlaylistVideos = false 33 noPlaylistVideos = false
32 currentPlaylistPosition = 1 34
35 currentPlaylistPosition: number
33 36
34 constructor ( 37 constructor (
35 private userService: UserService, 38 private userService: UserService,
@@ -44,6 +47,7 @@ export class VideoWatchPlaylistComponent {
44 this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn() 47 this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn()
45 ? this.auth.getUser().autoPlayNextVideoPlaylist 48 ? this.auth.getUser().autoPlayNextVideoPlaylist
46 : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false' 49 : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false'
50
47 this.setAutoPlayNextVideoPlaylistSwitchText() 51 this.setAutoPlayNextVideoPlaylistSwitchText()
48 52
49 // defaults to false 53 // defaults to false
@@ -51,12 +55,12 @@ export class VideoWatchPlaylistComponent {
51 this.setLoopPlaylistSwitchText() 55 this.setLoopPlaylistSwitchText()
52 } 56 }
53 57
54 onPlaylistVideosNearOfBottom () { 58 onPlaylistVideosNearOfBottom (position?: number) {
55 // Last page 59 // Last page
56 if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return 60 if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return
57 61
58 this.playlistPagination.currentPage += 1 62 this.playlistPagination.currentPage += 1
59 this.loadPlaylistElements(this.playlist,false) 63 this.loadPlaylistElements(this.playlist, false, position)
60 } 64 }
61 65
62 onElementRemoved (playlistElement: VideoPlaylistElement) { 66 onElementRemoved (playlistElement: VideoPlaylistElement) {
@@ -83,26 +87,26 @@ export class VideoWatchPlaylistComponent {
83 return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC 87 return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC
84 } 88 }
85 89
86 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { 90 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false, position?: number) {
87 this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination) 91 this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination)
88 .subscribe(({ total, data }) => { 92 .subscribe(({ total, data }) => {
89 this.playlistElements = this.playlistElements.concat(data) 93 this.playlistElements = this.playlistElements.concat(data)
90 this.playlistPagination.totalItems = total 94 this.playlistPagination.totalItems = total
91 95
92 const firstAvailableVideos = this.playlistElements.find(e => !!e.video) 96 const firstAvailableVideo = this.playlistElements.find(e => !!e.video)
93 if (!firstAvailableVideos) { 97 if (!firstAvailableVideo) {
94 this.noPlaylistVideos = true 98 this.noPlaylistVideos = true
95 return 99 return
96 } 100 }
97 101
98 this.updatePlaylistIndex(this.video) 102 if (position) this.updatePlaylistIndex(position)
99 103
100 if (redirectToFirst) { 104 if (redirectToFirst) {
101 const extras = { 105 const extras = {
102 queryParams: { 106 queryParams: {
103 start: firstAvailableVideos.startTimestamp, 107 start: firstAvailableVideo.startTimestamp,
104 stop: firstAvailableVideos.stopTimestamp, 108 stop: firstAvailableVideo.stopTimestamp,
105 videoId: firstAvailableVideos.video.uuid 109 playlistPosition: firstAvailableVideo.position
106 }, 110 },
107 replaceUrl: true 111 replaceUrl: true
108 } 112 }
@@ -111,18 +115,26 @@ export class VideoWatchPlaylistComponent {
111 }) 115 })
112 } 116 }
113 117
114 updatePlaylistIndex (video: VideoDetails) { 118 updatePlaylistIndex (position: number) {
115 if (this.playlistElements.length === 0 || !video) return 119 if (this.playlistElements.length === 0 || !position) return
116 120
117 for (const playlistElement of this.playlistElements) { 121 for (const playlistElement of this.playlistElements) {
118 if (playlistElement.video && playlistElement.video.id === video.id) { 122 // >= if the previous videos were not valid
123 if (playlistElement.video && playlistElement.position >= position) {
119 this.currentPlaylistPosition = playlistElement.position 124 this.currentPlaylistPosition = playlistElement.position
125
126 this.videoFound.emit(playlistElement.video.uuid)
127
128 setTimeout(() => {
129 document.querySelector('.element-' + this.currentPlaylistPosition).scrollIntoView(false)
130 }, 0)
131
120 return 132 return
121 } 133 }
122 } 134 }
123 135
124 // Load more videos to find our video 136 // Load more videos to find our video
125 this.onPlaylistVideosNearOfBottom() 137 this.onPlaylistVideosNearOfBottom(position)
126 } 138 }
127 139
128 findNextPlaylistVideo (position = this.currentPlaylistPosition): VideoPlaylistElement { 140 findNextPlaylistVideo (position = this.currentPlaylistPosition): VideoPlaylistElement {
@@ -147,9 +159,10 @@ export class VideoWatchPlaylistComponent {
147 navigateToNextPlaylistVideo () { 159 navigateToNextPlaylistVideo () {
148 const next = this.findNextPlaylistVideo(this.currentPlaylistPosition + 1) 160 const next = this.findNextPlaylistVideo(this.currentPlaylistPosition + 1)
149 if (!next) return 161 if (!next) return
162
150 const start = next.startTimestamp 163 const start = next.startTimestamp
151 const stop = next.stopTimestamp 164 const stop = next.stopTimestamp
152 this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } }) 165 this.router.navigate([],{ queryParams: { playlistPosition: next.position, start, stop } })
153 } 166 }
154 167
155 switchAutoPlayNextVideoPlaylist () { 168 switchAutoPlayNextVideoPlaylist () {
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 4279437d2..076c6205f 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -11,7 +11,8 @@
11 11
12 <my-video-watch-playlist 12 <my-video-watch-playlist
13 #videoWatchPlaylist 13 #videoWatchPlaylist
14 [video]="video" [playlist]="playlist" class="playlist" 14 [playlist]="playlist" class="playlist"
15 (videoFound)="onPlaylistVideoFound($event)"
15 ></my-video-watch-playlist> 16 ></my-video-watch-playlist>
16 </div> 17 </div>
17 18
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 fb89bf6cd..1b2820810 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -53,6 +53,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
53 video: VideoDetails = null 53 video: VideoDetails = null
54 videoCaptions: VideoCaption[] = [] 54 videoCaptions: VideoCaption[] = []
55 55
56 playlistPosition: number
56 playlist: VideoPlaylist = null 57 playlist: VideoPlaylist = null
57 58
58 completeDescriptionShown = false 59 completeDescriptionShown = false
@@ -140,9 +141,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
140 if (playlistId) this.loadPlaylist(playlistId) 141 if (playlistId) this.loadPlaylist(playlistId)
141 }) 142 })
142 143
143 this.queryParamsSub = this.route.queryParams.subscribe(async queryParams => { 144 this.queryParamsSub = this.route.queryParams.subscribe(queryParams => {
144 const videoId = queryParams[ 'videoId' ] 145 this.playlistPosition = queryParams[ 'playlistPosition' ]
145 if (videoId) this.loadVideo(videoId) 146 this.videoWatchPlaylist.updatePlaylistIndex(this.playlistPosition)
146 147
147 const start = queryParams[ 'start' ] 148 const start = queryParams[ 'start' ]
148 if (this.player && start) this.player.currentTime(parseInt(start, 10)) 149 if (this.player && start) this.player.currentTime(parseInt(start, 10))
@@ -335,6 +336,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
335 return genericChannelDisplayName.includes(this.video.channel.displayName) 336 return genericChannelDisplayName.includes(this.video.channel.displayName)
336 } 337 }
337 338
339 onPlaylistVideoFound (videoId: string) {
340 this.loadVideo(videoId)
341 }
342
338 private loadVideo (videoId: string) { 343 private loadVideo (videoId: string) {
339 // Video did not change 344 // Video did not change
340 if (this.video && this.video.uuid === videoId) return 345 if (this.video && this.video.uuid === videoId) return
@@ -362,6 +367,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
362 const queryParams = this.route.snapshot.queryParams 367 const queryParams = this.route.snapshot.queryParams
363 368
364 const urlOptions = { 369 const urlOptions = {
370 resume: queryParams.resume,
371
365 startTime: queryParams.start, 372 startTime: queryParams.start,
366 stopTime: queryParams.stop, 373 stopTime: queryParams.stop,
367 374
@@ -390,8 +397,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
390 .subscribe(playlist => { 397 .subscribe(playlist => {
391 this.playlist = playlist 398 this.playlist = playlist
392 399
393 const videoId = this.route.snapshot.queryParams['videoId'] 400 this.videoWatchPlaylist.loadPlaylistElements(playlist, !this.playlistPosition, this.playlistPosition)
394 this.videoWatchPlaylist.loadPlaylistElements(playlist, !videoId)
395 }) 401 })
396 } 402 }
397 403
@@ -456,8 +462,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
456 this.remoteServerDown = false 462 this.remoteServerDown = false
457 this.currentTime = undefined 463 this.currentTime = undefined
458 464
459 this.videoWatchPlaylist.updatePlaylistIndex(video)
460
461 if (this.isVideoBlur(this.video)) { 465 if (this.isVideoBlur(this.video)) {
462 const res = await this.confirmService.confirm( 466 const res = await this.confirmService.confirm(
463 $localize`This video contains mature or explicit content. Are you sure you want to watch it?`, 467 $localize`This video contains mature or explicit content. Are you sure you want to watch it?`,
@@ -544,7 +548,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
544 this.zone.run(() => this.theaterEnabled = enabled) 548 this.zone.run(() => this.theaterEnabled = enabled)
545 }) 549 })
546 550
547 this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { player: this.player }) 551 this.hooks.runAction('action:video-watch.player.loaded', 'video-watch', { player: this.player, videojs, video: this.video })
548 }) 552 })
549 553
550 this.setVideoDescriptionHTML() 554 this.setVideoDescriptionHTML()
@@ -657,16 +661,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
657 const byUrl = urlOptions.startTime !== undefined 661 const byUrl = urlOptions.startTime !== undefined
658 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) 662 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
659 663
660 if (byUrl) { 664 if (byUrl) return timeToInt(urlOptions.startTime)
661 return timeToInt(urlOptions.startTime) 665 if (byHistory) return video.userHistory.currentTime
662 } else if (byHistory) { 666
663 return video.userHistory.currentTime 667 return 0
664 } else {
665 return 0
666 }
667 } 668 }
668 669
669 let startTime = getStartTime() 670 let startTime = getStartTime()
671
670 // If we are at the end of the video, reset the timer 672 // If we are at the end of the video, reset the timer
671 if (video.duration - startTime <= 1) startTime = 0 673 if (video.duration - startTime <= 1) startTime = 0
672 674
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index e0e877fc6..cf5f0b2e8 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -20,7 +20,7 @@ const videosRoutes: Routes = [
20 component: VideoOverviewComponent, 20 component: VideoOverviewComponent,
21 data: { 21 data: {
22 meta: { 22 meta: {
23 title: 'Discover videos' 23 title: $localize`Discover videos`
24 } 24 }
25 } 25 }
26 }, 26 },
@@ -29,7 +29,7 @@ const videosRoutes: Routes = [
29 component: VideoTrendingComponent, 29 component: VideoTrendingComponent,
30 data: { 30 data: {
31 meta: { 31 meta: {
32 title: 'Trending videos' 32 title: $localize`Trending videos`
33 }, 33 },
34 reuse: { 34 reuse: {
35 enabled: true, 35 enabled: true,
@@ -42,7 +42,7 @@ const videosRoutes: Routes = [
42 component: VideoMostLikedComponent, 42 component: VideoMostLikedComponent,
43 data: { 43 data: {
44 meta: { 44 meta: {
45 title: 'Most liked videos' 45 title: $localize`Most liked videos`
46 }, 46 },
47 reuse: { 47 reuse: {
48 enabled: true, 48 enabled: true,
@@ -55,7 +55,7 @@ const videosRoutes: Routes = [
55 component: VideoRecentlyAddedComponent, 55 component: VideoRecentlyAddedComponent,
56 data: { 56 data: {
57 meta: { 57 meta: {
58 title: 'Recently added videos' 58 title: $localize`Recently added videos`
59 }, 59 },
60 reuse: { 60 reuse: {
61 enabled: true, 61 enabled: true,
@@ -68,7 +68,7 @@ const videosRoutes: Routes = [
68 component: VideoUserSubscriptionsComponent, 68 component: VideoUserSubscriptionsComponent,
69 data: { 69 data: {
70 meta: { 70 meta: {
71 title: 'Subscriptions' 71 title: $localize`Subscriptions`
72 }, 72 },
73 reuse: { 73 reuse: {
74 enabled: true, 74 enabled: true,
@@ -81,7 +81,7 @@ const videosRoutes: Routes = [
81 component: VideoLocalComponent, 81 component: VideoLocalComponent,
82 data: { 82 data: {
83 meta: { 83 meta: {
84 title: 'Local videos' 84 title: $localize`Local videos`
85 }, 85 },
86 reuse: { 86 reuse: {
87 enabled: true, 87 enabled: true,
@@ -94,7 +94,7 @@ const videosRoutes: Routes = [
94 loadChildren: () => import('@app/+videos/+video-edit/video-add.module').then(m => m.VideoAddModule), 94 loadChildren: () => import('@app/+videos/+video-edit/video-add.module').then(m => m.VideoAddModule),
95 data: { 95 data: {
96 meta: { 96 meta: {
97 title: 'Upload a video' 97 title: $localize`Upload a video`
98 } 98 }
99 } 99 }
100 }, 100 },
@@ -103,7 +103,7 @@ const videosRoutes: Routes = [
103 loadChildren: () => import('@app/+videos/+video-edit/video-update.module').then(m => m.VideoUpdateModule), 103 loadChildren: () => import('@app/+videos/+video-edit/video-update.module').then(m => m.VideoUpdateModule),
104 data: { 104 data: {
105 meta: { 105 meta: {
106 title: 'Edit a video' 106 title: $localize`Edit a video`
107 } 107 }
108 } 108 }
109 }, 109 },
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 5b0439e6b..edec3216e 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -180,7 +180,7 @@ export class AppComponent implements OnInit, AfterViewInit {
180 180
181 eventsObs.pipe( 181 eventsObs.pipe(
182 filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart), 182 filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
183 filter(() => this.screenService.isInSmallView() || !!this.screenService.isInTouchScreen()) 183 filter(() => this.screenService.isInSmallView() || this.screenService.isInTouchScreen())
184 ).subscribe(() => this.menu.setMenuDisplay(false)) // User clicked on a link in the menu, change the page 184 ).subscribe(() => this.menu.setMenuDisplay(false)) // User clicked on a link in the menu, change the page
185 } 185 }
186 186
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index 34efa24fc..f10b37e5a 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -25,11 +25,13 @@ export class AuthUser extends User implements ServerMyUserModel {
25 canSeeVideosLink = true 25 canSeeVideosLink = true
26 26
27 static load () { 27 static load () {
28 const userInfo = getUserInfoFromLocalStorage() 28 const tokens = Tokens.load()
29 if (!tokens) return null
29 30
31 const userInfo = getUserInfoFromLocalStorage()
30 if (!userInfo) return null 32 if (!userInfo) return null
31 33
32 return new AuthUser(userInfo, Tokens.load()) 34 return new AuthUser(userInfo, tokens)
33 } 35 }
34 36
35 static flush () { 37 static flush () {
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 671ee3e4f..9c0433bca 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -28,15 +28,16 @@ export class MenuService {
28 setMenuDisplay (display: boolean) { 28 setMenuDisplay (display: boolean) {
29 this.isMenuDisplayed = display 29 this.isMenuDisplayed = display
30 30
31 if (!this.screenService.isInTouchScreen()) return
32
31 // On touch screens, lock body scroll and display content overlay when memu is opened 33 // On touch screens, lock body scroll and display content overlay when memu is opened
32 if (this.screenService.isInTouchScreen()) { 34 if (this.isMenuDisplayed) {
33 if (this.isMenuDisplayed) { 35 document.body.classList.add('menu-open')
34 document.body.classList.add('menu-open') 36 this.screenService.onFingerSwipe('left', () => { this.setMenuDisplay(false) })
35 this.screenService.onFingerSwipe('left', () => { this.setMenuDisplay(false) }) 37 return
36 } else {
37 document.body.classList.remove('menu-open')
38 }
39 } 38 }
39
40 document.body.classList.remove('menu-open')
40 } 41 }
41 42
42 onResize () { 43 onResize () {
@@ -45,9 +46,7 @@ export class MenuService {
45 46
46 private handleWindowResize () { 47 private handleWindowResize () {
47 // On touch screens, do not handle window resize event since opened menu is handled with a content overlay 48 // On touch screens, do not handle window resize event since opened menu is handled with a content overlay
48 if (this.screenService.isInTouchScreen()) { 49 if (this.screenService.isInTouchScreen()) return
49 return
50 }
51 50
52 fromEvent(window, 'resize') 51 fromEvent(window, 'resize')
53 .pipe(debounceTime(200)) 52 .pipe(debounceTime(200))
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index dc115c0e1..4e44a1865 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -7,38 +7,22 @@ import { Notifier } from '@app/core/notification'
7import { MarkdownService } from '@app/core/renderer' 7import { MarkdownService } from '@app/core/renderer'
8import { RestExtractor } from '@app/core/rest' 8import { RestExtractor } from '@app/core/rest'
9import { ServerService } from '@app/core/server/server.service' 9import { ServerService } from '@app/core/server/server.service'
10import { getDevLocale, importModule, isOnDevLocale } from '@app/helpers' 10import { getDevLocale, isOnDevLocale } from '@app/helpers'
11import { CustomModalComponent } from '@app/modal/custom-modal.component' 11import { CustomModalComponent } from '@app/modal/custom-modal.component'
12import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
12import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 13import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
13import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
14import { 14import {
15 ClientHook, 15 ClientHook,
16 ClientHookName, 16 ClientHookName,
17 clientHookObject,
18 ClientScript,
19 PluginClientScope, 17 PluginClientScope,
20 PluginTranslation, 18 PluginTranslation,
21 PluginType, 19 PluginType,
22 PublicServerSetting, 20 PublicServerSetting,
23 RegisterClientHookOptions,
24 ServerConfigPlugin 21 ServerConfigPlugin
25} from '@shared/models' 22} from '@shared/models'
26import { environment } from '../../../environments/environment' 23import { environment } from '../../../environments/environment'
27import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
28import { RegisterClientHelpers } from '../../../types/register-client-option.model' 24import { RegisterClientHelpers } from '../../../types/register-client-option.model'
29 25
30interface HookStructValue extends RegisterClientHookOptions {
31 plugin: ServerConfigPlugin
32 clientScript: ClientScript
33}
34
35type PluginInfo = {
36 plugin: ServerConfigPlugin
37 clientScript: ClientScript
38 pluginType: PluginType
39 isTheme: boolean
40}
41
42@Injectable() 26@Injectable()
43export class PluginService implements ClientHook { 27export class PluginService implements ClientHook {
44 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins' 28 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
@@ -51,7 +35,9 @@ export class PluginService implements ClientHook {
51 search: new ReplaySubject<boolean>(1), 35 search: new ReplaySubject<boolean>(1),
52 'video-watch': new ReplaySubject<boolean>(1), 36 'video-watch': new ReplaySubject<boolean>(1),
53 signup: new ReplaySubject<boolean>(1), 37 signup: new ReplaySubject<boolean>(1),
54 login: new ReplaySubject<boolean>(1) 38 login: new ReplaySubject<boolean>(1),
39 'video-edit': new ReplaySubject<boolean>(1),
40 embed: new ReplaySubject<boolean>(1)
55 } 41 }
56 42
57 translationsObservable: Observable<PluginTranslation> 43 translationsObservable: Observable<PluginTranslation>
@@ -64,7 +50,10 @@ export class PluginService implements ClientHook {
64 private loadedScopes: PluginClientScope[] = [] 50 private loadedScopes: PluginClientScope[] = []
65 private loadingScopes: { [id in PluginClientScope]?: boolean } = {} 51 private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
66 52
67 private hooks: { [ name: string ]: HookStructValue[] } = {} 53 private hooks: Hooks = {}
54 private formFields: FormFields = {
55 video: []
56 }
68 57
69 constructor ( 58 constructor (
70 private authService: AuthService, 59 private authService: AuthService,
@@ -120,7 +109,7 @@ export class PluginService implements ClientHook {
120 this.scopes[scope].push({ 109 this.scopes[scope].push({
121 plugin, 110 plugin,
122 clientScript: { 111 clientScript: {
123 script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`, 112 script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
124 scopes: clientScript.scopes 113 scopes: clientScript.scopes
125 }, 114 },
126 pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN, 115 pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
@@ -184,20 +173,8 @@ export class PluginService implements ClientHook {
184 } 173 }
185 174
186 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> { 175 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
187 return this.zone.runOutsideAngular(async () => { 176 return this.zone.runOutsideAngular(() => {
188 if (!this.hooks[ hookName ]) return result 177 return runHook(this.hooks, hookName, result, params)
189
190 const hookType = getHookType(hookName)
191
192 for (const hook of this.hooks[ hookName ]) {
193 console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
194
195 result = await internalRunHook(hook.handler, hookType, result, params, err => {
196 console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
197 })
198 }
199
200 return result
201 }) 178 })
202 } 179 }
203 180
@@ -215,35 +192,18 @@ export class PluginService implements ClientHook {
215 : PluginType.THEME 192 : PluginType.THEME
216 } 193 }
217 194
218 private loadPlugin (pluginInfo: PluginInfo) { 195 getRegisteredVideoFormFields (type: 'import-url' | 'import-torrent' | 'upload' | 'update') {
219 const { plugin, clientScript } = pluginInfo 196 return this.formFields.video.filter(f => f.videoFormOptions.type === type)
220 197 }
221 const registerHook = (options: RegisterClientHookOptions) => {
222 if (clientHookObject[options.target] !== true) {
223 console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
224 return
225 }
226
227 if (!this.hooks[options.target]) this.hooks[options.target] = []
228
229 this.hooks[options.target].push({
230 plugin,
231 clientScript,
232 target: options.target,
233 handler: options.handler,
234 priority: options.priority || 0
235 })
236 }
237
238 const peertubeHelpers = this.buildPeerTubeHelpers(pluginInfo)
239
240 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
241 198
199 private loadPlugin (pluginInfo: PluginInfo) {
242 return this.zone.runOutsideAngular(() => { 200 return this.zone.runOutsideAngular(() => {
243 return importModule(clientScript.script) 201 return loadPlugin({
244 .then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers })) 202 hooks: this.hooks,
245 .then(() => this.sortHooksByPriority()) 203 formFields: this.formFields,
246 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err)) 204 pluginInfo,
205 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo)
206 })
247 }) 207 })
248 } 208 }
249 209
@@ -253,14 +213,6 @@ export class PluginService implements ClientHook {
253 } 213 }
254 } 214 }
255 215
256 private sortHooksByPriority () {
257 for (const hookName of Object.keys(this.hooks)) {
258 this.hooks[hookName].sort((a, b) => {
259 return b.priority - a.priority
260 })
261 }
262 }
263
264 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { 216 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
265 const { plugin } = pluginInfo 217 const { plugin } = pluginInfo
266 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 218 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts
index 88cf662b3..a085e5bdc 100644
--- a/client/src/app/core/wrappers/screen.service.ts
+++ b/client/src/app/core/wrappers/screen.service.ts
@@ -30,7 +30,7 @@ export class ScreenService {
30 } 30 }
31 31
32 isInTouchScreen () { 32 isInTouchScreen () {
33 return 'ontouchstart' in window || navigator.msMaxTouchPoints 33 return !!('ontouchstart' in window || navigator.msMaxTouchPoints)
34 } 34 }
35 35
36 getNumberOfAvailableMiniatures () { 36 getNumberOfAvailableMiniatures () {
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index d05541ca9..d9007dd77 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -148,41 +148,6 @@ function scrollToTop () {
148 window.scroll(0, 0) 148 window.scroll(0, 0)
149} 149}
150 150
151// Thanks: https://github.com/uupaa/dynamic-import-polyfill
152function importModule (path: string) {
153 return new Promise((resolve, reject) => {
154 const vector = '$importModule$' + Math.random().toString(32).slice(2)
155 const script = document.createElement('script')
156
157 const destructor = () => {
158 delete window[ vector ]
159 script.onerror = null
160 script.onload = null
161 script.remove()
162 URL.revokeObjectURL(script.src)
163 script.src = ''
164 }
165
166 script.defer = true
167 script.type = 'module'
168
169 script.onerror = () => {
170 reject(new Error(`Failed to import: ${path}`))
171 destructor()
172 }
173 script.onload = () => {
174 resolve(window[ vector ])
175 destructor()
176 }
177 const absURL = (environment.apiUrl || window.location.origin) + path
178 const loader = `import * as m from "${absURL}"; window.${vector} = m;` // export Module
179 const blob = new Blob([ loader ], { type: 'text/javascript' })
180 script.src = URL.createObjectURL(blob)
181
182 document.head.appendChild(script)
183 })
184}
185
186function isInViewport (el: HTMLElement) { 151function isInViewport (el: HTMLElement) {
187 const bounding = el.getBoundingClientRect() 152 const bounding = el.getBoundingClientRect()
188 return ( 153 return (
@@ -216,7 +181,6 @@ export {
216 getAbsoluteEmbedUrl, 181 getAbsoluteEmbedUrl,
217 objectLineFeedToHtml, 182 objectLineFeedToHtml,
218 removeElementFromArray, 183 removeElementFromArray,
219 importModule,
220 scrollToTop, 184 scrollToTop,
221 isInViewport, 185 isInViewport,
222 isXPercentInViewport 186 isXPercentInViewport
diff --git a/client/src/app/shared/form-validators/abuse-validators.ts b/client/src/app/shared/form-validators/abuse-validators.ts
new file mode 100644
index 000000000..75bfacf01
--- /dev/null
+++ b/client/src/app/shared/form-validators/abuse-validators.ts
@@ -0,0 +1,29 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const ABUSE_REASON_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [Validators.required, Validators.minLength(2), Validators.maxLength(3000)],
6 MESSAGES: {
7 'required': $localize`Report reason is required.`,
8 'minlength': $localize`Report reason must be at least 2 characters long.`,
9 'maxlength': $localize`Report reason cannot be more than 3000 characters long.`
10 }
11}
12
13export const ABUSE_MODERATION_COMMENT_VALIDATOR: BuildFormValidator = {
14 VALIDATORS: [Validators.required, Validators.minLength(2), Validators.maxLength(3000)],
15 MESSAGES: {
16 'required': $localize`Moderation comment is required.`,
17 'minlength': $localize`Moderation comment must be at least 2 characters long.`,
18 'maxlength': $localize`Moderation comment cannot be more than 3000 characters long.`
19 }
20}
21
22export const ABUSE_MESSAGE_VALIDATOR: BuildFormValidator = {
23 VALIDATORS: [Validators.required, Validators.minLength(2), Validators.maxLength(3000)],
24 MESSAGES: {
25 'required': $localize`Abuse message is required.`,
26 'minlength': $localize`Abuse message must be at least 2 characters long.`,
27 'maxlength': $localize`Abuse message cannot be more than 3000 characters long.`
28 }
29}
diff --git a/client/src/app/shared/form-validators/batch-domains-validators.ts b/client/src/app/shared/form-validators/batch-domains-validators.ts
new file mode 100644
index 000000000..423d1337f
--- /dev/null
+++ b/client/src/app/shared/form-validators/batch-domains-validators.ts
@@ -0,0 +1,60 @@
1import { AbstractControl, FormControl, ValidatorFn, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3import { validateHost } from './host'
4
5export function getNotEmptyHosts (hosts: string) {
6 return hosts
7 .split('\n')
8 .filter((host: string) => host && host.length !== 0) // Eject empty hosts
9}
10
11const validDomains: ValidatorFn = (control: FormControl) => {
12 if (!control.value) return null
13
14 const newHostsErrors = []
15 const hosts = getNotEmptyHosts(control.value)
16
17 for (const host of hosts) {
18 if (validateHost(host) === false) {
19 newHostsErrors.push($localize`${host} is not valid`)
20 }
21 }
22
23 /* Is not valid. */
24 if (newHostsErrors.length !== 0) {
25 return {
26 'validDomains': {
27 reason: 'invalid',
28 value: newHostsErrors.join('. ') + '.'
29 }
30 }
31 }
32
33 /* Is valid. */
34 return null
35}
36
37const isHostsUnique: ValidatorFn = (control: AbstractControl) => {
38 if (!control.value) return null
39
40 const hosts = getNotEmptyHosts(control.value)
41
42 if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
43 return null
44 } else {
45 return {
46 'uniqueDomains': {
47 reason: 'invalid'
48 }
49 }
50 }
51}
52
53export const DOMAINS_VALIDATOR: BuildFormValidator = {
54 VALIDATORS: [Validators.required, validDomains, isHostsUnique],
55 MESSAGES: {
56 'required': $localize`Domain is required.`,
57 'validDomains': $localize`Domains entered are invalid.`,
58 'uniqueDomains': $localize`Domains entered contain duplicates.`
59 }
60}
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts
new file mode 100644
index 000000000..41b3cbba9
--- /dev/null
+++ b/client/src/app/shared/form-validators/custom-config-validators.ts
@@ -0,0 +1,80 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const INSTANCE_NAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [Validators.required],
6 MESSAGES: {
7 'required': $localize`Instance name is required.`
8 }
9}
10
11export const INSTANCE_SHORT_DESCRIPTION_VALIDATOR: BuildFormValidator = {
12 VALIDATORS: [Validators.max(250)],
13 MESSAGES: {
14 'max': $localize`Short description should not be longer than 250 characters.`
15 }
16}
17
18export const SERVICES_TWITTER_USERNAME_VALIDATOR: BuildFormValidator = {
19 VALIDATORS: [Validators.required],
20 MESSAGES: {
21 'required': $localize`Twitter username is required.`
22 }
23}
24
25export const CACHE_PREVIEWS_SIZE_VALIDATOR: BuildFormValidator = {
26 VALIDATORS: [Validators.required, Validators.min(1), Validators.pattern('[0-9]+')],
27 MESSAGES: {
28 'required': $localize`Previews cache size is required.`,
29 'min': $localize`Previews cache size must be greater than 1.`,
30 'pattern': $localize`Previews cache size must be a number.`
31 }
32}
33
34export const CACHE_CAPTIONS_SIZE_VALIDATOR: BuildFormValidator = {
35 VALIDATORS: [Validators.required, Validators.min(1), Validators.pattern('[0-9]+')],
36 MESSAGES: {
37 'required': $localize`Captions cache size is required.`,
38 'min': $localize`Captions cache size must be greater than 1.`,
39 'pattern': $localize`Captions cache size must be a number.`
40 }
41}
42
43export const SIGNUP_LIMIT_VALIDATOR: BuildFormValidator = {
44 VALIDATORS: [Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+')],
45 MESSAGES: {
46 'required': $localize`Signup limit is required.`,
47 'min': $localize`Signup limit must be greater than 1.`,
48 'pattern': $localize`Signup limit must be a number.`
49 }
50}
51
52export const ADMIN_EMAIL_VALIDATOR: BuildFormValidator = {
53 VALIDATORS: [Validators.required, Validators.email],
54 MESSAGES: {
55 'required': $localize`Admin email is required.`,
56 'email': $localize`Admin email must be valid.`
57 }
58}
59
60export const TRANSCODING_THREADS_VALIDATOR: BuildFormValidator = {
61 VALIDATORS: [Validators.required, Validators.min(0)],
62 MESSAGES: {
63 'required': $localize`Transcoding threads is required.`,
64 'min': $localize`Transcoding threads must be greater or equal to 0.`
65 }
66}
67
68export const INDEX_URL_VALIDATOR: BuildFormValidator = {
69 VALIDATORS: [Validators.pattern(/^https:\/\//)],
70 MESSAGES: {
71 'pattern': $localize`Index URL should be a URL`
72 }
73}
74
75export const SEARCH_INDEX_URL_VALIDATOR: BuildFormValidator = {
76 VALIDATORS: [Validators.pattern(/^https?:\/\//)],
77 MESSAGES: {
78 'pattern': $localize`Search index URL should be a URL`
79 }
80}
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts
new file mode 100644
index 000000000..248a3b1d3
--- /dev/null
+++ b/client/src/app/shared/form-validators/form-validator.model.ts
@@ -0,0 +1,14 @@
1import { ValidatorFn } from '@angular/forms'
2
3export type BuildFormValidator = {
4 VALIDATORS: ValidatorFn[],
5 MESSAGES: { [ name: string ]: string }
6}
7
8export type BuildFormArgument = {
9 [ id: string ]: BuildFormValidator | BuildFormArgument
10}
11
12export type BuildFormDefaultValues = {
13 [ name: string ]: string | string[] | BuildFormDefaultValues
14}
diff --git a/client/src/app/shared/shared-forms/form-validators/host.ts b/client/src/app/shared/form-validators/host.ts
index c18a35f9b..c18a35f9b 100644
--- a/client/src/app/shared/shared-forms/form-validators/host.ts
+++ b/client/src/app/shared/form-validators/host.ts
diff --git a/client/src/app/shared/form-validators/index.ts b/client/src/app/shared/form-validators/index.ts
new file mode 100644
index 000000000..f621f03a4
--- /dev/null
+++ b/client/src/app/shared/form-validators/index.ts
@@ -0,0 +1,17 @@
1export * from './form-validator.model'
2export * from './host'
3
4// Don't re export const variables because webpack 4 cannot do tree shaking with them
5// export * from './abuse-validators'
6// export * from './batch-domains-validators'
7// export * from './custom-config-validators'
8// export * from './instance-validators'
9// export * from './login-validators'
10// export * from './reset-password-validators'
11// export * from './user-validators'
12// export * from './video-block-validators'
13// export * from './video-captions-validators'
14// export * from './video-channel-validators'
15// export * from './video-comment-validators'
16// export * from './video-playlist-validators'
17// export * from './video-validators'
diff --git a/client/src/app/shared/form-validators/instance-validators.ts b/client/src/app/shared/form-validators/instance-validators.ts
new file mode 100644
index 000000000..a72e28ba0
--- /dev/null
+++ b/client/src/app/shared/form-validators/instance-validators.ts
@@ -0,0 +1,49 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const FROM_EMAIL_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [Validators.required, Validators.email],
6 MESSAGES: {
7 'required': $localize`Email is required.`,
8 'email': $localize`Email must be valid.`
9 }
10}
11
12export const FROM_NAME_VALIDATOR: BuildFormValidator = {
13 VALIDATORS: [
14 Validators.required,
15 Validators.minLength(1),
16 Validators.maxLength(120)
17 ],
18 MESSAGES: {
19 'required': $localize`Your name is required.`,
20 'minlength': $localize`Your name must be at least 1 character long.`,
21 'maxlength': $localize`Your name cannot be more than 120 characters long.`
22 }
23}
24
25export const SUBJECT_VALIDATOR: BuildFormValidator = {
26 VALIDATORS: [
27 Validators.required,
28 Validators.minLength(1),
29 Validators.maxLength(120)
30 ],
31 MESSAGES: {
32 'required': $localize`A subject is required.`,
33 'minlength': $localize`The subject must be at least 1 character long.`,
34 'maxlength': $localize`The subject cannot be more than 120 characters long.`
35 }
36}
37
38export const BODY_VALIDATOR: BuildFormValidator = {
39 VALIDATORS: [
40 Validators.required,
41 Validators.minLength(3),
42 Validators.maxLength(5000)
43 ],
44 MESSAGES: {
45 'required': $localize`A message is required.`,
46 'minlength': $localize`The message must be at least 3 characters long.`,
47 'maxlength': $localize`The message cannot be more than 5000 characters long.`
48 }
49}
diff --git a/client/src/app/shared/form-validators/login-validators.ts b/client/src/app/shared/form-validators/login-validators.ts
new file mode 100644
index 000000000..1ceae1be3
--- /dev/null
+++ b/client/src/app/shared/form-validators/login-validators.ts
@@ -0,0 +1,20 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const LOGIN_USERNAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [
6 Validators.required
7 ],
8 MESSAGES: {
9 'required': $localize`Username is required.`
10 }
11}
12
13export const LOGIN_PASSWORD_VALIDATOR: BuildFormValidator = {
14 VALIDATORS: [
15 Validators.required
16 ],
17 MESSAGES: {
18 'required': $localize`Password is required.`
19 }
20}
diff --git a/client/src/app/shared/form-validators/reset-password-validators.ts b/client/src/app/shared/form-validators/reset-password-validators.ts
new file mode 100644
index 000000000..b87f2eab9
--- /dev/null
+++ b/client/src/app/shared/form-validators/reset-password-validators.ts
@@ -0,0 +1,11 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const RESET_PASSWORD_CONFIRM_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [
6 Validators.required
7 ],
8 MESSAGES: {
9 'required': $localize`Confirmation of the password is required.`
10 }
11}
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
new file mode 100644
index 000000000..18199505c
--- /dev/null
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -0,0 +1,144 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const USER_USERNAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [
6 Validators.required,
7 Validators.minLength(1),
8 Validators.maxLength(50),
9 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
10 ],
11 MESSAGES: {
12 'required': $localize`Username is required.`,
13 'minlength': $localize`Username must be at least 1 character long.`,
14 'maxlength': $localize`Username cannot be more than 50 characters long.`,
15 'pattern': $localize`Username should be lowercase alphanumeric; dots and underscores are allowed.`
16 }
17}
18
19export const USER_CHANNEL_NAME_VALIDATOR: BuildFormValidator = {
20 VALIDATORS: [
21 Validators.required,
22 Validators.minLength(1),
23 Validators.maxLength(50),
24 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
25 ],
26 MESSAGES: {
27 'required': $localize`Channel name is required.`,
28 'minlength': $localize`Channel name must be at least 1 character long.`,
29 'maxlength': $localize`Channel name cannot be more than 50 characters long.`,
30 'pattern': $localize`Channel name should be lowercase alphanumeric; dots and underscores are allowed.`
31 }
32}
33
34export const USER_EMAIL_VALIDATOR: BuildFormValidator = {
35 VALIDATORS: [ Validators.required, Validators.email ],
36 MESSAGES: {
37 'required': $localize`Email is required.`,
38 'email': $localize`Email must be valid.`
39 }
40}
41
42export const USER_PASSWORD_VALIDATOR: BuildFormValidator = {
43 VALIDATORS: [
44 Validators.required,
45 Validators.minLength(6),
46 Validators.maxLength(255)
47 ],
48 MESSAGES: {
49 'required': $localize`Password is required.`,
50 'minlength': $localize`Password must be at least 6 characters long.`,
51 'maxlength': $localize`Password cannot be more than 255 characters long.`
52 }
53}
54
55export const USER_PASSWORD_OPTIONAL_VALIDATOR: BuildFormValidator = {
56 VALIDATORS: [
57 Validators.minLength(6),
58 Validators.maxLength(255)
59 ],
60 MESSAGES: {
61 'minlength': $localize`Password must be at least 6 characters long.`,
62 'maxlength': $localize`Password cannot be more than 255 characters long.`
63 }
64}
65
66export const USER_CONFIRM_PASSWORD_VALIDATOR: BuildFormValidator = {
67 VALIDATORS: [],
68 MESSAGES: {
69 'matchPassword': $localize`The new password and the confirmed password do not correspond.`
70 }
71}
72
73export const USER_VIDEO_QUOTA_VALIDATOR: BuildFormValidator = {
74 VALIDATORS: [ Validators.required, Validators.min(-1) ],
75 MESSAGES: {
76 'required': $localize`Video quota is required.`,
77 'min': $localize`Quota must be greater than -1.`
78 }
79}
80export const USER_VIDEO_QUOTA_DAILY_VALIDATOR: BuildFormValidator = {
81 VALIDATORS: [ Validators.required, Validators.min(-1) ],
82 MESSAGES: {
83 'required': $localize`Daily upload limit is required.`,
84 'min': $localize`Daily upload limit must be greater than -1.`
85 }
86}
87
88export const USER_ROLE_VALIDATOR: BuildFormValidator = {
89 VALIDATORS: [ Validators.required ],
90 MESSAGES: {
91 'required': $localize`User role is required.`
92 }
93}
94
95export const USER_DISPLAY_NAME_REQUIRED_VALIDATOR = buildDisplayNameValidator(true)
96
97export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = {
98 VALIDATORS: [
99 Validators.minLength(3),
100 Validators.maxLength(1000)
101 ],
102 MESSAGES: {
103 'minlength': $localize`Description must be at least 3 characters long.`,
104 'maxlength': $localize`Description cannot be more than 1000 characters long.`
105 }
106}
107
108export const USER_TERMS_VALIDATOR: BuildFormValidator = {
109 VALIDATORS: [
110 Validators.requiredTrue
111 ],
112 MESSAGES: {
113 'required': $localize`You must agree with the instance terms in order to register on it.`
114 }
115}
116
117export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = {
118 VALIDATORS: [
119 Validators.minLength(3),
120 Validators.maxLength(250)
121 ],
122 MESSAGES: {
123 'minlength': $localize`Ban reason must be at least 3 characters long.`,
124 'maxlength': $localize`Ban reason cannot be more than 250 characters long.`
125 }
126}
127
128function buildDisplayNameValidator (required: boolean) {
129 const control = {
130 VALIDATORS: [
131 Validators.minLength(1),
132 Validators.maxLength(120)
133 ],
134 MESSAGES: {
135 'required': $localize`Display name is required.`,
136 'minlength': $localize`Display name must be at least 1 character long.`,
137 'maxlength': $localize`Display name cannot be more than 50 characters long.`
138 }
139 }
140
141 if (required) control.VALIDATORS.push(Validators.required)
142
143 return control
144}
diff --git a/client/src/app/shared/form-validators/video-block-validators.ts b/client/src/app/shared/form-validators/video-block-validators.ts
new file mode 100644
index 000000000..d3974aefe
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-block-validators.ts
@@ -0,0 +1,10 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const VIDEO_BLOCK_REASON_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(300) ],
6 MESSAGES: {
7 'minlength': $localize`Block reason must be at least 2 characters long.`,
8 'maxlength': $localize`Block reason cannot be more than 300 characters long.`
9 }
10}
diff --git a/client/src/app/shared/form-validators/video-captions-validators.ts b/client/src/app/shared/form-validators/video-captions-validators.ts
new file mode 100644
index 000000000..9742d2925
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-captions-validators.ts
@@ -0,0 +1,16 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const VIDEO_CAPTION_LANGUAGE_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required ],
6 MESSAGES: {
7 'required': $localize`Video caption language is required.`
8 }
9}
10
11export const VIDEO_CAPTION_FILE_VALIDATOR: BuildFormValidator = {
12 VALIDATORS: [ Validators.required ],
13 MESSAGES: {
14 'required': $localize`Video caption file is required.`
15 }
16}
diff --git a/client/src/app/shared/form-validators/video-channel-validators.ts b/client/src/app/shared/form-validators/video-channel-validators.ts
new file mode 100644
index 000000000..0daab22ce
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-channel-validators.ts
@@ -0,0 +1,52 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const VIDEO_CHANNEL_NAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [
6 Validators.required,
7 Validators.minLength(1),
8 Validators.maxLength(50),
9 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
10 ],
11 MESSAGES: {
12 'required': $localize`Name is required.`,
13 'minlength': $localize`Name must be at least 1 character long.`,
14 'maxlength': $localize`Name cannot be more than 50 characters long.`,
15 'pattern': $localize`Name should be lowercase alphanumeric; dots and underscores are allowed.`
16 }
17}
18
19export const VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR: BuildFormValidator = {
20 VALIDATORS: [
21 Validators.required,
22 Validators.minLength(1),
23 Validators.maxLength(50)
24 ],
25 MESSAGES: {
26 'required': $localize`Display name is required.`,
27 'minlength': $localize`Display name must be at least 1 character long.`,
28 'maxlength': $localize`Display name cannot be more than 50 characters long.`
29 }
30}
31
32export const VIDEO_CHANNEL_DESCRIPTION_VALIDATOR: BuildFormValidator = {
33 VALIDATORS: [
34 Validators.minLength(3),
35 Validators.maxLength(1000)
36 ],
37 MESSAGES: {
38 'minlength': $localize`Description must be at least 3 characters long.`,
39 'maxlength': $localize`Description cannot be more than 1000 characters long.`
40 }
41}
42
43export const VIDEO_CHANNEL_SUPPORT_VALIDATOR: BuildFormValidator = {
44 VALIDATORS: [
45 Validators.minLength(3),
46 Validators.maxLength(1000)
47 ],
48 MESSAGES: {
49 'minlength': $localize`Support text must be at least 3 characters long.`,
50 'maxlength': $localize`Support text cannot be more than 1000 characters long`
51 }
52}
diff --git a/client/src/app/shared/form-validators/video-comment-validators.ts b/client/src/app/shared/form-validators/video-comment-validators.ts
new file mode 100644
index 000000000..c56564d34
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-comment-validators.ts
@@ -0,0 +1,11 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const VIDEO_COMMENT_TEXT_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required, Validators.minLength(1), Validators.maxLength(3000) ],
6 MESSAGES: {
7 'required': $localize`Comment is required.`,
8 'minlength': $localize`Comment must be at least 2 characters long.`,
9 'maxlength': $localize`Comment cannot be more than 3000 characters long.`
10 }
11}
diff --git a/client/src/app/shared/form-validators/video-ownership-change-validators.ts b/client/src/app/shared/form-validators/video-ownership-change-validators.ts
new file mode 100644
index 000000000..e1a2df8a6
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-ownership-change-validators.ts
@@ -0,0 +1,25 @@
1import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const OWNERSHIP_CHANGE_CHANNEL_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required ],
6 MESSAGES: {
7 'required': $localize`The channel is required.`
8 }
9}
10
11export const OWNERSHIP_CHANGE_USERNAME_VALIDATOR: BuildFormValidator = {
12 VALIDATORS: [ Validators.required, localAccountValidator ],
13 MESSAGES: {
14 'required': $localize`The username is required.`,
15 'localAccountOnly': $localize`You can only transfer ownership to a local account`
16 }
17}
18
19function localAccountValidator (control: AbstractControl): ValidationErrors {
20 if (control.value.includes('@')) {
21 return { 'localAccountOnly': true }
22 }
23
24 return null
25}
diff --git a/client/src/app/shared/form-validators/video-playlist-validators.ts b/client/src/app/shared/form-validators/video-playlist-validators.ts
new file mode 100644
index 000000000..7e3d29458
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-playlist-validators.ts
@@ -0,0 +1,54 @@
1import { Validators, AbstractControl } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3import { VideoPlaylistPrivacy } from '@shared/models'
4
5export const VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR: BuildFormValidator = {
6 VALIDATORS: [
7 Validators.required,
8 Validators.minLength(1),
9 Validators.maxLength(120)
10 ],
11 MESSAGES: {
12 'required': $localize`Display name is required.`,
13 'minlength': $localize`Display name must be at least 1 character long.`,
14 'maxlength': $localize`Display name cannot be more than 120 characters long.`
15 }
16}
17
18export const VIDEO_PLAYLIST_PRIVACY_VALIDATOR: BuildFormValidator = {
19 VALIDATORS: [
20 Validators.required
21 ],
22 MESSAGES: {
23 'required': $localize`Privacy is required.`
24 }
25}
26
27export const VIDEO_PLAYLIST_DESCRIPTION_VALIDATOR: BuildFormValidator = {
28 VALIDATORS: [
29 Validators.minLength(3),
30 Validators.maxLength(1000)
31 ],
32 MESSAGES: {
33 'minlength': $localize`Description must be at least 3 characters long.`,
34 'maxlength': $localize`Description cannot be more than 1000 characters long.`
35 }
36}
37
38export const VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR: BuildFormValidator = {
39 VALIDATORS: [],
40 MESSAGES: {
41 'required': $localize`The channel is required when the playlist is public.`
42 }
43}
44
45export function setPlaylistChannelValidator (channelControl: AbstractControl, privacy: VideoPlaylistPrivacy) {
46 if (privacy.toString() === VideoPlaylistPrivacy.PUBLIC.toString()) {
47 channelControl.setValidators([Validators.required])
48 } else {
49 channelControl.setValidators(null)
50 }
51
52 channelControl.markAsDirty()
53 channelControl.updateValueAndValidity()
54}
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts
new file mode 100644
index 000000000..23f2391b2
--- /dev/null
+++ b/client/src/app/shared/form-validators/video-validators.ts
@@ -0,0 +1,101 @@
1import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export const VIDEO_NAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ],
6 MESSAGES: {
7 'required': $localize`Video name is required.`,
8 'minlength': $localize`Video name must be at least 3 characters long.`,
9 'maxlength': $localize`Video name cannot be more than 120 characters long.`
10 }
11}
12
13export const VIDEO_PRIVACY_VALIDATOR: BuildFormValidator = {
14 VALIDATORS: [ Validators.required ],
15 MESSAGES: {
16 'required': $localize`Video privacy is required.`
17 }
18}
19
20export const VIDEO_CATEGORY_VALIDATOR: BuildFormValidator = {
21 VALIDATORS: [ ],
22 MESSAGES: {}
23}
24
25export const VIDEO_LICENCE_VALIDATOR: BuildFormValidator = {
26 VALIDATORS: [ ],
27 MESSAGES: {}
28}
29
30export const VIDEO_LANGUAGE_VALIDATOR: BuildFormValidator = {
31 VALIDATORS: [ ],
32 MESSAGES: {}
33}
34
35export const VIDEO_IMAGE_VALIDATOR: BuildFormValidator = {
36 VALIDATORS: [ ],
37 MESSAGES: {}
38}
39
40export const VIDEO_CHANNEL_VALIDATOR: BuildFormValidator = {
41 VALIDATORS: [ Validators.required ],
42 MESSAGES: {
43 'required': $localize`Video channel is required.`
44 }
45}
46
47export const VIDEO_DESCRIPTION_VALIDATOR: BuildFormValidator = {
48 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(10000) ],
49 MESSAGES: {
50 'minlength': $localize`Video description must be at least 3 characters long.`,
51 'maxlength': $localize`Video description cannot be more than 10000 characters long.`
52 }
53}
54
55export const VIDEO_TAG_VALIDATOR: BuildFormValidator = {
56 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ],
57 MESSAGES: {
58 'minlength': $localize`A tag should be more than 2 characters long.`,
59 'maxlength': $localize`A tag should be less than 30 characters long.`
60 }
61}
62
63export const VIDEO_TAGS_ARRAY_VALIDATOR: BuildFormValidator = {
64 VALIDATORS: [ Validators.maxLength(5), arrayTagLengthValidator() ],
65 MESSAGES: {
66 'maxlength': $localize`A maximum of 5 tags can be used on a video.`,
67 'arrayTagLength': $localize`A tag should be more than 2, and less than 30 characters long.`
68 }
69}
70
71export const VIDEO_SUPPORT_VALIDATOR: BuildFormValidator = {
72 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ],
73 MESSAGES: {
74 'minlength': $localize`Video support must be at least 3 characters long.`,
75 'maxlength': $localize`Video support cannot be more than 1000 characters long.`
76 }
77}
78
79export const VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR: BuildFormValidator = {
80 VALIDATORS: [ ],
81 MESSAGES: {
82 'required': $localize`A date is required to schedule video update.`
83 }
84}
85
86export const VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR: BuildFormValidator = {
87 VALIDATORS: [ ],
88 MESSAGES: {}
89}
90
91function arrayTagLengthValidator (min = 2, max = 30): ValidatorFn {
92 return (control: AbstractControl): ValidationErrors => {
93 const array = control.value as Array<string>
94
95 if (array.every(e => e.length > min && e.length < max)) {
96 return null
97 }
98
99 return { 'arrayTagLength': true }
100 }
101}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
index 0c3c8ff48..9abb4c094 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -1,9 +1,10 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, HtmlRendererService, Notifier } from '@app/core' 2import { AuthService, HtmlRendererService, Notifier } from '@app/core'
3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { AbuseMessage, UserAbuse } from '@shared/models' 6import { AbuseMessage, UserAbuse } from '@shared/models'
7import { ABUSE_MESSAGE_VALIDATOR } from '../form-validators/abuse-validators'
7import { AbuseService } from '../shared-moderation' 8import { AbuseService } from '../shared-moderation'
8 9
9@Component({ 10@Component({
@@ -28,7 +29,6 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
28 29
29 constructor ( 30 constructor (
30 protected formValidatorService: FormValidatorService, 31 protected formValidatorService: FormValidatorService,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private modalService: NgbModal, 32 private modalService: NgbModal,
33 private htmlRenderer: HtmlRendererService, 33 private htmlRenderer: HtmlRendererService,
34 private auth: AuthService, 34 private auth: AuthService,
@@ -40,7 +40,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
40 40
41 ngOnInit () { 41 ngOnInit () {
42 this.buildForm({ 42 this.buildForm({
43 message: this.abuseValidatorsService.ABUSE_MESSAGE 43 message: ABUSE_MESSAGE_VALIDATOR
44 }) 44 })
45 } 45 }
46 46
diff --git a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
index fad7f888d..876aeea8a 100644
--- a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
@@ -1,10 +1,11 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { AbuseService } from '@app/shared/shared-moderation' 4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { AdminAbuse } from '@shared/models' 7import { AdminAbuse } from '@shared/models'
8import { ABUSE_MODERATION_COMMENT_VALIDATOR } from '../form-validators/abuse-validators'
8 9
9@Component({ 10@Component({
10 selector: 'my-moderation-comment-modal', 11 selector: 'my-moderation-comment-modal',
@@ -22,15 +23,14 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
22 protected formValidatorService: FormValidatorService, 23 protected formValidatorService: FormValidatorService,
23 private modalService: NgbModal, 24 private modalService: NgbModal,
24 private notifier: Notifier, 25 private notifier: Notifier,
25 private abuseService: AbuseService, 26 private abuseService: AbuseService
26 private abuseValidatorsService: AbuseValidatorsService
27 ) { 27 ) {
28 super() 28 super()
29 } 29 }
30 30
31 ngOnInit () { 31 ngOnInit () {
32 this.buildForm({ 32 this.buildForm({
33 moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT 33 moderationComment: ABUSE_MODERATION_COMMENT_VALIDATOR
34 }) 34 })
35 } 35 }
36 36
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.html b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
new file mode 100644
index 000000000..17b4a134f
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
@@ -0,0 +1,37 @@
1<div [formGroup]="form">
2 <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label>
3
4 <div *ngIf="setting.descriptionHTML" class="label-small-info" [innerHTML]="setting.descriptionHTML"></div>
5
6 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
7
8 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
9
10 <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
11
12 <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
13
14 <my-markdown-textarea
15 *ngIf="setting.type === 'markdown-text'"
16 markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
17 [classes]="{ 'input-error': formErrors['settings.name'] }"
18 ></my-markdown-textarea>
19
20 <my-markdown-textarea
21 *ngIf="setting.type === 'markdown-enhanced'"
22 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
23 [classes]="{ 'input-error': formErrors['settings.name'] }"
24 ></my-markdown-textarea>
25
26 <my-peertube-checkbox
27 *ngIf="setting.type === 'input-checkbox'"
28 [id]="setting.name"
29 [formControlName]="setting.name"
30 [labelInnerHTML]="setting.label"
31 ></my-peertube-checkbox>
32
33 <div *ngIf="formErrors[setting.name]" class="form-error">
34 {{ formErrors[setting.name] }}
35 </div>
36
37</div>
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.scss b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
new file mode 100644
index 000000000..89193ed85
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
@@ -0,0 +1,24 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]) {
5 @include peertube-input-text(340px);
6
7 display: block;
8}
9
10textarea {
11 @include peertube-textarea(340px, 200px);
12
13 display: block;
14}
15
16.peertube-select-container {
17 @include peertube-select-container(340px);
18}
19
20.label-small-info {
21 font-style: italic;
22 margin-bottom: 10px;
23 font-size: 13px;
24}
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.ts b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
new file mode 100644
index 000000000..b63890797
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
@@ -0,0 +1,15 @@
1import { Component, Input } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3import { RegisterClientFormFieldOptions } from '@shared/models'
4
5@Component({
6 selector: 'my-dynamic-form-field',
7 templateUrl: './dynamic-form-field.component.html',
8 styleUrls: [ './dynamic-form-field.component.scss' ]
9})
10
11export class DynamicFormFieldComponent {
12 @Input() form: FormGroup
13 @Input() formErrors: any
14 @Input() setting: RegisterClientFormFieldOptions
15}
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts
index caa31d831..adf6cb894 100644
--- a/client/src/app/shared/shared-forms/form-reactive.ts
+++ b/client/src/app/shared/shared-forms/form-reactive.ts
@@ -1,5 +1,6 @@
1import { FormGroup } from '@angular/forms' 1import { FormGroup } from '@angular/forms'
2import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from './form-validators' 2import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
3import { FormValidatorService } from './form-validator.service'
3 4
4export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } 5export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
5export type FormReactiveValidationMessages = { 6export type FormReactiveValidationMessages = {
diff --git a/client/src/app/shared/shared-forms/form-validators/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts
index dec7d8d9a..41c8b76bd 100644
--- a/client/src/app/shared/shared-forms/form-validators/form-validator.service.ts
+++ b/client/src/app/shared/shared-forms/form-validator.service.ts
@@ -1,17 +1,7 @@
1import { FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
2import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
3import { FormReactiveErrors, FormReactiveValidationMessages } from '../form-reactive' 2import { FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
4 3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
5export type BuildFormValidator = { 4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive'
6 VALIDATORS: ValidatorFn[],
7 MESSAGES: { [ name: string ]: string }
8}
9export type BuildFormArgument = {
10 [ id: string ]: BuildFormValidator | BuildFormArgument
11}
12export type BuildFormDefaultValues = {
13 [ name: string ]: string | string[] | BuildFormDefaultValues
14}
15 5
16@Injectable() 6@Injectable()
17export class FormValidatorService { 7export class FormValidatorService {
diff --git a/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts
deleted file mode 100644
index 56d30d6f9..000000000
--- a/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class AbuseValidatorsService {
7 readonly ABUSE_REASON: BuildFormValidator
8 readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
9 readonly ABUSE_MESSAGE: BuildFormValidator
10
11 constructor () {
12 this.ABUSE_REASON = {
13 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
14 MESSAGES: {
15 'required': $localize`Report reason is required.`,
16 'minlength': $localize`Report reason must be at least 2 characters long.`,
17 'maxlength': $localize`Report reason cannot be more than 3000 characters long.`
18 }
19 }
20
21 this.ABUSE_MODERATION_COMMENT = {
22 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
23 MESSAGES: {
24 'required': $localize`Moderation comment is required.`,
25 'minlength': $localize`Moderation comment must be at least 2 characters long.`,
26 'maxlength': $localize`Moderation comment cannot be more than 3000 characters long.`
27 }
28 }
29
30 this.ABUSE_MESSAGE = {
31 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
32 MESSAGES: {
33 'required': $localize`Abuse message is required.`,
34 'minlength': $localize`Abuse message must be at least 2 characters long.`,
35 'maxlength': $localize`Abuse message cannot be more than 3000 characters long.`
36 }
37 }
38 }
39}
diff --git a/client/src/app/shared/shared-forms/form-validators/batch-domains-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/batch-domains-validators.service.ts
deleted file mode 100644
index 6c7da833f..000000000
--- a/client/src/app/shared/shared-forms/form-validators/batch-domains-validators.service.ts
+++ /dev/null
@@ -1,68 +0,0 @@
1import { Injectable } from '@angular/core'
2import { ValidatorFn, Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4import { validateHost } from './host'
5
6@Injectable()
7export class BatchDomainsValidatorsService {
8 readonly DOMAINS: BuildFormValidator
9
10 constructor () {
11 this.DOMAINS = {
12 VALIDATORS: [ Validators.required, this.validDomains, this.isHostsUnique ],
13 MESSAGES: {
14 'required': $localize`Domain is required.`,
15 'validDomains': $localize`Domains entered are invalid.`,
16 'uniqueDomains': $localize`Domains entered contain duplicates.`
17 }
18 }
19 }
20
21 getNotEmptyHosts (hosts: string) {
22 return hosts
23 .split('\n')
24 .filter((host: string) => host && host.length !== 0) // Eject empty hosts
25 }
26
27 private validDomains: ValidatorFn = (control) => {
28 if (!control.value) return null
29
30 const newHostsErrors = []
31 const hosts = this.getNotEmptyHosts(control.value)
32
33 for (const host of hosts) {
34 if (validateHost(host) === false) {
35 newHostsErrors.push($localize`${host} is not valid`)
36 }
37 }
38
39 /* Is not valid. */
40 if (newHostsErrors.length !== 0) {
41 return {
42 'validDomains': {
43 reason: 'invalid',
44 value: newHostsErrors.join('. ') + '.'
45 }
46 }
47 }
48
49 /* Is valid. */
50 return null
51 }
52
53 private isHostsUnique: ValidatorFn = (control) => {
54 if (!control.value) return null
55
56 const hosts = this.getNotEmptyHosts(control.value)
57
58 if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
59 return null
60 } else {
61 return {
62 'uniqueDomains': {
63 reason: 'invalid'
64 }
65 }
66 }
67 }
68}
diff --git a/client/src/app/shared/shared-forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/custom-config-validators.service.ts
deleted file mode 100644
index 862ff5470..000000000
--- a/client/src/app/shared/shared-forms/form-validators/custom-config-validators.service.ts
+++ /dev/null
@@ -1,97 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class CustomConfigValidatorsService {
7 readonly INSTANCE_NAME: BuildFormValidator
8 readonly INSTANCE_SHORT_DESCRIPTION: BuildFormValidator
9 readonly SERVICES_TWITTER_USERNAME: BuildFormValidator
10 readonly CACHE_PREVIEWS_SIZE: BuildFormValidator
11 readonly CACHE_CAPTIONS_SIZE: BuildFormValidator
12 readonly SIGNUP_LIMIT: BuildFormValidator
13 readonly ADMIN_EMAIL: BuildFormValidator
14 readonly TRANSCODING_THREADS: BuildFormValidator
15 readonly INDEX_URL: BuildFormValidator
16 readonly SEARCH_INDEX_URL: BuildFormValidator
17
18 constructor () {
19 this.INSTANCE_NAME = {
20 VALIDATORS: [ Validators.required ],
21 MESSAGES: {
22 'required': $localize`Instance name is required.`
23 }
24 }
25
26 this.INSTANCE_SHORT_DESCRIPTION = {
27 VALIDATORS: [ Validators.max(250) ],
28 MESSAGES: {
29 'max': $localize`Short description should not be longer than 250 characters.`
30 }
31 }
32
33 this.SERVICES_TWITTER_USERNAME = {
34 VALIDATORS: [ Validators.required ],
35 MESSAGES: {
36 'required': $localize`Twitter username is required.`
37 }
38 }
39
40 this.CACHE_PREVIEWS_SIZE = {
41 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
42 MESSAGES: {
43 'required': $localize`Previews cache size is required.`,
44 'min': $localize`Previews cache size must be greater than 1.`,
45 'pattern': $localize`Previews cache size must be a number.`
46 }
47 }
48
49 this.CACHE_CAPTIONS_SIZE = {
50 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
51 MESSAGES: {
52 'required': $localize`Captions cache size is required.`,
53 'min': $localize`Captions cache size must be greater than 1.`,
54 'pattern': $localize`Captions cache size must be a number.`
55 }
56 }
57
58 this.SIGNUP_LIMIT = {
59 VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ],
60 MESSAGES: {
61 'required': $localize`Signup limit is required.`,
62 'min': $localize`Signup limit must be greater than 1.`,
63 'pattern': $localize`Signup limit must be a number.`
64 }
65 }
66
67 this.ADMIN_EMAIL = {
68 VALIDATORS: [ Validators.required, Validators.email ],
69 MESSAGES: {
70 'required': $localize`Admin email is required.`,
71 'email': $localize`Admin email must be valid.`
72 }
73 }
74
75 this.TRANSCODING_THREADS = {
76 VALIDATORS: [ Validators.required, Validators.min(0) ],
77 MESSAGES: {
78 'required': $localize`Transcoding threads is required.`,
79 'min': $localize`Transcoding threads must be greater or equal to 0.`
80 }
81 }
82
83 this.INDEX_URL = {
84 VALIDATORS: [ Validators.pattern(/^https:\/\//) ],
85 MESSAGES: {
86 'pattern': $localize`Index URL should be a URL`
87 }
88 }
89
90 this.SEARCH_INDEX_URL = {
91 VALIDATORS: [ Validators.pattern(/^https?:\/\//) ],
92 MESSAGES: {
93 'pattern': $localize`Search index URL should be a URL`
94 }
95 }
96 }
97}
diff --git a/client/src/app/shared/shared-forms/form-validators/index.ts b/client/src/app/shared/shared-forms/form-validators/index.ts
deleted file mode 100644
index b06a326ff..000000000
--- a/client/src/app/shared/shared-forms/form-validators/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
1export * from './abuse-validators.service'
2export * from './batch-domains-validators.service'
3export * from './custom-config-validators.service'
4export * from './form-validator.service'
5export * from './host'
6export * from './instance-validators.service'
7export * from './login-validators.service'
8export * from './reset-password-validators.service'
9export * from './user-validators.service'
10export * from './video-accept-ownership-validators.service'
11export * from './video-block-validators.service'
12export * from './video-captions-validators.service'
13export * from './video-change-ownership-validators.service'
14export * from './video-channel-validators.service'
15export * from './video-comment-validators.service'
16export * from './video-playlist-validators.service'
17export * from './video-validators.service'
diff --git a/client/src/app/shared/shared-forms/form-validators/instance-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/instance-validators.service.ts
deleted file mode 100644
index 3628f0b60..000000000
--- a/client/src/app/shared/shared-forms/form-validators/instance-validators.service.ts
+++ /dev/null
@@ -1,61 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class InstanceValidatorsService {
7 readonly FROM_EMAIL: BuildFormValidator
8 readonly FROM_NAME: BuildFormValidator
9 readonly SUBJECT: BuildFormValidator
10 readonly BODY: BuildFormValidator
11
12 constructor () {
13
14 this.FROM_EMAIL = {
15 VALIDATORS: [ Validators.required, Validators.email ],
16 MESSAGES: {
17 'required': $localize`Email is required.`,
18 'email': $localize`Email must be valid.`
19 }
20 }
21
22 this.FROM_NAME = {
23 VALIDATORS: [
24 Validators.required,
25 Validators.minLength(1),
26 Validators.maxLength(120)
27 ],
28 MESSAGES: {
29 'required': $localize`Your name is required.`,
30 'minlength': $localize`Your name must be at least 1 character long.`,
31 'maxlength': $localize`Your name cannot be more than 120 characters long.`
32 }
33 }
34
35 this.SUBJECT = {
36 VALIDATORS: [
37 Validators.required,
38 Validators.minLength(1),
39 Validators.maxLength(120)
40 ],
41 MESSAGES: {
42 'required': $localize`A subject is required.`,
43 'minlength': $localize`The subject must be at least 1 character long.`,
44 'maxlength': $localize`The subject cannot be more than 120 characters long.`
45 }
46 }
47
48 this.BODY = {
49 VALIDATORS: [
50 Validators.required,
51 Validators.minLength(3),
52 Validators.maxLength(5000)
53 ],
54 MESSAGES: {
55 'required': $localize`A message is required.`,
56 'minlength': $localize`The message must be at least 3 characters long.`,
57 'maxlength': $localize`The message cannot be more than 5000 characters long.`
58 }
59 }
60 }
61}
diff --git a/client/src/app/shared/shared-forms/form-validators/login-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/login-validators.service.ts
deleted file mode 100644
index 67ea11f20..000000000
--- a/client/src/app/shared/shared-forms/form-validators/login-validators.service.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class LoginValidatorsService {
7 readonly LOGIN_USERNAME: BuildFormValidator
8 readonly LOGIN_PASSWORD: BuildFormValidator
9
10 constructor () {
11 this.LOGIN_USERNAME = {
12 VALIDATORS: [
13 Validators.required
14 ],
15 MESSAGES: {
16 'required': $localize`Username is required.`
17 }
18 }
19
20 this.LOGIN_PASSWORD = {
21 VALIDATORS: [
22 Validators.required
23 ],
24 MESSAGES: {
25 'required': $localize`Password is required.`
26 }
27 }
28 }
29}
diff --git a/client/src/app/shared/shared-forms/form-validators/reset-password-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/reset-password-validators.service.ts
deleted file mode 100644
index 3d0b4dd64..000000000
--- a/client/src/app/shared/shared-forms/form-validators/reset-password-validators.service.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class ResetPasswordValidatorsService {
7 readonly RESET_PASSWORD_CONFIRM: BuildFormValidator
8
9 constructor () {
10 this.RESET_PASSWORD_CONFIRM = {
11 VALIDATORS: [
12 Validators.required
13 ],
14 MESSAGES: {
15 'required': $localize`Confirmation of the password is required.`
16 }
17 }
18 }
19}
diff --git a/client/src/app/shared/shared-forms/form-validators/user-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/user-validators.service.ts
deleted file mode 100644
index 312fc9b1e..000000000
--- a/client/src/app/shared/shared-forms/form-validators/user-validators.service.ts
+++ /dev/null
@@ -1,166 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class UserValidatorsService {
7 readonly USER_USERNAME: BuildFormValidator
8 readonly USER_CHANNEL_NAME: BuildFormValidator
9 readonly USER_EMAIL: BuildFormValidator
10 readonly USER_PASSWORD: BuildFormValidator
11 readonly USER_PASSWORD_OPTIONAL: BuildFormValidator
12 readonly USER_CONFIRM_PASSWORD: BuildFormValidator
13 readonly USER_VIDEO_QUOTA: BuildFormValidator
14 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
15 readonly USER_ROLE: BuildFormValidator
16 readonly USER_DISPLAY_NAME_REQUIRED: BuildFormValidator
17 readonly USER_DESCRIPTION: BuildFormValidator
18 readonly USER_TERMS: BuildFormValidator
19
20 readonly USER_BAN_REASON: BuildFormValidator
21
22 constructor () {
23
24 this.USER_USERNAME = {
25 VALIDATORS: [
26 Validators.required,
27 Validators.minLength(1),
28 Validators.maxLength(50),
29 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
30 ],
31 MESSAGES: {
32 'required': $localize`Username is required.`,
33 'minlength': $localize`Username must be at least 1 character long.`,
34 'maxlength': $localize`Username cannot be more than 50 characters long.`,
35 'pattern': $localize`Username should be lowercase alphanumeric; dots and underscores are allowed.`
36 }
37 }
38
39 this.USER_CHANNEL_NAME = {
40 VALIDATORS: [
41 Validators.required,
42 Validators.minLength(1),
43 Validators.maxLength(50),
44 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
45 ],
46 MESSAGES: {
47 'required': $localize`Channel name is required.`,
48 'minlength': $localize`Channel name must be at least 1 character long.`,
49 'maxlength': $localize`Channel name cannot be more than 50 characters long.`,
50 'pattern': $localize`Channel name should be lowercase alphanumeric; dots and underscores are allowed.`
51 }
52 }
53
54 this.USER_EMAIL = {
55 VALIDATORS: [ Validators.required, Validators.email ],
56 MESSAGES: {
57 'required': $localize`Email is required.`,
58 'email': $localize`Email must be valid.`
59 }
60 }
61
62 this.USER_PASSWORD = {
63 VALIDATORS: [
64 Validators.required,
65 Validators.minLength(6),
66 Validators.maxLength(255)
67 ],
68 MESSAGES: {
69 'required': $localize`Password is required.`,
70 'minlength': $localize`Password must be at least 6 characters long.`,
71 'maxlength': $localize`Password cannot be more than 255 characters long.`
72 }
73 }
74
75 this.USER_PASSWORD_OPTIONAL = {
76 VALIDATORS: [
77 Validators.minLength(6),
78 Validators.maxLength(255)
79 ],
80 MESSAGES: {
81 'minlength': $localize`Password must be at least 6 characters long.`,
82 'maxlength': $localize`Password cannot be more than 255 characters long.`
83 }
84 }
85
86 this.USER_CONFIRM_PASSWORD = {
87 VALIDATORS: [],
88 MESSAGES: {
89 'matchPassword': $localize`The new password and the confirmed password do not correspond.`
90 }
91 }
92
93 this.USER_VIDEO_QUOTA = {
94 VALIDATORS: [ Validators.required, Validators.min(-1) ],
95 MESSAGES: {
96 'required': $localize`Video quota is required.`,
97 'min': $localize`Quota must be greater than -1.`
98 }
99 }
100 this.USER_VIDEO_QUOTA_DAILY = {
101 VALIDATORS: [ Validators.required, Validators.min(-1) ],
102 MESSAGES: {
103 'required': $localize`Daily upload limit is required.`,
104 'min': $localize`Daily upload limit must be greater than -1.`
105 }
106 }
107
108 this.USER_ROLE = {
109 VALIDATORS: [ Validators.required ],
110 MESSAGES: {
111 'required': $localize`User role is required.`
112 }
113 }
114
115 this.USER_DISPLAY_NAME_REQUIRED = this.getDisplayName(true)
116
117 this.USER_DESCRIPTION = {
118 VALIDATORS: [
119 Validators.minLength(3),
120 Validators.maxLength(1000)
121 ],
122 MESSAGES: {
123 'minlength': $localize`Description must be at least 3 characters long.`,
124 'maxlength': $localize`Description cannot be more than 1000 characters long.`
125 }
126 }
127
128 this.USER_TERMS = {
129 VALIDATORS: [
130 Validators.requiredTrue
131 ],
132 MESSAGES: {
133 'required': $localize`You must agree with the instance terms in order to register on it.`
134 }
135 }
136
137 this.USER_BAN_REASON = {
138 VALIDATORS: [
139 Validators.minLength(3),
140 Validators.maxLength(250)
141 ],
142 MESSAGES: {
143 'minlength': $localize`Ban reason must be at least 3 characters long.`,
144 'maxlength': $localize`Ban reason cannot be more than 250 characters long.`
145 }
146 }
147 }
148
149 private getDisplayName (required: boolean) {
150 const control = {
151 VALIDATORS: [
152 Validators.minLength(1),
153 Validators.maxLength(120)
154 ],
155 MESSAGES: {
156 'required': $localize`Display name is required.`,
157 'minlength': $localize`Display name must be at least 1 character long.`,
158 'maxlength': $localize`Display name cannot be more than 50 characters long.`
159 }
160 }
161
162 if (required) control.VALIDATORS.push(Validators.required)
163
164 return control
165 }
166}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-accept-ownership-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-accept-ownership-validators.service.ts
deleted file mode 100644
index aed9e9cdd..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-accept-ownership-validators.service.ts
+++ /dev/null
@@ -1,17 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoAcceptOwnershipValidatorsService {
7 readonly CHANNEL: BuildFormValidator
8
9 constructor () {
10 this.CHANNEL = {
11 VALIDATORS: [ Validators.required ],
12 MESSAGES: {
13 'required': $localize`The channel is required.`
14 }
15 }
16 }
17}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-block-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-block-validators.service.ts
deleted file mode 100644
index bce1880dc..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-block-validators.service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoBlockValidatorsService {
7 readonly VIDEO_BLOCK_REASON: BuildFormValidator
8
9 constructor () {
10 this.VIDEO_BLOCK_REASON = {
11 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(300) ],
12 MESSAGES: {
13 'minlength': $localize`Block reason must be at least 2 characters long.`,
14 'maxlength': $localize`Block reason cannot be more than 300 characters long.`
15 }
16 }
17 }
18}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-captions-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-captions-validators.service.ts
deleted file mode 100644
index 7e90264e5..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-captions-validators.service.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoCaptionsValidatorsService {
7 readonly VIDEO_CAPTION_LANGUAGE: BuildFormValidator
8 readonly VIDEO_CAPTION_FILE: BuildFormValidator
9
10 constructor () {
11
12 this.VIDEO_CAPTION_LANGUAGE = {
13 VALIDATORS: [ Validators.required ],
14 MESSAGES: {
15 'required': $localize`Video caption language is required.`
16 }
17 }
18
19 this.VIDEO_CAPTION_FILE = {
20 VALIDATORS: [ Validators.required ],
21 MESSAGES: {
22 'required': $localize`Video caption file is required.`
23 }
24 }
25 }
26}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-change-ownership-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-change-ownership-validators.service.ts
deleted file mode 100644
index 8c809a0d5..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-change-ownership-validators.service.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import { Injectable } from '@angular/core'
2import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoChangeOwnershipValidatorsService {
7 readonly USERNAME: BuildFormValidator
8
9 constructor () {
10 this.USERNAME = {
11 VALIDATORS: [ Validators.required, this.localAccountValidator ],
12 MESSAGES: {
13 'required': $localize`The username is required.`,
14 'localAccountOnly': $localize`You can only transfer ownership to a local account`
15 }
16 }
17 }
18
19 localAccountValidator (control: AbstractControl): ValidationErrors {
20 if (control.value.includes('@')) {
21 return { 'localAccountOnly': true }
22 }
23
24 return null
25 }
26}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-channel-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-channel-validators.service.ts
deleted file mode 100644
index 3e7444196..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-channel-validators.service.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoChannelValidatorsService {
7 readonly VIDEO_CHANNEL_NAME: BuildFormValidator
8 readonly VIDEO_CHANNEL_DISPLAY_NAME: BuildFormValidator
9 readonly VIDEO_CHANNEL_DESCRIPTION: BuildFormValidator
10 readonly VIDEO_CHANNEL_SUPPORT: BuildFormValidator
11
12 constructor () {
13 this.VIDEO_CHANNEL_NAME = {
14 VALIDATORS: [
15 Validators.required,
16 Validators.minLength(1),
17 Validators.maxLength(50),
18 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
19 ],
20 MESSAGES: {
21 'required': $localize`Name is required.`,
22 'minlength': $localize`Name must be at least 1 character long.`,
23 'maxlength': $localize`Name cannot be more than 50 characters long.`,
24 'pattern': $localize`Name should be lowercase alphanumeric; dots and underscores are allowed.`
25 }
26 }
27
28 this.VIDEO_CHANNEL_DISPLAY_NAME = {
29 VALIDATORS: [
30 Validators.required,
31 Validators.minLength(1),
32 Validators.maxLength(50)
33 ],
34 MESSAGES: {
35 'required': $localize`Display name is required.`,
36 'minlength': $localize`Display name must be at least 1 character long.`,
37 'maxlength': $localize`Display name cannot be more than 50 characters long.`
38 }
39 }
40
41 this.VIDEO_CHANNEL_DESCRIPTION = {
42 VALIDATORS: [
43 Validators.minLength(3),
44 Validators.maxLength(1000)
45 ],
46 MESSAGES: {
47 'minlength': $localize`Description must be at least 3 characters long.`,
48 'maxlength': $localize`Description cannot be more than 1000 characters long.`
49 }
50 }
51
52 this.VIDEO_CHANNEL_SUPPORT = {
53 VALIDATORS: [
54 Validators.minLength(3),
55 Validators.maxLength(1000)
56 ],
57 MESSAGES: {
58 'minlength': $localize`Support text must be at least 3 characters long.`,
59 'maxlength': $localize`Support text cannot be more than 1000 characters long`
60 }
61 }
62 }
63}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-comment-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-comment-validators.service.ts
deleted file mode 100644
index 18e7ae264..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-comment-validators.service.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import { Injectable } from '@angular/core'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoCommentValidatorsService {
7 readonly VIDEO_COMMENT_TEXT: BuildFormValidator
8
9 constructor () {
10 this.VIDEO_COMMENT_TEXT = {
11 VALIDATORS: [ Validators.required, Validators.minLength(1), Validators.maxLength(3000) ],
12 MESSAGES: {
13 'required': $localize`Comment is required.`,
14 'minlength': $localize`Comment must be at least 2 characters long.`,
15 'maxlength': $localize`Comment cannot be more than 3000 characters long.`
16 }
17 }
18 }
19}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-playlist-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-playlist-validators.service.ts
deleted file mode 100644
index 3b45a40fd..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-playlist-validators.service.ts
+++ /dev/null
@@ -1,65 +0,0 @@
1import { Injectable } from '@angular/core'
2import { AbstractControl, Validators } from '@angular/forms'
3import { VideoPlaylistPrivacy } from '@shared/models'
4import { BuildFormValidator } from './form-validator.service'
5
6@Injectable()
7export class VideoPlaylistValidatorsService {
8 readonly VIDEO_PLAYLIST_DISPLAY_NAME: BuildFormValidator
9 readonly VIDEO_PLAYLIST_PRIVACY: BuildFormValidator
10 readonly VIDEO_PLAYLIST_DESCRIPTION: BuildFormValidator
11 readonly VIDEO_PLAYLIST_CHANNEL_ID: BuildFormValidator
12
13 constructor () {
14 this.VIDEO_PLAYLIST_DISPLAY_NAME = {
15 VALIDATORS: [
16 Validators.required,
17 Validators.minLength(1),
18 Validators.maxLength(120)
19 ],
20 MESSAGES: {
21 'required': $localize`Display name is required.`,
22 'minlength': $localize`Display name must be at least 1 character long.`,
23 'maxlength': $localize`Display name cannot be more than 120 characters long.`
24 }
25 }
26
27 this.VIDEO_PLAYLIST_PRIVACY = {
28 VALIDATORS: [
29 Validators.required
30 ],
31 MESSAGES: {
32 'required': $localize`Privacy is required.`
33 }
34 }
35
36 this.VIDEO_PLAYLIST_DESCRIPTION = {
37 VALIDATORS: [
38 Validators.minLength(3),
39 Validators.maxLength(1000)
40 ],
41 MESSAGES: {
42 'minlength': $localize`Description must be at least 3 characters long.`,
43 'maxlength': $localize`Description cannot be more than 1000 characters long.`
44 }
45 }
46
47 this.VIDEO_PLAYLIST_CHANNEL_ID = {
48 VALIDATORS: [ ],
49 MESSAGES: {
50 'required': $localize`The channel is required when the playlist is public.`
51 }
52 }
53 }
54
55 setChannelValidator (channelControl: AbstractControl, privacy: VideoPlaylistPrivacy) {
56 if (privacy.toString() === VideoPlaylistPrivacy.PUBLIC.toString()) {
57 channelControl.setValidators([ Validators.required ])
58 } else {
59 channelControl.setValidators(null)
60 }
61
62 channelControl.markAsDirty()
63 channelControl.updateValueAndValidity()
64 }
65}
diff --git a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts
deleted file mode 100644
index 8119c1ae7..000000000
--- a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts
+++ /dev/null
@@ -1,122 +0,0 @@
1import { Injectable } from '@angular/core'
2import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
3import { BuildFormValidator } from './form-validator.service'
4
5@Injectable()
6export class VideoValidatorsService {
7 readonly VIDEO_NAME: BuildFormValidator
8 readonly VIDEO_PRIVACY: BuildFormValidator
9 readonly VIDEO_CATEGORY: BuildFormValidator
10 readonly VIDEO_LICENCE: BuildFormValidator
11 readonly VIDEO_LANGUAGE: BuildFormValidator
12 readonly VIDEO_IMAGE: BuildFormValidator
13 readonly VIDEO_CHANNEL: BuildFormValidator
14 readonly VIDEO_DESCRIPTION: BuildFormValidator
15 readonly VIDEO_TAGS_ARRAY: BuildFormValidator
16 readonly VIDEO_TAG: BuildFormValidator
17 readonly VIDEO_SUPPORT: BuildFormValidator
18 readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
19 readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator
20
21 constructor () {
22
23 this.VIDEO_NAME = {
24 VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ],
25 MESSAGES: {
26 'required': $localize`Video name is required.`,
27 'minlength': $localize`Video name must be at least 3 characters long.`,
28 'maxlength': $localize`Video name cannot be more than 120 characters long.`
29 }
30 }
31
32 this.VIDEO_PRIVACY = {
33 VALIDATORS: [ Validators.required ],
34 MESSAGES: {
35 'required': $localize`Video privacy is required.`
36 }
37 }
38
39 this.VIDEO_CATEGORY = {
40 VALIDATORS: [ ],
41 MESSAGES: {}
42 }
43
44 this.VIDEO_LICENCE = {
45 VALIDATORS: [ ],
46 MESSAGES: {}
47 }
48
49 this.VIDEO_LANGUAGE = {
50 VALIDATORS: [ ],
51 MESSAGES: {}
52 }
53
54 this.VIDEO_IMAGE = {
55 VALIDATORS: [ ],
56 MESSAGES: {}
57 }
58
59 this.VIDEO_CHANNEL = {
60 VALIDATORS: [ Validators.required ],
61 MESSAGES: {
62 'required': $localize`Video channel is required.`
63 }
64 }
65
66 this.VIDEO_DESCRIPTION = {
67 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(10000) ],
68 MESSAGES: {
69 'minlength': $localize`Video description must be at least 3 characters long.`,
70 'maxlength': $localize`Video description cannot be more than 10000 characters long.`
71 }
72 }
73
74 this.VIDEO_TAG = {
75 VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ],
76 MESSAGES: {
77 'minlength': $localize`A tag should be more than 2 characters long.`,
78 'maxlength': $localize`A tag should be less than 30 characters long.`
79 }
80 }
81
82 this.VIDEO_TAGS_ARRAY = {
83 VALIDATORS: [ Validators.maxLength(5), this.arrayTagLengthValidator() ],
84 MESSAGES: {
85 'maxlength': $localize`A maximum of 5 tags can be used on a video.`,
86 'arrayTagLength': $localize`A tag should be more than 2, and less than 30 characters long.`
87 }
88 }
89
90 this.VIDEO_SUPPORT = {
91 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ],
92 MESSAGES: {
93 'minlength': $localize`Video support must be at least 3 characters long.`,
94 'maxlength': $localize`Video support cannot be more than 1000 characters long.`
95 }
96 }
97
98 this.VIDEO_SCHEDULE_PUBLICATION_AT = {
99 VALIDATORS: [ ],
100 MESSAGES: {
101 'required': $localize`A date is required to schedule video update.`
102 }
103 }
104
105 this.VIDEO_ORIGINALLY_PUBLISHED_AT = {
106 VALIDATORS: [ ],
107 MESSAGES: {}
108 }
109 }
110
111 arrayTagLengthValidator (min = 2, max = 30): ValidatorFn {
112 return (control: AbstractControl): ValidationErrors => {
113 const array = control.value as Array<string>
114
115 if (array.every(e => e.length > min && e.length < max)) {
116 return null
117 }
118
119 return { 'arrayTagLength': true }
120 }
121 }
122}
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index 747df65cf..b2c7fa9ba 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -1,4 +1,4 @@
1export * from './form-validators' 1export * from './form-validator.service'
2export * from './form-reactive' 2export * from './form-reactive'
3export * from './select' 3export * from './select'
4export * from './input-readonly-copy.component' 4export * from './input-readonly-copy.component'
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 0e0ed5bab..a28988f87 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -1,37 +1,21 @@
1 1
2import { NgModule } from '@angular/core'
3import { FormsModule, ReactiveFormsModule } from '@angular/forms'
4import { InputMaskModule } from 'primeng/inputmask' 2import { InputMaskModule } from 'primeng/inputmask'
5import { InputSwitchModule } from 'primeng/inputswitch' 3import { InputSwitchModule } from 'primeng/inputswitch'
4import { NgModule } from '@angular/core'
5import { FormsModule, ReactiveFormsModule } from '@angular/forms'
6import { NgSelectModule } from '@ng-select/ng-select' 6import { NgSelectModule } from '@ng-select/ng-select'
7import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service'
8import { SharedGlobalIconModule } from '../shared-icons' 7import { SharedGlobalIconModule } from '../shared-icons'
9import { SharedMainModule } from '../shared-main/shared-main.module' 8import { SharedMainModule } from '../shared-main/shared-main.module'
10import { 9import { FormValidatorService } from './form-validator.service'
11 CustomConfigValidatorsService,
12 FormValidatorService,
13 InstanceValidatorsService,
14 LoginValidatorsService,
15 ResetPasswordValidatorsService,
16 UserValidatorsService,
17 AbuseValidatorsService,
18 VideoAcceptOwnershipValidatorsService,
19 VideoBlockValidatorsService,
20 VideoCaptionsValidatorsService,
21 VideoChangeOwnershipValidatorsService,
22 VideoChannelValidatorsService,
23 VideoCommentValidatorsService,
24 VideoPlaylistValidatorsService,
25 VideoValidatorsService
26} from './form-validators'
27import { InputReadonlyCopyComponent } from './input-readonly-copy.component' 10import { InputReadonlyCopyComponent } from './input-readonly-copy.component'
28import { MarkdownTextareaComponent } from './markdown-textarea.component' 11import { MarkdownTextareaComponent } from './markdown-textarea.component'
29import { PeertubeCheckboxComponent } from './peertube-checkbox.component' 12import { PeertubeCheckboxComponent } from './peertube-checkbox.component'
30import { PreviewUploadComponent } from './preview-upload.component' 13import { PreviewUploadComponent } from './preview-upload.component'
31import { ReactiveFileComponent } from './reactive-file.component' 14import { ReactiveFileComponent } from './reactive-file.component'
15import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select'
32import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' 16import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
33import { TimestampInputComponent } from './timestamp-input.component' 17import { TimestampInputComponent } from './timestamp-input.component'
34import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select' 18import { DynamicFormFieldComponent } from './dynamic-form-field.component'
35 19
36@NgModule({ 20@NgModule({
37 imports: [ 21 imports: [
@@ -58,7 +42,9 @@ import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent
58 SelectChannelComponent, 42 SelectChannelComponent,
59 SelectOptionsComponent, 43 SelectOptionsComponent,
60 SelectTagsComponent, 44 SelectTagsComponent,
61 SelectCheckboxComponent 45 SelectCheckboxComponent,
46
47 DynamicFormFieldComponent
62 ], 48 ],
63 49
64 exports: [ 50 exports: [
@@ -80,27 +66,13 @@ import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent
80 SelectChannelComponent, 66 SelectChannelComponent,
81 SelectOptionsComponent, 67 SelectOptionsComponent,
82 SelectTagsComponent, 68 SelectTagsComponent,
83 SelectCheckboxComponent 69 SelectCheckboxComponent,
70
71 DynamicFormFieldComponent
84 ], 72 ],
85 73
86 providers: [ 74 providers: [
87 CustomConfigValidatorsService, 75 FormValidatorService
88 FormValidatorService,
89 LoginValidatorsService,
90 InstanceValidatorsService,
91 LoginValidatorsService,
92 ResetPasswordValidatorsService,
93 UserValidatorsService,
94 AbuseValidatorsService,
95 VideoAcceptOwnershipValidatorsService,
96 VideoBlockValidatorsService,
97 VideoCaptionsValidatorsService,
98 VideoChangeOwnershipValidatorsService,
99 VideoChannelValidatorsService,
100 VideoCommentValidatorsService,
101 VideoPlaylistValidatorsService,
102 VideoValidatorsService,
103 BatchDomainsValidatorsService
104 ] 76 ]
105}) 77})
106export class SharedFormModule { } 78export class SharedFormModule { }
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.ts b/client/src/app/shared/shared-forms/timestamp-input.component.ts
index 8d67a96ac..0ffd03d02 100644
--- a/client/src/app/shared/shared-forms/timestamp-input.component.ts
+++ b/client/src/app/shared/shared-forms/timestamp-input.component.ts
@@ -1,4 +1,4 @@
1import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { secondsToTime, timeToInt } from '../../../assets/player/utils' 3import { secondsToTime, timeToInt } from '../../../assets/player/utils'
4 4
@@ -19,6 +19,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
19 @Input() timestamp: number 19 @Input() timestamp: number
20 @Input() disabled = false 20 @Input() disabled = false
21 21
22 @Output() inputBlur = new EventEmitter()
23
22 timestampString: string 24 timestampString: string
23 25
24 constructor (private changeDetector: ChangeDetectorRef) {} 26 constructor (private changeDetector: ChangeDetectorRef) {}
@@ -57,5 +59,7 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
57 59
58 this.propagateChange(this.timestamp) 60 this.propagateChange(this.timestamp)
59 } 61 }
62
63 this.inputBlur.emit()
60 } 64 }
61} 65}
diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
index f09c3d1fc..d2cf53227 100644
--- a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
+++ b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
@@ -1,6 +1,6 @@
1import { fromEvent, Observable, Subscription } from 'rxjs'
1import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' 2import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' 3import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
3import { fromEvent, Observable, Subscription } from 'rxjs'
4 4
5@Directive({ 5@Directive({
6 selector: '[myInfiniteScroller]' 6 selector: '[myInfiniteScroller]'
@@ -80,7 +80,9 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterConten
80 } 80 }
81 81
82 private getMaximumScroll () { 82 private getMaximumScroll () {
83 return this.container.scrollHeight - window.innerHeight 83 const elementHeight = this.onItself ? this.container.clientHeight : window.innerHeight
84
85 return this.container.scrollHeight - elementHeight
84 } 86 }
85 87
86 private hasScroll () { 88 private hasScroll () {
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts
index 6a529e052..757b686c0 100644
--- a/client/src/app/shared/shared-main/video/video-edit.model.ts
+++ b/client/src/app/shared/shared-main/video/video-edit.model.ts
@@ -25,6 +25,8 @@ export class VideoEdit implements VideoUpdate {
25 scheduleUpdate?: VideoScheduleUpdate 25 scheduleUpdate?: VideoScheduleUpdate
26 originallyPublishedAt?: Date | string 26 originallyPublishedAt?: Date | string
27 27
28 pluginData?: any
29
28 constructor ( 30 constructor (
29 video?: Video & { 31 video?: Video & {
30 tags: string[], 32 tags: string[],
@@ -55,10 +57,12 @@ export class VideoEdit implements VideoUpdate {
55 57
56 this.scheduleUpdate = video.scheduledUpdate 58 this.scheduleUpdate = video.scheduledUpdate
57 this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null 59 this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null
60
61 this.pluginData = video.pluginData
58 } 62 }
59 } 63 }
60 64
61 patch (values: { [ id: string ]: string }) { 65 patch (values: { [ id: string ]: any }) {
62 Object.keys(values).forEach((key) => { 66 Object.keys(values).forEach((key) => {
63 this[ key ] = values[ key ] 67 this[ key ] = values[ key ]
64 }) 68 })
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 73f0198e2..0dca3da0d 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -84,6 +84,8 @@ export class Video implements VideoServerModel {
84 currentTime: number 84 currentTime: number
85 } 85 }
86 86
87 pluginData?: any
88
87 static buildClientUrl (videoUUID: string) { 89 static buildClientUrl (videoUUID: string) {
88 return '/videos/watch/' + videoUUID 90 return '/videos/watch/' + videoUUID
89 } 91 }
@@ -152,6 +154,8 @@ export class Video implements VideoServerModel {
152 154
153 this.originInstanceHost = this.account.host 155 this.originInstanceHost = this.account.host
154 this.originInstanceUrl = 'https://' + this.originInstanceHost 156 this.originInstanceUrl = 'https://' + this.originInstanceHost
157
158 this.pluginData = hash.pluginData
155 } 159 }
156 160
157 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 161 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 48aff82b4..8a688c8ed 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -96,6 +96,7 @@ export class VideoService implements VideosProvider {
96 downloadEnabled: video.downloadEnabled, 96 downloadEnabled: video.downloadEnabled,
97 thumbnailfile: video.thumbnailfile, 97 thumbnailfile: video.thumbnailfile,
98 previewfile: video.previewfile, 98 previewfile: video.previewfile,
99 pluginData: video.pluginData,
99 scheduleUpdate, 100 scheduleUpdate,
100 originallyPublishedAt 101 originallyPublishedAt
101 } 102 }
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
index 7193ccb1b..6edbb6023 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
@@ -1,7 +1,8 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { BatchDomainsValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { DOMAINS_VALIDATOR, getNotEmptyHosts } from '../form-validators/batch-domains-validators'
5 6
6@Component({ 7@Component({
7 selector: 'my-batch-domains-modal', 8 selector: 'my-batch-domains-modal',
@@ -18,8 +19,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
18 19
19 constructor ( 20 constructor (
20 protected formValidatorService: FormValidatorService, 21 protected formValidatorService: FormValidatorService,
21 private modalService: NgbModal, 22 private modalService: NgbModal
22 private batchDomainsValidatorsService: BatchDomainsValidatorsService
23 ) { 23 ) {
24 super() 24 super()
25 } 25 }
@@ -28,7 +28,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
28 if (!this.action) this.action = $localize`Process domains` 28 if (!this.action) this.action = $localize`Process domains`
29 29
30 this.buildForm({ 30 this.buildForm({
31 domains: this.batchDomainsValidatorsService.DOMAINS 31 domains: DOMAINS_VALIDATOR
32 }) 32 })
33 } 33 }
34 34
@@ -42,7 +42,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
42 42
43 submit () { 43 submit () {
44 this.domains.emit( 44 this.domains.emit(
45 this.batchDomainsValidatorsService.getNotEmptyHosts(this.form.controls['domains'].value) 45 getNotEmptyHosts(this.form.controls['domains'].value)
46 ) 46 )
47 this.form.reset() 47 this.form.reset()
48 this.hide() 48 this.hide()
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
index 8ab2fe940..cc8875f77 100644
--- a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
@@ -1,7 +1,8 @@
1import { mapValues, pickBy } from 'lodash-es' 1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core' 2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { Account } from '@app/shared/shared-main' 6import { Account } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -28,7 +29,6 @@ export class AccountReportComponent extends FormReactive implements OnInit {
28 constructor ( 29 constructor (
29 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal, 31 private modalService: NgbModal,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier 33 private notifier: Notifier
34 ) { 34 ) {
@@ -51,7 +51,7 @@ export class AccountReportComponent extends FormReactive implements OnInit {
51 this.modalTitle = $localize`Report ${this.account.displayName}` 51 this.modalTitle = $localize`Report ${this.account.displayName}`
52 52
53 this.buildForm({ 53 this.buildForm({
54 reason: this.abuseValidatorsService.ABUSE_REASON, 54 reason: ABUSE_REASON_VALIDATOR,
55 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) 55 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
56 }) 56 })
57 57
diff --git a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
index d75f4d717..c7395c7b7 100644
--- a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
@@ -1,7 +1,8 @@
1import { mapValues, pickBy } from 'lodash-es' 1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core' 2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { VideoComment } from '@app/shared/shared-video-comment' 6import { VideoComment } from '@app/shared/shared-video-comment'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -28,7 +29,6 @@ export class CommentReportComponent extends FormReactive implements OnInit {
28 constructor ( 29 constructor (
29 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal, 31 private modalService: NgbModal,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier 33 private notifier: Notifier
34 ) { 34 ) {
@@ -51,7 +51,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
51 this.modalTitle = $localize`Report comment` 51 this.modalTitle = $localize`Report comment`
52 52
53 this.buildForm({ 53 this.buildForm({
54 reason: this.abuseValidatorsService.ABUSE_REASON, 54 reason: ABUSE_REASON_VALIDATOR,
55 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) 55 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
56 }) 56 })
57 57
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index edff6d325..5b06c0bc7 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -3,7 +3,8 @@ import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/uti
3import { Component, Input, OnInit, ViewChild } from '@angular/core' 3import { Component, Input, OnInit, ViewChild } from '@angular/core'
4import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 4import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' 10import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
@@ -30,7 +31,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
30 constructor ( 31 constructor (
31 protected formValidatorService: FormValidatorService, 32 protected formValidatorService: FormValidatorService,
32 private modalService: NgbModal, 33 private modalService: NgbModal,
33 private abuseValidatorsService: AbuseValidatorsService,
34 private abuseService: AbuseService, 34 private abuseService: AbuseService,
35 private notifier: Notifier, 35 private notifier: Notifier,
36 private sanitizer: DomSanitizer 36 private sanitizer: DomSanitizer
@@ -68,7 +68,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
68 68
69 ngOnInit () { 69 ngOnInit () {
70 this.buildForm({ 70 this.buildForm({
71 reason: this.abuseValidatorsService.ABUSE_REASON, 71 reason: ABUSE_REASON_VALIDATOR,
72 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null), 72 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
73 timestamp: { 73 timestamp: {
74 hasStart: null, 74 hasStart: null,
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
index f9a0381c5..afc69a1b8 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
@@ -1,9 +1,10 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, UserService } from '@app/core' 2import { Notifier, UserService } from '@app/core'
3import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { User } from '@shared/models' 6import { User } from '@shared/models'
7import { USER_BAN_REASON_VALIDATOR } from '../form-validators/user-validators'
7 8
8@Component({ 9@Component({
9 selector: 'my-user-ban-modal', 10 selector: 'my-user-ban-modal',
@@ -21,15 +22,14 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
21 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
22 private modalService: NgbModal, 23 private modalService: NgbModal,
23 private notifier: Notifier, 24 private notifier: Notifier,
24 private userService: UserService, 25 private userService: UserService
25 private userValidatorsService: UserValidatorsService
26 ) { 26 ) {
27 super() 27 super()
28 } 28 }
29 29
30 ngOnInit () { 30 ngOnInit () {
31 this.buildForm({ 31 this.buildForm({
32 reason: this.userValidatorsService.USER_BAN_REASON 32 reason: USER_BAN_REASON_VALIDATOR
33 }) 33 })
34 } 34 }
35 35
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts
index 2bef9efdd..fb47989dc 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,9 +1,10 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, VideoBlockValidatorsService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { Video } from '@app/shared/shared-main' 4import { Video } from '@app/shared/shared-main'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { VIDEO_BLOCK_REASON_VALIDATOR } from '../form-validators/video-block-validators'
7import { VideoBlockService } from './video-block.service' 8import { VideoBlockService } from './video-block.service'
8 9
9@Component({ 10@Component({
@@ -25,7 +26,6 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
25 constructor ( 26 constructor (
26 protected formValidatorService: FormValidatorService, 27 protected formValidatorService: FormValidatorService,
27 private modalService: NgbModal, 28 private modalService: NgbModal,
28 private videoBlockValidatorsService: VideoBlockValidatorsService,
29 private videoBlocklistService: VideoBlockService, 29 private videoBlocklistService: VideoBlockService,
30 private notifier: Notifier 30 private notifier: Notifier
31 ) { 31 ) {
@@ -36,7 +36,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
36 const defaultValues = { unfederate: 'true' } 36 const defaultValues = { unfederate: 'true' }
37 37
38 this.buildForm({ 38 this.buildForm({
39 reason: this.videoBlockValidatorsService.VIDEO_BLOCK_REASON, 39 reason: VIDEO_BLOCK_REASON_VALIDATOR,
40 unfederate: null 40 unfederate: null
41 }, defaultValues) 41 }, defaultValues)
42 } 42 }
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index 8d8e8a3a5..f57a50770 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -37,6 +37,7 @@ export class VideoShareComponent {
37 @Input() video: VideoDetails = null 37 @Input() video: VideoDetails = null
38 @Input() videoCaptions: VideoCaption[] = [] 38 @Input() videoCaptions: VideoCaption[] = []
39 @Input() playlist: VideoPlaylist = null 39 @Input() playlist: VideoPlaylist = null
40 @Input() playlistPosition: number = null
40 41
41 activeVideoId: TabId = 'url' 42 activeVideoId: TabId = 'url'
42 activePlaylistId: TabId = 'url' 43 activePlaylistId: TabId = 'url'
@@ -45,8 +46,6 @@ export class VideoShareComponent {
45 isAdvancedCustomizationCollapsed = true 46 isAdvancedCustomizationCollapsed = true
46 includeVideoInPlaylist = false 47 includeVideoInPlaylist = false
47 48
48 private playlistPosition: number = null
49
50 constructor (private modalService: NgbModal) { } 49 constructor (private modalService: NgbModal) { }
51 50
52 show (currentVideoTimestamp?: number, currentPlaylistPosition?: number) { 51 show (currentVideoTimestamp?: number, currentPlaylistPosition?: number) {
@@ -107,7 +106,7 @@ export class VideoShareComponent {
107 106
108 if (!this.includeVideoInPlaylist) return base 107 if (!this.includeVideoInPlaylist) return base
109 108
110 return base + '?videoId=' + this.video.uuid 109 return base + '?playlistPosition=' + this.playlistPosition
111 } 110 }
112 111
113 notSecure () { 112 notSecure () {
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
index ab77f6f9c..2497e001c 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
@@ -95,27 +95,22 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
95 const autoPlayVideo = this.form.value['autoPlayVideo'] 95 const autoPlayVideo = this.form.value['autoPlayVideo']
96 const autoPlayNextVideo = this.form.value['autoPlayNextVideo'] 96 const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
97 97
98 let videoLanguages: string[] = this.form.value['videoLanguages'] 98 const videoLanguagesForm = this.form.value['videoLanguages']
99 99
100 if (Array.isArray(videoLanguages)) { 100 if (Array.isArray(videoLanguagesForm)) {
101 if (videoLanguages.length > 20) { 101 if (videoLanguagesForm.length > 20) {
102 this.notifier.error($localize`Too many languages are enabled. Please enable them all or stay below 20 enabled languages.`) 102 this.notifier.error($localize`Too many languages are enabled. Please enable them all or stay below 20 enabled languages.`)
103 return 103 return
104 } 104 }
105 105
106 if (videoLanguages.length === 0) { 106 if (videoLanguagesForm.length === 0) {
107 this.notifier.error($localize`You need to enable at least 1 video language.`) 107 this.notifier.error($localize`You need to enable at least 1 video language.`)
108 return 108 return
109 } 109 }
110
111 if (
112 videoLanguages.length === this.languageItems.length ||
113 (videoLanguages.length === 1 && videoLanguages[0] === this.allLanguagesGroup)
114 ) {
115 videoLanguages = null // null means "All"
116 }
117 } 110 }
118 111
112 const videoLanguages = this.getVideoLanguages(videoLanguagesForm)
113
119 let details: UserUpdateMe = { 114 let details: UserUpdateMe = {
120 nsfwPolicy, 115 nsfwPolicy,
121 webTorrentEnabled, 116 webTorrentEnabled,
@@ -124,6 +119,10 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
124 videoLanguages 119 videoLanguages
125 } 120 }
126 121
122 if (videoLanguages) {
123 details = Object.assign(details, videoLanguages)
124 }
125
127 if (onlyKeys) details = pick(details, onlyKeys) 126 if (onlyKeys) details = pick(details, onlyKeys)
128 127
129 if (this.authService.isLoggedIn()) { 128 if (this.authService.isLoggedIn()) {
@@ -141,4 +140,29 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
141 if (this.notifyOnUpdate) this.notifier.success($localize`Display/Video settings updated.`) 140 if (this.notifyOnUpdate) this.notifier.success($localize`Display/Video settings updated.`)
142 } 141 }
143 } 142 }
143
144 private getVideoLanguages (videoLanguages: ItemSelectCheckboxValue[]) {
145 if (!Array.isArray(videoLanguages)) return undefined
146
147 // null means "All"
148 if (videoLanguages.length === this.languageItems.length) return null
149
150 if (videoLanguages.length === 1) {
151 const videoLanguage = videoLanguages[0]
152
153 if (typeof videoLanguage === 'string') {
154 if (videoLanguage === this.allLanguagesGroup) return null
155 } else {
156 if (videoLanguage.group === this.allLanguagesGroup) return null
157 }
158 }
159
160 return videoLanguages.map(l => {
161 if (typeof l === 'string') return l
162
163 if (l.group) return l.group
164
165 return l.id + ''
166 })
167 }
144} 168}
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
index 286ecac02..b46c91bf8 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
@@ -1,5 +1,6 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
3import { USER_EMAIL_VALIDATOR } from '../form-validators/user-validators'
3 4
4@Component({ 5@Component({
5 selector: 'my-remote-subscribe', 6 selector: 'my-remote-subscribe',
@@ -12,15 +13,14 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
12 @Input() showHelp = false 13 @Input() showHelp = false
13 14
14 constructor ( 15 constructor (
15 protected formValidatorService: FormValidatorService, 16 protected formValidatorService: FormValidatorService
16 private userValidatorsService: UserValidatorsService
17 ) { 17 ) {
18 super() 18 super()
19 } 19 }
20 20
21 ngOnInit () { 21 ngOnInit () {
22 this.buildForm({ 22 this.buildForm({
23 text: this.userValidatorsService.USER_EMAIL 23 text: USER_EMAIL_VALIDATOR
24 }) 24 })
25 } 25 }
26 26
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
index a40e0699e..37d5017cf 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
@@ -2,58 +2,60 @@
2 <div class="header"> 2 <div class="header">
3 <div class="first-row"> 3 <div class="first-row">
4 <div i18n class="title">Save to</div> 4 <div i18n class="title">Save to</div>
5
6 <div class="options" (click)="displayOptions = !displayOptions">
7 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
8
9 <span i18n>Options</span>
10 </div>
11 </div> 5 </div>
6 </div>
12 7
13 <div class="options-row" *ngIf="displayOptions"> 8 <div class="input-container">
14 <div> 9 <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
15 <my-peertube-checkbox 10 </div>
16 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
17 i18n-labelText labelText="Start at"
18 ></my-peertube-checkbox>
19
20 <my-timestamp-input
21 [timestamp]="timestampOptions.startTimestamp"
22 [maxTimestamp]="video.duration"
23 [disabled]="!timestampOptions.startTimestampEnabled"
24 [(ngModel)]="timestampOptions.startTimestamp"
25 ></my-timestamp-input>
26 </div>
27 11
28 <div> 12 <div class="playlists">
13 <div
14 *ngFor="let playlist of videoPlaylists"
15 class="playlist dropdown-item" [ngClass]="{ 'has-optional-row': playlist.optionalRowDisplayed }"
16 >
17 <div class="primary-row">
29 <my-peertube-checkbox 18 <my-peertube-checkbox
30 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" 19 [disabled]="isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed" [inputName]="getPrimaryInputName(playlist)"
31 i18n-labelText labelText="Stop at" 20 [ngModel]="isPrimaryCheckboxChecked(playlist)" [onPushWorkaround]="true"
21 (click)="toggleMainPlaylist($event, playlist)"
32 ></my-peertube-checkbox> 22 ></my-peertube-checkbox>
33 23
34 <my-timestamp-input 24 <label class="display-name" (click)="toggleMainPlaylist($event, playlist)">
35 [timestamp]="timestampOptions.stopTimestamp" 25 {{ playlist.displayName }}
36 [maxTimestamp]="video.duration" 26 </label>
37 [disabled]="!timestampOptions.stopTimestampEnabled" 27
38 [(ngModel)]="timestampOptions.stopTimestamp" 28 <div class="optional-row-icon" *ngIf="isPrimaryCheckboxChecked(playlist)" (click)="toggleOptionalRow(playlist)">
39 ></my-timestamp-input> 29 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
30 </div>
40 </div> 31 </div>
41 </div>
42 </div>
43 32
44 <div class="input-container"> 33 <div class="optional-rows" *ngIf="playlist.optionalRowDisplayed">
45 <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> 34 <div class="labels">
46 </div> 35 <div i18n>Start at</div>
36 <div i18n>Stop at</div>
37 </div>
47 38
48 <div class="playlists"> 39 <div *ngFor="let element of buildOptionalRowElements(playlist)">
49 <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> 40 <my-peertube-checkbox
50 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> 41 [inputName]="getOptionalInputName(playlist, element)"
42 [ngModel]="element.enabled" [onPushWorkaround]="true"
43 (click)="toggleOptionalPlaylist($event, playlist, element, startAt.timestamp, stopAt.timestamp)"
44 ></my-peertube-checkbox>
51 45
52 <div class="display-name"> 46 <my-timestamp-input
53 {{ playlist.displayName }} 47 [maxTimestamp]="video.duration"
48 [(ngModel)]="element.startTimestamp"
49 (inputBlur)="onElementTimestampUpdate(playlist, element)"
50 #startAt
51 ></my-timestamp-input>
54 52
55 <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> 53 <my-timestamp-input
56 {{ formatTimestamp(playlist) }} 54 [maxTimestamp]="video.duration"
55 [(ngModel)]="element.stopTimestamp"
56 (inputBlur)="onElementTimestampUpdate(playlist, element)"
57 #stopAt
58 ></my-timestamp-input>
57 </div> 59 </div>
58 </div> 60 </div>
59 </div> 61 </div>
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
index 47baa997b..d2c8804e3 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
@@ -1,12 +1,20 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4$optional-rows-checkbox-width: 34px;
5$timestamp-width: 50px;
6$timestamp-margin-right: 10px;
7
4.header, 8.header,
5.dropdown-item, 9.dropdown-item,
6.input-container { 10.input-container {
7 padding: 8px 24px; 11 padding: 8px 24px;
8} 12}
9 13
14.dropdown-item:active {
15 color: inherit;
16}
17
10.header { 18.header {
11 min-width: 240px; 19 min-width: 240px;
12 margin-bottom: 10px; 20 margin-bottom: 10px;
@@ -20,31 +28,6 @@
20 font-size: 18px; 28 font-size: 18px;
21 flex-grow: 1; 29 flex-grow: 1;
22 } 30 }
23
24 .options {
25 display: flex;
26 align-items: center;
27 font-size: 14px;
28 cursor: pointer;
29
30 my-global-icon {
31 @include apply-svg-color(#333);
32
33 width: 16px;
34 height: 23px;
35 margin-right: 3px;
36 }
37 }
38 }
39
40 .options-row {
41 margin-top: 10px;
42 padding-left: 10px;
43
44 > div {
45 display: flex;
46 align-items: center;
47 }
48 } 31 }
49} 32}
50 33
@@ -54,8 +37,16 @@
54} 37}
55 38
56.playlist { 39.playlist {
57 display: inline-flex; 40 padding: 8px 10px 8px 24px;
58 cursor: pointer; 41
42 &.has-optional-row:hover {
43 background-color: inherit;
44 }
45}
46
47.primary-row,
48.optional-rows > div {
49 display: flex;
59 50
60 my-peertube-checkbox { 51 my-peertube-checkbox {
61 margin-right: 10px; 52 margin-right: 10px;
@@ -65,11 +56,58 @@
65 .display-name { 56 .display-name {
66 display: flex; 57 display: flex;
67 align-items: flex-end; 58 align-items: flex-end;
59 flex-grow: 1;
60 margin: 0;
61 font-weight: $font-regular;
62 cursor: pointer;
63 }
64
65 .optional-row-icon {
66 display: flex;
67 align-items: center;
68 font-size: 14px;
69 cursor: pointer;
70
71 my-global-icon {
72 @include apply-svg-color(#333);
73
74 width: 19px;
75 height: 19px;
76 margin-right: 0;
77 }
78 }
79
80 my-timestamp-input {
81 margin-right: $timestamp-margin-right;
82
83 ::ng-deep .ui-inputtext {
84 padding: 0;
85 width: $timestamp-width;
86 }
87 }
88}
89
90.optional-rows {
91 > div {
92 padding: 8px 5px 5px 10px;
93 }
94
95 my-peertube-checkbox {
96 display: block;
97 width: $optional-rows-checkbox-width;
98 margin-right: 0 !important;
99 }
100
101 .labels {
102 margin-left: $optional-rows-checkbox-width;
103 font-size: 13px;
104 color: pvar(--greyForegroundColor);
105 padding-top: 5px;
106 padding-bottom: 0;
68 107
69 .timestamp-info { 108 div {
70 font-size: 0.9em; 109 margin-right: $timestamp-margin-right;
71 color: pvar(--greyForegroundColor); 110 width: $timestamp-width;
72 margin-left: 5px;
73 } 111 }
74 } 112 }
75} 113}
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index 757ffa099..681e5becd 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -3,21 +3,34 @@ import { Subject, Subscription } from 'rxjs'
3import { debounceTime, filter } from 'rxjs/operators' 3import { debounceTime, filter } from 'rxjs/operators'
4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' 4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5import { AuthService, DisableForReuseHook, Notifier } from '@app/core' 5import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6import { FormReactive, FormValidatorService, VideoPlaylistValidatorsService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' 7import {
8 Video,
9 VideoExistInPlaylist,
10 VideoPlaylistCreate,
11 VideoPlaylistElementCreate,
12 VideoPlaylistElementUpdate,
13 VideoPlaylistPrivacy
14} from '@shared/models'
8import { secondsToTime } from '../../../assets/player/utils' 15import { secondsToTime } from '../../../assets/player/utils'
16import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
9import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service' 17import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
10 18
11const logger = debug('peertube:playlists:VideoAddToPlaylistComponent') 19const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
12 20
21type PlaylistElement = {
22 enabled: boolean
23 playlistElementId?: number
24 startTimestamp?: number
25 stopTimestamp?: number
26}
27
13type PlaylistSummary = { 28type PlaylistSummary = {
14 id: number 29 id: number
15 inPlaylist: boolean
16 displayName: string 30 displayName: string
31 optionalRowDisplayed: boolean
17 32
18 playlistElementId?: number 33 elements: PlaylistElement[]
19 startTimestamp?: number
20 stopTimestamp?: number
21} 34}
22 35
23@Component({ 36@Component({
@@ -32,16 +45,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
32 @Input() lazyLoad = false 45 @Input() lazyLoad = false
33 46
34 isNewPlaylistBlockOpened = false 47 isNewPlaylistBlockOpened = false
48
35 videoPlaylistSearch: string 49 videoPlaylistSearch: string
36 videoPlaylistSearchChanged = new Subject<string>() 50 videoPlaylistSearchChanged = new Subject<string>()
51
37 videoPlaylists: PlaylistSummary[] = [] 52 videoPlaylists: PlaylistSummary[] = []
38 timestampOptions: {
39 startTimestampEnabled: boolean
40 startTimestamp: number
41 stopTimestampEnabled: boolean
42 stopTimestamp: number
43 }
44 displayOptions = false
45 53
46 private disabled = false 54 private disabled = false
47 55
@@ -53,7 +61,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
53 private authService: AuthService, 61 private authService: AuthService,
54 private notifier: Notifier, 62 private notifier: Notifier,
55 private videoPlaylistService: VideoPlaylistService, 63 private videoPlaylistService: VideoPlaylistService,
56 private videoPlaylistValidatorsService: VideoPlaylistValidatorsService,
57 private cd: ChangeDetectorRef 64 private cd: ChangeDetectorRef
58 ) { 65 ) {
59 super() 66 super()
@@ -65,7 +72,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
65 72
66 ngOnInit () { 73 ngOnInit () {
67 this.buildForm({ 74 this.buildForm({
68 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME 75 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR
69 }) 76 })
70 77
71 this.videoPlaylistService.listenToMyAccountPlaylistsChange() 78 this.videoPlaylistService.listenToMyAccountPlaylistsChange()
@@ -106,7 +113,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
106 this.videoPlaylists = [] 113 this.videoPlaylists = []
107 this.videoPlaylistSearch = undefined 114 this.videoPlaylistSearch = undefined
108 115
109 this.resetOptions(true)
110 this.load() 116 this.load()
111 117
112 this.cd.markForCheck() 118 this.cd.markForCheck()
@@ -115,7 +121,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
115 load () { 121 load () {
116 logger('Loading component') 122 logger('Loading component')
117 123
118 this.listenToPlaylistChanges() 124 this.listenToVideoPlaylistChange()
119 125
120 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch) 126 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
121 .subscribe(playlistsResult => { 127 .subscribe(playlistsResult => {
@@ -128,7 +134,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
128 openChange (opened: boolean) { 134 openChange (opened: boolean) {
129 if (opened === false) { 135 if (opened === false) {
130 this.isNewPlaylistBlockOpened = false 136 this.isNewPlaylistBlockOpened = false
131 this.displayOptions = false
132 } 137 }
133 } 138 }
134 139
@@ -138,17 +143,49 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
138 this.isNewPlaylistBlockOpened = true 143 this.isNewPlaylistBlockOpened = true
139 } 144 }
140 145
141 togglePlaylist (event: Event, playlist: PlaylistSummary) { 146 toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
142 event.preventDefault() 147 e.preventDefault()
148
149 if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
143 150
144 if (playlist.inPlaylist === true) { 151 if (playlist.elements.length === 0) {
145 this.removeVideoFromPlaylist(playlist) 152 const element: PlaylistElement = {
153 enabled: true,
154 playlistElementId: undefined,
155 startTimestamp: 0,
156 stopTimestamp: this.video.duration
157 }
158
159 this.addVideoInPlaylist(playlist, element)
146 } else { 160 } else {
147 this.addVideoInPlaylist(playlist) 161 this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
162 playlist.elements = []
148 } 163 }
149 164
150 playlist.inPlaylist = !playlist.inPlaylist 165 this.cd.markForCheck()
151 this.resetOptions() 166 }
167
168 toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) {
169 e.preventDefault()
170
171 if (element.enabled) {
172 this.removeVideoFromPlaylist(playlist, element.playlistElementId)
173 element.enabled = false
174
175 // Hide optional rows pane when the user unchecked all the playlists
176 if (this.isPrimaryCheckboxChecked(playlist) === false) {
177 playlist.optionalRowDisplayed = false
178 }
179 } else {
180 const element: PlaylistElement = {
181 enabled: true,
182 playlistElementId: undefined,
183 startTimestamp,
184 stopTimestamp
185 }
186
187 this.addVideoInPlaylist(playlist, element)
188 }
152 189
153 this.cd.markForCheck() 190 this.cd.markForCheck()
154 } 191 }
@@ -172,34 +209,99 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
172 ) 209 )
173 } 210 }
174 211
175 resetOptions (resetTimestamp = false) { 212 onVideoPlaylistSearchChanged () {
176 this.displayOptions = false 213 this.videoPlaylistSearchChanged.next()
214 }
177 215
178 this.timestampOptions = {} as any 216 isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
179 this.timestampOptions.startTimestampEnabled = false 217 return playlist.elements.filter(e => e.enabled)
180 this.timestampOptions.stopTimestampEnabled = false 218 .length !== 0
219 }
181 220
182 if (resetTimestamp) { 221 toggleOptionalRow (playlist: PlaylistSummary) {
183 this.timestampOptions.startTimestamp = 0 222 playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
184 this.timestampOptions.stopTimestamp = this.video.duration 223
185 } 224 this.cd.markForCheck()
186 } 225 }
187 226
188 formatTimestamp (playlist: PlaylistSummary) { 227 getPrimaryInputName (playlist: PlaylistSummary) {
189 const start = playlist.startTimestamp ? secondsToTime(playlist.startTimestamp) : '' 228 return 'in-playlist-primary-' + playlist.id
190 const stop = playlist.stopTimestamp ? secondsToTime(playlist.stopTimestamp) : '' 229 }
191 230
192 return `(${start}-${stop})` 231 getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
232 const suffix = element
233 ? '-' + element.playlistElementId
234 : ''
235
236 return 'in-playlist-optional-' + playlist.id + suffix
193 } 237 }
194 238
195 onVideoPlaylistSearchChanged () { 239 buildOptionalRowElements (playlist: PlaylistSummary) {
196 this.videoPlaylistSearchChanged.next() 240 const elements = playlist.elements
241
242 const lastElement = elements.length === 0
243 ? undefined
244 : elements[elements.length - 1]
245
246 // Build an empty last element
247 if (!lastElement || lastElement.enabled === true) {
248 elements.push({
249 enabled: false,
250 startTimestamp: 0,
251 stopTimestamp: this.video.duration
252 })
253 }
254
255 return elements
256 }
257
258 isPresentMultipleTimes (playlist: PlaylistSummary) {
259 return playlist.elements.filter(e => e.enabled === true).length > 1
260 }
261
262 onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) {
263 if (!element.playlistElementId || element.enabled === false) return
264
265 const body: VideoPlaylistElementUpdate = {
266 startTimestamp: element.startTimestamp,
267 stopTimestamp: element.stopTimestamp
268 }
269
270 this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id)
271 .subscribe(
272 () => {
273 this.notifier.success($localize`Timestamps updated`)
274 },
275
276 err => {
277 this.notifier.error(err.message)
278 },
279
280 () => this.cd.markForCheck()
281 )
197 } 282 }
198 283
199 private removeVideoFromPlaylist (playlist: PlaylistSummary) { 284 private isOptionalRowDisplayed (playlist: PlaylistSummary) {
200 if (!playlist.playlistElementId) return 285 const elements = playlist.elements.filter(e => e.enabled)
286
287 if (elements.length > 1) return true
201 288
202 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, playlist.playlistElementId, this.video.id) 289 if (elements.length === 1) {
290 const element = elements[0]
291
292 if (
293 (element.startTimestamp && element.startTimestamp !== 0) ||
294 (element.stopTimestamp && element.stopTimestamp !== this.video.duration)
295 ) {
296 return true
297 }
298 }
299
300 return false
301 }
302
303 private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) {
304 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id)
203 .subscribe( 305 .subscribe(
204 () => { 306 () => {
205 this.notifier.success($localize`Video removed from ${playlist.displayName}`) 307 this.notifier.success($localize`Video removed from ${playlist.displayName}`)
@@ -213,7 +315,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
213 ) 315 )
214 } 316 }
215 317
216 private listenToPlaylistChanges () { 318 private listenToVideoPlaylistChange () {
217 this.unsubscribePlaylistChanges() 319 this.unsubscribePlaylistChanges()
218 320
219 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id) 321 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
@@ -231,18 +333,30 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
231 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { 333 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
232 logger('Got existing results for %d.', this.video.id, existResult) 334 logger('Got existing results for %d.', this.video.id, existResult)
233 335
336 const oldPlaylists = this.videoPlaylists
337
234 this.videoPlaylists = [] 338 this.videoPlaylists = []
235 for (const playlist of this.playlistsData) { 339 for (const playlist of this.playlistsData) {
236 const existingPlaylist = existResult.find(p => p.playlistId === playlist.id) 340 const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
237 341
238 this.videoPlaylists.push({ 342 const playlistSummary = {
239 id: playlist.id, 343 id: playlist.id,
344 optionalRowDisplayed: false,
240 displayName: playlist.displayName, 345 displayName: playlist.displayName,
241 inPlaylist: !!existingPlaylist, 346 elements: existingPlaylists.map(e => ({
242 playlistElementId: existingPlaylist ? existingPlaylist.playlistElementId : undefined, 347 enabled: true,
243 startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, 348 playlistElementId: e.playlistElementId,
244 stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined 349 startTimestamp: e.startTimestamp || 0,
245 }) 350 stopTimestamp: e.stopTimestamp || this.video.duration
351 }))
352 }
353
354 const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id)
355 playlistSummary.optionalRowDisplayed = oldPlaylist
356 ? oldPlaylist.optionalRowDisplayed
357 : this.isOptionalRowDisplayed(playlistSummary)
358
359 this.videoPlaylists.push(playlistSummary)
246 } 360 }
247 361
248 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists) 362 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
@@ -250,20 +364,22 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
250 this.cd.markForCheck() 364 this.cd.markForCheck()
251 } 365 }
252 366
253 private addVideoInPlaylist (playlist: PlaylistSummary) { 367 private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
254 const body: VideoPlaylistElementCreate = { videoId: this.video.id } 368 const body: VideoPlaylistElementCreate = { videoId: this.video.id }
255 369
256 if (this.timestampOptions.startTimestampEnabled) body.startTimestamp = this.timestampOptions.startTimestamp 370 if (element.startTimestamp) body.startTimestamp = element.startTimestamp
257 if (this.timestampOptions.stopTimestampEnabled) body.stopTimestamp = this.timestampOptions.stopTimestamp 371 if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
258 372
259 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body) 373 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
260 .subscribe( 374 .subscribe(
261 () => { 375 res => {
262 const message = body.startTimestamp || body.stopTimestamp 376 const message = body.startTimestamp || body.stopTimestamp
263 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(playlist)}` 377 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}`
264 : $localize`Video added in ${playlist.displayName}` 378 : $localize`Video added in ${playlist.displayName}`
265 379
266 this.notifier.success(message) 380 this.notifier.success(message)
381
382 if (element) element.playlistElementId = res.videoPlaylistElement.id
267 }, 383 },
268 384
269 err => { 385 err => {
@@ -273,4 +389,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
273 () => this.cd.markForCheck() 389 () => this.cd.markForCheck()
274 ) 390 )
275 } 391 }
392
393 private formatTimestamp (element: PlaylistElement) {
394 const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : ''
395 const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : ''
396
397 return `(${start}-${stop})`
398 }
276} 399}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
index afd775b25..082a5d9b2 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
@@ -22,12 +22,16 @@ my-video-thumbnail,
22} 22}
23 23
24.video { 24.video {
25 display: flex; 25 display: grid;
26 align-items: center; 26 grid-template-columns: 1fr auto;
27 background-color: pvar(--mainBackgroundColor); 27 background-color: pvar(--mainBackgroundColor);
28 padding: 10px; 28 padding: 10px;
29 border-bottom: 1px solid $separator-border-color; 29 border-bottom: 1px solid $separator-border-color;
30 30
31 .more {
32 display: flex;
33 }
34
31 &:hover { 35 &:hover {
32 background-color: rgba(0, 0, 0, 0.05); 36 background-color: rgba(0, 0, 0, 0.05);
33 37
@@ -164,6 +168,7 @@ my-video-thumbnail,
164 my-edit-button { 168 my-edit-button {
165 display: inline-flex; 169 display: inline-flex;
166 height: max-content; 170 height: max-content;
171 margin: auto;
167 } 172 }
168 } 173 }
169 174
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
index 5879c4978..7c083ae26 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
@@ -78,7 +78,7 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
78 if (!this.playlistElement || !this.playlistElement.video) return {} 78 if (!this.playlistElement || !this.playlistElement.video) return {}
79 79
80 return { 80 return {
81 videoId: this.playlistElement.video.uuid, 81 playlistPosition: this.playlistElement.position,
82 start: this.playlistElement.startTimestamp, 82 start: this.playlistElement.startTimestamp,
83 stop: this.playlistElement.stopTimestamp, 83 stop: this.playlistElement.stopTimestamp,
84 resume: true 84 resume: true
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index cc3d04b9e..1b87e0b2a 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -1,7 +1,7 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { uniq } from 'lodash-es' 2import { uniq } from 'lodash-es'
3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' 3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs'
4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' 4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable, NgZone } from '@angular/core' 6import { Injectable, NgZone } from '@angular/core'
7import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' 7import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
@@ -53,6 +53,7 @@ export class VideoPlaylistService {
53 ) { 53 ) {
54 this.videoExistsInPlaylistObservable = merge( 54 this.videoExistsInPlaylistObservable = merge(
55 this.videoExistsInPlaylistNotifier.pipe( 55 this.videoExistsInPlaylistNotifier.pipe(
56 distinctUntilChanged(),
56 // We leave Angular zone so Protractor does not get stuck 57 // We leave Angular zone so Protractor does not get stuck
57 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), 58 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)),
58 filter(videoIds => videoIds.length !== 0), 59 filter(videoIds => videoIds.length !== 0),
@@ -215,10 +216,13 @@ export class VideoPlaylistService {
215 map(this.restExtractor.extractDataBool), 216 map(this.restExtractor.extractDataBool),
216 tap(() => { 217 tap(() => {
217 const existsResult = this.videoExistsCache[videoId] 218 const existsResult = this.videoExistsCache[videoId]
218 const elem = existsResult.find(e => e.playlistElementId === playlistElementId)
219 219
220 elem.startTimestamp = body.startTimestamp 220 if (existsResult) {
221 elem.stopTimestamp = body.stopTimestamp 221 const elem = existsResult.find(e => e.playlistElementId === playlistElementId)
222
223 elem.startTimestamp = body.startTimestamp
224 elem.stopTimestamp = body.stopTimestamp
225 }
222 226
223 this.runPlaylistCheck(videoId) 227 this.runPlaylistCheck(videoId)
224 }), 228 }),
@@ -233,7 +237,11 @@ export class VideoPlaylistService {
233 tap(() => { 237 tap(() => {
234 if (!videoId) return 238 if (!videoId) return
235 239
236 this.videoExistsCache[videoId] = this.videoExistsCache[videoId].filter(e => e.playlistElementId !== playlistElementId) 240 if (this.videoExistsCache[videoId]) {
241 this.videoExistsCache[videoId] = this.videoExistsCache[videoId]
242 .filter(e => e.playlistElementId !== playlistElementId)
243 }
244
237 this.runPlaylistCheck(videoId) 245 this.runPlaylistCheck(videoId)
238 }), 246 }),
239 catchError(err => this.restExtractor.handleError(err)) 247 catchError(err => this.restExtractor.handleError(err))