aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-edit/video-add-components
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-06-23 14:49:20 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-06-23 16:00:49 +0200
commit1942f11d5ee6926ad93dc1b79fae18325ba5de18 (patch)
tree3f2a3cd9466a56c419d197ac832a3e9cbc86bec4 /client/src/app/+videos/+video-edit/video-add-components
parent67ed6552b831df66713bac9e672738796128d33f (diff)
downloadPeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.gz
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.zst
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.zip
Lazy load all routes
Diffstat (limited to 'client/src/app/+videos/+video-edit/video-add-components')
-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
11 files changed, 1083 insertions, 0 deletions
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}