aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-edit/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-06-23 14:49:20 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-06-23 16:00:49 +0200
commit1942f11d5ee6926ad93dc1b79fae18325ba5de18 (patch)
tree3f2a3cd9466a56c419d197ac832a3e9cbc86bec4 /client/src/app/+videos/+video-edit/shared
parent67ed6552b831df66713bac9e672738796128d33f (diff)
downloadPeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.gz
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.zst
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.zip
Lazy load all routes
Diffstat (limited to 'client/src/app/+videos/+video-edit/shared')
-rw-r--r--client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts94
-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.scss20
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts85
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html280
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss197
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts274
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.module.ts38
8 files changed, 1035 insertions, 0 deletions
diff --git a/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts b/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts
new file mode 100644
index 000000000..b05852ff8
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/i18n-primeng-calendar.service.ts
@@ -0,0 +1,94 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Injectable } from '@angular/core'
3
4@Injectable()
5export class I18nPrimengCalendarService {
6 private readonly calendarLocale: any = {}
7
8 constructor (private i18n: I18n) {
9 this.calendarLocale = {
10 firstDayOfWeek: 0,
11 dayNames: [
12 this.i18n('Sunday'),
13 this.i18n('Monday'),
14 this.i18n('Tuesday'),
15 this.i18n('Wednesday'),
16 this.i18n('Thursday'),
17 this.i18n('Friday'),
18 this.i18n('Saturday')
19 ],
20
21 dayNamesShort: [
22 this.i18n({ value: 'Sun', description: 'Day name short' }),
23 this.i18n({ value: 'Mon', description: 'Day name short' }),
24 this.i18n({ value: 'Tue', description: 'Day name short' }),
25 this.i18n({ value: 'Wed', description: 'Day name short' }),
26 this.i18n({ value: 'Thu', description: 'Day name short' }),
27 this.i18n({ value: 'Fri', description: 'Day name short' }),
28 this.i18n({ value: 'Sat', description: 'Day name short' })
29 ],
30
31 dayNamesMin: [
32 this.i18n({ value: 'Su', description: 'Day name min' }),
33 this.i18n({ value: 'Mo', description: 'Day name min' }),
34 this.i18n({ value: 'Tu', description: 'Day name min' }),
35 this.i18n({ value: 'We', description: 'Day name min' }),
36 this.i18n({ value: 'Th', description: 'Day name min' }),
37 this.i18n({ value: 'Fr', description: 'Day name min' }),
38 this.i18n({ value: 'Sa', description: 'Day name min' })
39 ],
40
41 monthNames: [
42 this.i18n('January'),
43 this.i18n('February'),
44 this.i18n('March'),
45 this.i18n('April'),
46 this.i18n('May'),
47 this.i18n('June'),
48 this.i18n('July'),
49 this.i18n('August'),
50 this.i18n('September'),
51 this.i18n('October'),
52 this.i18n('November'),
53 this.i18n('December')
54 ],
55
56 monthNamesShort: [
57 this.i18n({ value: 'Jan', description: 'Month name short' }),
58 this.i18n({ value: 'Feb', description: 'Month name short' }),
59 this.i18n({ value: 'Mar', description: 'Month name short' }),
60 this.i18n({ value: 'Apr', description: 'Month name short' }),
61 this.i18n({ value: 'May', description: 'Month name short' }),
62 this.i18n({ value: 'Jun', description: 'Month name short' }),
63 this.i18n({ value: 'Jul', description: 'Month name short' }),
64 this.i18n({ value: 'Aug', description: 'Month name short' }),
65 this.i18n({ value: 'Sep', description: 'Month name short' }),
66 this.i18n({ value: 'Oct', description: 'Month name short' }),
67 this.i18n({ value: 'Nov', description: 'Month name short' }),
68 this.i18n({ value: 'Dec', description: 'Month name short' })
69 ],
70
71 today: this.i18n('Today'),
72
73 clear: this.i18n('Clear')
74 }
75 }
76
77 getCalendarLocale () {
78 return this.calendarLocale
79 }
80
81 getTimezone () {
82 const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
83 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
84
85 return `${timezone} - ${gmt}`
86 }
87
88 getDateFormat () {
89 return this.i18n({
90 value: 'yy-mm-dd ',
91 description: 'Date format in this locale.'
92 })
93 }
94}
diff --git a/client/src/app/+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..6a9e31b5a
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -0,0 +1,47 @@
1<ng-template #modal>
2 <ng-container [formGroup]="form">
3
4 <div class="modal-header">
5 <h4 i18n class="modal-title">Add caption</h4>
6 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
7 </div>
8
9 <div class="modal-body">
10 <label i18n for="language">Language</label>
11 <div class="peertube-select-container">
12 <select id="language" formControlName="language" class="form-control">
13 <option></option>
14 <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option>
15 </select>
16 </div>
17
18 <div *ngIf="formErrors.language" class="form-error">
19 {{ formErrors.language }}
20 </div>
21
22 <div class="caption-file">
23 <my-reactive-file
24 formControlName="captionfile" inputName="captionfile" i18n-inputLabel inputLabel="Select the caption file"
25 [extensions]="videoCaptionExtensions" [maxFileSize]="videoCaptionMaxSize" [displayFilename]="true"
26 i18n-ngbTooltip [ngbTooltip]="'(extensions: ' + videoCaptionExtensions.join(', ') + ')'"
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 </div>
34
35 <div class="modal-footer inputs">
36 <input
37 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
38 (click)="hide()" (key.enter)="hide()"
39 >
40
41 <input
42 type="submit" i18n-value value="Add this caption" class="action-button-submit"
43 [disabled]="!form.valid" (click)="addCaption()"
44 >
45 </div>
46 </ng-container>
47</ng-template>
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..b257a16a9
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss
@@ -0,0 +1,20 @@
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 width: max-content;
11
12 ::ng-deep .root {
13 width: max-content;
14 }
15}
16
17.warning-replace-caption {
18 color: red;
19 margin-top: 10px;
20} \ 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..a90d04ce8
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -0,0 +1,85 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ServerService } from '@app/core'
3import { FormReactive, FormValidatorService, VideoCaptionsValidatorsService } from '@app/shared/shared-forms'
4import { VideoCaptionEdit } from '@app/shared/shared-main'
5import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
6import { ServerConfig, VideoConstant } from '@shared/models'
7
8@Component({
9 selector: 'my-video-caption-add-modal',
10 styleUrls: [ './video-caption-add-modal.component.scss' ],
11 templateUrl: './video-caption-add-modal.component.html'
12})
13
14export class VideoCaptionAddModalComponent extends FormReactive implements OnInit {
15 @Input() existingCaptions: string[]
16 @Input() serverConfig: ServerConfig
17
18 @Output() captionAdded = new EventEmitter<VideoCaptionEdit>()
19
20 @ViewChild('modal', { static: true }) modal: ElementRef
21
22 videoCaptionLanguages: VideoConstant<string>[] = []
23
24 private openedModal: NgbModalRef
25 private closingModal = false
26
27 constructor (
28 protected formValidatorService: FormValidatorService,
29 private modalService: NgbModal,
30 private serverService: ServerService,
31 private videoCaptionsValidatorsService: VideoCaptionsValidatorsService
32 ) {
33 super()
34 }
35
36 get videoCaptionExtensions () {
37 return this.serverConfig.videoCaption.file.extensions
38 }
39
40 get videoCaptionMaxSize () {
41 return this.serverConfig.videoCaption.file.size.max
42 }
43
44 ngOnInit () {
45 this.serverService.getVideoLanguages()
46 .subscribe(languages => this.videoCaptionLanguages = languages)
47
48 this.buildForm({
49 language: this.videoCaptionsValidatorsService.VIDEO_CAPTION_LANGUAGE,
50 captionfile: this.videoCaptionsValidatorsService.VIDEO_CAPTION_FILE
51 })
52 }
53
54 show () {
55 this.closingModal = false
56
57 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
58 }
59
60 hide () {
61 this.closingModal = true
62 this.openedModal.close()
63 this.form.reset()
64 }
65
66 isReplacingExistingCaption () {
67 if (this.closingModal === true) return false
68
69 const languageId = this.form.value[ 'language' ]
70
71 return languageId && this.existingCaptions.indexOf(languageId) !== -1
72 }
73
74 async addCaption () {
75 const languageId = this.form.value[ 'language' ]
76 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
77
78 this.captionAdded.emit({
79 language: languageObject,
80 captionfile: this.form.value[ 'captionfile' ]
81 })
82
83 this.hide()
84 }
85}
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
new file mode 100644
index 000000000..c11a60dce
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -0,0 +1,280 @@
1<div class="video-edit" [formGroup]="form">
2 <div ngbNav #nav="ngbNav" class="nav-tabs">
3
4 <ng-container ngbNavItem>
5 <a ngbNavLink i18n>Basic info</a>
6
7 <ng-template ngbNavContent>
8 <div class="row">
9 <div class="col-video-edit">
10 <div class="form-group">
11 <label i18n for="name">Title</label>
12 <input type="text" id="name" class="form-control" formControlName="name" />
13 <div *ngIf="formErrors.name" class="form-error">
14 {{ formErrors.name }}
15 </div>
16 </div>
17
18 <div class="form-group">
19 <label i18n class="label-tags">Tags</label>
20
21 <my-help>
22 <ng-template ptTemplate="customHtml">
23 <ng-container i18n>
24 Tags could be used to suggest relevant recommendations. <br />
25 There is a maximum of 5 tags. <br />
26 Press Enter to add a new tag.
27 </ng-container>
28 </ng-template>
29 </my-help>
30
31 <tag-input
32 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
33 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
34 formControlName="tags" [maxItems]="5" [modelAsStrings]="true"
35 ></tag-input>
36 </div>
37
38 <div class="form-group">
39 <label i18n for="description">Description</label>
40
41 <my-help helpType="markdownText">
42 <ng-template ptTemplate="preHtml">
43 <ng-container i18n>
44 Video descriptions are truncated by default and require manual action to expand them.
45 </ng-container>
46 </ng-template>
47 </my-help>
48
49 <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="true"></my-markdown-textarea>
50
51 <div *ngIf="formErrors.description" class="form-error">
52 {{ formErrors.description }}
53 </div>
54 </div>
55 </div>
56
57 <div class="col-video-edit">
58 <div class="form-group">
59 <label i18n>Channel</label>
60 <div class="peertube-select-container">
61 <select formControlName="channelId" class="form-control">
62 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
63 </select>
64 </div>
65 </div>
66
67 <div class="form-group">
68 <label i18n for="category">Category</label>
69 <div class="peertube-select-container">
70 <select id="category" formControlName="category" class="form-control">
71 <option></option>
72 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
73 </select>
74 </div>
75
76 <div *ngIf="formErrors.category" class="form-error">
77 {{ formErrors.category }}
78 </div>
79 </div>
80
81 <div class="form-group">
82 <label i18n for="licence">Licence</label>
83 <div class="peertube-select-container">
84 <select id="licence" formControlName="licence" class="form-control">
85 <option></option>
86 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
87 </select>
88 </div>
89
90 <div *ngIf="formErrors.licence" class="form-error">
91 {{ formErrors.licence }}
92 </div>
93 </div>
94
95 <div class="form-group">
96 <label i18n for="language">Language</label>
97 <div class="peertube-select-container">
98 <select id="language" formControlName="language" class="form-control">
99 <option></option>
100 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
101 </select>
102 </div>
103
104 <div *ngIf="formErrors.language" class="form-error">
105 {{ formErrors.language }}
106 </div>
107 </div>
108
109 <div class="form-group">
110 <label i18n for="privacy">Privacy</label>
111 <div class="peertube-select-container">
112 <select id="privacy" formControlName="privacy" class="form-control">
113 <option></option>
114 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
115 <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
116 </select>
117 </div>
118
119 <div *ngIf="formErrors.privacy" class="form-error">
120 {{ formErrors.privacy }}
121 </div>
122 </div>
123
124 <div *ngIf="schedulePublicationEnabled" class="form-group">
125 <label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
126 <p-calendar
127 id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
128 [locale]="calendarLocale" [minDate]="minScheduledDate" [showTime]="true" [hideOnDateTimeSelect]="true"
129 >
130 </p-calendar>
131
132 <div *ngIf="formErrors.schedulePublicationAt" class="form-error">
133 {{ formErrors.schedulePublicationAt }}
134 </div>
135 </div>
136
137 <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
138 <ng-template ptTemplate="label">
139 <ng-container i18n>This video contains mature or explicit content</ng-container>
140 </ng-template>
141
142 <ng-template ptTemplate="help">
143 <ng-container i18n>Some instances do not list videos containing mature or explicit content by default.</ng-container>
144 </ng-template>
145 </my-peertube-checkbox>
146
147 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
148 <ng-template ptTemplate="label">
149 <ng-container i18n>Wait transcoding before publishing the video</ng-container>
150 </ng-template>
151
152 <ng-template ptTemplate="help">
153 <ng-container i18n>If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends.</ng-container>
154 </ng-template>
155 </my-peertube-checkbox>
156
157 </div>
158 </div>
159 </ng-template>
160 </ng-container>
161
162 <ng-container ngbNavItem>
163 <a ngbNavLink i18n>Captions</a>
164
165 <ng-template ngbNavContent>
166 <div class="captions">
167
168 <div class="captions-header">
169 <a (click)="openAddCaptionModal()" class="create-caption">
170 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
171 <ng-container i18n>Add another caption</ng-container>
172 </a>
173 </div>
174
175 <div class="form-group" *ngFor="let videoCaption of videoCaptions">
176
177 <div class="caption-entry">
178 <ng-container *ngIf="!videoCaption.action">
179 <a
180 i18n-title title="See the subtitle file" class="caption-entry-label" target="_blank" rel="noopener noreferrer"
181 [href]="videoCaption.captionPath"
182 >{{ videoCaption.language.label }}</a>
183
184 <div i18n class="caption-entry-state">Already uploaded &#10004;</div>
185
186 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
187 </ng-container>
188
189 <ng-container *ngIf="videoCaption.action === 'CREATE'">
190 <span class="caption-entry-label">{{ videoCaption.language.label }}</span>
191
192 <div i18n class="caption-entry-state caption-entry-state-create">Will be created on update</div>
193
194 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span>
195 </ng-container>
196
197 <ng-container *ngIf="videoCaption.action === 'REMOVE'">
198 <span class="caption-entry-label">{{ videoCaption.language.label }}</span>
199
200 <div i18n class="caption-entry-state caption-entry-state-delete">Will be deleted on update</div>
201
202 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
203 </ng-container>
204 </div>
205 </div>
206
207 <div i18n class="no-caption" *ngIf="videoCaptions?.length === 0">
208 No captions for now.
209 </div>
210
211 </div>
212 </ng-template>
213 </ng-container>
214
215 <ng-container ngbNavItem>
216 <a ngbNavLink i18n>Advanced settings</a>
217
218 <ng-template ngbNavContent>
219 <div class="row advanced-settings">
220 <div class="col-md-12 col-xl-8">
221
222 <div class="form-group">
223 <label i18n for="previewfile">Video preview</label>
224
225 <my-preview-upload
226 i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
227 previewWidth="360px" previewHeight="200px"
228 ></my-preview-upload>
229 </div>
230
231 <div class="form-group">
232 <label i18n for="support">Support</label>
233 <my-help helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support you (membership platform...)."></my-help>
234 <my-markdown-textarea
235 id="support" formControlName="support" markdownType="enhanced"
236 [classes]="{ 'input-error': formErrors['support'] }"
237 ></my-markdown-textarea>
238 <div *ngIf="formErrors.support" class="form-error">
239 {{ formErrors.support }}
240 </div>
241 </div>
242 </div>
243
244 <div class="col-md-12 col-xl-4">
245 <div class="form-group originally-published-at">
246 <label i18n for="originallyPublishedAt">Original publication date</label>
247 <my-help i18n-preHtml preHtml="This is the date when the content was originally published (e.g. the release date for a film)"></my-help>
248 <p-calendar
249 id="originallyPublishedAt" formControlName="originallyPublishedAt" [dateFormat]="calendarDateFormat"
250 [locale]="calendarLocale" [showTime]="true" [hideOnDateTimeSelect]="true" [monthNavigator]="true" [yearNavigator]="true" [yearRange]="myYearRange"
251 >
252 </p-calendar>
253
254 <div *ngIf="formErrors.originallyPublishedAt" class="form-error">
255 {{ formErrors.originallyPublishedAt }}
256 </div>
257 </div>
258
259 <my-peertube-checkbox
260 inputName="commentsEnabled" formControlName="commentsEnabled"
261 i18n-labelText labelText="Enable video comments"
262 ></my-peertube-checkbox>
263
264 <my-peertube-checkbox
265 inputName="downloadEnabled" formControlName="downloadEnabled"
266 i18n-labelText labelText="Enable download"
267 ></my-peertube-checkbox>
268 </div>
269 </div>
270 </ng-template>
271 </ng-container>
272
273 </div>
274
275 <div [ngbNavOutlet]="nav"></div>
276</div>
277
278<my-video-caption-add-modal
279 #videoCaptionAddModal [existingCaptions]="existingCaptions" [serverConfig]="serverConfig" (captionAdded)="onCaptionAdded($event)"
280></my-video-caption-add-modal>
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
new file mode 100644
index 000000000..69b907288
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -0,0 +1,197 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import 'variables';
8@import 'mixins';
9
10label {
11 font-weight: $font-regular;
12 font-size: 100%;
13}
14
15.peertube-select-container {
16 @include peertube-select-container(auto);
17}
18
19.title-page a {
20 color: pvar(--mainForegroundColor);
21
22 &:hover {
23 text-decoration: none;
24 opacity: .8;
25 }
26}
27
28my-peertube-checkbox {
29 display: block;
30 margin-bottom: 1rem;
31}
32
33.nav-tabs {
34 margin-bottom: 15px;
35}
36
37.video-edit {
38 height: 100%;
39 min-height: 300px;
40
41 .form-group {
42 margin-bottom: 25px;
43 }
44
45 input {
46 @include peertube-input-text(100%);
47 display: block;
48 }
49
50 .label-tags + span {
51 font-size: 15px;
52 }
53
54 .advanced-settings .form-group {
55 margin-bottom: 20px;
56 }
57}
58
59.captions {
60
61 .captions-header {
62 text-align: right;
63 margin-bottom: 1rem;
64
65 .create-caption {
66 @include create-button;
67 }
68 }
69
70 .caption-entry {
71 display: flex;
72 height: 40px;
73 align-items: center;
74
75 a.caption-entry-label {
76 @include disable-default-a-behaviour;
77
78 flex-grow: 1;
79 color: #000;
80
81 &:hover {
82 opacity: 0.8;
83 }
84 }
85
86 .caption-entry-label {
87 font-size: 15px;
88 font-weight: bold;
89
90 margin-right: 20px;
91 width: 150px;
92 }
93
94 .caption-entry-state {
95 width: 200px;
96
97 &.caption-entry-state-create {
98 color: #39CC0B;
99 }
100
101 &.caption-entry-state-delete {
102 color: #FF0000;
103 }
104 }
105
106 .caption-entry-delete {
107 @include peertube-button;
108 @include grey-button;
109 }
110 }
111
112 .no-caption {
113 text-align: center;
114 font-size: 15px;
115 }
116}
117
118.submit-container {
119 text-align: right;
120
121 .message-submit {
122 display: inline-block;
123 margin-right: 25px;
124
125 color: pvar(--greyForegroundColor);
126 font-size: 15px;
127 }
128
129 .submit-button {
130 @include peertube-button;
131 @include orange-button;
132 @include button-with-icon(20px, 1px);
133
134 display: inline-block;
135
136 input {
137 cursor: inherit;
138 background-color: inherit;
139 border: none;
140 padding: 0;
141 outline: 0;
142 color: inherit;
143 font-weight: $font-semibold;
144 }
145 }
146}
147
148p-calendar {
149 display: block;
150
151 ::ng-deep {
152 input,
153 .ui-calendar {
154 width: 100%;
155 }
156
157 input {
158 @include peertube-input-text(100%);
159 color: #000;
160 }
161 }
162}
163
164@include ng2-tags;
165
166// columns for the video
167.col-video-edit {
168 @include make-col-ready();
169
170 @include media-breakpoint-up(md) {
171 @include make-col(7);
172
173 & + .col-video-edit {
174 @include make-col(5);
175 }
176 }
177
178 @include media-breakpoint-up(xl) {
179 @include make-col(8);
180
181 & + .col-video-edit {
182 @include make-col(4);
183 }
184 }
185}
186
187:host-context(.expanded) {
188 .col-video-edit {
189 @include media-breakpoint-up(md) {
190 @include make-col(8);
191
192 & + .col-video-edit {
193 @include make-col(4);
194 }
195 }
196 }
197}
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
new file mode 100644
index 000000000..239e453ad
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -0,0 +1,274 @@
1import { map } from 'rxjs/operators'
2import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
4import { ServerService } from '@app/core'
5import { removeElementFromArray } from '@app/helpers'
6import { FormReactiveValidationMessages, FormValidatorService, VideoValidatorsService } from '@app/shared/shared-forms'
7import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
8import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
9import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
10import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
11
12@Component({
13 selector: 'my-video-edit',
14 styleUrls: [ './video-edit.component.scss' ],
15 templateUrl: './video-edit.component.html'
16})
17export class VideoEditComponent implements OnInit, OnDestroy {
18 @Input() form: FormGroup
19 @Input() formErrors: { [ id: string ]: string } = {}
20 @Input() validationMessages: FormReactiveValidationMessages = {}
21 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
22 @Input() schedulePublicationPossible = true
23 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
24 @Input() waitTranscodingEnabled = true
25
26 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
27
28 // So that it can be accessed in the template
29 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
30
31 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
32 videoCategories: VideoConstant<number>[] = []
33 videoLicences: VideoConstant<number>[] = []
34 videoLanguages: VideoConstant<string>[] = []
35
36 tagValidators: ValidatorFn[]
37 tagValidatorsMessages: { [ name: string ]: string }
38
39 schedulePublicationEnabled = false
40
41 calendarLocale: any = {}
42 minScheduledDate = new Date()
43 myYearRange = '1880:' + (new Date()).getFullYear()
44
45 calendarTimezone: string
46 calendarDateFormat: string
47
48 serverConfig: ServerConfig
49
50 private schedulerInterval: any
51 private firstPatchDone = false
52 private initialVideoCaptions: string[] = []
53
54 constructor (
55 private formValidatorService: FormValidatorService,
56 private videoValidatorsService: VideoValidatorsService,
57 private videoService: VideoService,
58 private serverService: ServerService,
59 private i18nPrimengCalendarService: I18nPrimengCalendarService,
60 private ngZone: NgZone
61 ) {
62 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
63 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
64
65 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
66 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
67 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
68 }
69
70 get existingCaptions () {
71 return this.videoCaptions
72 .filter(c => c.action !== 'REMOVE')
73 .map(c => c.language.id)
74 }
75
76 updateForm () {
77 const defaultValues: any = {
78 nsfw: 'false',
79 commentsEnabled: 'true',
80 downloadEnabled: 'true',
81 waitTranscoding: 'true',
82 tags: []
83 }
84 const obj: any = {
85 name: this.videoValidatorsService.VIDEO_NAME,
86 privacy: this.videoValidatorsService.VIDEO_PRIVACY,
87 channelId: this.videoValidatorsService.VIDEO_CHANNEL,
88 nsfw: null,
89 commentsEnabled: null,
90 downloadEnabled: null,
91 waitTranscoding: null,
92 category: this.videoValidatorsService.VIDEO_CATEGORY,
93 licence: this.videoValidatorsService.VIDEO_LICENCE,
94 language: this.videoValidatorsService.VIDEO_LANGUAGE,
95 description: this.videoValidatorsService.VIDEO_DESCRIPTION,
96 tags: null,
97 previewfile: null,
98 support: this.videoValidatorsService.VIDEO_SUPPORT,
99 schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,
100 originallyPublishedAt: this.videoValidatorsService.VIDEO_ORIGINALLY_PUBLISHED_AT
101 }
102
103 this.formValidatorService.updateForm(
104 this.form,
105 this.formErrors,
106 this.validationMessages,
107 obj,
108 defaultValues
109 )
110
111 this.form.addControl('captions', new FormArray([
112 new FormGroup({
113 language: new FormControl(),
114 captionfile: new FormControl()
115 })
116 ]))
117
118 this.trackChannelChange()
119 this.trackPrivacyChange()
120 }
121
122 ngOnInit () {
123 this.updateForm()
124
125 this.serverService.getVideoCategories()
126 .subscribe(res => this.videoCategories = res)
127 this.serverService.getVideoLicences()
128 .subscribe(res => this.videoLicences = res)
129 this.serverService.getVideoLanguages()
130 .subscribe(res => this.videoLanguages = res)
131
132 this.serverService.getVideoPrivacies()
133 .subscribe(privacies => this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies))
134
135 this.serverConfig = this.serverService.getTmpConfig()
136 this.serverService.getConfig()
137 .subscribe(config => this.serverConfig = config)
138
139 this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
140
141 this.ngZone.runOutsideAngular(() => {
142 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
143 })
144 }
145
146 ngOnDestroy () {
147 if (this.schedulerInterval) clearInterval(this.schedulerInterval)
148 }
149
150 onCaptionAdded (caption: VideoCaptionEdit) {
151 const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
152
153 // Replace existing caption?
154 if (existingCaption) {
155 Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
156 } else {
157 this.videoCaptions.push(
158 Object.assign(caption, { action: 'CREATE' as 'CREATE' })
159 )
160 }
161
162 this.sortVideoCaptions()
163 }
164
165 async deleteCaption (caption: VideoCaptionEdit) {
166 // Caption recovers his former state
167 if (caption.action && this.initialVideoCaptions.indexOf(caption.language.id) !== -1) {
168 caption.action = undefined
169 return
170 }
171
172 // This caption is not on the server, just remove it from our array
173 if (caption.action === 'CREATE') {
174 removeElementFromArray(this.videoCaptions, caption)
175 return
176 }
177
178 caption.action = 'REMOVE' as 'REMOVE'
179 }
180
181 openAddCaptionModal () {
182 this.videoCaptionAddModal.show()
183 }
184
185 private sortVideoCaptions () {
186 this.videoCaptions.sort((v1, v2) => {
187 if (v1.language.label < v2.language.label) return -1
188 if (v1.language.label === v2.language.label) return 0
189
190 return 1
191 })
192 }
193
194 private trackPrivacyChange () {
195 // We will update the schedule input and the wait transcoding checkbox validators
196 this.form.controls[ 'privacy' ]
197 .valueChanges
198 .pipe(map(res => parseInt(res.toString(), 10)))
199 .subscribe(
200 newPrivacyId => {
201
202 this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
203
204 // Value changed
205 const scheduleControl = this.form.get('schedulePublicationAt')
206 const waitTranscodingControl = this.form.get('waitTranscoding')
207
208 if (this.schedulePublicationEnabled) {
209 scheduleControl.setValidators([ Validators.required ])
210
211 waitTranscodingControl.disable()
212 waitTranscodingControl.setValue(false)
213 } else {
214 scheduleControl.clearValidators()
215
216 waitTranscodingControl.enable()
217
218 // Do not update the control value on first patch (values come from the server)
219 if (this.firstPatchDone === true) {
220 waitTranscodingControl.setValue(true)
221 }
222 }
223
224 scheduleControl.updateValueAndValidity()
225 waitTranscodingControl.updateValueAndValidity()
226
227 this.firstPatchDone = true
228
229 }
230 )
231 }
232
233 private trackChannelChange () {
234 // We will update the "support" field depending on the channel
235 this.form.controls[ 'channelId' ]
236 .valueChanges
237 .pipe(map(res => parseInt(res.toString(), 10)))
238 .subscribe(
239 newChannelId => {
240 const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
241
242 // Not initialized yet
243 if (isNaN(newChannelId)) return
244 const newChannel = this.userVideoChannels.find(c => c.id === newChannelId)
245 if (!newChannel) return
246
247 // Wait support field update
248 setTimeout(() => {
249 const currentSupport = this.form.value[ 'support' ]
250
251 // First time we set the channel?
252 if (isNaN(oldChannelId) && !currentSupport) return this.updateSupportField(newChannel.support)
253
254 const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
255 if (!newChannel || !oldChannel) {
256 console.error('Cannot find new or old channel.')
257 return
258 }
259
260 // If the current support text is not the same than the old channel, the user updated it.
261 // We don't want the user to lose his text, so stop here
262 if (currentSupport && currentSupport !== oldChannel.support) return
263
264 // Update the support text with our new channel
265 this.updateSupportField(newChannel.support)
266 })
267 }
268 )
269 }
270
271 private updateSupportField (support: string) {
272 return this.form.patchValue({ support: support || '' })
273 }
274}
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
new file mode 100644
index 000000000..96061a300
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts
@@ -0,0 +1,38 @@
1import { TagInputModule } from 'ngx-chips'
2import { CalendarModule } from 'primeng/calendar'
3import { NgModule } from '@angular/core'
4import { SharedFormModule } from '@app/shared/shared-forms'
5import { SharedGlobalIconModule } from '@app/shared/shared-icons'
6import { SharedMainModule } from '@app/shared/shared-main'
7import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
8import { VideoEditComponent } from './video-edit.component'
9
10@NgModule({
11 imports: [
12 TagInputModule,
13 CalendarModule,
14
15 SharedMainModule,
16 SharedFormModule,
17 SharedGlobalIconModule
18 ],
19
20 declarations: [
21 VideoEditComponent,
22 VideoCaptionAddModalComponent
23 ],
24
25 exports: [
26 TagInputModule,
27 CalendarModule,
28
29 SharedMainModule,
30 SharedFormModule,
31 SharedGlobalIconModule,
32
33 VideoEditComponent
34 ],
35
36 providers: []
37})
38export class VideoEditModule { }