aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-07-12 19:02:00 +0200
committerChocobozzz <me@florianbigard.com>2018-07-16 11:50:08 +0200
commit40e87e9ecc54e3513fb586928330a7855eb192c6 (patch)
treeaf1111ecba85f9cd8286811ff332a67cf21be2f6 /client
parentd4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff)
downloadPeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.gz
PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.zst
PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.zip
Implement captions/subtitles
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html21
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts4
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts10
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/video-captions-validators.service.ts27
-rw-r--r--client/src/app/shared/forms/index.ts1
-rw-r--r--client/src/app/shared/forms/reactive-file.component.html14
-rw-r--r--client/src/app/shared/forms/reactive-file.component.scss24
-rw-r--r--client/src/app/shared/forms/reactive-file.component.ts75
-rw-r--r--client/src/app/shared/misc/utils.ts10
-rw-r--r--client/src/app/shared/shared.module.ts10
-rw-r--r--client/src/app/shared/video-caption/index.ts1
-rw-r--r--client/src/app/shared/video-caption/video-caption-edit.model.ts9
-rw-r--r--client/src/app/shared/video-caption/video-caption.service.ts61
-rw-r--r--client/src/app/shared/video/video.model.ts2
-rw-r--r--client/src/app/shared/video/video.service.ts4
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html47
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss15
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts80
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html32
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss35
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts53
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.module.ts4
-rw-r--r--client/src/app/videos/+video-edit/shared/video-image.component.html15
-rw-r--r--client/src/app/videos/+video-edit/shared/video-image.component.scss10
-rw-r--r--client/src/app/videos/+video-edit/shared/video-image.component.ts26
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts50
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html1
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts48
31 files changed, 602 insertions, 96 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 1e5308531..97900e523 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -206,15 +206,17 @@ Check this checkbox, save the configuration and test with a video URL of your in
206 </div> 206 </div>
207 </ng-template> 207 </ng-template>
208 208
209 <div i18n class="inner-form-title">Cache</div> 209 <div i18n class="inner-form-title">
210 Cache
210 211
211 <div class="form-group">
212 <label i18n for="cachePreviewsSize">Previews cache size</label>
213 <my-help 212 <my-help
214 helpType="custom" i18n-customHtml 213 helpType="custom" i18n-customHtml
215 customHtml="Previews are not federated. We fetch them directly from the origin instance and cache them." 214 customHtml="Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them."
216 ></my-help> 215 ></my-help>
216 </div>
217 217
218 <div class="form-group">
219 <label i18n for="cachePreviewsSize">Previews cache size</label>
218 <input 220 <input
219 type="text" id="cachePreviewsSize" 221 type="text" id="cachePreviewsSize"
220 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 222 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
@@ -224,6 +226,17 @@ Check this checkbox, save the configuration and test with a video URL of your in
224 </div> 226 </div>
225 </div> 227 </div>
226 228
229 <div class="form-group">
230 <label i18n for="cachePreviewsSize">Video captions cache size</label>
231 <input
232 type="text" id="cacheCaptionsSize"
233 formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }"
234 >
235 <div *ngIf="formErrors.cacheCaptionsSize" class="form-error">
236 {{ formErrors.cacheCaptionsSize }}
237 </div>
238 </div>
239
227 <div i18n class="inner-form-title">Customizations</div> 240 <div i18n class="inner-form-title">Customizations</div>
228 241
229 <div class="form-group"> 242 <div class="form-group">
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 7b3e72803..8d476393f 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
@@ -67,6 +67,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
67 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 67 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
68 servicesTwitterWhitelisted: null, 68 servicesTwitterWhitelisted: null,
69 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, 69 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE,
70 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
70 signupEnabled: null, 71 signupEnabled: null,
71 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 72 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
72 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 73 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
@@ -156,6 +157,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
156 cache: { 157 cache: {
157 previews: { 158 previews: {
158 size: this.form.value['cachePreviewsSize'] 159 size: this.form.value['cachePreviewsSize']
160 },
161 captions: {
162 size: this.form.value['cacheCaptionsSize']
159 } 163 }
160 }, 164 },
161 signup: { 165 signup: {
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 74363e6a1..3baefb6a7 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -59,6 +59,12 @@ export class ServerService {
59 extensions: [] 59 extensions: []
60 } 60 }
61 }, 61 },
62 videoCaption: {
63 file: {
64 size: { max: 0 },
65 extensions: []
66 }
67 },
62 user: { 68 user: {
63 videoQuota: -1 69 videoQuota: -1
64 } 70 }
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 1b36bbc6b..0c2489a9d 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -9,6 +9,7 @@ export class CustomConfigValidatorsService {
9 readonly INSTANCE_SHORT_DESCRIPTION: BuildFormValidator 9 readonly INSTANCE_SHORT_DESCRIPTION: BuildFormValidator
10 readonly SERVICES_TWITTER_USERNAME: BuildFormValidator 10 readonly SERVICES_TWITTER_USERNAME: BuildFormValidator
11 readonly CACHE_PREVIEWS_SIZE: BuildFormValidator 11 readonly CACHE_PREVIEWS_SIZE: BuildFormValidator
12 readonly CACHE_CAPTIONS_SIZE: BuildFormValidator
12 readonly SIGNUP_LIMIT: BuildFormValidator 13 readonly SIGNUP_LIMIT: BuildFormValidator
13 readonly ADMIN_EMAIL: BuildFormValidator 14 readonly ADMIN_EMAIL: BuildFormValidator
14 readonly TRANSCODING_THREADS: BuildFormValidator 15 readonly TRANSCODING_THREADS: BuildFormValidator
@@ -44,6 +45,15 @@ export class CustomConfigValidatorsService {
44 } 45 }
45 } 46 }
46 47
48 this.CACHE_CAPTIONS_SIZE = {
49 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
50 MESSAGES: {
51 'required': this.i18n('Captions cache size is required.'),
52 'min': this.i18n('Captions cache size must be greater than 1.'),
53 'pattern': this.i18n('Captions cache size must be a number.')
54 }
55 }
56
47 this.SIGNUP_LIMIT = { 57 this.SIGNUP_LIMIT = {
48 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], 58 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
49 MESSAGES: { 59 MESSAGES: {
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
index 487683088..60d735ef7 100644
--- a/client/src/app/shared/forms/form-validators/index.ts
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -8,3 +8,4 @@ export * from './video-abuse-validators.service'
8export * from './video-channel-validators.service' 8export * from './video-channel-validators.service'
9export * from './video-comment-validators.service' 9export * from './video-comment-validators.service'
10export * from './video-validators.service' 10export * from './video-validators.service'
11export * from './video-captions-validators.service'
diff --git a/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts b/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts
new file mode 100644
index 000000000..d1b4667bb
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts
@@ -0,0 +1,27 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms'
3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from '@app/shared'
5
6@Injectable()
7export class VideoCaptionsValidatorsService {
8 readonly VIDEO_CAPTION_LANGUAGE: BuildFormValidator
9 readonly VIDEO_CAPTION_FILE: BuildFormValidator
10
11 constructor (private i18n: I18n) {
12
13 this.VIDEO_CAPTION_LANGUAGE = {
14 VALIDATORS: [ Validators.required ],
15 MESSAGES: {
16 'required': this.i18n('Video caption language is required.')
17 }
18 }
19
20 this.VIDEO_CAPTION_FILE = {
21 VALIDATORS: [ Validators.required ],
22 MESSAGES: {
23 'required': this.i18n('Video caption file is required.')
24 }
25 }
26 }
27}
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts
index 7464bb022..41c321c4c 100644
--- a/client/src/app/shared/forms/index.ts
+++ b/client/src/app/shared/forms/index.ts
@@ -1,2 +1,3 @@
1export * from './form-validators' 1export * from './form-validators'
2export * from './form-reactive' 2export * from './form-reactive'
3export * from './reactive-file.component'
diff --git a/client/src/app/shared/forms/reactive-file.component.html b/client/src/app/shared/forms/reactive-file.component.html
new file mode 100644
index 000000000..9fb1c9e3e
--- /dev/null
+++ b/client/src/app/shared/forms/reactive-file.component.html
@@ -0,0 +1,14 @@
1<div class="root">
2 <div class="button-file">
3 <span>{{ inputLabel }}</span>
4 <input
5 type="file"
6 [name]="inputName" [id]="inputName" [accept]="extensions"
7 (change)="fileChange($event)"
8 />
9 </div>
10
11 <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div>
12
13 <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div>
14</div>
diff --git a/client/src/app/shared/forms/reactive-file.component.scss b/client/src/app/shared/forms/reactive-file.component.scss
new file mode 100644
index 000000000..d89844264
--- /dev/null
+++ b/client/src/app/shared/forms/reactive-file.component.scss
@@ -0,0 +1,24 @@
1@import '_variables';
2@import '_mixins';
3
4.root {
5 height: auto;
6 display: flex;
7 align-items: center;
8
9 .button-file {
10 @include peertube-button-file(auto);
11
12 min-width: 190px;
13 }
14
15 .file-constraints {
16 margin-left: 5px;
17 font-size: 13px;
18 }
19
20 .filename {
21 font-weight: $font-semibold;
22 margin-left: 5px;
23 }
24}
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts
new file mode 100644
index 000000000..f5758b643
--- /dev/null
+++ b/client/src/app/shared/forms/reactive-file.component.ts
@@ -0,0 +1,75 @@
1import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { NotificationsService } from 'angular2-notifications'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5
6@Component({
7 selector: 'my-reactive-file',
8 styleUrls: [ './reactive-file.component.scss' ],
9 templateUrl: './reactive-file.component.html',
10 providers: [
11 {
12 provide: NG_VALUE_ACCESSOR,
13 useExisting: forwardRef(() => ReactiveFileComponent),
14 multi: true
15 }
16 ]
17})
18export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
19 @Input() inputLabel: string
20 @Input() inputName: string
21 @Input() extensions: string[] = []
22 @Input() maxFileSize: number
23 @Input() displayFilename = false
24
25 @Output() fileChanged = new EventEmitter<Blob>()
26
27 allowedExtensionsMessage = ''
28
29 private file: File
30
31 constructor (
32 private notificationsService: NotificationsService,
33 private i18n: I18n
34 ) {}
35
36 get filename () {
37 if (!this.file) return ''
38
39 return this.file.name
40 }
41
42 ngOnInit () {
43 this.allowedExtensionsMessage = this.extensions.join(', ')
44 }
45
46 fileChange (event: any) {
47 if (event.target.files && event.target.files.length) {
48 const [ file ] = event.target.files
49
50 if (file.size > this.maxFileSize) {
51 this.notificationsService.error(this.i18n('Error'), this.i18n('This file is too large.'))
52 return
53 }
54
55 this.file = file
56
57 this.propagateChange(this.file)
58 this.fileChanged.emit(this.file)
59 }
60 }
61
62 propagateChange = (_: any) => { /* empty */ }
63
64 writeValue (file: any) {
65 this.file = file
66 }
67
68 registerOnChange (fn: (_: any) => void) {
69 this.propagateChange = fn
70 }
71
72 registerOnTouched () {
73 // Unused
74 }
75}
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 53aff1b24..8381745f5 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -81,7 +81,7 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
81 } 81 }
82 82
83 if (obj[key] !== null && typeof obj[ key ] === 'object' && !(obj[ key ] instanceof File)) { 83 if (obj[key] !== null && typeof obj[ key ] === 'object' && !(obj[ key ] instanceof File)) {
84 objectToFormData(obj[ key ], fd, key) 84 objectToFormData(obj[ key ], fd, formKey)
85 } else { 85 } else {
86 fd.append(formKey, obj[ key ]) 86 fd.append(formKey, obj[ key ])
87 } 87 }
@@ -96,6 +96,11 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
96 }) 96 })
97} 97}
98 98
99function removeElementFromArray <T> (arr: T[], elem: T) {
100 const index = arr.indexOf(elem)
101 if (index !== -1) arr.splice(index, 1)
102}
103
99export { 104export {
100 objectToUrlEncoded, 105 objectToUrlEncoded,
101 getParameterByName, 106 getParameterByName,
@@ -104,5 +109,6 @@ export {
104 dateToHuman, 109 dateToHuman,
105 immutableAssign, 110 immutableAssign,
106 objectToFormData, 111 objectToFormData,
107 lineFeedToHtml 112 lineFeedToHtml,
113 removeElementFromArray
108} 114}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 97e49e7ab..c3f4bf88b 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -37,12 +37,14 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
37import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 37import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
38import { 38import {
39 CustomConfigValidatorsService, 39 CustomConfigValidatorsService,
40 LoginValidatorsService, 40 LoginValidatorsService, ReactiveFileComponent,
41 ResetPasswordValidatorsService, 41 ResetPasswordValidatorsService,
42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService 42 UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService
43} from '@app/shared/forms' 43} from '@app/shared/forms'
44import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' 44import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
45import { ScreenService } from '@app/shared/misc/screen.service' 45import { ScreenService } from '@app/shared/misc/screen.service'
46import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service'
47import { VideoCaptionService } from '@app/shared/video-caption'
46 48
47@NgModule({ 49@NgModule({
48 imports: [ 50 imports: [
@@ -74,7 +76,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
74 FromNowPipe, 76 FromNowPipe,
75 MarkdownTextareaComponent, 77 MarkdownTextareaComponent,
76 InfiniteScrollerDirective, 78 InfiniteScrollerDirective,
77 HelpComponent 79 HelpComponent,
80 ReactiveFileComponent
78 ], 81 ],
79 82
80 exports: [ 83 exports: [
@@ -102,6 +105,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
102 MarkdownTextareaComponent, 105 MarkdownTextareaComponent,
103 InfiniteScrollerDirective, 106 InfiniteScrollerDirective,
104 HelpComponent, 107 HelpComponent,
108 ReactiveFileComponent,
105 109
106 NumberFormatterPipe, 110 NumberFormatterPipe,
107 ObjectLengthPipe, 111 ObjectLengthPipe,
@@ -119,6 +123,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
119 AccountService, 123 AccountService,
120 MarkdownService, 124 MarkdownService,
121 VideoChannelService, 125 VideoChannelService,
126 VideoCaptionService,
122 127
123 FormValidatorService, 128 FormValidatorService,
124 CustomConfigValidatorsService, 129 CustomConfigValidatorsService,
@@ -129,6 +134,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
129 VideoChannelValidatorsService, 134 VideoChannelValidatorsService,
130 VideoCommentValidatorsService, 135 VideoCommentValidatorsService,
131 VideoValidatorsService, 136 VideoValidatorsService,
137 VideoCaptionsValidatorsService,
132 138
133 I18nPrimengCalendarService, 139 I18nPrimengCalendarService,
134 ScreenService, 140 ScreenService,
diff --git a/client/src/app/shared/video-caption/index.ts b/client/src/app/shared/video-caption/index.ts
new file mode 100644
index 000000000..c48a70558
--- /dev/null
+++ b/client/src/app/shared/video-caption/index.ts
@@ -0,0 +1 @@
export * from './video-caption.service'
diff --git a/client/src/app/shared/video-caption/video-caption-edit.model.ts b/client/src/app/shared/video-caption/video-caption-edit.model.ts
new file mode 100644
index 000000000..732f20158
--- /dev/null
+++ b/client/src/app/shared/video-caption/video-caption-edit.model.ts
@@ -0,0 +1,9 @@
1export interface VideoCaptionEdit {
2 language: {
3 id: string
4 label?: string
5 }
6
7 action?: 'CREATE' | 'REMOVE'
8 captionfile?: any
9}
diff --git a/client/src/app/shared/video-caption/video-caption.service.ts b/client/src/app/shared/video-caption/video-caption.service.ts
new file mode 100644
index 000000000..4ae8ebd0a
--- /dev/null
+++ b/client/src/app/shared/video-caption/video-caption.service.ts
@@ -0,0 +1,61 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { forkJoin, Observable } from 'rxjs'
5import { ResultList } from '../../../../../shared'
6import { RestExtractor, RestService } from '../rest'
7import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model'
8import { VideoService } from '@app/shared/video/video.service'
9import { objectToFormData } from '@app/shared/misc/utils'
10import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
11
12@Injectable()
13export class VideoCaptionService {
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) {}
19
20 listCaptions (videoId: number | string): Observable<ResultList<VideoCaption>> {
21 return this.authHttp.get<ResultList<VideoCaption>>(VideoService.BASE_VIDEO_URL + videoId + '/captions')
22 .pipe(catchError(res => this.restExtractor.handleError(res)))
23 }
24
25 removeCaption (videoId: number | string, language: string) {
26 return this.authHttp.delete(VideoService.BASE_VIDEO_URL + videoId + '/captions/' + language)
27 .pipe(
28 map(this.restExtractor.extractDataBool),
29 catchError(res => this.restExtractor.handleError(res))
30 )
31 }
32
33 addCaption (videoId: number | string, language: string, captionfile: File) {
34 const body = { captionfile }
35 const data = objectToFormData(body)
36
37 return this.authHttp.put(VideoService.BASE_VIDEO_URL + videoId + '/captions/' + language, data)
38 .pipe(
39 map(this.restExtractor.extractDataBool),
40 catchError(res => this.restExtractor.handleError(res))
41 )
42 }
43
44 updateCaptions (videoId: number | string, videoCaptions: VideoCaptionEdit[]) {
45 const observables: Observable<any>[] = []
46
47 for (const videoCaption of videoCaptions) {
48 if (videoCaption.action === 'CREATE') {
49 observables.push(
50 this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile)
51 )
52 } else if (videoCaption.action === 'REMOVE') {
53 observables.push(
54 this.removeCaption(videoId, videoCaption.language.id)
55 )
56 }
57 }
58
59 return forkJoin(observables)
60 }
61}
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 5c820a227..6b1a299ea 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -1,7 +1,7 @@
1import { User } from '../' 1import { User } from '../'
2import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' 2import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
3import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 3import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
4import { VideoConstant } from '../../../../../shared/models/videos/video.model' 4import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
5import { getAbsoluteAPIUrl } from '../misc/utils' 5import { getAbsoluteAPIUrl } from '../misc/utils'
6import { ServerConfig } from '../../../../../shared/models' 6import { ServerConfig } from '../../../../../shared/models'
7import { Actor } from '@app/shared/actor/actor.model' 7import { Actor } from '@app/shared/actor/actor.model'
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index 9498a06fe..b4c1f10f9 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -28,8 +28,8 @@ import { ServerService } from '@app/core'
28 28
29@Injectable() 29@Injectable()
30export class VideoService { 30export class VideoService {
31 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 31 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
32 private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' 32 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
33 33
34 constructor ( 34 constructor (
35 private authHttp: HttpClient, 35 private authHttp: HttpClient,
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
new file mode 100644
index 000000000..9cd303b29
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -0,0 +1,47 @@
1<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
2 <div class="modal-dialog">
3 <div class="modal-content" [formGroup]="form">
4
5 <div class="modal-header">
6 <span class="close" aria-hidden="true" (click)="hide()"></span>
7 <h4 i18n class="modal-title">Add caption</h4>
8 </div>
9
10 <div class="modal-body">
11 <label i18n for="language">Language</label>
12 <div class="peertube-select-container">
13 <select id="language" formControlName="language">
14 <option></option>
15 <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option>
16 </select>
17 </div>
18
19 <div *ngIf="formErrors.language" class="form-error">
20 {{ formErrors.language }}
21 </div>
22
23 <div class="caption-file">
24 <my-reactive-file
25 formControlName="captionfile" inputName="captionfile" i18n-inputLabel inputLabel="Select the caption file"
26 [extensions]="videoCaptionExtensions" [maxFileSize]="videoCaptionMaxSize" [displayFilename]="true"
27 ></my-reactive-file>
28 </div>
29
30 <div *ngIf="isReplacingExistingCaption()" class="warning-replace-caption" i18n>
31 This will replace an existing caption!
32 </div>
33
34 <div class="form-group inputs">
35 <span i18n class="action-button action-button-cancel" (click)="hide()">
36 Cancel
37 </span>
38
39 <input
40 type="submit" i18n-value value="Add this caption" class="action-button-submit"
41 [disabled]="!form.valid" (click)="addCaption()"
42 >
43 </div>
44 </div>
45 </div>
46 </div>
47</div>
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss
new file mode 100644
index 000000000..c6da1877e
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss
@@ -0,0 +1,15 @@
1@import '_variables';
2@import '_mixins';
3
4.peertube-select-container {
5 @include peertube-select-container(auto);
6}
7
8.caption-file {
9 margin-top: 20px;
10}
11
12.warning-replace-caption {
13 color: red;
14 margin-top: 10px;
15} \ No newline at end of file
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
new file mode 100644
index 000000000..45b8c71f8
--- /dev/null
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -0,0 +1,80 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ModalDirective } from 'ngx-bootstrap/modal'
3import { FormReactive } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service'
6import { ServerService } from '@app/core'
7import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
8
9@Component({
10 selector: 'my-video-caption-add-modal',
11 styleUrls: [ './video-caption-add-modal.component.scss' ],
12 templateUrl: './video-caption-add-modal.component.html'
13})
14
15export class VideoCaptionAddModalComponent extends FormReactive implements OnInit {
16 @Input() existingCaptions: string[]
17
18 @Output() captionAdded = new EventEmitter<VideoCaptionEdit>()
19
20 @ViewChild('modal') modal: ModalDirective
21
22 videoCaptionLanguages = []
23
24 private closingModal = false
25
26 constructor (
27 protected formValidatorService: FormValidatorService,
28 private serverService: ServerService,
29 private videoCaptionsValidatorsService: VideoCaptionsValidatorsService
30 ) {
31 super()
32 }
33
34 get videoCaptionExtensions () {
35 return this.serverService.getConfig().videoCaption.file.extensions
36 }
37
38 get videoCaptionMaxSize () {
39 return this.serverService.getConfig().videoCaption.file.size.max
40 }
41
42 ngOnInit () {
43 this.videoCaptionLanguages = this.serverService.getVideoLanguages()
44
45 this.buildForm({
46 language: this.videoCaptionsValidatorsService.VIDEO_CAPTION_LANGUAGE,
47 captionfile: this.videoCaptionsValidatorsService.VIDEO_CAPTION_FILE
48 })
49 }
50
51 show () {
52 this.modal.show()
53 }
54
55 hide () {
56 this.modal.hide()
57 }
58
59 isReplacingExistingCaption () {
60 if (this.closingModal === true) return false
61
62 const languageId = this.form.value[ 'language' ]
63
64 return languageId && this.existingCaptions.indexOf(languageId) !== -1
65 }
66
67 async addCaption () {
68 this.closingModal = true
69
70 const languageId = this.form.value[ 'language' ]
71 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
72
73 this.captionAdded.emit({
74 language: languageObject,
75 captionfile: this.form.value['captionfile']
76 })
77
78 this.hide()
79 }
80}
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 447c5ab9b..14d5f3614 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
@@ -132,13 +132,39 @@
132 <label i18n for="waitTranscoding">Wait transcoding before publishing the video</label> 132 <label i18n for="waitTranscoding">Wait transcoding before publishing the video</label>
133 <my-help 133 <my-help
134 tooltipPlacement="top" helpType="custom" i18n-customHtml 134 tooltipPlacement="top" helpType="custom" i18n-customHtml
135 customHtml="If you decide to not wait transcoding before publishing the video, it can be unplayable until it transcoding ends." 135 customHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
136 ></my-help> 136 ></my-help>
137 </div> 137 </div>
138 138
139 </div> 139 </div>
140 </tab> 140 </tab>
141 141
142 <tab i18n-heading heading="Captions">
143 <div class="col-md-12 captions">
144
145 <div class="captions-header">
146 <a (click)="openAddCaptionModal()" class="create-caption">
147 <span class="icon icon-add"></span>
148 <ng-container i18n>Add another caption</ng-container>
149 </a>
150 </div>
151
152 <div class="form-group" *ngFor="let videoCaption of videoCaptions">
153
154 <div class="caption-entry">
155 <div class="caption-entry-label">{{ videoCaption.language.label }}</div>
156
157 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
158 </div>
159 </div>
160
161 <div class="no-caption" *ngIf="videoCaptions?.length === 0">
162 No captions for now.
163 </div>
164
165 </div>
166 </tab>
167
142 <tab i18n-heading heading="Advanced settings"> 168 <tab i18n-heading heading="Advanced settings">
143 <div class="col-md-12 advanced-settings"> 169 <div class="col-md-12 advanced-settings">
144 <div class="form-group"> 170 <div class="form-group">
@@ -172,3 +198,7 @@
172 </tabset> 198 </tabset>
173 199
174</div> 200</div>
201
202<my-video-caption-add-modal
203 #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" (captionAdded)="onCaptionAdded($event)"
204></my-video-caption-add-modal> \ No newline at end of file
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 061eca4a7..03b8359de 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,6 +7,7 @@
7 7
8.video-edit { 8.video-edit {
9 height: 100%; 9 height: 100%;
10 min-height: 300px;
10 11
11 .form-group { 12 .form-group {
12 margin-bottom: 25px; 13 margin-bottom: 25px;
@@ -49,6 +50,40 @@
49 } 50 }
50} 51}
51 52
53.captions {
54
55 .captions-header {
56 text-align: right;
57
58 .create-caption {
59 @include create-button('../../../../assets/images/global/add.svg');
60 }
61 }
62
63 .caption-entry {
64 display: flex;
65 height: 40px;
66 align-items: center;
67
68 .caption-entry-label {
69 font-size: 15px;
70 font-weight: bold;
71
72 margin-right: 20px;
73 }
74
75 .caption-entry-delete {
76 @include peertube-button;
77 @include grey-button;
78 }
79 }
80
81 .no-caption {
82 text-align: center;
83 font-size: 15px;
84 }
85}
86
52.submit-container { 87.submit-container {
53 text-align: right; 88 text-align: right;
54 89
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 66eb6611a..9394d7dab 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { FormGroup, ValidatorFn, Validators } from '@angular/forms' 2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
@@ -8,6 +8,10 @@ import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' 10import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
11import { VideoCaptionService } from '@app/shared/video-caption'
12import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/video-caption-add-modal.component'
13import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
14import { removeElementFromArray } from '@app/shared/misc/utils'
11 15
12@Component({ 16@Component({
13 selector: 'my-video-edit', 17 selector: 'my-video-edit',
@@ -15,13 +19,16 @@ import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calend
15 templateUrl: './video-edit.component.html' 19 templateUrl: './video-edit.component.html'
16}) 20})
17 21
18export class VideoEditComponent implements OnInit { 22export class VideoEditComponent implements OnInit, OnDestroy {
19 @Input() form: FormGroup 23 @Input() form: FormGroup
20 @Input() formErrors: { [ id: string ]: string } = {} 24 @Input() formErrors: { [ id: string ]: string } = {}
21 @Input() validationMessages: FormReactiveValidationMessages = {} 25 @Input() validationMessages: FormReactiveValidationMessages = {}
22 @Input() videoPrivacies = [] 26 @Input() videoPrivacies = []
23 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
24 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: VideoCaptionEdit[] = []
30
31 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
25 32
26 // So that it can be accessed in the template 33 // So that it can be accessed in the template
27 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 34 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
@@ -41,9 +48,12 @@ export class VideoEditComponent implements OnInit {
41 calendarTimezone: string 48 calendarTimezone: string
42 calendarDateFormat: string 49 calendarDateFormat: string
43 50
51 private schedulerInterval
52
44 constructor ( 53 constructor (
45 private formValidatorService: FormValidatorService, 54 private formValidatorService: FormValidatorService,
46 private videoValidatorsService: VideoValidatorsService, 55 private videoValidatorsService: VideoValidatorsService,
56 private videoCaptionService: VideoCaptionService,
47 private route: ActivatedRoute, 57 private route: ActivatedRoute,
48 private router: Router, 58 private router: Router,
49 private notificationsService: NotificationsService, 59 private notificationsService: NotificationsService,
@@ -91,6 +101,13 @@ export class VideoEditComponent implements OnInit {
91 defaultValues 101 defaultValues
92 ) 102 )
93 103
104 this.form.addControl('captions', new FormArray([
105 new FormGroup({
106 language: new FormControl(),
107 captionfile: new FormControl()
108 })
109 ]))
110
94 this.trackChannelChange() 111 this.trackChannelChange()
95 this.trackPrivacyChange() 112 this.trackPrivacyChange()
96 } 113 }
@@ -102,7 +119,35 @@ export class VideoEditComponent implements OnInit {
102 this.videoLicences = this.serverService.getVideoLicences() 119 this.videoLicences = this.serverService.getVideoLicences()
103 this.videoLanguages = this.serverService.getVideoLanguages() 120 this.videoLanguages = this.serverService.getVideoLanguages()
104 121
105 setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute 122 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
123 }
124
125 ngOnDestroy () {
126 if (this.schedulerInterval) clearInterval(this.schedulerInterval)
127 }
128
129 getExistingCaptions () {
130 return this.videoCaptions.map(c => c.language.id)
131 }
132
133 onCaptionAdded (caption: VideoCaptionEdit) {
134 this.videoCaptions.push(
135 Object.assign(caption, { action: 'CREATE' as 'CREATE' })
136 )
137 }
138
139 deleteCaption (caption: VideoCaptionEdit) {
140 // This caption is not on the server, just remove it from our array
141 if (caption.action === 'CREATE') {
142 removeElementFromArray(this.videoCaptions, caption)
143 return
144 }
145
146 caption.action = 'REMOVE' as 'REMOVE'
147 }
148
149 openAddCaptionModal () {
150 this.videoCaptionAddModal.show()
106 } 151 }
107 152
108 private trackPrivacyChange () { 153 private trackPrivacyChange () {
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
index 6bf3e34b1..f6bd65fdc 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts
@@ -5,6 +5,7 @@ import { SharedModule } from '../../../shared/'
5import { VideoEditComponent } from './video-edit.component' 5import { VideoEditComponent } from './video-edit.component'
6import { VideoImageComponent } from './video-image.component' 6import { VideoImageComponent } from './video-image.component'
7import { CalendarModule } from 'primeng/components/calendar/calendar' 7import { CalendarModule } from 'primeng/components/calendar/calendar'
8import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
8 9
9@NgModule({ 10@NgModule({
10 imports: [ 11 imports: [
@@ -16,7 +17,8 @@ import { CalendarModule } from 'primeng/components/calendar/calendar'
16 17
17 declarations: [ 18 declarations: [
18 VideoEditComponent, 19 VideoEditComponent,
19 VideoImageComponent 20 VideoImageComponent,
21 VideoCaptionAddModalComponent
20 ], 22 ],
21 23
22 exports: [ 24 exports: [
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.html b/client/src/app/videos/+video-edit/shared/video-image.component.html
index e319d7ee7..c09c862c4 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-image.component.html
@@ -1,15 +1,8 @@
1<div class="root"> 1<div class="root">
2 <div> 2 <my-reactive-file
3 <div class="button-file"> 3 [inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
4 <span>{{ inputLabel }}</span> 4 (fileChanged)="onFileChanged($event)"
5 <input 5 ></my-reactive-file>
6 type="file"
7 [name]="inputName" [id]="inputName" [accept]="videoImageExtensions"
8 (change)="fileChange($event)"
9 />
10 </div>
11 <div i18n class="image-constraints">(extensions: {{ videoImageExtensions }}, max size: {{ maxVideoImageSize | bytes }})</div>
12 </div>
13 6
14 <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" /> 7 <img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
15 <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div> 8 <div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.scss b/client/src/app/videos/+video-edit/shared/video-image.component.scss
index d4901e7ab..b63963bca 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-image.component.scss
@@ -6,16 +6,6 @@
6 display: flex; 6 display: flex;
7 align-items: center; 7 align-items: center;
8 8
9 .button-file {
10 @include peertube-button-file(auto);
11
12 min-width: 190px;
13 }
14
15 .image-constraints {
16 font-size: 13px;
17 }
18
19 .preview { 9 .preview {
20 border: 2px solid grey; 10 border: 2px solid grey;
21 border-radius: 4px; 11 border-radius: 4px;
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.ts b/client/src/app/videos/+video-edit/shared/video-image.component.ts
index 25955baaa..a604cde90 100644
--- a/client/src/app/videos/+video-edit/shared/video-image.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-image.component.ts
@@ -2,8 +2,6 @@ import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' 3import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5import { NotificationsService } from 'angular2-notifications'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7 5
8@Component({ 6@Component({
9 selector: 'my-video-image', 7 selector: 'my-video-image',
@@ -25,36 +23,26 @@ export class VideoImageComponent implements ControlValueAccessor {
25 23
26 imageSrc: SafeResourceUrl 24 imageSrc: SafeResourceUrl
27 25
28 private file: Blob 26 private file: File
29 27
30 constructor ( 28 constructor (
31 private sanitizer: DomSanitizer, 29 private sanitizer: DomSanitizer,
32 private serverService: ServerService, 30 private serverService: ServerService
33 private notificationsService: NotificationsService,
34 private i18n: I18n
35 ) {} 31 ) {}
36 32
37 get videoImageExtensions () { 33 get videoImageExtensions () {
38 return this.serverService.getConfig().video.image.extensions.join(',') 34 return this.serverService.getConfig().video.image.extensions
39 } 35 }
40 36
41 get maxVideoImageSize () { 37 get maxVideoImageSize () {
42 return this.serverService.getConfig().video.image.size.max 38 return this.serverService.getConfig().video.image.size.max
43 } 39 }
44 40
45 fileChange (event: any) { 41 onFileChanged (file: File) {
46 if (event.target.files && event.target.files.length) { 42 this.file = file
47 const [ file ] = event.target.files
48
49 if (file.size > this.maxVideoImageSize) {
50 this.notificationsService.error(this.i18n('Error'), this.i18n('This image is too large.'))
51 return
52 }
53 43
54 this.file = file 44 this.propagateChange(this.file)
55 this.propagateChange(this.file) 45 this.updatePreview()
56 this.updatePreview()
57 }
58 } 46 }
59 47
60 propagateChange = (_: any) => { /* empty */ } 48 propagateChange = (_: any) => { /* empty */ }
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index 7d9443209..9c2c01c65 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -46,7 +46,7 @@
46 <!-- Hidden because we want to load the component --> 46 <!-- Hidden because we want to load the component -->
47 <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 47 <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
48 <my-video-edit 48 <my-video-edit
49 [form]="form" [formErrors]="formErrors" 49 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
50 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 50 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
51 ></my-video-edit> 51 ></my-video-edit>
52 52
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 7c4b6260b..8c30cedfb 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -15,6 +15,8 @@ import { VideoEdit } from '../../shared/video/video-edit.model'
15import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
16import { I18n } from '@ngx-translate/i18n-polyfill' 16import { I18n } from '@ngx-translate/i18n-polyfill'
17import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 17import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
18import { switchMap } from 'rxjs/operators'
19import { VideoCaptionService } from '@app/shared/video-caption'
18 20
19@Component({ 21@Component({
20 selector: 'my-videos-add', 22 selector: 'my-videos-add',
@@ -46,6 +48,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
46 videoPrivacies = [] 48 videoPrivacies = []
47 firstStepPrivacyId = 0 49 firstStepPrivacyId = 0
48 firstStepChannelId = 0 50 firstStepChannelId = 0
51 videoCaptions = []
49 52
50 constructor ( 53 constructor (
51 protected formValidatorService: FormValidatorService, 54 protected formValidatorService: FormValidatorService,
@@ -56,7 +59,8 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
56 private serverService: ServerService, 59 private serverService: ServerService,
57 private videoService: VideoService, 60 private videoService: VideoService,
58 private loadingBar: LoadingBarService, 61 private loadingBar: LoadingBarService,
59 private i18n: I18n 62 private i18n: I18n,
63 private videoCaptionService: VideoCaptionService
60 ) { 64 ) {
61 super() 65 super()
62 } 66 }
@@ -159,11 +163,8 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
159 let name: string 163 let name: string
160 164
161 // If the name of the file is very small, keep the extension 165 // If the name of the file is very small, keep the extension
162 if (nameWithoutExtension.length < 3) { 166 if (nameWithoutExtension.length < 3) name = videofile.name
163 name = videofile.name 167 else name = nameWithoutExtension
164 } else {
165 name = nameWithoutExtension
166 }
167 168
168 const privacy = this.firstStepPrivacyId.toString() 169 const privacy = this.firstStepPrivacyId.toString()
169 const nsfw = false 170 const nsfw = false
@@ -225,22 +226,25 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
225 this.isUpdatingVideo = true 226 this.isUpdatingVideo = true
226 this.loadingBar.start() 227 this.loadingBar.start()
227 this.videoService.updateVideo(video) 228 this.videoService.updateVideo(video)
228 .subscribe( 229 .pipe(
229 () => { 230 // Then update captions
230 this.isUpdatingVideo = false 231 switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions))
231 this.isUploadingVideo = false 232 )
232 this.loadingBar.complete() 233 .subscribe(
233 234 () => {
234 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) 235 this.isUpdatingVideo = false
235 this.router.navigate([ '/videos/watch', video.uuid ]) 236 this.isUploadingVideo = false
236 }, 237 this.loadingBar.complete()
237 238
238 err => { 239 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.'))
239 this.isUpdatingVideo = false 240 this.router.navigate([ '/videos/watch', video.uuid ])
240 this.notificationsService.error(this.i18n('Error'), err.message) 241 },
241 console.error(err) 242
242 } 243 err => {
243 ) 244 this.isUpdatingVideo = false
244 245 this.notificationsService.error(this.i18n('Error'), err.message)
246 console.error(err)
247 }
248 )
245 } 249 }
246} 250}
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 5cb16c8ab..9242c30a0 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -8,6 +8,7 @@
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions"
11 ></my-video-edit> 12 ></my-video-edit>
12 13
13 <div class="submit-container"> 14 <div class="submit-container">
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 c4e6f44de..b67874401 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -12,6 +12,7 @@ import { VideoService } from '../../shared/video/video.service'
12import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 12import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 14import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
15import { VideoCaptionService } from '@app/shared/video-caption'
15 16
16@Component({ 17@Component({
17 selector: 'my-videos-update', 18 selector: 'my-videos-update',
@@ -25,6 +26,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
25 videoPrivacies = [] 26 videoPrivacies = []
26 userVideoChannels = [] 27 userVideoChannels = []
27 schedulePublicationPossible = false 28 schedulePublicationPossible = false
29 videoCaptions = []
28 30
29 constructor ( 31 constructor (
30 protected formValidatorService: FormValidatorService, 32 protected formValidatorService: FormValidatorService,
@@ -36,6 +38,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
36 private authService: AuthService, 38 private authService: AuthService,
37 private loadingBar: LoadingBarService, 39 private loadingBar: LoadingBarService,
38 private videoChannelService: VideoChannelService, 40 private videoChannelService: VideoChannelService,
41 private videoCaptionService: VideoCaptionService,
39 private i18n: I18n 42 private i18n: I18n
40 ) { 43 ) {
41 super() 44 super()
@@ -63,12 +66,21 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
63 map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support }))), 66 map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support }))),
64 map(videoChannels => ({ video, videoChannels })) 67 map(videoChannels => ({ video, videoChannels }))
65 ) 68 )
69 }),
70 switchMap(({ video, videoChannels }) => {
71 return this.videoCaptionService
72 .listCaptions(video.id)
73 .pipe(
74 map(result => result.data),
75 map(videoCaptions => ({ video, videoChannels, videoCaptions }))
76 )
66 }) 77 })
67 ) 78 )
68 .subscribe( 79 .subscribe(
69 ({ video, videoChannels }) => { 80 ({ video, videoChannels, videoCaptions }) => {
70 this.video = new VideoEdit(video) 81 this.video = new VideoEdit(video)
71 this.userVideoChannels = videoChannels 82 this.userVideoChannels = videoChannels
83 this.videoCaptions = videoCaptions
72 84
73 // We cannot set private a video that was not private 85 // We cannot set private a video that was not private
74 if (this.video.privacy !== VideoPrivacy.PRIVATE) { 86 if (this.video.privacy !== VideoPrivacy.PRIVATE) {
@@ -102,21 +114,27 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
102 114
103 this.loadingBar.start() 115 this.loadingBar.start()
104 this.isUpdatingVideo = true 116 this.isUpdatingVideo = true
117
118 // Update the video
105 this.videoService.updateVideo(this.video) 119 this.videoService.updateVideo(this.video)
106 .subscribe( 120 .pipe(
107 () => { 121 // Then update captions
108 this.isUpdatingVideo = false 122 switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions))
109 this.loadingBar.complete() 123 )
110 this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) 124 .subscribe(
111 this.router.navigate([ '/videos/watch', this.video.uuid ]) 125 () => {
112 }, 126 this.isUpdatingVideo = false
113 127 this.loadingBar.complete()
114 err => { 128 this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.'))
115 this.isUpdatingVideo = false 129 this.router.navigate([ '/videos/watch', this.video.uuid ])
116 this.notificationsService.error(this.i18n('Error'), err.message) 130 },
117 console.error(err) 131
118 } 132 err => {
119 ) 133 this.isUpdatingVideo = false
134 this.notificationsService.error(this.i18n('Error'), err.message)
135 console.error(err)
136 }
137 )
120 138
121 } 139 }
122 140