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/video-add.component.html68
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.scss117
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts252
-rw-r--r--client/src/app/videos/+video-edit/video-add.module.ts6
-rw-r--r--client/src/app/videos/+video-edit/video-import.component.html55
-rw-r--r--client/src/app/videos/+video-edit/video-import.component.scss37
-rw-r--r--client/src/app/videos/+video-edit/video-import.component.ts161
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-upload.component.html58
-rw-r--r--client/src/app/videos/+video-edit/video-upload.component.scss85
-rw-r--r--client/src/app/videos/+video-edit/video-upload.component.ts251
11 files changed, 712 insertions, 379 deletions
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index 9c2c01c65..ed8d91c11 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -1,65 +1,17 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div class="title-page title-page-single"> 2 <div class="title-page title-page-single">
3 <ng-container *ngIf="!videoFileName" i18n>Upload your video</ng-container> 3 <ng-container *ngIf="secondStepType === 'import'" i18n>Import {{ videoName }}</ng-container>
4 <ng-container *ngIf="videoFileName" i18n>Upload {{ videoFileName }}</ng-container> 4 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
5 </div> 5 </div>
6 6
7 <div *ngIf="!isUploadingVideo" class="upload-video-container"> 7 <tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
8 <div class="upload-video">
9 <div class="icon icon-upload"></div>
10 8
11 <div class="button-file"> 9 <tab i18n-heading heading="Upload your video">
12 <span i18n>Select the file to upload</span> 10 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload>
13 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 11 </tab>
14 </div>
15 <span class="button-file-extension">(.mp4, .webm, .ogv)</span>
16 12
17 <div class="form-group form-group-channel"> 13 <tab i18n-heading heading="Import your video">
18 <label i18n for="first-step-channel">Channel</label> 14 <my-video-import #videoImport (firstStepDone)="onFirstStepDone('import', $event)"></my-video-import>
19 <div class="peertube-select-container"> 15 </tab>
20 <select id="first-step-channel" [(ngModel)]="firstStepChannelId"> 16 </tabset>
21 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
22 </select>
23 </div>
24 </div>
25
26 <div class="form-group">
27 <label i18n for="first-step-privacy">Privacy</label>
28 <div class="peertube-select-container">
29 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
30 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
31 <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
32 </select>
33 </div>
34 </div>
35 </div>
36 </div>
37
38 <div *ngIf="isUploadingVideo" class="upload-progress-cancel">
39 <p-progressBar
40 [value]="videoUploadPercents"
41 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
42 ></p-progressBar>
43 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
44 </div>
45
46 <!-- Hidden because we want to load the component -->
47 <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
48 <my-video-edit
49 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
50 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
51 ></my-video-edit>
52
53 <div class="submit-container">
54 <div i18n *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div>
55
56 <div class="submit-button"
57 (click)="updateSecondStep()"
58 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true || videoUploaded !== true }"
59 >
60 <span class="icon icon-validate"></span>
61 <input type="button" i18n-value value="Publish" />
62 </div>
63 </div>
64 </form>
65</div> 17</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
index c0b5f3d07..a811b9cf0 100644
--- a/client/src/app/videos/+video-edit/video-add.component.scss
+++ b/client/src/app/videos/+video-edit/video-add.component.scss
@@ -1,101 +1,54 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.upload-video-container { 4$border-width: 3px;
5 border-radius: 3px; 5$border-type: solid;
6 background-color: #F7F7F7; 6$border-color: #EAEAEA;
7 border: 3px solid #EAEAEA;
8 width: 100%;
9 height: 440px;
10 margin-top: 40px;
11 display: flex;
12 justify-content: center;
13 align-items: center;
14 7
15 .peertube-select-container { 8$background-color: #F7F7F7;
16 @include peertube-select-container(190px);
17 }
18
19 .upload-video {
20 display: flex;
21 flex-direction: column;
22 align-items: center;
23
24 .form-group-channel {
25 margin-bottom: 20px;
26 }
27
28 .icon.icon-upload {
29 @include icon(90px);
30 margin-bottom: 25px;
31 cursor: default;
32
33 background-image: url('../../../assets/images/video/upload.svg');
34 }
35
36 .button-file {
37 @include peertube-button-file(auto);
38
39 min-width: 190px;
40 }
41 9
42 .button-file-extension { 10/deep/ tabset.root-tabset.video-add-tabset {
43 display: block; 11 &.hide-nav .nav {
44 font-size: 12px; 12 display: none !important;
45 margin-top: 5px;
46 }
47 }
48
49 .form-group-channel {
50 margin-top: 35px;
51 } 13 }
52}
53 14
54.upload-progress-cancel { 15 & > .nav {
55 display: flex;
56 margin-top: 25px;
57 margin-bottom: 40px;
58 16
59 p-progressBar { 17 border-bottom: $border-width $border-type $border-color;
60 flex-grow: 1; 18 margin: 0 !important;
61
62 /deep/ .ui-progressbar {
63 font-size: 15px !important;
64 color: #fff !important;
65 height: 30px !important;
66 line-height: 30px !important;
67 border-radius: 3px !important;
68 background-color: rgba(11, 204, 41, 0.16) !important;
69
70 .ui-progressbar-value {
71 background-color: #0BCC29 !important;
72 }
73 19
74 .ui-progressbar-label { 20 & > li {
75 text-align: left; 21 margin-bottom: -$border-width;
76 padding-left: 18px;
77 margin-top: 0 !important;
78 }
79 } 22 }
80 23
81 &.processing { 24 .nav-link {
82 /deep/ .ui-progressbar-label { 25 height: 40px !important;
83 // Same color as background to hide "100%" 26 padding: 0 30px !important;
84 color: rgba(11, 204, 41, 0.16) !important; 27 font-size: 15px;
28
29 &.active {
30 border: $border-width $border-type $border-color;
31 border-bottom: none;
32 background-color: $background-color !important;
85 33
86 &::before { 34 span {
87 content: 'Processing...'; 35 border-bottom: 2px solid #F1680D;
88 color: #fff; 36 font-weight: $font-bold;
89 } 37 }
90 } 38 }
91 } 39 }
92 } 40 }
93 41
94 input { 42 .upload-video-container {
95 @include peertube-button; 43 border: $border-width $border-type $border-color;
96 @include grey-button; 44 border-top: none;
97 45
98 margin-left: 10px; 46 background-color: $background-color;
47 border-radius: 3px;
48 width: 100%;
49 height: 440px;
50 display: flex;
51 justify-content: center;
52 align-items: center;
99 } 53 }
100} 54} \ No newline at end of file
101
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 651ee8dd2..64071b40c 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -1,251 +1,29 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http' 1import { Component, ViewChild } from '@angular/core'
2import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { Router } from '@angular/router'
4import { UserService } from '@app/shared'
5import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 2import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
6import { LoadingBarService } from '@ngx-loading-bar/core' 3import { VideoImportComponent } from '@app/videos/+video-edit/video-import.component'
7import { NotificationsService } from 'angular2-notifications' 4import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component'
8import { BytesPipe } from 'ngx-pipes'
9import { Subscription } from 'rxjs'
10import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
11import { AuthService, ServerService } from '../../core'
12import { FormReactive } from '../../shared'
13import { populateAsyncUserVideoChannels } from '../../shared/misc/utils'
14import { VideoEdit } from '../../shared/video/video-edit.model'
15import { VideoService } from '../../shared/video/video.service'
16import { I18n } from '@ngx-translate/i18n-polyfill'
17import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
18import { switchMap } from 'rxjs/operators'
19import { VideoCaptionService } from '@app/shared/video-caption'
20import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
21 5
22@Component({ 6@Component({
23 selector: 'my-videos-add', 7 selector: 'my-videos-add',
24 templateUrl: './video-add.component.html', 8 templateUrl: './video-add.component.html',
25 styleUrls: [ 9 styleUrls: [ './video-add.component.scss' ]
26 './shared/video-edit.component.scss',
27 './video-add.component.scss'
28 ]
29}) 10})
30export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { 11export class VideoAddComponent implements CanComponentDeactivate {
31 @ViewChild('videofileInput') videofileInput 12 @ViewChild('videoUpload') videoUpload: VideoUploadComponent
13 @ViewChild('videoImport') videoImport: VideoImportComponent
32 14
33 // So that it can be accessed in the template 15 secondStepType: 'upload' | 'import'
34 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 16 videoName: string
35 17
36 isUploadingVideo = false 18 onFirstStepDone (type: 'upload' | 'import', videoName: string) {
37 isUpdatingVideo = false 19 this.secondStepType = type
38 videoUploaded = false 20 this.videoName = videoName
39 videoUploadObservable: Subscription = null
40 videoUploadPercents = 0
41 videoUploadedIds = {
42 id: 0,
43 uuid: ''
44 }
45 videoFileName: string
46
47 userVideoChannels: { id: number, label: string, support: string }[] = []
48 userVideoQuotaUsed = 0
49 videoPrivacies: VideoConstant<string>[] = []
50 firstStepPrivacyId = 0
51 firstStepChannelId = 0
52 videoCaptions: VideoCaptionEdit[] = []
53
54 constructor (
55 protected formValidatorService: FormValidatorService,
56 private router: Router,
57 private notificationsService: NotificationsService,
58 private authService: AuthService,
59 private userService: UserService,
60 private serverService: ServerService,
61 private videoService: VideoService,
62 private loadingBar: LoadingBarService,
63 private i18n: I18n,
64 private videoCaptionService: VideoCaptionService
65 ) {
66 super()
67 }
68
69 get videoExtensions () {
70 return this.serverService.getConfig().video.file.extensions.join(',')
71 }
72
73 ngOnInit () {
74 this.buildForm({})
75
76 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
77 .then(() => this.firstStepChannelId = this.userVideoChannels[0].id)
78
79 this.userService.getMyVideoQuotaUsed()
80 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
81
82 this.serverService.videoPrivaciesLoaded
83 .subscribe(
84 () => {
85 this.videoPrivacies = this.serverService.getVideoPrivacies()
86
87 // Public by default
88 this.firstStepPrivacyId = VideoPrivacy.PUBLIC
89 })
90 }
91
92 ngOnDestroy () {
93 if (this.videoUploadObservable) {
94 this.videoUploadObservable.unsubscribe()
95 }
96 } 21 }
97 22
98 canDeactivate () { 23 canDeactivate () {
99 let text = '' 24 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
100 25 if (this.secondStepType === 'import') return this.videoImport.canDeactivate()
101 if (this.videoUploaded === true) {
102 // FIXME: cannot concatenate strings inside i18n service :/
103 text = this.i18n('Your video was uploaded in your account and is private.') +
104 this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?')
105 } else {
106 text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?')
107 }
108
109 return {
110 canDeactivate: !this.isUploadingVideo,
111 text
112 }
113 }
114
115 fileChange () {
116 this.uploadFirstStep()
117 }
118
119 checkForm () {
120 this.forceCheck()
121
122 return this.form.valid
123 }
124
125 cancelUpload () {
126 if (this.videoUploadObservable !== null) {
127 this.videoUploadObservable.unsubscribe()
128 this.isUploadingVideo = false
129 this.videoUploadPercents = 0
130 this.videoUploadObservable = null
131 this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled'))
132 }
133 }
134
135 uploadFirstStep () {
136 const videofile = this.videofileInput.nativeElement.files[0] as File
137 if (!videofile) return
138
139 // Cannot upload videos > 8GB for now
140 if (videofile.size > 8 * 1024 * 1024 * 1024) {
141 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
142 return
143 }
144
145 const videoQuota = this.authService.getUser().videoQuota
146 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
147 const bytePipes = new BytesPipe()
148
149 const msg = this.i18n(
150 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})',
151 {
152 videoSize: bytePipes.transform(videofile.size, 0),
153 videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
154 videoQuota: bytePipes.transform(videoQuota, 0)
155 }
156 )
157 this.notificationsService.error(this.i18n('Error'), msg)
158 return
159 }
160
161 this.videoFileName = videofile.name
162
163 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
164 let name: string
165
166 // If the name of the file is very small, keep the extension
167 if (nameWithoutExtension.length < 3) name = videofile.name
168 else name = nameWithoutExtension
169
170 const privacy = this.firstStepPrivacyId.toString()
171 const nsfw = false
172 const waitTranscoding = true
173 const commentsEnabled = true
174 const channelId = this.firstStepChannelId.toString()
175
176 const formData = new FormData()
177 formData.append('name', name)
178 // Put the video "private" -> we are waiting the user validation of the second step
179 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
180 formData.append('nsfw', '' + nsfw)
181 formData.append('commentsEnabled', '' + commentsEnabled)
182 formData.append('waitTranscoding', '' + waitTranscoding)
183 formData.append('channelId', '' + channelId)
184 formData.append('videofile', videofile)
185
186 this.isUploadingVideo = true
187 this.form.patchValue({
188 name,
189 privacy,
190 nsfw,
191 channelId
192 })
193
194 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
195 event => {
196 if (event.type === HttpEventType.UploadProgress) {
197 this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
198 } else if (event instanceof HttpResponse) {
199 this.videoUploaded = true
200
201 this.videoUploadedIds = event.body.video
202
203 this.videoUploadObservable = null
204 }
205 },
206
207 err => {
208 // Reset progress
209 this.isUploadingVideo = false
210 this.videoUploadPercents = 0
211 this.videoUploadObservable = null
212 this.notificationsService.error(this.i18n('Error'), err.message)
213 }
214 )
215 }
216
217 updateSecondStep () {
218 if (this.checkForm() === false) {
219 return
220 }
221
222 const video = new VideoEdit()
223 video.patch(this.form.value)
224 video.id = this.videoUploadedIds.id
225 video.uuid = this.videoUploadedIds.uuid
226
227 this.isUpdatingVideo = true
228 this.loadingBar.start()
229 this.videoService.updateVideo(video)
230 .pipe(
231 // Then update captions
232 switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions))
233 )
234 .subscribe(
235 () => {
236 this.isUpdatingVideo = false
237 this.isUploadingVideo = false
238 this.loadingBar.complete()
239
240 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.'))
241 this.router.navigate([ '/videos/watch', video.uuid ])
242 },
243 26
244 err => { 27 return { canDeactivate: true }
245 this.isUpdatingVideo = false
246 this.notificationsService.error(this.i18n('Error'), err.message)
247 console.error(err)
248 }
249 )
250 } 28 }
251} 29}
diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts
index 1bfedf251..91f544971 100644
--- a/client/src/app/videos/+video-edit/video-add.module.ts
+++ b/client/src/app/videos/+video-edit/video-add.module.ts
@@ -5,6 +5,8 @@ import { VideoEditModule } from './shared/video-edit.module'
5import { VideoAddRoutingModule } from './video-add-routing.module' 5import { VideoAddRoutingModule } from './video-add-routing.module'
6import { VideoAddComponent } from './video-add.component' 6import { VideoAddComponent } from './video-add.component'
7import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service' 7import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service'
8import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component'
9import { VideoImportComponent } from '@app/videos/+video-edit/video-import.component'
8 10
9@NgModule({ 11@NgModule({
10 imports: [ 12 imports: [
@@ -14,7 +16,9 @@ import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.ser
14 ProgressBarModule 16 ProgressBarModule
15 ], 17 ],
16 declarations: [ 18 declarations: [
17 VideoAddComponent 19 VideoAddComponent,
20 VideoUploadComponent,
21 VideoImportComponent
18 ], 22 ],
19 exports: [ 23 exports: [
20 VideoAddComponent 24 VideoAddComponent
diff --git a/client/src/app/videos/+video-edit/video-import.component.html b/client/src/app/videos/+video-edit/video-import.component.html
new file mode 100644
index 000000000..9d71a0717
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-import.component.html
@@ -0,0 +1,55 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="import-video">
3 <div class="icon icon-upload"></div>
4
5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label>
7 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
8 </div>
9
10 <div class="form-group">
11 <label i18n for="first-step-channel">Channel</label>
12 <div class="peertube-select-container">
13 <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
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">
23 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
24 </select>
25 </div>
26 </div>
27
28 <input
29 type="button" i18n-value value="Import"
30 [disabled]="!isTargetUrlValid() || isImportingVideo" (click)="importVideo()"
31 />
32 </div>
33</div>
34
35<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
36 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
37</div>
38
39<!-- Hidden because we want to load the component -->
40<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
41 <my-video-edit
42 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
43 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
44 ></my-video-edit>
45
46 <div class="submit-container">
47 <div class="submit-button"
48 (click)="updateSecondStep()"
49 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
50 >
51 <span class="icon icon-validate"></span>
52 <input type="button" i18n-value value="Update" />
53 </div>
54 </div>
55</form>
diff --git a/client/src/app/videos/+video-edit/video-import.component.scss b/client/src/app/videos/+video-edit/video-import.component.scss
new file mode 100644
index 000000000..9ada9db19
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-import.component.scss
@@ -0,0 +1,37 @@
1@import '_variables';
2@import '_mixins';
3
4$width-size: 190px;
5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.import-video {
11 display: flex;
12 flex-direction: column;
13 align-items: center;
14
15 .icon.icon-upload {
16 @include icon(90px);
17 margin-bottom: 25px;
18 cursor: default;
19
20 background-image: url('../../../assets/images/video/upload.svg');
21 }
22
23 input[type=text] {
24 @include peertube-input-text($width-size);
25 display: block;
26 }
27
28 input[type=button] {
29 @include peertube-button;
30 @include orange-button;
31
32 width: $width-size;
33 margin-top: 30px;
34 }
35}
36
37
diff --git a/client/src/app/videos/+video-edit/video-import.component.ts b/client/src/app/videos/+video-edit/video-import.component.ts
new file mode 100644
index 000000000..bd4482e17
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-import.component.ts
@@ -0,0 +1,161 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { Router } from '@angular/router'
3import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
4import { NotificationsService } from 'angular2-notifications'
5import { VideoConstant, VideoPrivacy, VideoUpdate } from '../../../../../shared/models/videos'
6import { AuthService, ServerService } from '../../core'
7import { FormReactive } from '../../shared'
8import { populateAsyncUserVideoChannels } from '../../shared/misc/utils'
9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
12import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
13import { VideoImportService } from '@app/shared/video-import'
14import { VideoEdit } from '@app/shared/video/video-edit.model'
15import { switchMap } from 'rxjs/operators'
16import { LoadingBarService } from '@ngx-loading-bar/core'
17import { VideoCaptionService } from '@app/shared/video-caption'
18
19@Component({
20 selector: 'my-video-import',
21 templateUrl: './video-import.component.html',
22 styleUrls: [
23 './shared/video-edit.component.scss',
24 './video-import.component.scss'
25 ]
26})
27export class VideoImportComponent extends FormReactive implements OnInit, CanComponentDeactivate {
28 @Output() firstStepDone = new EventEmitter<string>()
29
30 targetUrl = ''
31 videoFileName: string
32
33 isImportingVideo = false
34 hasImportedVideo = false
35 isUpdatingVideo = false
36
37 userVideoChannels: { id: number, label: string, support: string }[] = []
38 videoPrivacies: VideoConstant<string>[] = []
39 videoCaptions: VideoCaptionEdit[] = []
40
41 firstStepPrivacyId = 0
42 firstStepChannelId = 0
43 video: VideoEdit
44
45 constructor (
46 protected formValidatorService: FormValidatorService,
47 private router: Router,
48 private loadingBar: LoadingBarService,
49 private notificationsService: NotificationsService,
50 private authService: AuthService,
51 private serverService: ServerService,
52 private videoService: VideoService,
53 private videoImportService: VideoImportService,
54 private videoCaptionService: VideoCaptionService,
55 private i18n: I18n
56 ) {
57 super()
58 }
59
60 ngOnInit () {
61 this.buildForm({})
62
63 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
64 .then(() => this.firstStepChannelId = this.userVideoChannels[ 0 ].id)
65
66 this.serverService.videoPrivaciesLoaded
67 .subscribe(
68 () => {
69 this.videoPrivacies = this.serverService.getVideoPrivacies()
70
71 // Private by default
72 this.firstStepPrivacyId = VideoPrivacy.PRIVATE
73 })
74 }
75
76 canDeactivate () {
77 return { canDeactivate: true }
78 }
79
80 checkForm () {
81 this.forceCheck()
82
83 return this.form.valid
84 }
85
86 isTargetUrlValid () {
87 return this.targetUrl && this.targetUrl.match(/https?:\/\//)
88 }
89
90 importVideo () {
91 this.isImportingVideo = true
92
93 const videoUpdate: VideoUpdate = {
94 privacy: this.firstStepPrivacyId,
95 waitTranscoding: false,
96 commentsEnabled: true,
97 channelId: this.firstStepChannelId
98 }
99
100 this.videoImportService.importVideo(this.targetUrl, videoUpdate).subscribe(
101 res => {
102 this.firstStepDone.emit(res.video.name)
103 this.isImportingVideo = false
104 this.hasImportedVideo = true
105
106 this.video = new VideoEdit(Object.assign(res.video, {
107 commentsEnabled: videoUpdate.commentsEnabled,
108 support: null,
109 thumbnailUrl: null,
110 previewUrl: null
111 }))
112 this.hydrateFormFromVideo()
113 },
114
115 err => {
116 this.isImportingVideo = false
117 this.notificationsService.error(this.i18n('Error'), err.message)
118 }
119 )
120 }
121
122 updateSecondStep () {
123 if (this.checkForm() === false) {
124 return
125 }
126
127 this.video.patch(this.form.value)
128
129 this.loadingBar.start()
130 this.isUpdatingVideo = true
131
132 // Update the video
133 this.videoService.updateVideo(this.video)
134 .pipe(
135 // Then update captions
136 switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions))
137 )
138 .subscribe(
139 () => {
140 this.isUpdatingVideo = false
141 this.loadingBar.complete()
142 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.'))
143
144 // TODO: route to imports list
145 // this.router.navigate([ '/videos/watch', this.video.uuid ])
146 },
147
148 err => {
149 this.loadingBar.complete()
150 this.isUpdatingVideo = false
151 this.notificationsService.error(this.i18n('Error'), err.message)
152 console.error(err)
153 }
154 )
155
156 }
157
158 private hydrateFormFromVideo () {
159 this.form.patchValue(this.video.toFormPatch())
160 }
161}
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 798c48f3c..0c60e3439 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -126,7 +126,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
126 console.error(err) 126 console.error(err)
127 } 127 }
128 ) 128 )
129
130 } 129 }
131 130
132 private hydrateFormFromVideo () { 131 private hydrateFormFromVideo () {
diff --git a/client/src/app/videos/+video-edit/video-upload.component.html b/client/src/app/videos/+video-edit/video-upload.component.html
new file mode 100644
index 000000000..8c0723155
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-upload.component.html
@@ -0,0 +1,58 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container">
2 <div class="upload-video">
3 <div class="icon icon-upload"></div>
4
5 <div class="button-file">
6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
8 </div>
9 <span class="button-file-extension">(.mp4, .webm, .ogv)</span>
10
11 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label>
13 <div class="peertube-select-container">
14 <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
15 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
16 </select>
17 </div>
18 </div>
19
20 <div class="form-group">
21 <label i18n for="first-step-privacy">Privacy</label>
22 <div class="peertube-select-container">
23 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
24 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
25 <option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
26 </select>
27 </div>
28 </div>
29 </div>
30</div>
31
32<div *ngIf="isUploadingVideo" class="upload-progress-cancel">
33 <p-progressBar
34 [value]="videoUploadPercents"
35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
36 ></p-progressBar>
37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
38</div>
39
40<!-- Hidden because we want to load the component -->
41<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
42 <my-video-edit
43 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
44 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
45 ></my-video-edit>
46
47 <div class="submit-container">
48 <div i18n *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div>
49
50 <div class="submit-button"
51 (click)="updateSecondStep()"
52 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true || videoUploaded !== true }"
53 >
54 <span class="icon icon-validate"></span>
55 <input type="button" i18n-value value="Publish" />
56 </div>
57 </div>
58</form> \ No newline at end of file
diff --git a/client/src/app/videos/+video-edit/video-upload.component.scss b/client/src/app/videos/+video-edit/video-upload.component.scss
new file mode 100644
index 000000000..015835672
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-upload.component.scss
@@ -0,0 +1,85 @@
1@import '_variables';
2@import '_mixins';
3
4.peertube-select-container {
5 @include peertube-select-container(190px);
6}
7
8.upload-video {
9 display: flex;
10 flex-direction: column;
11 align-items: center;
12
13 .form-group-channel {
14 margin-bottom: 20px;
15 margin-top: 35px;
16 }
17
18 .icon.icon-upload {
19 @include icon(90px);
20 margin-bottom: 25px;
21 cursor: default;
22
23 background-image: url('../../../assets/images/video/upload.svg');
24 }
25
26 .button-file {
27 @include peertube-button-file(auto);
28
29 min-width: 190px;
30 }
31
32 .button-file-extension {
33 display: block;
34 font-size: 12px;
35 margin-top: 5px;
36 }
37}
38
39.upload-progress-cancel {
40 display: flex;
41 margin-top: 25px;
42 margin-bottom: 40px;
43
44 p-progressBar {
45 flex-grow: 1;
46
47 /deep/ .ui-progressbar {
48 font-size: 15px !important;
49 color: #fff !important;
50 height: 30px !important;
51 line-height: 30px !important;
52 border-radius: 3px !important;
53 background-color: rgba(11, 204, 41, 0.16) !important;
54
55 .ui-progressbar-value {
56 background-color: #0BCC29 !important;
57 }
58
59 .ui-progressbar-label {
60 text-align: left;
61 padding-left: 18px;
62 margin-top: 0 !important;
63 }
64 }
65
66 &.processing {
67 /deep/ .ui-progressbar-label {
68 // Same color as background to hide "100%"
69 color: rgba(11, 204, 41, 0.16) !important;
70
71 &::before {
72 content: 'Processing...';
73 color: #fff;
74 }
75 }
76 }
77 }
78
79 input {
80 @include peertube-button;
81 @include grey-button;
82
83 margin-left: 10px;
84 }
85} \ No newline at end of file
diff --git a/client/src/app/videos/+video-edit/video-upload.component.ts b/client/src/app/videos/+video-edit/video-upload.component.ts
new file mode 100644
index 000000000..e6c391d2f
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-upload.component.ts
@@ -0,0 +1,251 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
3import { Router } from '@angular/router'
4import { UserService } from '@app/shared'
5import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
6import { LoadingBarService } from '@ngx-loading-bar/core'
7import { NotificationsService } from 'angular2-notifications'
8import { BytesPipe } from 'ngx-pipes'
9import { Subscription } from 'rxjs'
10import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
11import { AuthService, ServerService } from '../../core'
12import { FormReactive } from '../../shared'
13import { populateAsyncUserVideoChannels } from '../../shared/misc/utils'
14import { VideoEdit } from '../../shared/video/video-edit.model'
15import { VideoService } from '../../shared/video/video.service'
16import { I18n } from '@ngx-translate/i18n-polyfill'
17import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
18import { switchMap } from 'rxjs/operators'
19import { VideoCaptionService } from '@app/shared/video-caption'
20import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
21
22@Component({
23 selector: 'my-video-upload',
24 templateUrl: './video-upload.component.html',
25 styleUrls: [
26 './shared/video-edit.component.scss',
27 './video-upload.component.scss'
28 ]
29})
30export class VideoUploadComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
31 @Output() firstStepDone = new EventEmitter<string>()
32 @ViewChild('videofileInput') videofileInput
33
34 // So that it can be accessed in the template
35 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
36
37 isUploadingVideo = false
38 isUpdatingVideo = false
39 videoUploaded = false
40 videoUploadObservable: Subscription = null
41 videoUploadPercents = 0
42 videoUploadedIds = {
43 id: 0,
44 uuid: ''
45 }
46
47 userVideoChannels: { id: number, label: string, support: string }[] = []
48 userVideoQuotaUsed = 0
49 videoPrivacies: VideoConstant<string>[] = []
50 firstStepPrivacyId = 0
51 firstStepChannelId = 0
52 videoCaptions: VideoCaptionEdit[] = []
53
54 constructor (
55 protected formValidatorService: FormValidatorService,
56 private router: Router,
57 private notificationsService: NotificationsService,
58 private authService: AuthService,
59 private userService: UserService,
60 private serverService: ServerService,
61 private videoService: VideoService,
62 private loadingBar: LoadingBarService,
63 private i18n: I18n,
64 private videoCaptionService: VideoCaptionService
65 ) {
66 super()
67 }
68
69 get videoExtensions () {
70 return this.serverService.getConfig().video.file.extensions.join(',')
71 }
72
73 ngOnInit () {
74 this.buildForm({})
75
76 populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
77 .then(() => this.firstStepChannelId = this.userVideoChannels[0].id)
78
79 this.userService.getMyVideoQuotaUsed()
80 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
81
82 this.serverService.videoPrivaciesLoaded
83 .subscribe(
84 () => {
85 this.videoPrivacies = this.serverService.getVideoPrivacies()
86
87 // Public by default
88 this.firstStepPrivacyId = VideoPrivacy.PUBLIC
89 })
90 }
91
92 ngOnDestroy () {
93 if (this.videoUploadObservable) {
94 this.videoUploadObservable.unsubscribe()
95 }
96 }
97
98 canDeactivate () {
99 let text = ''
100
101 if (this.videoUploaded === true) {
102 // FIXME: cannot concatenate strings inside i18n service :/
103 text = this.i18n('Your video was uploaded in your account and is private.') +
104 this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?')
105 } else {
106 text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?')
107 }
108
109 return {
110 canDeactivate: !this.isUploadingVideo,
111 text
112 }
113 }
114
115 fileChange () {
116 this.uploadFirstStep()
117 }
118
119 checkForm () {
120 this.forceCheck()
121
122 return this.form.valid
123 }
124
125 cancelUpload () {
126 if (this.videoUploadObservable !== null) {
127 this.videoUploadObservable.unsubscribe()
128 this.isUploadingVideo = false
129 this.videoUploadPercents = 0
130 this.videoUploadObservable = null
131 this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled'))
132 }
133 }
134
135 uploadFirstStep () {
136 const videofile = this.videofileInput.nativeElement.files[0] as File
137 if (!videofile) return
138
139 // Cannot upload videos > 8GB for now
140 if (videofile.size > 8 * 1024 * 1024 * 1024) {
141 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
142 return
143 }
144
145 const videoQuota = this.authService.getUser().videoQuota
146 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
147 const bytePipes = new BytesPipe()
148
149 const msg = this.i18n(
150 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})',
151 {
152 videoSize: bytePipes.transform(videofile.size, 0),
153 videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
154 videoQuota: bytePipes.transform(videoQuota, 0)
155 }
156 )
157 this.notificationsService.error(this.i18n('Error'), msg)
158 return
159 }
160
161 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
162 let name: string
163
164 // If the name of the file is very small, keep the extension
165 if (nameWithoutExtension.length < 3) name = videofile.name
166 else name = nameWithoutExtension
167
168 const privacy = this.firstStepPrivacyId.toString()
169 const nsfw = false
170 const waitTranscoding = true
171 const commentsEnabled = true
172 const channelId = this.firstStepChannelId.toString()
173
174 const formData = new FormData()
175 formData.append('name', name)
176 // Put the video "private" -> we are waiting the user validation of the second step
177 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
178 formData.append('nsfw', '' + nsfw)
179 formData.append('commentsEnabled', '' + commentsEnabled)
180 formData.append('waitTranscoding', '' + waitTranscoding)
181 formData.append('channelId', '' + channelId)
182 formData.append('videofile', videofile)
183
184 this.isUploadingVideo = true
185 this.firstStepDone.emit(name)
186
187 this.form.patchValue({
188 name,
189 privacy,
190 nsfw,
191 channelId
192 })
193
194 this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe(
195 event => {
196 if (event.type === HttpEventType.UploadProgress) {
197 this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
198 } else if (event instanceof HttpResponse) {
199 this.videoUploaded = true
200
201 this.videoUploadedIds = event.body.video
202
203 this.videoUploadObservable = null
204 }
205 },
206
207 err => {
208 // Reset progress
209 this.isUploadingVideo = false
210 this.videoUploadPercents = 0
211 this.videoUploadObservable = null
212 this.notificationsService.error(this.i18n('Error'), err.message)
213 }
214 )
215 }
216
217 updateSecondStep () {
218 if (this.checkForm() === false) {
219 return
220 }
221
222 const video = new VideoEdit()
223 video.patch(this.form.value)
224 video.id = this.videoUploadedIds.id
225 video.uuid = this.videoUploadedIds.uuid
226
227 this.isUpdatingVideo = true
228 this.loadingBar.start()
229 this.videoService.updateVideo(video)
230 .pipe(
231 // Then update captions
232 switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions))
233 )
234 .subscribe(
235 () => {
236 this.isUpdatingVideo = false
237 this.isUploadingVideo = false
238 this.loadingBar.complete()
239
240 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.'))
241 this.router.navigate([ '/videos/watch', video.uuid ])
242 },
243
244 err => {
245 this.isUpdatingVideo = false
246 this.notificationsService.error(this.i18n('Error'), err.message)
247 console.error(err)
248 }
249 )
250 }
251}