diff options
author | Chocobozzz <me@florianbigard.com> | 2022-03-22 16:58:49 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-03-22 17:24:32 +0100 |
commit | 92e66e04f7f51d37b465cff442ce47f6d6d7cadd (patch) | |
tree | 4475c5c601c0f6673ca56afba5b7f70a4fae4ec3 /client/src/app/+video-studio | |
parent | 1808a1f8e4b7b102823492a2007a46929aebf189 (diff) | |
download | PeerTube-92e66e04f7f51d37b465cff442ce47f6d6d7cadd.tar.gz PeerTube-92e66e04f7f51d37b465cff442ce47f6d6d7cadd.tar.zst PeerTube-92e66e04f7f51d37b465cff442ce47f6d6d7cadd.zip |
Rename studio to editor
Diffstat (limited to 'client/src/app/+video-studio')
10 files changed, 474 insertions, 0 deletions
diff --git a/client/src/app/+video-studio/edit/index.ts b/client/src/app/+video-studio/edit/index.ts new file mode 100644 index 000000000..ff1d77fc0 --- /dev/null +++ b/client/src/app/+video-studio/edit/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-studio-edit.component' | ||
2 | export * from './video-studio-edit.resolver' | ||
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.component.html b/client/src/app/+video-studio/edit/video-studio-edit.component.html new file mode 100644 index 000000000..a9f34811f --- /dev/null +++ b/client/src/app/+video-studio/edit/video-studio-edit.component.html | |||
@@ -0,0 +1,88 @@ | |||
1 | <div class="margin-content"> | ||
2 | <h1 class="title-page title-page-single" i18n>Studio for {{ video.name }}</h1> | ||
3 | |||
4 | <div class="columns"> | ||
5 | <form role="form" [formGroup]="form"> | ||
6 | |||
7 | <div class="section cut" formGroupName="cut"> | ||
8 | <h2 i18n>CUT VIDEO</h2> | ||
9 | |||
10 | <div i18n class="description">Set a new start/end.</div> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label i18n for="cutStart">New start</label> | ||
14 | <my-timestamp-input inputName="cutStart" [disableBorder]="false" [maxTimestamp]="video.duration" formControlName="start"></my-timestamp-input> | ||
15 | </div> | ||
16 | |||
17 | <div class="form-group"> | ||
18 | <label i18n for="cutEnd">New end</label> | ||
19 | <my-timestamp-input inputName="cutEnd" [disableBorder]="false" [maxTimestamp]="video.duration" formControlName="end"></my-timestamp-input> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <div class="section" formGroupName="add-intro"> | ||
24 | <h2 i18n>ADD INTRO</h2> | ||
25 | |||
26 | <div i18n class="description">Concatenate a file at the beginning of the video.</div> | ||
27 | |||
28 | <div class="form-group"> | ||
29 | <my-reactive-file | ||
30 | formControlName="file" inputName="addIntroFile" i18n-inputLabel inputLabel="Select the intro video file" | ||
31 | [extensions]="videoExtensions" [displayFilename]="true" | ||
32 | [ngbTooltip]="getIntroOutroTooltip()" | ||
33 | ></my-reactive-file> | ||
34 | </div> | ||
35 | </div> | ||
36 | |||
37 | <div class="section" formGroupName="add-outro"> | ||
38 | <h2 i18n>ADD OUTRO</h2> | ||
39 | |||
40 | <div i18n class="description">Concatenate a file at the end of the video.</div> | ||
41 | |||
42 | <div class="form-group"> | ||
43 | <my-reactive-file | ||
44 | formControlName="file" inputName="addOutroFile" i18n-inputLabel inputLabel="Select the outro video file" | ||
45 | [extensions]="videoExtensions" [displayFilename]="true" | ||
46 | [ngbTooltip]="getIntroOutroTooltip()" | ||
47 | ></my-reactive-file> | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <div class="section" formGroupName="add-watermark"> | ||
52 | <h2 i18n>ADD WATERMARK</h2> | ||
53 | |||
54 | <div i18n class="description">Add a watermark image to the video.</div> | ||
55 | |||
56 | <div class="form-group"> | ||
57 | <my-reactive-file | ||
58 | formControlName="file" inputName="addWatermarkFile" i18n-inputLabel inputLabel="Select watermark image file" | ||
59 | [extensions]="imageExtensions" [displayFilename]="true" | ||
60 | [ngbTooltip]="getWatermarkTooltip()" | ||
61 | ></my-reactive-file> | ||
62 | </div> | ||
63 | </div> | ||
64 | |||
65 | <my-button | ||
66 | className="orange-button" i18n-label label="Run video edition" icon="circle-tick" | ||
67 | (click)="runEdition()" (keydown.enter)="runEdition()" | ||
68 | [disabled]="!form.valid || isRunningEdition || noEdition()" | ||
69 | ></my-button> | ||
70 | </form> | ||
71 | |||
72 | |||
73 | <div class="information"> | ||
74 | <div> | ||
75 | <label i18n>Video before edition</label> | ||
76 | <my-embed [video]="video"></my-embed> | ||
77 | </div> | ||
78 | |||
79 | <div *ngIf="!noEdition()"> | ||
80 | <label i18n>Edition tasks:</label> | ||
81 | |||
82 | <ol> | ||
83 | <li *ngFor="let task of getTasksSummary()">{{ task }}</li> | ||
84 | </ol> | ||
85 | </div> | ||
86 | </div> | ||
87 | </div> | ||
88 | </div> | ||
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.component.scss b/client/src/app/+video-studio/edit/video-studio-edit.component.scss new file mode 100644 index 000000000..43f336f59 --- /dev/null +++ b/client/src/app/+video-studio/edit/video-studio-edit.component.scss | |||
@@ -0,0 +1,76 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | .columns { | ||
5 | display: flex; | ||
6 | |||
7 | .information { | ||
8 | width: 100%; | ||
9 | margin-left: 50px; | ||
10 | |||
11 | > div { | ||
12 | margin-bottom: 30px; | ||
13 | } | ||
14 | |||
15 | @media screen and (max-width: $small-view) { | ||
16 | display: none; | ||
17 | } | ||
18 | } | ||
19 | } | ||
20 | |||
21 | h1 { | ||
22 | font-size: 20px; | ||
23 | } | ||
24 | |||
25 | h2 { | ||
26 | font-weight: $font-bold; | ||
27 | font-size: 16px; | ||
28 | color: pvar(--mainColor); | ||
29 | background-color: pvar(--mainBackgroundColor); | ||
30 | padding: 0 5px; | ||
31 | width: fit-content; | ||
32 | margin: -8px 0 0; | ||
33 | } | ||
34 | |||
35 | .section { | ||
36 | $min-width: 600px; | ||
37 | |||
38 | @include padding-left(10px); | ||
39 | |||
40 | min-width: $min-width; | ||
41 | |||
42 | margin-bottom: 50px; | ||
43 | border: 1px solid $separator-border-color; | ||
44 | border-radius: 5px; | ||
45 | width: fit-content; | ||
46 | |||
47 | .form-group, | ||
48 | .description { | ||
49 | @include margin-left(5px); | ||
50 | } | ||
51 | |||
52 | .description { | ||
53 | color: pvar(--greyForegroundColor); | ||
54 | margin-top: 5px; | ||
55 | margin-bottom: 15px; | ||
56 | } | ||
57 | |||
58 | @media screen and (max-width: $min-width) { | ||
59 | min-width: none; | ||
60 | } | ||
61 | } | ||
62 | |||
63 | my-timestamp-input { | ||
64 | display: block; | ||
65 | } | ||
66 | |||
67 | my-embed { | ||
68 | display: block; | ||
69 | max-width: 500px; | ||
70 | width: 100%; | ||
71 | } | ||
72 | |||
73 | my-reactive-file { | ||
74 | display: block; | ||
75 | width: fit-content; | ||
76 | } | ||
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.component.ts b/client/src/app/+video-studio/edit/video-studio-edit.component.ts new file mode 100644 index 000000000..392b65767 --- /dev/null +++ b/client/src/app/+video-studio/edit/video-studio-edit.component.ts | |||
@@ -0,0 +1,204 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { ConfirmService, Notifier, ServerService } from '@app/core' | ||
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
5 | import { VideoDetails } from '@app/shared/shared-main' | ||
6 | import { LoadingBarService } from '@ngx-loading-bar/core' | ||
7 | import { secondsToTime } from '@shared/core-utils' | ||
8 | import { VideoStudioTask, VideoStudioTaskCut } from '@shared/models' | ||
9 | import { VideoStudioService } from '../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-video-studio-edit', | ||
13 | templateUrl: './video-studio-edit.component.html', | ||
14 | styleUrls: [ './video-studio-edit.component.scss' ] | ||
15 | }) | ||
16 | export class VideoStudioEditComponent extends FormReactive implements OnInit { | ||
17 | isRunningEdition = false | ||
18 | |||
19 | video: VideoDetails | ||
20 | |||
21 | constructor ( | ||
22 | protected formValidatorService: FormValidatorService, | ||
23 | private serverService: ServerService, | ||
24 | private notifier: Notifier, | ||
25 | private router: Router, | ||
26 | private route: ActivatedRoute, | ||
27 | private videoStudioService: VideoStudioService, | ||
28 | private loadingBar: LoadingBarService, | ||
29 | private confirmService: ConfirmService | ||
30 | ) { | ||
31 | super() | ||
32 | } | ||
33 | |||
34 | ngOnInit () { | ||
35 | this.video = this.route.snapshot.data.video | ||
36 | |||
37 | const defaultValues = { | ||
38 | cut: { | ||
39 | start: 0, | ||
40 | end: this.video.duration | ||
41 | } | ||
42 | } | ||
43 | |||
44 | this.buildForm({ | ||
45 | cut: { | ||
46 | start: null, | ||
47 | end: null | ||
48 | }, | ||
49 | 'add-intro': { | ||
50 | file: null | ||
51 | }, | ||
52 | 'add-outro': { | ||
53 | file: null | ||
54 | }, | ||
55 | 'add-watermark': { | ||
56 | file: null | ||
57 | } | ||
58 | }, defaultValues) | ||
59 | } | ||
60 | |||
61 | get videoExtensions () { | ||
62 | return this.serverService.getHTMLConfig().video.file.extensions | ||
63 | } | ||
64 | |||
65 | get imageExtensions () { | ||
66 | return this.serverService.getHTMLConfig().video.image.extensions | ||
67 | } | ||
68 | |||
69 | async runEdition () { | ||
70 | if (this.isRunningEdition) return | ||
71 | |||
72 | const title = $localize`Are you sure you want to edit "${this.video.name}"?` | ||
73 | const listHTML = this.getTasksSummary().map(t => `<li>${t}</li>`).join('') | ||
74 | |||
75 | // eslint-disable-next-line max-len | ||
76 | const confirmHTML = $localize`The current video will be overwritten by this edited video and <strong>you won't be able to recover it</strong>.<br /><br />` + | ||
77 | $localize`As a reminder, the following tasks will be executed: <ol>${listHTML}</ol>` | ||
78 | |||
79 | if (await this.confirmService.confirm(confirmHTML, title) !== true) return | ||
80 | |||
81 | this.isRunningEdition = true | ||
82 | |||
83 | const tasks = this.buildTasks() | ||
84 | |||
85 | this.loadingBar.useRef().start() | ||
86 | |||
87 | return this.videoStudioService.editVideo(this.video.uuid, tasks) | ||
88 | .subscribe({ | ||
89 | next: () => { | ||
90 | this.notifier.success($localize`Edition tasks created.`) | ||
91 | |||
92 | // Don't redirect to old video version watch page that could be confusing for users | ||
93 | this.router.navigateByUrl('/my-library/videos') | ||
94 | }, | ||
95 | |||
96 | error: err => { | ||
97 | this.loadingBar.useRef().complete() | ||
98 | this.isRunningEdition = false | ||
99 | this.notifier.error(err.message) | ||
100 | console.error(err) | ||
101 | } | ||
102 | }) | ||
103 | } | ||
104 | |||
105 | getIntroOutroTooltip () { | ||
106 | return $localize`(extensions: ${this.videoExtensions.join(', ')})` | ||
107 | } | ||
108 | |||
109 | getWatermarkTooltip () { | ||
110 | return $localize`(extensions: ${this.imageExtensions.join(', ')})` | ||
111 | } | ||
112 | |||
113 | noEdition () { | ||
114 | return this.buildTasks().length === 0 | ||
115 | } | ||
116 | |||
117 | getTasksSummary () { | ||
118 | const tasks = this.buildTasks() | ||
119 | |||
120 | return tasks.map(t => { | ||
121 | if (t.name === 'add-intro') { | ||
122 | return $localize`"${this.getFilename(t.options.file)}" will be added at the beginning of the video` | ||
123 | } | ||
124 | |||
125 | if (t.name === 'add-outro') { | ||
126 | return $localize`"${this.getFilename(t.options.file)}" will be added at the end of the video` | ||
127 | } | ||
128 | |||
129 | if (t.name === 'add-watermark') { | ||
130 | return $localize`"${this.getFilename(t.options.file)}" image watermark will be added to the video` | ||
131 | } | ||
132 | |||
133 | if (t.name === 'cut') { | ||
134 | const { start, end } = t.options | ||
135 | |||
136 | if (start !== undefined && end !== undefined) { | ||
137 | return $localize`Video will begin at ${secondsToTime(start)} and stop at ${secondsToTime(end)}` | ||
138 | } | ||
139 | |||
140 | if (start !== undefined) { | ||
141 | return $localize`Video will begin at ${secondsToTime(start)}` | ||
142 | } | ||
143 | |||
144 | if (end !== undefined) { | ||
145 | return $localize`Video will stop at ${secondsToTime(end)}` | ||
146 | } | ||
147 | } | ||
148 | |||
149 | return '' | ||
150 | }) | ||
151 | } | ||
152 | |||
153 | private getFilename (obj: any) { | ||
154 | return obj.name | ||
155 | } | ||
156 | |||
157 | private buildTasks () { | ||
158 | const tasks: VideoStudioTask[] = [] | ||
159 | const value = this.form.value | ||
160 | |||
161 | const cut = value['cut'] | ||
162 | if (cut['start'] !== 0 || cut['end'] !== this.video.duration) { | ||
163 | |||
164 | const options: VideoStudioTaskCut['options'] = {} | ||
165 | if (cut['start'] !== 0) options.start = cut['start'] | ||
166 | if (cut['end'] !== this.video.duration) options.end = cut['end'] | ||
167 | |||
168 | tasks.push({ | ||
169 | name: 'cut', | ||
170 | options | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | if (value['add-intro']?.['file']) { | ||
175 | tasks.push({ | ||
176 | name: 'add-intro', | ||
177 | options: { | ||
178 | file: value['add-intro']['file'] | ||
179 | } | ||
180 | }) | ||
181 | } | ||
182 | |||
183 | if (value['add-outro']?.['file']) { | ||
184 | tasks.push({ | ||
185 | name: 'add-outro', | ||
186 | options: { | ||
187 | file: value['add-outro']['file'] | ||
188 | } | ||
189 | }) | ||
190 | } | ||
191 | |||
192 | if (value['add-watermark']?.['file']) { | ||
193 | tasks.push({ | ||
194 | name: 'add-watermark', | ||
195 | options: { | ||
196 | file: value['add-watermark']['file'] | ||
197 | } | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | return tasks | ||
202 | } | ||
203 | |||
204 | } | ||
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.resolver.ts b/client/src/app/+video-studio/edit/video-studio-edit.resolver.ts new file mode 100644 index 000000000..c658be50b --- /dev/null +++ b/client/src/app/+video-studio/edit/video-studio-edit.resolver.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | |||
2 | import { Injectable } from '@angular/core' | ||
3 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | ||
4 | import { VideoService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Injectable() | ||
7 | export class VideoStudioEditResolver implements Resolve<any> { | ||
8 | constructor ( | ||
9 | private videoService: VideoService | ||
10 | ) { | ||
11 | } | ||
12 | |||
13 | resolve (route: ActivatedRouteSnapshot) { | ||
14 | const videoId: string = route.params['videoId'] | ||
15 | |||
16 | return this.videoService.getVideo({ videoId }) | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/+video-studio/index.ts b/client/src/app/+video-studio/index.ts new file mode 100644 index 000000000..d50c21cdc --- /dev/null +++ b/client/src/app/+video-studio/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-studio.module' | |||
diff --git a/client/src/app/+video-studio/shared/index.ts b/client/src/app/+video-studio/shared/index.ts new file mode 100644 index 000000000..9940ac6a9 --- /dev/null +++ b/client/src/app/+video-studio/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-studio.service' | |||
diff --git a/client/src/app/+video-studio/shared/video-studio.service.ts b/client/src/app/+video-studio/shared/video-studio.service.ts new file mode 100644 index 000000000..8d8b2f0e5 --- /dev/null +++ b/client/src/app/+video-studio/shared/video-studio.service.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { catchError } from 'rxjs' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { objectToFormData } from '@app/helpers' | ||
6 | import { VideoService } from '@app/shared/shared-main' | ||
7 | import { VideoStudioCreateEdition, VideoStudioTask } from '@shared/models' | ||
8 | |||
9 | @Injectable() | ||
10 | export class VideoStudioService { | ||
11 | |||
12 | constructor ( | ||
13 | private authHttp: HttpClient, | ||
14 | private restExtractor: RestExtractor | ||
15 | ) {} | ||
16 | |||
17 | editVideo (videoId: number | string, tasks: VideoStudioTask[]) { | ||
18 | const url = VideoService.BASE_VIDEO_URL + '/' + videoId + '/studio/edit' | ||
19 | const body: VideoStudioCreateEdition = { | ||
20 | tasks | ||
21 | } | ||
22 | |||
23 | const data = objectToFormData(body) | ||
24 | |||
25 | return this.authHttp.post(url, data) | ||
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/+video-studio/video-studio-routing.module.ts b/client/src/app/+video-studio/video-studio-routing.module.ts new file mode 100644 index 000000000..bcd9b79a5 --- /dev/null +++ b/client/src/app/+video-studio/video-studio-routing.module.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | import { VideoStudioEditComponent, VideoStudioEditResolver } from './edit' | ||
4 | |||
5 | const videoStudioRoutes: Routes = [ | ||
6 | { | ||
7 | path: '', | ||
8 | children: [ | ||
9 | { | ||
10 | path: 'edit/:videoId', | ||
11 | component: VideoStudioEditComponent, | ||
12 | data: { | ||
13 | meta: { | ||
14 | title: $localize`Studio` | ||
15 | } | ||
16 | }, | ||
17 | resolve: { | ||
18 | video: VideoStudioEditResolver | ||
19 | } | ||
20 | } | ||
21 | ] | ||
22 | } | ||
23 | ] | ||
24 | |||
25 | @NgModule({ | ||
26 | imports: [ RouterModule.forChild(videoStudioRoutes) ], | ||
27 | exports: [ RouterModule ] | ||
28 | }) | ||
29 | export class VideoStudioRoutingModule {} | ||
diff --git a/client/src/app/+video-studio/video-studio.module.ts b/client/src/app/+video-studio/video-studio.module.ts new file mode 100644 index 000000000..1a8763539 --- /dev/null +++ b/client/src/app/+video-studio/video-studio.module.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedFormModule } from '@app/shared/shared-forms' | ||
3 | import { SharedMainModule } from '@app/shared/shared-main' | ||
4 | import { VideoStudioEditComponent, VideoStudioEditResolver } from './edit' | ||
5 | import { VideoStudioService } from './shared' | ||
6 | import { VideoStudioRoutingModule } from './video-studio-routing.module' | ||
7 | |||
8 | @NgModule({ | ||
9 | imports: [ | ||
10 | VideoStudioRoutingModule, | ||
11 | |||
12 | SharedMainModule, | ||
13 | SharedFormModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | VideoStudioEditComponent | ||
18 | ], | ||
19 | |||
20 | exports: [], | ||
21 | |||
22 | providers: [ | ||
23 | VideoStudioService, | ||
24 | VideoStudioEditResolver | ||
25 | ] | ||
26 | }) | ||
27 | export class VideoStudioModule { } | ||