aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html7
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts12
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts10
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html52
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss10
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts66
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts2
-rw-r--r--client/src/app/+my-account/my-account.component.html2
-rw-r--r--client/src/app/+my-account/my-account.component.ts12
-rw-r--r--client/src/app/+my-account/my-account.module.ts8
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/shared/misc/peertube-local-storage.ts4
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/shared/video-import/index.ts1
-rw-r--r--client/src/app/shared/video-import/video-import.service.ts88
-rw-r--r--client/src/app/shared/video/video-edit.model.ts40
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.html2
-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.ts253
-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.html60
-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.ts164
-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
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts7
-rw-r--r--client/src/app/videos/shared/markdown.service.ts1
31 files changed, 1033 insertions, 404 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 6e3f83ccf..13b43306b 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -101,6 +101,13 @@
101 </div> 101 </div>
102 </div> 102 </div>
103 103
104 <div i18n class="inner-form-title">Import</div>
105
106 <my-peertube-checkbox
107 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
108 i18n-labelText labelText="Video import with HTTP enabled"
109 ></my-peertube-checkbox>
110
104 <div i18n class="inner-form-title">Administrator</div> 111 <div i18n class="inner-form-title">Administrator</div>
105 112
106 <div class="form-group"> 113 <div class="form-group">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index c77249a02..bc5ce6e5d 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -29,6 +29,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
29 { value: 50 * 1024 * 1024 * 1024, label: '50GB' } 29 { value: 50 * 1024 * 1024 * 1024, label: '50GB' }
30 ] 30 ]
31 transcodingThreadOptions = [ 31 transcodingThreadOptions = [
32 { value: 0, label: 'Auto (via ffmpeg)' },
32 { value: 1, label: '1' }, 33 { value: 1, label: '1' },
33 { value: 2, label: '2' }, 34 { value: 2, label: '2' },
34 { value: 4, label: '4' }, 35 { value: 4, label: '4' },
@@ -70,6 +71,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
70 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, 71 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
71 signupEnabled: null, 72 signupEnabled: null,
72 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 73 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
74 importVideosHttpEnabled: null,
73 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 75 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
74 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 76 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
75 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 77 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
@@ -182,6 +184,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
182 '720p': this.form.value[this.getResolutionKey('720p')], 184 '720p': this.form.value[this.getResolutionKey('720p')],
183 '1080p': this.form.value[this.getResolutionKey('1080p')] 185 '1080p': this.form.value[this.getResolutionKey('1080p')]
184 } 186 }
187 },
188 import: {
189 videos: {
190 http: {
191 enabled: this.form.value['importVideosHttpEnabled']
192 }
193 }
185 } 194 }
186 } 195 }
187 196
@@ -221,7 +230,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
221 transcodingThreads: this.customConfig.transcoding.threads, 230 transcodingThreads: this.customConfig.transcoding.threads,
222 transcodingEnabled: this.customConfig.transcoding.enabled, 231 transcodingEnabled: this.customConfig.transcoding.enabled,
223 customizationJavascript: this.customConfig.instance.customizations.javascript, 232 customizationJavascript: this.customConfig.instance.customizations.javascript,
224 customizationCSS: this.customConfig.instance.customizations.css 233 customizationCSS: this.customConfig.instance.customizations.css,
234 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled
225 } 235 }
226 236
227 for (const resolution of this.resolutions) { 237 for (const resolution of this.resolutions) {
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 91b464f75..6f0806e8a 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -8,6 +8,7 @@ import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.
8import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' 8import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component'
9import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' 9import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
10import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' 10import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
11 12
12const myAccountRoutes: Routes = [ 13const myAccountRoutes: Routes = [
13 { 14 {
@@ -64,6 +65,15 @@ const myAccountRoutes: Routes = [
64 title: 'Account videos' 65 title: 'Account videos'
65 } 66 }
66 } 67 }
68 },
69 {
70 path: 'video-imports',
71 component: MyAccountVideoImportsComponent,
72 data: {
73 meta: {
74 title: 'Account video imports'
75 }
76 }
67 } 77 }
68 ] 78 ]
69 } 79 }
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
new file mode 100644
index 000000000..00b2d7cb0
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html
@@ -0,0 +1,52 @@
1<p-table
2 [value]="videoImports" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
4>
5 <ng-template pTemplate="header">
6 <tr>
7 <th style="width: 40px;"></th>
8 <th i18n>URL</th>
9 <th i18n>Video</th>
10 <th i18n style="width: 150px">State</th>
11 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
12 <th></th>
13 </tr>
14 </ng-template>
15
16 <ng-template pTemplate="body" let-expanded="expanded" let-videoImport>
17 <tr>
18 <td>
19 <span *ngIf="videoImport.error" class="expander" [pRowToggler]="videoImport">
20 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
21 </span>
22 </td>
23
24 <td>
25 <a [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a>
26 </td>
27
28 <td *ngIf="isVideoImportPending(videoImport)">
29 {{ videoImport.video.name }}
30 </td>
31 <td *ngIf="isVideoImportSuccess(videoImport)">
32 <a [href]="getVideoUrl(videoImport.video)" target="_blank" rel="noopener noreferrer">{{ videoImport.video.name }}</a>
33 </td>
34 <td *ngIf="isVideoImportFailed(videoImport)"></td>
35
36 <td>{{ videoImport.state.label }}</td>
37 <td>{{ videoImport.createdAt }}</td>
38
39 <td class="action-cell">
40 <my-edit-button *ngIf="isVideoImportSuccess(videoImport)" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
41 </td>
42 </tr>
43 </ng-template>
44
45 <ng-template pTemplate="rowexpansion" let-videoImport>
46 <tr class="video-import-error" *ngIf="videoImport.error">
47 <td colspan="6">
48 <pre>{{ videoImport.error }}</pre>
49 </td>
50 </tr>
51 </ng-template>
52</p-table>
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss
new file mode 100644
index 000000000..bdd2f8270
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4pre {
5 font-size: 11px;
6}
7
8.video-import-error {
9 color: red;
10} \ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts
new file mode 100644
index 000000000..31ccb0bc8
--- /dev/null
+++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts
@@ -0,0 +1,66 @@
1import { Component, OnInit } from '@angular/core'
2import { RestPagination, RestTable } from '@app/shared'
3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { NotificationsService } from 'angular2-notifications'
5import { ConfirmService } from '@app/core'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { VideoImport, VideoImportState } from '../../../../../shared/models/videos'
8import { VideoImportService } from '@app/shared/video-import'
9
10@Component({
11 selector: 'my-account-video-imports',
12 templateUrl: './my-account-video-imports.component.html',
13 styleUrls: [ './my-account-video-imports.component.scss' ]
14})
15export class MyAccountVideoImportsComponent extends RestTable implements OnInit {
16 videoImports: VideoImport[] = []
17 totalRecords = 0
18 rowsPerPage = 10
19 sort: SortMeta = { field: 'createdAt', order: 1 }
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21
22 constructor (
23 private notificationsService: NotificationsService,
24 private confirmService: ConfirmService,
25 private videoImportService: VideoImportService,
26 private i18n: I18n
27 ) {
28 super()
29 }
30
31 ngOnInit () {
32 this.loadSort()
33 }
34
35 isVideoImportSuccess (videoImport: VideoImport) {
36 return videoImport.state.id === VideoImportState.SUCCESS
37 }
38
39 isVideoImportPending (videoImport: VideoImport) {
40 return videoImport.state.id === VideoImportState.PENDING
41 }
42
43 isVideoImportFailed (videoImport: VideoImport) {
44 return videoImport.state.id === VideoImportState.FAILED
45 }
46
47 getVideoUrl (video: { uuid: string }) {
48 return '/videos/watch/' + video.uuid
49 }
50
51 getEditVideoUrl (video: { uuid: string }) {
52 return '/videos/update/' + video.uuid
53 }
54
55 protected loadData () {
56 this.videoImportService.getMyVideoImports(this.pagination, this.sort)
57 .subscribe(
58 resultList => {
59 this.videoImports = resultList.data
60 this.totalRecords = resultList.total
61 },
62
63 err => this.notificationsService.error(this.i18n('Error'), err.message)
64 )
65 }
66}
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index 54830c75e..01e1ef1da 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -145,6 +145,8 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
145 suffix = this.i18n('Waiting transcoding') 145 suffix = this.i18n('Waiting transcoding')
146 } else if (video.state.id === VideoState.TO_TRANSCODE) { 146 } else if (video.state.id === VideoState.TO_TRANSCODE) {
147 suffix = this.i18n('To transcode') 147 suffix = this.i18n('To transcode')
148 } else if (video.state.id === VideoState.TO_IMPORT) {
149 suffix = this.i18n('To import')
148 } else { 150 } else {
149 return '' 151 return ''
150 } 152 }
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index 48db55ad3..ddb0570db 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -5,6 +5,8 @@
5 <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a> 5 <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a>
6 6
7 <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a> 7 <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
8
9 <a *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My video imports</a>
8 </div> 10 </div>
9 11
10 <div class="margin-content"> 12 <div class="margin-content">
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index 7bb461d3c..6e29cdd83 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -1,7 +1,17 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2import { ServerService } from '@app/core'
2 3
3@Component({ 4@Component({
4 selector: 'my-my-account', 5 selector: 'my-my-account',
5 templateUrl: './my-account.component.html' 6 templateUrl: './my-account.component.html'
6}) 7})
7export class MyAccountComponent {} 8export class MyAccountComponent {
9
10 constructor (
11 private serverService: ServerService
12 ) {}
13
14 isVideoImportEnabled () {
15 return this.serverService.getConfig().import.videos.http.enabled
16 }
17}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 2088273e6..5403ab649 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -1,3 +1,4 @@
1import { TableModule } from 'primeng/table'
1import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
2import { SharedModule } from '../shared' 3import { SharedModule } from '../shared'
3import { MyAccountRoutingModule } from './my-account-routing.module' 4import { MyAccountRoutingModule } from './my-account-routing.module'
@@ -11,11 +12,13 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid
11import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' 12import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
12import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' 13import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
13import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' 14import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
15import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
14 16
15@NgModule({ 17@NgModule({
16 imports: [ 18 imports: [
17 MyAccountRoutingModule, 19 MyAccountRoutingModule,
18 SharedModule 20 SharedModule,
21 TableModule
19 ], 22 ],
20 23
21 declarations: [ 24 declarations: [
@@ -28,7 +31,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
28 MyAccountVideoChannelsComponent, 31 MyAccountVideoChannelsComponent,
29 MyAccountVideoChannelCreateComponent, 32 MyAccountVideoChannelCreateComponent,
30 MyAccountVideoChannelUpdateComponent, 33 MyAccountVideoChannelUpdateComponent,
31 ActorAvatarInfoComponent 34 ActorAvatarInfoComponent,
35 MyAccountVideoImportsComponent
32 ], 36 ],
33 37
34 exports: [ 38 exports: [
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 7b11c068e..ab317f0aa 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -68,6 +68,13 @@ export class ServerService {
68 }, 68 },
69 user: { 69 user: {
70 videoQuota: -1 70 videoQuota: -1
71 },
72 import: {
73 videos: {
74 http: {
75 enabled: false
76 }
77 }
71 } 78 }
72 } 79 }
73 private videoCategories: Array<VideoConstant<string>> = [] 80 private videoCategories: Array<VideoConstant<string>> = []
diff --git a/client/src/app/shared/misc/peertube-local-storage.ts b/client/src/app/shared/misc/peertube-local-storage.ts
index ad761c82f..260f994b6 100644
--- a/client/src/app/shared/misc/peertube-local-storage.ts
+++ b/client/src/app/shared/misc/peertube-local-storage.ts
@@ -48,7 +48,7 @@ try {
48 const instance = new MemoryStorage() 48 const instance = new MemoryStorage()
49 49
50 peertubeLocalStorage = new Proxy(instance, { 50 peertubeLocalStorage = new Proxy(instance, {
51 set: function (obj, prop, value) { 51 set: function (obj, prop: string | number, value) {
52 if (MemoryStorage.prototype.hasOwnProperty(prop)) { 52 if (MemoryStorage.prototype.hasOwnProperty(prop)) {
53 instance[prop] = value 53 instance[prop] = value
54 } else { 54 } else {
@@ -56,7 +56,7 @@ try {
56 } 56 }
57 return true 57 return true
58 }, 58 },
59 get: function (target, name) { 59 get: function (target, name: string | number) {
60 if (MemoryStorage.prototype.hasOwnProperty(name)) { 60 if (MemoryStorage.prototype.hasOwnProperty(name)) {
61 return instance[name] 61 return instance[name]
62 } 62 }
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 99df61cdb..62ce97102 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -51,6 +51,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
51import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' 51import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service'
52import { VideoCaptionService } from '@app/shared/video-caption' 52import { VideoCaptionService } from '@app/shared/video-caption'
53import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component' 53import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
54import { VideoImportService } from '@app/shared/video-import/video-import.service'
54 55
55@NgModule({ 56@NgModule({
56 imports: [ 57 imports: [
@@ -143,6 +144,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
143 VideoCommentValidatorsService, 144 VideoCommentValidatorsService,
144 VideoValidatorsService, 145 VideoValidatorsService,
145 VideoCaptionsValidatorsService, 146 VideoCaptionsValidatorsService,
147 VideoImportService,
146 148
147 I18nPrimengCalendarService, 149 I18nPrimengCalendarService,
148 ScreenService, 150 ScreenService,
diff --git a/client/src/app/shared/video-import/index.ts b/client/src/app/shared/video-import/index.ts
new file mode 100644
index 000000000..9bb73ec2c
--- /dev/null
+++ b/client/src/app/shared/video-import/index.ts
@@ -0,0 +1 @@
export * from './video-import.service'
diff --git a/client/src/app/shared/video-import/video-import.service.ts b/client/src/app/shared/video-import/video-import.service.ts
new file mode 100644
index 000000000..59b58ab38
--- /dev/null
+++ b/client/src/app/shared/video-import/video-import.service.ts
@@ -0,0 +1,88 @@
1import { catchError, map, switchMap } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { Observable } from 'rxjs'
5import { VideoImport } from '../../../../../shared'
6import { environment } from '../../../environments/environment'
7import { RestExtractor, RestService } from '../rest'
8import { VideoImportCreate } from '../../../../../shared/models/videos/video-import-create.model'
9import { objectToFormData } from '@app/shared/misc/utils'
10import { VideoUpdate } from '../../../../../shared/models/videos'
11import { ResultList } from '../../../../../shared/models/result-list.model'
12import { UserService } from '@app/shared/users/user.service'
13import { SortMeta } from 'primeng/components/common/sortmeta'
14import { RestPagination } from '@app/shared/rest'
15import { ServerService } from '@app/core'
16import { peertubeTranslate } from '@app/shared/i18n/i18n-utils'
17
18@Injectable()
19export class VideoImportService {
20 private static BASE_VIDEO_IMPORT_URL = environment.apiUrl + '/api/v1/videos/imports/'
21
22 constructor (
23 private authHttp: HttpClient,
24 private restService: RestService,
25 private restExtractor: RestExtractor,
26 private serverService: ServerService
27 ) {}
28
29 importVideo (targetUrl: string, video: VideoUpdate): Observable<VideoImport> {
30 const url = VideoImportService.BASE_VIDEO_IMPORT_URL
31 const language = video.language || null
32 const licence = video.licence || null
33 const category = video.category || null
34 const description = video.description || null
35 const support = video.support || null
36 const scheduleUpdate = video.scheduleUpdate || null
37
38 const body: VideoImportCreate = {
39 targetUrl,
40
41 name: video.name,
42 category,
43 licence,
44 language,
45 support,
46 description,
47 channelId: video.channelId,
48 privacy: video.privacy,
49 tags: video.tags,
50 nsfw: video.nsfw,
51 waitTranscoding: video.waitTranscoding,
52 commentsEnabled: video.commentsEnabled,
53 thumbnailfile: video.thumbnailfile,
54 previewfile: video.previewfile,
55 scheduleUpdate
56 }
57
58 const data = objectToFormData(body)
59 return this.authHttp.post<VideoImport>(url, data)
60 .pipe(catchError(res => this.restExtractor.handleError(res)))
61 }
62
63 getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> {
64 let params = new HttpParams()
65 params = this.restService.addRestGetParams(params, pagination, sort)
66
67 return this.authHttp
68 .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
69 .pipe(
70 switchMap(res => this.extractVideoImports(res)),
71 map(res => this.restExtractor.convertResultListDateToHuman(res)),
72 catchError(err => this.restExtractor.handleError(err))
73 )
74 }
75
76 private extractVideoImports (result: ResultList<VideoImport>): Observable<ResultList<VideoImport>> {
77 return this.serverService.localeObservable
78 .pipe(
79 map(translations => {
80 result.data.forEach(d =>
81 d.state.label = peertubeTranslate(d.state.label, translations)
82 )
83
84 return result
85 })
86 )
87 }
88}
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index 8562f8d25..0046be964 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -1,7 +1,7 @@
1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 1import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
3import { VideoUpdate } from '../../../../../shared/models/videos' 2import { VideoUpdate } from '../../../../../shared/models/videos'
4import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' 3import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
4import { Video } from '../../../../../shared/models/videos/video.model'
5 5
6export class VideoEdit implements VideoUpdate { 6export class VideoEdit implements VideoUpdate {
7 static readonly SPECIAL_SCHEDULED_PRIVACY = -1 7 static readonly SPECIAL_SCHEDULED_PRIVACY = -1
@@ -26,26 +26,26 @@ export class VideoEdit implements VideoUpdate {
26 id?: number 26 id?: number
27 scheduleUpdate?: VideoScheduleUpdate 27 scheduleUpdate?: VideoScheduleUpdate
28 28
29 constructor (videoDetails?: VideoDetails) { 29 constructor (video?: Video & { tags: string[], commentsEnabled: boolean, support: string, thumbnailUrl: string, previewUrl: string }) {
30 if (videoDetails) { 30 if (video) {
31 this.id = videoDetails.id 31 this.id = video.id
32 this.uuid = videoDetails.uuid 32 this.uuid = video.uuid
33 this.category = videoDetails.category.id 33 this.category = video.category.id
34 this.licence = videoDetails.licence.id 34 this.licence = video.licence.id
35 this.language = videoDetails.language.id 35 this.language = video.language.id
36 this.description = videoDetails.description 36 this.description = video.description
37 this.name = videoDetails.name 37 this.name = video.name
38 this.tags = videoDetails.tags 38 this.tags = video.tags
39 this.nsfw = videoDetails.nsfw 39 this.nsfw = video.nsfw
40 this.commentsEnabled = videoDetails.commentsEnabled 40 this.commentsEnabled = video.commentsEnabled
41 this.waitTranscoding = videoDetails.waitTranscoding 41 this.waitTranscoding = video.waitTranscoding
42 this.channelId = videoDetails.channel.id 42 this.channelId = video.channel.id
43 this.privacy = videoDetails.privacy.id 43 this.privacy = video.privacy.id
44 this.support = videoDetails.support 44 this.support = video.support
45 this.thumbnailUrl = videoDetails.thumbnailUrl 45 this.thumbnailUrl = video.thumbnailUrl
46 this.previewUrl = videoDetails.previewUrl 46 this.previewUrl = video.previewUrl
47 47
48 this.scheduleUpdate = videoDetails.scheduledUpdate 48 this.scheduleUpdate = video.scheduledUpdate
49 } 49 }
50 } 50 }
51 51
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html
index 4909cf3f1..c1d45ea18 100644
--- a/client/src/app/shared/video/video-thumbnail.component.html
+++ b/client/src/app/shared/video/video-thumbnail.component.html
@@ -2,7 +2,7 @@
2 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" 2 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
3 class="video-thumbnail" 3 class="video-thumbnail"
4> 4>
5<img alt="" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> 5<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
6 6
7<div class="video-thumbnail-overlay"> 7<div class="video-thumbnail-overlay">
8 {{ video.durationLabel }} 8 {{ video.durationLabel }}
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..1575007d2 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 *ngIf="isVideoImportEnabled()" 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..69b364ddd 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,38 @@
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' 5import { ServerService } from '@app/core'
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 6
22@Component({ 7@Component({
23 selector: 'my-videos-add', 8 selector: 'my-videos-add',
24 templateUrl: './video-add.component.html', 9 templateUrl: './video-add.component.html',
25 styleUrls: [ 10 styleUrls: [ './video-add.component.scss' ]
26 './shared/video-edit.component.scss',
27 './video-add.component.scss'
28 ]
29}) 11})
30export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { 12export class VideoAddComponent implements CanComponentDeactivate {
31 @ViewChild('videofileInput') videofileInput 13 @ViewChild('videoUpload') videoUpload: VideoUploadComponent
14 @ViewChild('videoImport') videoImport: VideoImportComponent
32 15
33 // So that it can be accessed in the template 16 secondStepType: 'upload' | 'import'
34 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 17 videoName: string
35
36 isUploadingVideo = false
37 isUpdatingVideo = false
38 videoUploaded = false
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 18
54 constructor ( 19 constructor (
55 protected formValidatorService: FormValidatorService, 20 private serverService: ServerService
56 private router: Router, 21 ) {}
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 22
87 // Public by default 23 onFirstStepDone (type: 'upload' | 'import', videoName: string) {
88 this.firstStepPrivacyId = VideoPrivacy.PUBLIC 24 this.secondStepType = type
89 }) 25 this.videoName = videoName
90 }
91
92 ngOnDestroy () {
93 if (this.videoUploadObservable) {
94 this.videoUploadObservable.unsubscribe()
95 }
96 } 26 }
97 27
98 canDeactivate () { 28 canDeactivate () {
99 let text = '' 29 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
100 30 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 31
125 cancelUpload () { 32 return { canDeactivate: true }
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 } 33 }
134 34
135 uploadFirstStep () { 35 isVideoImportEnabled () {
136 const videofile = this.videofileInput.nativeElement.files[0] as File 36 return this.serverService.getConfig().import.videos.http.enabled
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
244 err => {
245 this.isUpdatingVideo = false
246 this.notificationsService.error(this.i18n('Error'), err.message)
247 console.error(err)
248 }
249 )
250 } 37 }
251} 38}
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..6b431f6f6
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-import.component.html
@@ -0,0 +1,60 @@
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 <my-help
8 helpType="custom" i18n-customHtml
9 customHtml="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> or URL that points to a raw MP4 file. 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."
10 ></my-help>
11
12 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
13 </div>
14
15 <div class="form-group">
16 <label i18n for="first-step-channel">Channel</label>
17 <div class="peertube-select-container">
18 <select id="first-step-channel" [(ngModel)]="firstStepChannelId">
19 <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
20 </select>
21 </div>
22 </div>
23
24 <div class="form-group">
25 <label i18n for="first-step-privacy">Privacy</label>
26 <div class="peertube-select-container">
27 <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
28 <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
29 </select>
30 </div>
31 </div>
32
33 <input
34 type="button" i18n-value value="Import"
35 [disabled]="!isTargetUrlValid() || isImportingVideo" (click)="importVideo()"
36 />
37 </div>
38</div>
39
40<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
41 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
42</div>
43
44<!-- Hidden because we want to load the component -->
45<form [hidden]="!hasImportedVideo" novalidate [formGroup]="form">
46 <my-video-edit
47 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
48 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
49 ></my-video-edit>
50
51 <div class="submit-container">
52 <div class="submit-button"
53 (click)="updateSecondStep()"
54 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
55 >
56 <span class="icon icon-validate"></span>
57 <input type="button" i18n-value value="Update" />
58 </div>
59 </div>
60</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..5f14efd54
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-import.component.ts
@@ -0,0 +1,164 @@
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.loadingBar.start()
101
102 this.videoImportService.importVideo(this.targetUrl, videoUpdate).subscribe(
103 res => {
104 this.loadingBar.complete()
105 this.firstStepDone.emit(res.video.name)
106 this.isImportingVideo = false
107 this.hasImportedVideo = true
108
109 this.video = new VideoEdit(Object.assign(res.video, {
110 commentsEnabled: videoUpdate.commentsEnabled,
111 support: null,
112 thumbnailUrl: null,
113 previewUrl: null
114 }))
115 this.hydrateFormFromVideo()
116 },
117
118 err => {
119 this.loadingBar.complete()
120 this.isImportingVideo = false
121 this.notificationsService.error(this.i18n('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.loadingBar.start()
134 this.isUpdatingVideo = true
135
136 // Update the video
137 this.videoService.updateVideo(this.video)
138 .pipe(
139 // Then update captions
140 switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions))
141 )
142 .subscribe(
143 () => {
144 this.isUpdatingVideo = false
145 this.loadingBar.complete()
146 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.'))
147
148 this.router.navigate([ '/my-account', 'video-imports' ])
149 },
150
151 err => {
152 this.loadingBar.complete()
153 this.isUpdatingVideo = false
154 this.notificationsService.error(this.i18n('Error'), err.message)
155 console.error(err)
156 }
157 )
158
159 }
160
161 private hydrateFormFromVideo () {
162 this.form.patchValue(this.video.toFormPatch())
163 }
164}
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..c5e9c1592
--- /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 to 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}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index f39b5a94a..5a132112d 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -8,6 +8,10 @@
8 </div> 8 </div>
9 </div> 9 </div>
10 10
11 <div i18n class="alert alert-warning" *ngIf="isVideoToImport()">
12 The video is being imported, it will be available when the import is finished.
13 </div>
14
11 <div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()"> 15 <div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()">
12 The video is being transcoded, it may not work properly. 16 The video is being transcoded, it may not work properly.
13 </div> 17 </div>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index afbb0c596..04bcc6cd1 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -10,7 +10,7 @@ import { forkJoin, Subscription } from 'rxjs'
10import * as videojs from 'video.js' 10import * as videojs from 'video.js'
11import 'videojs-hotkeys' 11import 'videojs-hotkeys'
12import * as WebTorrent from 'webtorrent' 12import * as WebTorrent from 'webtorrent'
13import { ResultList, UserVideoRateType, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared' 13import { UserVideoRateType, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared'
14import '../../../assets/player/peertube-videojs-plugin' 14import '../../../assets/player/peertube-videojs-plugin'
15import { AuthService, ConfirmService } from '../../core' 15import { AuthService, ConfirmService } from '../../core'
16import { RestExtractor, VideoBlacklistService } from '../../shared' 16import { RestExtractor, VideoBlacklistService } from '../../shared'
@@ -28,7 +28,6 @@ import { environment } from '../../../environments/environment'
28import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 28import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
29import { VideoCaptionService } from '@app/shared/video-caption' 29import { VideoCaptionService } from '@app/shared/video-caption'
30import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model' 30import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model'
31import { VideoJSCaption } from '../../../assets/player/peertube-videojs-typings'
32 31
33@Component({ 32@Component({
34 selector: 'my-video-watch', 33 selector: 'my-video-watch',
@@ -290,6 +289,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
290 return this.video && this.video.state.id === VideoState.TO_TRANSCODE 289 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
291 } 290 }
292 291
292 isVideoToImport () {
293 return this.video && this.video.state.id === VideoState.TO_IMPORT
294 }
295
293 hasVideoScheduledPublication () { 296 hasVideoScheduledPublication () {
294 return this.video && this.video.scheduledUpdate !== undefined 297 return this.video && this.video.scheduledUpdate !== undefined
295 } 298 }
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts
index 14eeba777..3ef16fdb9 100644
--- a/client/src/app/videos/shared/markdown.service.ts
+++ b/client/src/app/videos/shared/markdown.service.ts
@@ -72,5 +72,6 @@ export class MarkdownService {
72 72
73 private avoidTruncatedLinks (html: string) { 73 private avoidTruncatedLinks (html: string) {
74 return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...') 74 return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
75 .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
75 } 76 }
76} 77}