diff options
author | Chocobozzz <me@florianbigard.com> | 2018-07-12 19:02:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-07-16 11:50:08 +0200 |
commit | 40e87e9ecc54e3513fb586928330a7855eb192c6 (patch) | |
tree | af1111ecba85f9cd8286811ff332a67cf21be2f6 /client/src/app | |
parent | d4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff) | |
download | PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.gz PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.zst PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.zip |
Implement captions/subtitles
Diffstat (limited to 'client/src/app')
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' | |||
8 | export * from './video-channel-validators.service' | 8 | export * from './video-channel-validators.service' |
9 | export * from './video-comment-validators.service' | 9 | export * from './video-comment-validators.service' |
10 | export * from './video-validators.service' | 10 | export * from './video-validators.service' |
11 | export * 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 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Validators } from '@angular/forms' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { BuildFormValidator } from '@app/shared' | ||
5 | |||
6 | @Injectable() | ||
7 | export 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 @@ | |||
1 | export * from './form-validators' | 1 | export * from './form-validators' |
2 | export * from './form-reactive' | 2 | export * from './form-reactive' |
3 | export * 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 @@ | |||
1 | import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' | ||
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { 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 | }) | ||
18 | export 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 | ||
99 | function removeElementFromArray <T> (arr: T[], elem: T) { | ||
100 | const index = arr.indexOf(elem) | ||
101 | if (index !== -1) arr.splice(index, 1) | ||
102 | } | ||
103 | |||
99 | export { | 104 | export { |
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' | |||
37 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 37 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
38 | import { | 38 | import { |
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' |
44 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' | 44 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' |
45 | import { ScreenService } from '@app/shared/misc/screen.service' | 45 | import { ScreenService } from '@app/shared/misc/screen.service' |
46 | import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' | ||
47 | import { 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 @@ | |||
1 | export 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 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { forkJoin, Observable } from 'rxjs' | ||
5 | import { ResultList } from '../../../../../shared' | ||
6 | import { RestExtractor, RestService } from '../rest' | ||
7 | import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model' | ||
8 | import { VideoService } from '@app/shared/video/video.service' | ||
9 | import { objectToFormData } from '@app/shared/misc/utils' | ||
10 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | ||
11 | |||
12 | @Injectable() | ||
13 | export 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 @@ | |||
1 | import { User } from '../' | 1 | import { User } from '../' |
2 | import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' | 2 | import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' |
3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
4 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' | 4 | import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' |
5 | import { getAbsoluteAPIUrl } from '../misc/utils' | 5 | import { getAbsoluteAPIUrl } from '../misc/utils' |
6 | import { ServerConfig } from '../../../../../shared/models' | 6 | import { ServerConfig } from '../../../../../shared/models' |
7 | import { Actor } from '@app/shared/actor/actor.model' | 7 | import { Actor } from '@app/shared/actor/actor.model' |
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() |
30 | export class VideoService { | 30 | export 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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { ModalDirective } from 'ngx-bootstrap/modal' | ||
3 | import { FormReactive } from '@app/shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' | ||
6 | import { ServerService } from '@app/core' | ||
7 | import { 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 | |||
15 | export 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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core' |
2 | import { FormGroup, ValidatorFn, Validators } from '@angular/forms' | 2 | import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' | 4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
@@ -8,6 +8,10 @@ import { VideoEdit } from '../../../shared/video/video-edit.model' | |||
8 | import { map } from 'rxjs/operators' | 8 | import { map } from 'rxjs/operators' |
9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
10 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' | 10 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' |
11 | import { VideoCaptionService } from '@app/shared/video-caption' | ||
12 | import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/video-caption-add-modal.component' | ||
13 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | ||
14 | import { 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 | ||
18 | export class VideoEditComponent implements OnInit { | 22 | export 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/' | |||
5 | import { VideoEditComponent } from './video-edit.component' | 5 | import { VideoEditComponent } from './video-edit.component' |
6 | import { VideoImageComponent } from './video-image.component' | 6 | import { VideoImageComponent } from './video-image.component' |
7 | import { CalendarModule } from 'primeng/components/calendar/calendar' | 7 | import { CalendarModule } from 'primeng/components/calendar/calendar' |
8 | import { 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' | |||
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | 3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' |
4 | import { ServerService } from '@app/core' | 4 | import { ServerService } from '@app/core' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { 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' | |||
15 | import { VideoService } from '../../shared/video/video.service' | 15 | import { VideoService } from '../../shared/video/video.service' |
16 | import { I18n } from '@ngx-translate/i18n-polyfill' | 16 | import { I18n } from '@ngx-translate/i18n-polyfill' |
17 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 17 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
18 | import { switchMap } from 'rxjs/operators' | ||
19 | import { 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' | |||
12 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 12 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 14 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
15 | import { 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 | ||