aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-edit
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+videos/+video-edit')
-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
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/drag-drop.directive.ts30
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html76
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss18
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts147
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html72
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts178
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.scss46
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.ts71
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html90
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss49
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts306
-rw-r--r--client/src/app/+videos/+video-edit/video-add-routing.module.ts20
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html46
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.scss89
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.ts77
-rw-r--r--client/src/app/+videos/+video-edit/video-add.module.ts32
-rw-r--r--client/src/app/+videos/+video-edit/video-update-routing.module.ts24
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html22
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts155
-rw-r--r--client/src/app/+videos/+video-edit/video-update.module.ts26
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts44
29 files changed, 2653 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 { }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/drag-drop.directive.ts b/client/src/app/+videos/+video-edit/video-add-components/drag-drop.directive.ts
new file mode 100644
index 000000000..7b1a38c62
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/drag-drop.directive.ts
@@ -0,0 +1,30 @@
1import { Directive, Output, EventEmitter, HostBinding, HostListener } from '@angular/core'
2
3@Directive({
4 selector: '[dragDrop]'
5})
6export class DragDropDirective {
7 @Output() fileDropped = new EventEmitter<FileList>()
8
9 @HostBinding('class.dragover') dragover = false
10
11 @HostListener('dragover', ['$event']) onDragOver (e: Event) {
12 e.preventDefault()
13 e.stopPropagation()
14 this.dragover = true
15 }
16
17 @HostListener('dragleave', ['$event']) public onDragLeave (e: Event) {
18 e.preventDefault()
19 e.stopPropagation()
20 this.dragover = false
21 }
22
23 @HostListener('drop', ['$event']) public ondrop (e: DragEvent) {
24 e.preventDefault()
25 e.stopPropagation()
26 this.dragover = false
27 const files = e.dataTransfer.files
28 if (files.length > 0) this.fileDropped.emit(files)
29 }
30}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
new file mode 100644
index 000000000..7287f799d
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -0,0 +1,76 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container" dragDrop (fileDropped)="setTorrentFile($event)">
2 <div class="first-step-block">
3 <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
4
5 <div class="button-file form-control" [ngbTooltip]="'(extensions: .torrent)'">
6 <span i18n>Select the torrent to import</span>
7 <input #torrentfileInput type="file" name="torrentfile" id="torrentfile" accept=".torrent" (change)="fileChange()" />
8 </div>
9
10 <div class="torrent-or-magnet" i18n-data-content data-content="OR"></div>
11
12 <div class="form-group form-group-magnet-uri">
13 <label i18n for="magnetUri">Paste magnet URI</label>
14 <my-help>
15 <ng-template ptTemplate="customHtml">
16 <ng-container i18n>
17 You can import any torrent file that points to a mp4 file.
18 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
19 </ng-container>
20 </ng-template>
21 </my-help>
22
23 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" class="form-control" />
24 </div>
25
26 <div class="form-group">
27 <label i18n for="first-step-channel">Channel</label>
28 <div class="peertube-select-container">
29 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
30 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
31 </select>
32 </div>
33 </div>
34
35 <div class="form-group">
36 <label i18n for="first-step-privacy">Privacy</label>
37 <div class="peertube-select-container">
38 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
39 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
40 </select>
41 </div>
42 </div>
43
44 <input
45 type="button" i18n-value value="Import"
46 [disabled]="!isMagnetUrlValid() || isImportingVideo" (click)="importVideo()"
47 />
48 </div>
49</div>
50
51<div *ngIf="error" class="alert alert-danger">
52 <div i18n>Sorry, but something went wrong</div>
53 {{ error }}
54</div>
55
56<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
57 Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
58</div>
59
60<!-- Hidden because we want to load the component -->
61<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
62 <my-video-edit
63 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
64 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
65 ></my-video-edit>
66
67 <div class="submit-container">
68 <div class="submit-button"
69 (click)="updateSecondStep()"
70 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
71 >
72 <my-global-icon iconName="validate" aria-hidden="true"></my-global-icon>
73 <input type="button" i18n-value value="Update" />
74 </div>
75 </div>
76</form>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
new file mode 100644
index 000000000..1fef74994
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -0,0 +1,18 @@
1@import 'variables';
2@import 'mixins';
3
4.first-step-block {
5 .torrent-or-magnet {
6 @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuColor));
7
8 &[data-content] {
9 margin: 1.5rem 0;
10 }
11 }
12
13 .form-group-magnet-uri {
14 margin-bottom: 40px;
15 }
16}
17
18
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
new file mode 100644
index 000000000..538a187a8
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -0,0 +1,147 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router'
3import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
4import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
7import { VideoSend } from './video-send'
8import { LoadingBarService } from '@ngx-loading-bar/core'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { VideoPrivacy, VideoUpdate } from '@shared/models'
11
12@Component({
13 selector: 'my-video-import-torrent',
14 templateUrl: './video-import-torrent.component.html',
15 styleUrls: [
16 '../shared/video-edit.component.scss',
17 './video-import-torrent.component.scss',
18 './video-send.scss'
19 ]
20})
21export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>()
24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
25
26 magnetUri = ''
27
28 isImportingVideo = false
29 hasImportedVideo = false
30 isUpdatingVideo = false
31
32 video: VideoEdit
33 error: string
34
35 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
36
37 constructor (
38 protected formValidatorService: FormValidatorService,
39 protected loadingBar: LoadingBarService,
40 protected notifier: Notifier,
41 protected authService: AuthService,
42 protected serverService: ServerService,
43 protected videoService: VideoService,
44 protected videoCaptionService: VideoCaptionService,
45 private router: Router,
46 private videoImportService: VideoImportService,
47 private i18n: I18n
48 ) {
49 super()
50 }
51
52 ngOnInit () {
53 super.ngOnInit()
54 }
55
56 canDeactivate () {
57 return { canDeactivate: true }
58 }
59
60 isMagnetUrlValid () {
61 return !!this.magnetUri
62 }
63
64 fileChange () {
65 const torrentfile = this.torrentfileInput.nativeElement.files[0]
66 if (!torrentfile) return
67
68 this.importVideo(torrentfile)
69 }
70
71 setTorrentFile (files: FileList) {
72 this.torrentfileInput.nativeElement.files = files
73 this.fileChange()
74 }
75
76 importVideo (torrentfile?: Blob) {
77 this.isImportingVideo = true
78
79 const videoUpdate: VideoUpdate = {
80 privacy: this.firstStepPrivacyId,
81 waitTranscoding: false,
82 commentsEnabled: true,
83 downloadEnabled: true,
84 channelId: this.firstStepChannelId
85 }
86
87 this.loadingBar.start()
88
89 this.videoImportService.importVideoTorrent(torrentfile || this.magnetUri, videoUpdate).subscribe(
90 res => {
91 this.loadingBar.complete()
92 this.firstStepDone.emit(res.video.name)
93 this.isImportingVideo = false
94 this.hasImportedVideo = true
95
96 this.video = new VideoEdit(Object.assign(res.video, {
97 commentsEnabled: videoUpdate.commentsEnabled,
98 downloadEnabled: videoUpdate.downloadEnabled,
99 support: null,
100 thumbnailUrl: null,
101 previewUrl: null
102 }))
103
104 this.hydrateFormFromVideo()
105 },
106
107 err => {
108 this.loadingBar.complete()
109 this.isImportingVideo = false
110 this.firstStepError.emit()
111 this.notifier.error(err.message)
112 }
113 )
114 }
115
116 updateSecondStep () {
117 if (this.checkForm() === false) {
118 return
119 }
120
121 this.video.patch(this.form.value)
122
123 this.isUpdatingVideo = true
124
125 // Update the video
126 this.updateVideoAndCaptions(this.video)
127 .subscribe(
128 () => {
129 this.isUpdatingVideo = false
130 this.notifier.success(this.i18n('Video to import updated.'))
131
132 this.router.navigate([ '/my-account', 'video-imports' ])
133 },
134
135 err => {
136 this.error = err.message
137 scrollToTop()
138 console.error(err)
139 }
140 )
141
142 }
143
144 private hydrateFormFromVideo () {
145 this.form.patchValue(this.video.toFormPatch())
146 }
147}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
new file mode 100644
index 000000000..1910da403
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
@@ -0,0 +1,72 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="first-step-block">
3 <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
4
5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label>
7
8 <my-help>
9 <ng-template ptTemplate="customHtml">
10 <ng-container i18n>
11 You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a>
12 or URL that points to a raw MP4 file.
13 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
14 </ng-container>
15 </ng-template>
16 </my-help>
17
18 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" class="form-control" />
19 </div>
20
21 <div class="form-group">
22 <label i18n for="first-step-channel">Channel</label>
23 <div class="peertube-select-container">
24 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
25 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
26 </select>
27 </div>
28 </div>
29
30 <div class="form-group">
31 <label i18n for="first-step-privacy">Privacy</label>
32 <div class="peertube-select-container">
33 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
34 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
35 </select>
36 </div>
37 </div>
38
39 <input
40 type="button" i18n-value value="Import"
41 [disabled]="!isTargetUrlValid() || isImportingVideo" (click)="importVideo()"
42 />
43 </div>
44</div>
45
46
47<div *ngIf="error" class="alert alert-danger">
48 <div i18n>Sorry, but something went wrong</div>
49 {{ error }}
50</div>
51
52<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
53 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
54</div>
55
56<!-- Hidden because we want to load the component -->
57<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
58 <my-video-edit
59 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
60 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
61 ></my-video-edit>
62
63 <div class="submit-container">
64 <div class="submit-button"
65 (click)="updateSecondStep()"
66 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
67 >
68 <my-global-icon iconName="validate" aria-hidden="true"></my-global-icon>
69 <input type="button" i18n-value value="Update" />
70 </div>
71 </div>
72</form>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
new file mode 100644
index 000000000..6508eef7e
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -0,0 +1,178 @@
1import { map, switchMap } from 'rxjs/operators'
2import { Component, EventEmitter, OnInit, Output } from '@angular/core'
3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
8import { VideoSend } from './video-send'
9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { VideoPrivacy, VideoUpdate } from '@shared/models'
12
13@Component({
14 selector: 'my-video-import-url',
15 templateUrl: './video-import-url.component.html',
16 styleUrls: [
17 '../shared/video-edit.component.scss',
18 './video-send.scss'
19 ]
20})
21export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>()
24
25 targetUrl = ''
26
27 isImportingVideo = false
28 hasImportedVideo = false
29 isUpdatingVideo = false
30
31 video: VideoEdit
32 error: string
33
34 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
35
36 constructor (
37 protected formValidatorService: FormValidatorService,
38 protected loadingBar: LoadingBarService,
39 protected notifier: Notifier,
40 protected authService: AuthService,
41 protected serverService: ServerService,
42 protected videoService: VideoService,
43 protected videoCaptionService: VideoCaptionService,
44 private router: Router,
45 private videoImportService: VideoImportService,
46 private i18n: I18n
47 ) {
48 super()
49 }
50
51 ngOnInit () {
52 super.ngOnInit()
53 }
54
55 canDeactivate () {
56 return { canDeactivate: true }
57 }
58
59 isTargetUrlValid () {
60 return this.targetUrl && this.targetUrl.match(/https?:\/\//)
61 }
62
63 importVideo () {
64 this.isImportingVideo = true
65
66 const videoUpdate: VideoUpdate = {
67 privacy: this.firstStepPrivacyId,
68 waitTranscoding: false,
69 commentsEnabled: true,
70 downloadEnabled: true,
71 channelId: this.firstStepChannelId
72 }
73
74 this.loadingBar.start()
75
76 this.videoImportService
77 .importVideoUrl(this.targetUrl, videoUpdate)
78 .pipe(
79 switchMap(res => {
80 return this.videoCaptionService
81 .listCaptions(res.video.id)
82 .pipe(
83 map(result => ({ video: res.video, videoCaptions: result.data }))
84 )
85 })
86 )
87 .subscribe(
88 ({ video, videoCaptions }) => {
89 this.loadingBar.complete()
90 this.firstStepDone.emit(video.name)
91 this.isImportingVideo = false
92 this.hasImportedVideo = true
93
94 const absoluteAPIUrl = getAbsoluteAPIUrl()
95
96 const thumbnailUrl = video.thumbnailPath
97 ? absoluteAPIUrl + video.thumbnailPath
98 : null
99
100 const previewUrl = video.previewPath
101 ? absoluteAPIUrl + video.previewPath
102 : null
103
104 this.video = new VideoEdit(Object.assign(video, {
105 commentsEnabled: videoUpdate.commentsEnabled,
106 downloadEnabled: videoUpdate.downloadEnabled,
107 support: null,
108 thumbnailUrl,
109 previewUrl
110 }))
111
112 this.videoCaptions = videoCaptions
113
114 this.hydrateFormFromVideo()
115 },
116
117 err => {
118 this.loadingBar.complete()
119 this.isImportingVideo = false
120 this.firstStepError.emit()
121 this.notifier.error(err.message)
122 }
123 )
124 }
125
126 updateSecondStep () {
127 if (this.checkForm() === false) {
128 return
129 }
130
131 this.video.patch(this.form.value)
132
133 this.isUpdatingVideo = true
134
135 // Update the video
136 this.updateVideoAndCaptions(this.video)
137 .subscribe(
138 () => {
139 this.isUpdatingVideo = false
140 this.notifier.success(this.i18n('Video to import updated.'))
141
142 this.router.navigate([ '/my-account', 'video-imports' ])
143 },
144
145 err => {
146 this.error = err.message
147 scrollToTop()
148 console.error(err)
149 }
150 )
151
152 }
153
154 private hydrateFormFromVideo () {
155 this.form.patchValue(this.video.toFormPatch())
156
157 const objects = [
158 {
159 url: 'thumbnailUrl',
160 name: 'thumbnailfile'
161 },
162 {
163 url: 'previewUrl',
164 name: 'previewfile'
165 }
166 ]
167
168 for (const obj of objects) {
169 fetch(this.video[obj.url])
170 .then(response => response.blob())
171 .then(data => {
172 this.form.patchValue({
173 [ obj.name ]: data
174 })
175 })
176 }
177 }
178}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.scss b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss
new file mode 100644
index 000000000..ebe14c59e
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss
@@ -0,0 +1,46 @@
1@import 'variables';
2@import 'mixins';
3
4$width-size: 190px;
5
6.alert.alert-danger {
7 text-align: center;
8
9 & > div {
10 font-weight: $font-semibold;
11 }
12}
13
14.first-step-block {
15 display: flex;
16 flex-direction: column;
17 align-items: center;
18
19 .upload-icon {
20 width: 90px;
21 margin-bottom: 25px;
22
23 @include apply-svg-color(#C6C6C6);
24 }
25
26 .peertube-select-container {
27 @include peertube-select-container($width-size);
28 }
29
30 input[type=text] {
31 @include peertube-input-text($width-size);
32 display: block;
33 }
34
35 input[type=button] {
36 @include peertube-button;
37 @include orange-button;
38
39 width: $width-size;
40 margin-top: 30px;
41 }
42
43 .button-file {
44 @include peertube-button-file(max-content);
45 }
46}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
new file mode 100644
index 000000000..94479321d
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts
@@ -0,0 +1,71 @@
1import { catchError, switchMap, tap } from 'rxjs/operators'
2import { EventEmitter, OnInit } from '@angular/core'
3import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core'
4import { populateAsyncUserVideoChannels } from '@app/helpers'
5import { FormReactive } from '@app/shared/shared-forms'
6import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
9
10export abstract class VideoSend extends FormReactive implements OnInit {
11 userVideoChannels: { id: number, label: string, support: string }[] = []
12 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
13 videoCaptions: VideoCaptionEdit[] = []
14
15 firstStepPrivacyId = 0
16 firstStepChannelId = 0
17
18 abstract firstStepDone: EventEmitter<string>
19 abstract firstStepError: EventEmitter<void>
20 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
21
22 protected loadingBar: LoadingBarService
23 protected notifier: Notifier
24 protected authService: AuthService
25 protected serverService: ServerService
26 protected videoService: VideoService
27 protected videoCaptionService: VideoCaptionService
28 protected serverConfig: ServerConfig
29
30 abstract canDeactivate (): CanComponentDeactivateResult
31
32 ngOnInit () {
33 this.buildForm({})
34
35 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
36 .then(() => this.firstStepChannelId = this.userVideoChannels[ 0 ].id)
37
38 this.serverConfig = this.serverService.getTmpConfig()
39 this.serverService.getConfig()
40 .subscribe(config => this.serverConfig = config)
41
42 this.serverService.getVideoPrivacies()
43 .subscribe(
44 privacies => {
45 this.videoPrivacies = privacies
46
47 this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY
48 })
49 }
50
51 checkForm () {
52 this.forceCheck()
53
54 return this.form.valid
55 }
56
57 protected updateVideoAndCaptions (video: VideoEdit) {
58 this.loadingBar.start()
59
60 return this.videoService.updateVideo(video)
61 .pipe(
62 // Then update captions
63 switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)),
64 tap(() => this.loadingBar.complete()),
65 catchError(err => {
66 this.loadingBar.complete()
67 throw err
68 })
69 )
70 }
71}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
new file mode 100644
index 000000000..dad88a661
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -0,0 +1,90 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)">
2 <div class="first-step-block">
3 <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
4
5 <div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus />
8 </div>
9
10 <div class="form-group form-group-channel">
11 <label i18n for="first-step-channel">Channel</label>
12 <div class="peertube-select-container">
13 <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
14 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
15 </select>
16 </div>
17 </div>
18
19 <div class="form-group">
20 <label i18n for="first-step-privacy">Privacy</label>
21 <div class="peertube-select-container">
22 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
23 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
24 <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
25 </select>
26 </div>
27 </div>
28
29 <ng-container *ngIf="isUploadingAudioFile">
30 <div class="form-group audio-preview">
31 <label i18n for="previewfileUpload">Video background image</label>
32
33 <div i18n class="audio-image-info">
34 Image that will be merged with your audio file.
35 <br />
36 The chosen image will be definitive and cannot be modified.
37 </div>
38
39 <my-preview-upload
40 i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="previewfileUpload"
41 previewWidth="360px" previewHeight="200px"
42 ></my-preview-upload>
43 </div>
44
45 <div class="form-group upload-audio-button">
46 <my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button>
47 </div>
48 </ng-container>
49 </div>
50</div>
51
52<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
53 <div class="progress" i18n-title title="Total video quota">
54 <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100">
55 <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
56 <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
57 </div>
58 </div>
59 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
60</div>
61
62<div *ngIf="error" class="alert alert-danger">
63 <div i18n>Sorry, but something went wrong</div>
64 {{ error }}
65</div>
66
67<div *ngIf="videoUploaded && !error" class="alert alert-info" i18n>
68 Congratulations! Your video is now available in your private library.
69</div>
70
71<!-- Hidden because we want to load the component -->
72<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form" class="mb-3">
73 <my-video-edit
74 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
75 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
76 [waitTranscodingEnabled]="waitTranscodingEnabled"
77 ></my-video-edit>
78
79 <div class="submit-container">
80 <div i18n *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div>
81
82 <div class="submit-button"
83 (click)="updateSecondStep()"
84 [ngClass]="{ disabled: isPublishingButtonDisabled() }"
85 >
86 <my-global-icon iconName="validate" aria-hidden="true"></my-global-icon>
87 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
88 </div>
89 </div>
90</form>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
new file mode 100644
index 000000000..a4f87b0b8
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
@@ -0,0 +1,49 @@
1@import 'variables';
2@import 'mixins';
3
4.first-step-block {
5 .form-group-channel {
6 margin-bottom: 20px;
7 margin-top: 35px;
8 }
9
10 .audio-image-info {
11 margin-bottom: 10px;
12 }
13
14 .audio-preview {
15 margin: 30px 0;
16 }
17}
18
19.upload-progress-cancel {
20 display: flex;
21 margin-top: 25px;
22 margin-bottom: 40px;
23
24 .progress {
25 @include progressbar;
26 flex-grow: 1;
27 height: 30px;
28 font-size: 15px;
29 background-color: rgba(11, 204, 41, 0.16);
30
31 .progress-bar {
32 background-color: $green;
33 line-height: 30px;
34 text-align: left;
35 font-weight: $font-bold;
36
37 span {
38 margin-left: 18px;
39 }
40 }
41 }
42
43 input {
44 @include peertube-button;
45 @include grey-button;
46
47 margin-left: 10px;
48 }
49}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
new file mode 100644
index 000000000..e46ce6599
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -0,0 +1,306 @@
1import { BytesPipe } from 'ngx-pipes'
2import { Subscription } from 'rxjs'
3import { HttpEventType, HttpResponse } from '@angular/common/http'
4import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
5import { Router } from '@angular/router'
6import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core'
7import { scrollToTop } from '@app/helpers'
8import { FormValidatorService } from '@app/shared/shared-forms'
9import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
10import { LoadingBarService } from '@ngx-loading-bar/core'
11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { VideoPrivacy } from '@shared/models'
13import { VideoSend } from './video-send'
14
15@Component({
16 selector: 'my-video-upload',
17 templateUrl: './video-upload.component.html',
18 styleUrls: [
19 '../shared/video-edit.component.scss',
20 './video-upload.component.scss',
21 './video-send.scss'
22 ]
23})
24export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>()
26 @Output() firstStepError = new EventEmitter<void>()
27 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
28
29 // So that it can be accessed in the template
30 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
31
32 userVideoQuotaUsed = 0
33 userVideoQuotaUsedDaily = 0
34
35 isUploadingAudioFile = false
36 isUploadingVideo = false
37 isUpdatingVideo = false
38
39 videoUploaded = false
40 videoUploadObservable: Subscription = null
41 videoUploadPercents = 0
42 videoUploadedIds = {
43 id: 0,
44 uuid: ''
45 }
46
47 waitTranscodingEnabled = true
48 previewfileUpload: File
49
50 error: string
51
52 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
53
54 constructor (
55 protected formValidatorService: FormValidatorService,
56 protected loadingBar: LoadingBarService,
57 protected notifier: Notifier,
58 protected authService: AuthService,
59 protected serverService: ServerService,
60 protected videoService: VideoService,
61 protected videoCaptionService: VideoCaptionService,
62 private userService: UserService,
63 private router: Router,
64 private i18n: I18n
65 ) {
66 super()
67 }
68
69 get videoExtensions () {
70 return this.serverConfig.video.file.extensions.join(', ')
71 }
72
73 ngOnInit () {
74 super.ngOnInit()
75
76 this.userService.getMyVideoQuotaUsed()
77 .subscribe(data => {
78 this.userVideoQuotaUsed = data.videoQuotaUsed
79 this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
80 })
81 }
82
83 ngOnDestroy () {
84 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
85 }
86
87 canDeactivate () {
88 let text = ''
89
90 if (this.videoUploaded === true) {
91 // FIXME: cannot concatenate strings inside i18n service :/
92 text = this.i18n('Your video was uploaded to your account and is private.') + ' ' +
93 this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?')
94 } else {
95 text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?')
96 }
97
98 return {
99 canDeactivate: !this.isUploadingVideo,
100 text
101 }
102 }
103
104 getVideoFile () {
105 return this.videofileInput.nativeElement.files[0]
106 }
107
108 setVideoFile (files: FileList) {
109 this.videofileInput.nativeElement.files = files
110 this.fileChange()
111 }
112
113 getAudioUploadLabel () {
114 const videofile = this.getVideoFile()
115 if (!videofile) return this.i18n('Upload')
116
117 return this.i18n('Upload {{videofileName}}', { videofileName: videofile.name })
118 }
119
120 fileChange () {
121 this.uploadFirstStep()
122 }
123
124 cancelUpload () {
125 if (this.videoUploadObservable !== null) {
126 this.videoUploadObservable.unsubscribe()
127
128 this.isUploadingVideo = false
129 this.videoUploadPercents = 0
130 this.videoUploadObservable = null
131
132 this.firstStepError.emit()
133
134 this.notifier.info(this.i18n('Upload cancelled'))
135 }
136 }
137
138 uploadFirstStep (clickedOnButton = false) {
139 const videofile = this.getVideoFile()
140 if (!videofile) return
141
142 if (!this.checkGlobalUserQuota(videofile)) return
143 if (!this.checkDailyUserQuota(videofile)) return
144
145 if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
146 this.isUploadingAudioFile = true
147 return
148 }
149
150 // Build name field
151 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
152 let name: string
153
154 // If the name of the file is very small, keep the extension
155 if (nameWithoutExtension.length < 3) name = videofile.name
156 else name = nameWithoutExtension
157
158 // Force user to wait transcoding for unsupported video types in web browsers
159 if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
160 this.waitTranscodingEnabled = false
161 }
162
163 const privacy = this.firstStepPrivacyId.toString()
164 const nsfw = this.serverConfig.instance.isNSFW
165 const waitTranscoding = true
166 const commentsEnabled = true
167 const downloadEnabled = true
168 const channelId = this.firstStepChannelId.toString()
169
170 const formData = new FormData()
171 formData.append('name', name)
172 // Put the video "private" -> we are waiting the user validation of the second step
173 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
174 formData.append('nsfw', '' + nsfw)
175 formData.append('commentsEnabled', '' + commentsEnabled)
176 formData.append('downloadEnabled', '' + downloadEnabled)
177 formData.append('waitTranscoding', '' + waitTranscoding)
178 formData.append('channelId', '' + channelId)
179 formData.append('videofile', videofile)
180
181 if (this.previewfileUpload) {
182 formData.append('previewfile', this.previewfileUpload)
183 formData.append('thumbnailfile', this.previewfileUpload)
184 }
185
186 this.isUploadingVideo = true
187 this.firstStepDone.emit(name)
188
189 this.form.patchValue({
190 name,
191 privacy,
192 nsfw,
193 channelId,
194 previewfile: this.previewfileUpload
195 })
196
197 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
198 event => {
199 if (event.type === HttpEventType.UploadProgress) {
200 this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
201 } else if (event instanceof HttpResponse) {
202 this.videoUploaded = true
203
204 this.videoUploadedIds = event.body.video
205
206 this.videoUploadObservable = null
207 }
208 },
209
210 err => {
211 // Reset progress
212 this.isUploadingVideo = false
213 this.videoUploadPercents = 0
214 this.videoUploadObservable = null
215 this.firstStepError.emit()
216 this.notifier.error(err.message)
217 }
218 )
219 }
220
221 isPublishingButtonDisabled () {
222 return !this.form.valid ||
223 this.isUpdatingVideo === true ||
224 this.videoUploaded !== true
225 }
226
227 updateSecondStep () {
228 if (this.checkForm() === false) {
229 return
230 }
231
232 const video = new VideoEdit()
233 video.patch(this.form.value)
234 video.id = this.videoUploadedIds.id
235 video.uuid = this.videoUploadedIds.uuid
236
237 this.isUpdatingVideo = true
238
239 this.updateVideoAndCaptions(video)
240 .subscribe(
241 () => {
242 this.isUpdatingVideo = false
243 this.isUploadingVideo = false
244
245 this.notifier.success(this.i18n('Video published.'))
246 this.router.navigate([ '/videos/watch', video.uuid ])
247 },
248
249 err => {
250 this.error = err.message
251 scrollToTop()
252 console.error(err)
253 }
254 )
255 }
256
257 private checkGlobalUserQuota (videofile: File) {
258 const bytePipes = new BytesPipe()
259
260 // Check global user quota
261 const videoQuota = this.authService.getUser().videoQuota
262 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
263 const msg = this.i18n(
264 'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})',
265 {
266 videoSize: bytePipes.transform(videofile.size, 0),
267 videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
268 videoQuota: bytePipes.transform(videoQuota, 0)
269 }
270 )
271 this.notifier.error(msg)
272
273 return false
274 }
275
276 return true
277 }
278
279 private checkDailyUserQuota (videofile: File) {
280 const bytePipes = new BytesPipe()
281
282 // Check daily user quota
283 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
284 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
285 const msg = this.i18n(
286 'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})',
287 {
288 videoSize: bytePipes.transform(videofile.size, 0),
289 quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
290 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
291 }
292 )
293 this.notifier.error(msg)
294
295 return false
296 }
297
298 return true
299 }
300
301 private isAudioFile (filename: string) {
302 const extensions = [ '.mp3', '.flac', '.ogg', '.wma', '.wav' ]
303
304 return extensions.some(e => filename.endsWith(e))
305 }
306}
diff --git a/client/src/app/+videos/+video-edit/video-add-routing.module.ts b/client/src/app/+videos/+video-edit/video-add-routing.module.ts
new file mode 100644
index 000000000..9ff66bea0
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add-routing.module.ts
@@ -0,0 +1,20 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoAddComponent } from './video-add.component'
6
7const videoAddRoutes: Routes = [
8 {
9 path: '',
10 component: VideoAddComponent,
11 canActivate: [ MetaGuard, LoginGuard ],
12 canDeactivate: [ CanDeactivateGuard ]
13 }
14]
15
16@NgModule({
17 imports: [ RouterModule.forChild(videoAddRoutes) ],
18 exports: [ RouterModule ]
19})
20export class VideoAddRoutingModule {}
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html
new file mode 100644
index 000000000..79bfc6e5c
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add.component.html
@@ -0,0 +1,46 @@
1<div class="margin-content">
2 <div class="alert alert-warning" *ngIf="isRootUser()" i18n>
3 We recommend you to not use the <strong>root</strong> user to publish your videos, since it's the super-admin account of your instance.
4 <br />
5 Instead, <a routerLink="/admin/users">create a dedicated account</a> to upload your videos.
6 </div>
7
8 <div class="title-page title-page-single" *ngIf="isInSecondStep()">
9 <ng-container *ngIf="secondStepType === 'import-url' || secondStepType === 'import-torrent'" i18n>Import {{ videoName }}</ng-container>
10 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
11 </div>
12
13 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
14 <ng-container ngbNavItem>
15 <a ngbNavLink>
16 <span i18n>Upload a file</span>
17 </a>
18
19 <ng-template ngbNavContent>
20 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
21 </ng-template>
22 </ng-container>
23
24 <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
25 <a ngbNavLink>
26 <span i18n>Import with URL</span>
27 </a>
28
29 <ng-template ngbNavContent>
30 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
31 </ng-template>
32 </ng-container>
33
34 <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
35 <a ngbNavLink>
36 <span i18n>Import with torrent</span>
37 </a>
38
39 <ng-template ngbNavContent>
40 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
41 </ng-template>
42 </ng-container>
43 </div>
44
45 <div [ngbNavOutlet]="nav"></div>
46</div>
diff --git a/client/src/app/+videos/+video-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss
new file mode 100644
index 000000000..0ad57d897
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add.component.scss
@@ -0,0 +1,89 @@
1@import '_variables';
2@import '_mixins';
3
4$border-width: 3px;
5$border-type: solid;
6$border-color: #EAEAEA;
7$nav-link-height: 40px;
8
9.margin-content {
10 padding-top: 50px;
11}
12
13.alert {
14 font-size: 15px;
15}
16
17::ng-deep .video-add-nav {
18 border-bottom: $border-width $border-type $border-color;
19 margin: 50px 0 0 0 !important;
20
21 &.hide-nav {
22 display: none !important;
23 }
24
25 a.nav-link {
26 @include disable-default-a-behaviour;
27
28 margin-bottom: -$border-width;
29 height: $nav-link-height !important;
30 padding: 0 30px !important;
31 font-size: 15px;
32
33 &.active {
34 border: $border-width $border-type $border-color;
35 border-bottom: none;
36 background-color: pvar(--submenuColor) !important;
37
38 span {
39 border-bottom: 2px solid pvar(--mainColor);
40 font-weight: $font-bold;
41 }
42 }
43 }
44}
45
46::ng-deep .upload-video-container {
47 border: $border-width $border-type $border-color;
48 border-top: transparent;
49
50 background-color: pvar(--submenuColor);
51 border-bottom-left-radius: 3px;
52 border-bottom-right-radius: 3px;
53 width: 100%;
54 min-height: 440px;
55 padding-bottom: 20px;
56 display: flex;
57 justify-content: center;
58 align-items: center;
59
60 &.dragover {
61 border: 3px dashed pvar(--mainColor);
62 }
63}
64
65@mixin nav-scroll {
66 ::ng-deep .video-add-nav {
67 height: #{$nav-link-height + $border-width * 2};
68 overflow-x: auto;
69 white-space: nowrap;
70 flex-wrap: unset;
71
72 /* Hide active tab style to not have a moving tab effect */
73 a.nav-link.active {
74 border: none;
75 background-color: pvar(--mainBackgroundColor) !important;
76 }
77 }
78}
79
80/* Make .video-add-nav tabs scrollable on small devices */
81@media screen and (max-width: $small-view) {
82 @include nav-scroll();
83}
84
85@media screen and (max-width: #{$small-view + $menu-width}) {
86 :host-context(.main-col:not(.expanded)) {
87 @include nav-scroll();
88 }
89}
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts
new file mode 100644
index 000000000..5bd768809
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add.component.ts
@@ -0,0 +1,77 @@
1import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
2import { AuthService, CanComponentDeactivate, ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models'
4import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
5import { VideoImportUrlComponent } from './video-add-components/video-import-url.component'
6import { VideoUploadComponent } from './video-add-components/video-upload.component'
7
8@Component({
9 selector: 'my-videos-add',
10 templateUrl: './video-add.component.html',
11 styleUrls: [ './video-add.component.scss' ]
12})
13export class VideoAddComponent implements OnInit, CanComponentDeactivate {
14 @ViewChild('videoUpload') videoUpload: VideoUploadComponent
15 @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent
16 @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent
17
18 secondStepType: 'upload' | 'import-url' | 'import-torrent'
19 videoName: string
20 serverConfig: ServerConfig
21
22 constructor (
23 private auth: AuthService,
24 private serverService: ServerService
25 ) {}
26
27 ngOnInit () {
28 this.serverConfig = this.serverService.getTmpConfig()
29
30 this.serverService.getConfig()
31 .subscribe(config => this.serverConfig = config)
32 }
33
34 onFirstStepDone (type: 'upload' | 'import-url' | 'import-torrent', videoName: string) {
35 this.secondStepType = type
36 this.videoName = videoName
37 }
38
39 onError () {
40 this.videoName = undefined
41 this.secondStepType = undefined
42 }
43
44 @HostListener('window:beforeunload', [ '$event' ])
45 onUnload (event: any) {
46 const { text, canDeactivate } = this.canDeactivate()
47
48 if (canDeactivate) return
49
50 event.returnValue = text
51 return text
52 }
53
54 canDeactivate (): { canDeactivate: boolean, text?: string} {
55 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
56 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
57 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate()
58
59 return { canDeactivate: true }
60 }
61
62 isVideoImportHttpEnabled () {
63 return this.serverConfig.import.videos.http.enabled
64 }
65
66 isVideoImportTorrentEnabled () {
67 return this.serverConfig.import.videos.torrent.enabled
68 }
69
70 isInSecondStep () {
71 return !!this.secondStepType
72 }
73
74 isRootUser () {
75 return this.auth.getUser().username === 'root'
76 }
77}
diff --git a/client/src/app/+videos/+video-edit/video-add.module.ts b/client/src/app/+videos/+video-edit/video-add.module.ts
new file mode 100644
index 000000000..477c1cf5e
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-add.module.ts
@@ -0,0 +1,32 @@
1import { NgModule } from '@angular/core'
2import { CanDeactivateGuard } from '@app/core'
3import { VideoEditModule } from './shared/video-edit.module'
4import { DragDropDirective } from './video-add-components/drag-drop.directive'
5import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
6import { VideoImportUrlComponent } from './video-add-components/video-import-url.component'
7import { VideoUploadComponent } from './video-add-components/video-upload.component'
8import { VideoAddRoutingModule } from './video-add-routing.module'
9import { VideoAddComponent } from './video-add.component'
10
11@NgModule({
12 imports: [
13 VideoAddRoutingModule,
14
15 VideoEditModule
16 ],
17
18 declarations: [
19 VideoAddComponent,
20 VideoUploadComponent,
21 VideoImportUrlComponent,
22 VideoImportTorrentComponent,
23 DragDropDirective
24 ],
25
26 exports: [ ],
27
28 providers: [
29 CanDeactivateGuard
30 ]
31})
32export class VideoAddModule { }
diff --git a/client/src/app/+videos/+video-edit/video-update-routing.module.ts b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
new file mode 100644
index 000000000..a04351b05
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
@@ -0,0 +1,24 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoUpdateComponent } from './video-update.component'
6import { VideoUpdateResolver } from './video-update.resolver'
7
8const videoUpdateRoutes: Routes = [
9 {
10 path: '',
11 component: VideoUpdateComponent,
12 canActivate: [ MetaGuard, LoginGuard ],
13 canDeactivate: [ CanDeactivateGuard ],
14 resolve: {
15 videoData: VideoUpdateResolver
16 }
17 }
18]
19
20@NgModule({
21 imports: [ RouterModule.forChild(videoUpdateRoutes) ],
22 exports: [ RouterModule ]
23})
24export class VideoUpdateRoutingModule {}
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
new file mode 100644
index 000000000..fbc642db9
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -0,0 +1,22 @@
1<div class="margin-content">
2 <div class="title-page title-page-single">
3 <span class="mr-1" i18n>Update</span>
4 <a [routerLink]="[ '/videos/watch', video.uuid ]">{{ video?.name }}</a>
5 </div>
6
7 <form novalidate [formGroup]="form">
8
9 <my-video-edit
10 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
13 ></my-video-edit>
14
15 <div class="submit-container">
16 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }">
17 <my-global-icon iconName="validate" aria-hidden="true"></my-global-icon>
18 <input type="button" i18n-value value="Update" />
19 </div>
20 </div>
21 </form>
22</div>
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
new file mode 100644
index 000000000..7bd6eb553
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -0,0 +1,155 @@
1import { map, switchMap } from 'rxjs/operators'
2import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { Notifier } from '@app/core'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { VideoPrivacy } from '@shared/models'
10
11@Component({
12 selector: 'my-videos-update',
13 styleUrls: [ './shared/video-edit.component.scss' ],
14 templateUrl: './video-update.component.html'
15})
16export class VideoUpdateComponent extends FormReactive implements OnInit {
17 video: VideoEdit
18
19 isUpdatingVideo = false
20 userVideoChannels: { id: number, label: string, support: string }[] = []
21 schedulePublicationPossible = false
22 videoCaptions: VideoCaptionEdit[] = []
23 waitTranscodingEnabled = true
24
25 private updateDone = false
26
27 constructor (
28 protected formValidatorService: FormValidatorService,
29 private route: ActivatedRoute,
30 private router: Router,
31 private notifier: Notifier,
32 private videoService: VideoService,
33 private loadingBar: LoadingBarService,
34 private videoCaptionService: VideoCaptionService,
35 private i18n: I18n
36 ) {
37 super()
38 }
39
40 ngOnInit () {
41 this.buildForm({})
42
43 this.route.data
44 .pipe(map(data => data.videoData))
45 .subscribe(({ video, videoChannels, videoCaptions }) => {
46 this.video = new VideoEdit(video)
47 this.userVideoChannels = videoChannels
48 this.videoCaptions = videoCaptions
49
50 this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
51
52 const videoFiles = (video as VideoDetails).getFiles()
53 if (videoFiles.length > 1) { // Already transcoded
54 this.waitTranscodingEnabled = false
55 }
56
57 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
58 setTimeout(() => this.hydrateFormFromVideo())
59 },
60
61 err => {
62 console.error(err)
63 this.notifier.error(err.message)
64 }
65 )
66 }
67
68 @HostListener('window:beforeunload', [ '$event' ])
69 onUnload (event: any) {
70 const { text, canDeactivate } = this.canDeactivate()
71
72 if (canDeactivate) return
73
74 event.returnValue = text
75 return text
76 }
77
78 canDeactivate (): { canDeactivate: boolean, text?: string } {
79 if (this.updateDone === true) return { canDeactivate: true }
80
81 const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.')
82
83 for (const caption of this.videoCaptions) {
84 if (caption.action) return { canDeactivate: false, text }
85 }
86
87 return { canDeactivate: this.formChanged === false, text }
88 }
89
90 checkForm () {
91 this.forceCheck()
92
93 return this.form.valid
94 }
95
96 update () {
97 if (this.checkForm() === false
98 || this.isUpdatingVideo === true) {
99 return
100 }
101
102 this.video.patch(this.form.value)
103
104 this.loadingBar.start()
105 this.isUpdatingVideo = true
106
107 // Update the video
108 this.videoService.updateVideo(this.video)
109 .pipe(
110 // Then update captions
111 switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions))
112 )
113 .subscribe(
114 () => {
115 this.updateDone = true
116 this.isUpdatingVideo = false
117 this.loadingBar.complete()
118 this.notifier.success(this.i18n('Video updated.'))
119 this.router.navigate([ '/videos/watch', this.video.uuid ])
120 },
121
122 err => {
123 this.loadingBar.complete()
124 this.isUpdatingVideo = false
125 this.notifier.error(err.message)
126 console.error(err)
127 }
128 )
129 }
130
131 private hydrateFormFromVideo () {
132 this.form.patchValue(this.video.toFormPatch())
133
134 const objects = [
135 {
136 url: 'thumbnailUrl',
137 name: 'thumbnailfile'
138 },
139 {
140 url: 'previewUrl',
141 name: 'previewfile'
142 }
143 ]
144
145 for (const obj of objects) {
146 fetch(this.video[obj.url])
147 .then(response => response.blob())
148 .then(data => {
149 this.form.patchValue({
150 [ obj.name ]: data
151 })
152 })
153 }
154 }
155}
diff --git a/client/src/app/+videos/+video-edit/video-update.module.ts b/client/src/app/+videos/+video-edit/video-update.module.ts
new file mode 100644
index 000000000..99cd8bea1
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-update.module.ts
@@ -0,0 +1,26 @@
1import { NgModule } from '@angular/core'
2import { CanDeactivateGuard } from '@app/core'
3import { VideoEditModule } from './shared/video-edit.module'
4import { VideoUpdateRoutingModule } from './video-update-routing.module'
5import { VideoUpdateComponent } from './video-update.component'
6import { VideoUpdateResolver } from './video-update.resolver'
7
8@NgModule({
9 imports: [
10 VideoUpdateRoutingModule,
11
12 VideoEditModule
13 ],
14
15 declarations: [
16 VideoUpdateComponent
17 ],
18
19 exports: [ ],
20
21 providers: [
22 VideoUpdateResolver,
23 CanDeactivateGuard
24 ]
25})
26export class VideoUpdateModule { }
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts
new file mode 100644
index 000000000..30bcf4d74
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts
@@ -0,0 +1,44 @@
1import { forkJoin } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
5import { VideoCaptionService, VideoChannelService, VideoService } from '@app/shared/shared-main'
6
7@Injectable()
8export class VideoUpdateResolver implements Resolve<any> {
9 constructor (
10 private videoService: VideoService,
11 private videoChannelService: VideoChannelService,
12 private videoCaptionService: VideoCaptionService
13 ) {
14 }
15
16 resolve (route: ActivatedRouteSnapshot) {
17 const uuid: string = route.params[ 'uuid' ]
18
19 return this.videoService.getVideo({ videoId: uuid })
20 .pipe(
21 switchMap(video => {
22 return forkJoin([
23 this.videoService
24 .loadCompleteDescription(video.descriptionPath)
25 .pipe(map(description => Object.assign(video, { description }))),
26
27 this.videoChannelService
28 .listAccountVideoChannels(video.account)
29 .pipe(
30 map(result => result.data),
31 map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support })))
32 ),
33
34 this.videoCaptionService
35 .listCaptions(video.id)
36 .pipe(
37 map(result => result.data)
38 )
39 ])
40 }),
41 map(([ video, videoChannels, videoCaptions ]) => ({ video, videoChannels, videoCaptions }))
42 )
43 }
44}