aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/admin.module.ts22
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html107
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts14
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html500
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts83
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts90
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html1192
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts280
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html228
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.ts16
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html155
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts67
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html201
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts78
-rw-r--r--client/src/app/+admin/config/edit-custom-config/index.ts6
-rw-r--r--client/src/app/+admin/config/shared/config.service.ts14
16 files changed, 1622 insertions, 1431 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 5c0864f48..fd648a425 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -10,7 +10,16 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
10import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' 10import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
11import { AdminRoutingModule } from './admin-routing.module' 11import { AdminRoutingModule } from './admin-routing.module'
12import { AdminComponent } from './admin.component' 12import { AdminComponent } from './admin.component'
13import { ConfigComponent, EditCustomConfigComponent } from './config' 13import {
14 ConfigComponent,
15 EditAdvancedConfigurationComponent,
16 EditBasicConfigurationComponent,
17 EditConfigurationService,
18 EditCustomConfigComponent,
19 EditInstanceInformationComponent,
20 EditLiveConfigurationComponent,
21 EditVODTranscodingComponent
22} from './config'
14import { ConfigService } from './config/shared/config.service' 23import { ConfigService } from './config/shared/config.service'
15import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' 24import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
16import { FollowingListComponent } from './follows/following-list/following-list.component' 25import { FollowingListComponent } from './follows/following-list/following-list.component'
@@ -81,7 +90,13 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
81 DebugComponent, 90 DebugComponent,
82 91
83 ConfigComponent, 92 ConfigComponent,
84 EditCustomConfigComponent 93
94 EditCustomConfigComponent,
95 EditBasicConfigurationComponent,
96 EditVODTranscodingComponent,
97 EditLiveConfigurationComponent,
98 EditAdvancedConfigurationComponent,
99 EditInstanceInformationComponent
85 ], 100 ],
86 101
87 exports: [ 102 exports: [
@@ -93,7 +108,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
93 LogsService, 108 LogsService,
94 DebugService, 109 DebugService,
95 ConfigService, 110 ConfigService,
96 PluginApiService 111 PluginApiService,
112 EditConfigurationService
97 ] 113 ]
98}) 114})
99export class AdminModule { } 115export class AdminModule { }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html
new file mode 100644
index 000000000..db3036c4e
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html
@@ -0,0 +1,107 @@
1<ng-container [formGroup]="form">
2
3 <div class="form-row mt-5"> <!-- cache grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">CACHE</div>
6 <div i18n class="inner-form-description">
7 Some files are not federated, and fetched when necessary. Define their caching policies.
8 </div>
9 </div>
10
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
12
13 <ng-container formGroupName="cache">
14 <div class="form-group" formGroupName="previews">
15 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
16 <div class="number-with-unit">
17 <input
18 type="number" min="0" id="cachePreviewsSize" class="form-control"
19 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
20 >
21 <span i18n>{form.value['cache']['previews']['size'], plural, =1 {cached image} other {cached images}}</span>
22 </div>
23 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
24 </div>
25
26 <div class="form-group" formGroupName="captions">
27 <label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
28 <div class="number-with-unit">
29 <input
30 type="number" min="0" id="cacheCaptionsSize" class="form-control"
31 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
32 >
33 <span i18n>{form.value['cache']['captions']['size'], plural, =1 {cached image} other {cached images}}</span>
34 </div>
35 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
36 </div>
37 </ng-container>
38
39 </div>
40 </div>
41
42 <div class="form-row mt-4"> <!-- cache grid -->
43 <div class="form-group col-12 col-lg-4 col-xl-3">
44 <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
45 <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
46 <div i18n class="inner-form-description">
47 Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
48 </div>
49 </div>
50
51 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
52
53 <ng-container formGroupName="instance">
54 <ng-container formGroupName="customizations">
55 <div class="form-group">
56 <label i18n for="customizationJavascript">JavaScript</label>
57 <my-help>
58 <ng-template ptTemplate="customHtml">
59 <ng-container i18n>
60 Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
61 </ng-container>
62 </ng-template>
63 </my-help>
64
65 <textarea
66 id="customizationJavascript" formControlName="javascript" class="form-control"
67 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
68 ></textarea>
69
70 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
71 </div>
72
73 <div class="form-group">
74 <label for="customizationCSS">CSS</label>
75
76 <my-help>
77 <ng-template ptTemplate="customHtml">
78 <ng-container i18n>
79 Write CSS code directly. Example:<br /><br />
80<pre>
81#custom-css {{ '{' }}
82color: red;
83{{ '}' }}
84</pre>
85 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
86<pre>
87#custom-css .logged-in-email {{ '{' }}
88color: red;
89{{ '}' }}
90</pre>
91 </ng-container>
92 </ng-template>
93 </my-help>
94
95 <textarea
96 id="customizationCSS" formControlName="css" class="form-control"
97 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
98 ></textarea>
99 <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
100 </div>
101 </ng-container>
102 </ng-container>
103
104 </div>
105 </div>
106
107</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts
new file mode 100644
index 000000000..a37b7b7d5
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts
@@ -0,0 +1,14 @@
1
2import { SelectOptionsItem } from 'src/types/select-options-item.model'
3import { Component, Input } from '@angular/core'
4import { FormGroup } from '@angular/forms'
5
6@Component({
7 selector: 'my-edit-advanced-configuration',
8 templateUrl: './edit-advanced-configuration.component.html',
9 styleUrls: [ './edit-custom-config.component.scss' ]
10})
11export class EditAdvancedConfigurationComponent {
12 @Input() form: FormGroup
13 @Input() formErrors: any
14}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
new file mode 100644
index 000000000..ac1a11b4d
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -0,0 +1,500 @@
1<ng-container [formGroup]="form">
2 <div class="form-row mt-5"> <!-- appearance grid -->
3 <div class="form-group col-12 col-lg-4 col-xl-3">
4 <div i18n class="inner-form-title">APPEARANCE</div>
5 <div i18n class="inner-form-description">
6 Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="advanced-configuration">add slight customizations</a>.
7 </div>
8 </div>
9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
11
12 <ng-container formGroupName="theme">
13 <div class="form-group">
14 <label i18n for="themeDefault">Theme</label>
15
16 <div class="peertube-select-container">
17 <select formControlName="default" id="themeDefault" class="form-control">
18 <option i18n value="default">default</option>
19
20 <option *ngFor="let theme of getAvailableThemes()" [value]="theme">{{ theme }}</option>
21 </select>
22 </div>
23 </div>
24 </ng-container>
25
26 <div class="form-group" formGroupName="instance">
27 <label i18n for="instanceDefaultClientRoute">Landing page</label>
28 <div class="peertube-select-container">
29 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
30 <option i18n value="/videos/overview">Discover videos</option>
31
32 <optgroup i18n-label label="Trending pages">
33 <option i18n value="/videos/trending">Default trending page</option>
34 <option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
35 <option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
36 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
37 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
38 </optgroup>
39
40 <option i18n value="/videos/recently-added">Recently added videos</option>
41 <option i18n value="/videos/local">Local videos</option>
42 </select>
43 </div>
44 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
45 </div>
46
47 <div class="form-group" formGroupName="trending">
48 <ng-container formGroupName="videos">
49 <ng-container formGroupName="algorithms">
50 <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
51 <div class="peertube-select-container">
52 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
53 <option i18n value="best">Best videos</option>
54 <option i18n value="hot">Hot videos</option>
55 <option i18n value="most-viewed">Most viewed videos</option>
56 <option i18n value="most-liked">Most liked videos</option>
57 </select>
58 </div>
59 <div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
60 </ng-container>
61 </ng-container>
62 </div>
63
64 </div>
65 </div>
66
67 <div class="form-row mt-4"> <!-- broadcast grid -->
68 <div class="form-group col-12 col-lg-4 col-xl-3">
69 <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
70 <div i18n class="inner-for-description">
71 Display a message on your instance
72 </div>
73 </div>
74
75 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
76
77 <ng-container formGroupName="broadcastMessage">
78
79 <div class="form-group">
80 <my-peertube-checkbox
81 inputName="broadcastMessageEnabled" formControlName="enabled"
82 i18n-labelText labelText="Enable broadcast message"
83 ></my-peertube-checkbox>
84 </div>
85
86 <div class="form-group">
87 <my-peertube-checkbox
88 inputName="broadcastMessageDismissable" formControlName="dismissable"
89 i18n-labelText labelText="Allow users to dismiss the broadcast message "
90 ></my-peertube-checkbox>
91 </div>
92
93 <div class="form-group">
94 <label i18n for="broadcastMessageLevel">Broadcast message level</label>
95 <div class="peertube-select-container">
96 <select id="broadcastMessageLevel" formControlName="level" class="form-control">
97 <option value="info">info</option>
98 <option value="warning">warning</option>
99 <option value="error">error</option>
100 </select>
101 </div>
102 <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
103 </div>
104
105 <div class="form-group">
106 <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
107 <my-markdown-textarea
108 name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
109 [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
110 ></my-markdown-textarea>
111 <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
112 </div>
113
114 </ng-container>
115
116 </div>
117 </div>
118
119 <div class="form-row mt-4"> <!-- new users grid -->
120 <div class="form-group col-12 col-lg-4 col-xl-3">
121 <div i18n class="inner-form-title">NEW USERS</div>
122 <div i18n class="inner-for-description">
123 Manage <a routerLink="/admin/users">users</a> to set their quota individually.
124 </div>
125 </div>
126
127 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
128
129 <ng-container formGroupName="signup">
130 <div class="form-group">
131 <my-peertube-checkbox
132 inputName="signupEnabled" formControlName="enabled"
133 i18n-labelText labelText="Enable Signup"
134 >
135 <ng-container ngProjectAs="description">
136 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
137
138 <div class="alert alert-info alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div>
139 </ng-container>
140
141 <ng-container ngProjectAs="extra">
142 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }"
143 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
144 i18n-labelText labelText="Signup requires email verification"
145 ></my-peertube-checkbox>
146
147 <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3">
148 <label i18n for="signupLimit">Signup limit</label>
149 <div class="number-with-unit">
150 <input
151 type="number" min="-1" id="signupLimit" class="form-control"
152 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
153 >
154 <span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
155 </div>
156 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
157 <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small>
158 </div>
159 </ng-container>
160 </my-peertube-checkbox>
161 </div>
162 </ng-container>
163
164 <ng-container formGroupName="user">
165 <div class="form-group">
166 <label i18n for="userVideoQuota">Default video quota per user</label>
167
168 <my-select-custom-value
169 id="userVideoQuota"
170 [items]="getVideoQuotaOptions()"
171 formControlName="videoQuota"
172 i18n-inputSuffix inputSuffix="bytes" inputType="number"
173 [clearable]="false"
174 ></my-select-custom-value>
175
176 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
177 </div>
178
179 <div class="form-group">
180 <label i18n for="userVideoQuotaDaily">Default daily upload limit per user</label>
181
182 <my-select-custom-value
183 id="userVideoQuotaDaily"
184 [items]="getVideoQuotaDailyOptions()"
185 formControlName="videoQuotaDaily"
186 i18n-inputSuffix inputSuffix="bytes" inputType="number"
187 [clearable]="false"
188 ></my-select-custom-value>
189
190 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
191 </div>
192 </ng-container>
193
194 </div>
195 </div>
196
197 <div class="form-row mt-4"> <!-- videos grid -->
198 <div class="form-group col-12 col-lg-4 col-xl-3">
199 <div i18n class="inner-form-title">VIDEOS</div>
200 </div>
201
202 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
203
204 <ng-container formGroupName="import">
205
206 <ng-container formGroupName="videos">
207
208 <div class="form-group mt-4">
209 <label i18n for="importConcurrency">Import jobs concurrency</label>
210 <span class="text-muted ml-1">
211 <span i18n>allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
212 </span>
213
214 <div class="number-with-unit">
215 <input type="number" name="importConcurrency" formControlName="concurrency" />
216 <span i18n>jobs in parallel</span>
217 </div>
218
219 <div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
220 </div>
221
222 <div class="form-group" formGroupName="http">
223 <my-peertube-checkbox
224 inputName="importVideosHttpEnabled" formControlName="enabled"
225 i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
226 ></my-peertube-checkbox>
227 </div>
228
229 <div class="form-group" formGroupName="torrent">
230 <my-peertube-checkbox
231 inputName="importVideosTorrentEnabled" formControlName="enabled"
232 i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
233 ></my-peertube-checkbox>
234 </div>
235
236 </ng-container>
237 </ng-container>
238
239 <ng-container formGroupName="autoBlacklist">
240 <ng-container formGroupName="videos">
241 <ng-container formGroupName="ofUsers">
242
243 <div class="form-group">
244 <my-peertube-checkbox
245 inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
246 i18n-labelText labelText="Block new videos automatically"
247 >
248 <ng-container ngProjectAs="description">
249 <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
250 </ng-container>
251 </my-peertube-checkbox>
252 </div>
253
254 </ng-container>
255 </ng-container>
256 </ng-container>
257
258 </div>
259 </div>
260
261 <div class="form-row mt-4"> <!-- search grid -->
262 <div class="form-group col-12 col-lg-4 col-xl-3">
263 <div i18n class="inner-form-title">SEARCH</div>
264 </div>
265
266 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
267
268 <ng-container formGroupName="search">
269 <ng-container formGroupName="remoteUri">
270
271 <div class="form-group">
272 <my-peertube-checkbox
273 inputName="searchRemoteUriUsers" formControlName="users"
274 i18n-labelText labelText="Allow users to do remote URI/handle search"
275 >
276 <ng-container ngProjectAs="description">
277 <span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
278 </ng-container>
279 </my-peertube-checkbox>
280 </div>
281
282 <div class="form-group">
283 <my-peertube-checkbox
284 inputName="searchRemoteUriAnonymous" formControlName="anonymous"
285 i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
286 >
287 <ng-container ngProjectAs="description">
288 <span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
289 </ng-container>
290 </my-peertube-checkbox>
291 </div>
292
293 </ng-container>
294
295 <ng-container formGroupName="searchIndex">
296 <div class="form-group">
297 <my-peertube-checkbox
298 inputName="searchIndexEnabled" formControlName="enabled"
299 i18n-labelText labelText="Enable global search"
300 >
301 <ng-container ngProjectAs="description">
302 <p i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</p>
303
304 <span i18n>
305 You should only use moderated search indexes in production, or <a href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
306 </span>
307 </ng-container>
308
309 <ng-container ngProjectAs="extra">
310 <div [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }">
311 <label i18n for="searchIndexUrl">Search index URL</label>
312 <input
313 type="text" id="searchIndexUrl" class="form-control"
314 formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
315 >
316 <div *ngIf="formErrors.search.searchIndex.url" class="form-error">{{ formErrors.search.searchIndex.url }}</div>
317 </div>
318
319 <div class="mt-3">
320 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
321 inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
322 i18n-labelText labelText="Disable local search in search bar"
323 ></my-peertube-checkbox>
324 </div>
325
326 <div class="mt-3">
327 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
328 inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
329 i18n-labelText labelText="Search bar uses the global search index by default"
330 >
331 <ng-container ngProjectAs="description">
332 <span i18n>Otherwise the local search stays used by default</span>
333 </ng-container>
334 </my-peertube-checkbox>
335 </div>
336
337 </ng-container>
338 </my-peertube-checkbox>
339 </div>
340
341 </ng-container>
342
343 </ng-container>
344
345 </div>
346 </div>
347
348 <div class="form-row mt-4"> <!-- federation grid -->
349 <div class="form-group col-12 col-lg-4 col-xl-3">
350 <div i18n class="inner-form-title">FEDERATION</div>
351 <div i18n class="inner-form-description">
352 Manage <a routerLink="/admin/follows">relations</a> with other instances.
353 </div>
354 </div>
355
356 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
357
358 <ng-container formGroupName="followers">
359 <ng-container formGroupName="instance">
360
361 <div class="form-group">
362 <my-peertube-checkbox
363 inputName="followersInstanceEnabled" formControlName="enabled"
364 i18n-labelText labelText="Other instances can follow yours"
365 ></my-peertube-checkbox>
366 </div>
367
368 <div class="form-group">
369 <my-peertube-checkbox
370 inputName="followersInstanceManualApproval" formControlName="manualApproval"
371 i18n-labelText labelText="Manually approve new instance followers"
372 ></my-peertube-checkbox>
373 </div>
374 </ng-container>
375 </ng-container>
376
377 <ng-container formGroupName="followings">
378 <ng-container formGroupName="instance">
379
380 <ng-container formGroupName="autoFollowBack">
381 <div class="form-group">
382 <my-peertube-checkbox
383 inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
384 i18n-labelText labelText="Automatically follow back instances"
385 >
386 <ng-container ngProjectAs="description">
387 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
388 </ng-container>
389 </my-peertube-checkbox>
390 </div>
391 </ng-container>
392
393 <ng-container formGroupName="autoFollowIndex">
394 <div class="form-group">
395 <my-peertube-checkbox
396 inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
397 i18n-labelText labelText="Automatically follow instances of a public index"
398 >
399 <ng-container ngProjectAs="description">
400 <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p>
401
402 <span i18n>
403 You should only follow moderated indexes in production, or <a href="https://framagit.org/framasoft/peertube/instances-peertube#peertube-auto-follow">host your own</a>.
404 </span>
405 </ng-container>
406
407 <ng-container ngProjectAs="extra">
408 <div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
409 <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
410 <input
411 type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
412 formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
413 >
414 <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
415 </div>
416 </ng-container>
417 </my-peertube-checkbox>
418 </div>
419
420 </ng-container>
421 </ng-container>
422 </ng-container>
423
424 </div>
425 </div>
426
427 <div class="form-row mt-4"> <!-- administrators grid -->
428 <div class="form-group col-12 col-lg-4 col-xl-3">
429 <div i18n class="inner-form-title">ADMINISTRATORS</div>
430 </div>
431
432 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
433
434 <div class="form-group" formGroupName="admin">
435 <label i18n for="adminEmail">Admin email</label>
436 <input
437 type="text" id="adminEmail" class="form-control"
438 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
439 >
440 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
441 </div>
442
443 <div class="form-group" formGroupName="contactForm">
444 <my-peertube-checkbox
445 inputName="enableContactForm" formControlName="enabled"
446 i18n-labelText labelText="Enable contact form"
447 ></my-peertube-checkbox>
448 </div>
449
450 </div>
451 </div>
452
453 <div class="form-row mt-4"> <!-- Twitter grid -->
454 <div class="form-group col-12 col-lg-4 col-xl-3">
455 <div i18n class="inner-form-title">TWITTER</div>
456 <div i18n class="inner-form-description">
457 Provide the Twitter account representing your instance to improve link previews.
458 If you don't have a Twitter account, just leave the default value.
459 </div>
460 </div>
461
462 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
463
464 <ng-container formGroupName="services">
465 <ng-container formGroupName="twitter">
466
467 <div class="form-group">
468 <label i18n for="signupLimit">Your Twitter username</label>
469
470 <input
471 type="text" id="servicesTwitterUsername" class="form-control"
472 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
473 >
474 <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
475 </div>
476
477 <div class="form-group">
478 <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
479 <ng-template ptTemplate="label">
480 <ng-container i18n>Instance allowed by Twitter</ng-container>
481 </ng-template>
482
483 <ng-template ptTemplate="help">
484 <ng-container i18n>
485 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
486 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
487 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
488 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
489 to see if you instance is allowed.
490 </ng-container>
491 </ng-template>
492 </my-peertube-checkbox>
493 </div>
494
495 </ng-container>
496 </ng-container>
497
498 </div>
499 </div>
500</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
new file mode 100644
index 000000000..9a19c2913
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -0,0 +1,83 @@
1
2import { pairwise } from 'rxjs/operators'
3import { Component, Input, OnInit } from '@angular/core'
4import { FormGroup } from '@angular/forms'
5import { ServerConfig } from '@shared/models'
6import { ConfigService } from '../shared/config.service'
7
8@Component({
9 selector: 'my-edit-basic-configuration',
10 templateUrl: './edit-basic-configuration.component.html',
11 styleUrls: [ './edit-custom-config.component.scss' ]
12})
13export class EditBasicConfigurationComponent implements OnInit {
14 @Input() form: FormGroup
15 @Input() formErrors: any
16
17 @Input() serverConfig: ServerConfig
18
19 signupAlertMessage: string
20
21 constructor (
22 private configService: ConfigService
23 ) { }
24
25 ngOnInit () {
26 this.checkSignupField()
27 }
28
29 getVideoQuotaOptions () {
30 return this.configService.videoQuotaOptions
31 }
32
33 getVideoQuotaDailyOptions () {
34 return this.configService.videoQuotaDailyOptions
35 }
36
37 getAvailableThemes () {
38 return this.serverConfig.theme.registered
39 .map(t => t.name)
40 }
41
42 doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
43 const enabled = this.form.value['trending']['videos']['algorithms']['enabled']
44 if (!Array.isArray(enabled)) return false
45
46 return !!enabled.find((e: string) => e === algorithm)
47 }
48
49 isSignupEnabled () {
50 return this.form.value['signup']['enabled'] === true
51 }
52
53 isSearchIndexEnabled () {
54 return this.form.value['search']['searchIndex']['enabled'] === true
55 }
56
57 isAutoFollowIndexEnabled () {
58 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
59 }
60
61 private checkSignupField () {
62 const signupControl = this.form.get('signup.enabled')
63
64 signupControl.valueChanges
65 .pipe(pairwise())
66 .subscribe(([ oldValue, newValue ]) => {
67 if (oldValue !== true && newValue === true) {
68 // tslint:disable:max-line-length
69 this.signupAlertMessage = $localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
70
71 this.form.patchValue({
72 autoBlacklist: {
73 videos: {
74 ofUsers: {
75 enabled: true
76 }
77 }
78 }
79 })
80 }
81 })
82 }
83}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts
new file mode 100644
index 000000000..63e0343fa
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts
@@ -0,0 +1,90 @@
1import { Injectable } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3
4export type ResolutionOption = {
5 id: string
6 label: string
7 description?: string
8}
9
10@Injectable()
11export class EditConfigurationService {
12
13 getVODResolutions () {
14 return [
15 {
16 id: '0p',
17 label: $localize`Audio-only`,
18 description: $localize`A <code>.mp4</code> that keeps the original audio track, with no video`
19 },
20 {
21 id: '240p',
22 label: $localize`240p`
23 },
24 {
25 id: '360p',
26 label: $localize`360p`
27 },
28 {
29 id: '480p',
30 label: $localize`480p`
31 },
32 {
33 id: '720p',
34 label: $localize`720p`
35 },
36 {
37 id: '1080p',
38 label: $localize`1080p`
39 },
40 {
41 id: '1440p',
42 label: $localize`1440p`
43 },
44 {
45 id: '2160p',
46 label: $localize`2160p`
47 }
48 ]
49 }
50
51 getLiveResolutions () {
52 return this.getVODResolutions().filter(r => r.id !== '0p')
53 }
54
55 isTranscodingEnabled (form: FormGroup) {
56 return form.value['transcoding']['enabled'] === true
57 }
58
59 isLiveEnabled (form: FormGroup) {
60 return form.value['live']['enabled'] === true
61 }
62
63 isLiveTranscodingEnabled (form: FormGroup) {
64 return form.value['live']['transcoding']['enabled'] === true
65 }
66
67 getTotalTranscodingThreads (form: FormGroup) {
68 const transcodingEnabled = form.value['transcoding']['enabled']
69 const transcodingThreads = form.value['transcoding']['threads']
70 const liveTranscodingEnabled = form.value['live']['transcoding']['enabled']
71 const liveTranscodingThreads = form.value['live']['transcoding']['threads']
72
73 // checks whether all enabled method are on fixed values and not on auto (= 0)
74 let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
75 noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
76
77 // count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
78 let value = 0
79 if (transcodingEnabled) value += +transcodingThreads || 1
80 if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
81
82 return {
83 value,
84 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
85 unit: value > 1
86 ? $localize`threads`
87 : $localize`thread`
88 }
89 }
90}
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 135276192..534b03517 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
@@ -7,231 +7,8 @@
7 <a ngbNavLink i18n>Instance information</a> 7 <a ngbNavLink i18n>Instance information</a>
8 8
9 <ng-template ngbNavContent> 9 <ng-template ngbNavContent>
10 10 <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
11 <ng-container formGroupName="instance"> 11 </my-edit-instance-information>
12
13 <div class="form-row mt-5"> <!-- instance grid -->
14 <div class="form-group col-12 col-lg-4 col-xl-3">
15 <div i18n class="inner-form-title">INSTANCE</div>
16 </div>
17
18 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
19
20 <div class="form-group">
21 <label i18n for="instanceName">Name</label>
22 <input
23 type="text" id="instanceName" class="form-control"
24 formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
25 >
26 <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
27 </div>
28
29 <div class="form-group">
30 <label i18n for="instanceShortDescription">Short description</label>
31 <textarea
32 id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
33 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
34 ></textarea>
35 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
36 </div>
37
38 <div class="form-group">
39 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
40 <my-markdown-textarea
41 name="instanceDescription" formControlName="description" textareaMaxWidth="500px"
42 [classes]="{ 'input-error': formErrors['instance.description'] }"
43 ></my-markdown-textarea>
44 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
45 </div>
46
47 <div class="form-group">
48 <label i18n for="instanceCategories">Main instance categories</label>
49
50 <div>
51 <my-select-checkbox
52 id="instanceCategories"
53 formControlName="categories" [availableItems]="categoryItems"
54 [selectableGroup]="false"
55 i18n-placeholder placeholder="Add a new category"
56 >
57 </my-select-checkbox>
58 </div>
59 </div>
60
61 <div class="form-group">
62 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
63
64 <div>
65 <my-select-checkbox
66 id="instanceLanguages"
67 formControlName="languages" [availableItems]="languageItems"
68 [selectableGroup]="false"
69 i18n-placeholder placeholder="Add a new language"
70 >
71 </my-select-checkbox>
72 </div>
73 </div>
74
75 </div>
76 </div>
77
78 <div class="form-row mt-4"> <!-- moderation & nsfw grid -->
79 <div class="form-group col-12 col-lg-4 col-xl-3">
80 <div i18n class="inner-form-title">MODERATION & NSFW</div>
81 <div i18n class="inner-for-description">
82 Manage <a routerLink="/admin/users">users</a> to build a moderation team.
83 </div>
84 </div>
85
86 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
87
88 <div class="form-group">
89 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
90 <ng-template ptTemplate="label">
91 <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
92 </ng-template>
93
94 <ng-template ptTemplate="help">
95 <ng-container i18n>
96 Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
97 Moreover, the NSFW checkbox on video upload will be automatically checked by default.
98 </ng-container>
99 </ng-template>
100 </my-peertube-checkbox>
101 </div>
102
103 <div class="form-group">
104 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
105
106 <my-help>
107 <ng-template ptTemplate="customHtml">
108 <ng-container i18n>
109 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
110 </ng-container>
111 </ng-template>
112 </my-help>
113
114 <div class="peertube-select-container">
115 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy" class="form-control">
116 <option i18n value="undefined" disabled>Policy for sensitive videos</option>
117 <option i18n value="do_not_list">Do not list</option>
118 <option i18n value="blur">Blur thumbnails</option>
119 <option i18n value="display">Display</option>
120 </select>
121 </div>
122 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
123 </div>
124
125 <div class="form-group">
126 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
127 <my-markdown-textarea
128 name="instanceTerms" formControlName="terms" textareaMaxWidth="500px"
129 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
130 ></my-markdown-textarea>
131 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
132 </div>
133
134 <div class="form-group">
135 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
136 <my-markdown-textarea
137 name="instanceCodeOfConduct" formControlName="codeOfConduct" textareaMaxWidth="500px"
138 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
139 ></my-markdown-textarea>
140 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
141 </div>
142
143 <div class="form-group">
144 <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
145 <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
146
147 <my-markdown-textarea
148 name="instanceModerationInformation" formControlName="moderationInformation" textareaMaxWidth="500px"
149 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
150 ></my-markdown-textarea>
151 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
152 </div>
153
154 </div>
155 </div>
156
157 <div class="form-row mt-4"> <!-- you and your instance grid -->
158 <div class="form-group col-12 col-lg-4 col-xl-3">
159 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
160 </div>
161
162 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
163
164 <div class="form-group">
165 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
166 <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
167
168 <my-markdown-textarea
169 name="instanceAdministrator" formControlName="administrator" textareaMaxWidth="500px"
170 [classes]="{ 'input-error': formErrors['instance.administrator'] }"
171 ></my-markdown-textarea>
172
173 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
174 </div>
175
176 <div class="form-group">
177 <label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
178 <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
179
180 <my-markdown-textarea
181 name="instanceCreationReason" formControlName="creationReason" textareaMaxWidth="500px"
182 [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
183 ></my-markdown-textarea>
184 <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
185 </div>
186
187 <div class="form-group">
188 <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
189 <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
190
191 <my-markdown-textarea
192 name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" textareaMaxWidth="500px"
193 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
194 ></my-markdown-textarea>
195 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
196 </div>
197
198 <div class="form-group">
199 <label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
200 <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
201
202 <my-markdown-textarea
203 name="instanceBusinessModel" formControlName="businessModel" textareaMaxWidth="500px"
204 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
205 ></my-markdown-textarea>
206 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
207 </div>
208
209 </div>
210 </div>
211
212 <div class="form-row mt-4"> <!-- other information grid -->
213 <div class="form-group col-12 col-lg-4 col-xl-3">
214 <div i18n class="inner-form-title">OTHER INFORMATION</div>
215 </div>
216
217 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
218
219 <div class="form-group">
220 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
221 <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
222
223 <my-markdown-textarea
224 name="instanceHardwareInformation" formControlName="hardwareInformation" textareaMaxWidth="500px"
225 [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
226 ></my-markdown-textarea>
227
228 <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
229 </div>
230
231 </div>
232 </div>
233
234 </ng-container>
235 </ng-template> 12 </ng-template>
236 </ng-container> 13 </ng-container>
237 14
@@ -239,502 +16,8 @@
239 <a ngbNavLink i18n>Basic configuration</a> 16 <a ngbNavLink i18n>Basic configuration</a>
240 17
241 <ng-template ngbNavContent> 18 <ng-template ngbNavContent>
242 19 <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
243 <div class="form-row mt-5"> <!-- appearance grid --> 20 </my-edit-basic-configuration>
244 <div class="form-group col-12 col-lg-4 col-xl-3">
245 <div i18n class="inner-form-title">APPEARANCE</div>
246 <div i18n class="inner-form-description">
247 Use <a routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or <a routerLink="/admin/config/edit-custom" fragment="customizations" (click)="gotoAnchor()">add slight customizations</a>.
248 </div>
249 </div>
250
251 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
252
253 <ng-container formGroupName="theme">
254 <div class="form-group">
255 <label i18n for="themeDefault">Theme</label>
256
257 <div class="peertube-select-container">
258 <select formControlName="default" id="themeDefault" class="form-control">
259 <option i18n value="default">default</option>
260
261 <option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
262 </select>
263 </div>
264 </div>
265 </ng-container>
266
267 <div class="form-group" formGroupName="instance">
268 <label i18n for="instanceDefaultClientRoute">Landing page</label>
269 <div class="peertube-select-container">
270 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
271 <option i18n value="/videos/overview">Discover videos</option>
272 <optgroup i18n-label label="Trending pages">
273 <option i18n value="/videos/trending">Default trending page</option>
274 <option i18n value="/videos/trending?alg=best" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('best')">Best videos</option>
275 <option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
276 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
277 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
278 </optgroup>
279 <option i18n value="/videos/recently-added">Recently added videos</option>
280 <option i18n value="/videos/local">Local videos</option>
281 </select>
282 </div>
283 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
284 </div>
285
286 <div class="form-group" formGroupName="trending">
287 <ng-container formGroupName="videos">
288 <ng-container formGroupName="algorithms">
289 <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
290 <div class="peertube-select-container">
291 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
292 <option i18n value="best">Best videos</option>
293 <option i18n value="hot">Hot videos</option>
294 <option i18n value="most-viewed">Most viewed videos</option>
295 <option i18n value="most-liked">Most liked videos</option>
296 </select>
297 </div>
298 <div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
299 </ng-container>
300 </ng-container>
301 </div>
302
303 </div>
304 </div>
305
306 <div class="form-row mt-4"> <!-- broadcast grid -->
307 <div class="form-group col-12 col-lg-4 col-xl-3">
308 <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
309 <div i18n class="inner-for-description">
310 Display a message on your instance
311 </div>
312 </div>
313
314 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
315
316 <ng-container formGroupName="broadcastMessage">
317
318 <div class="form-group">
319 <my-peertube-checkbox
320 inputName="broadcastMessageEnabled" formControlName="enabled"
321 i18n-labelText labelText="Enable broadcast message"
322 ></my-peertube-checkbox>
323 </div>
324
325 <div class="form-group">
326 <my-peertube-checkbox
327 inputName="broadcastMessageDismissable" formControlName="dismissable"
328 i18n-labelText labelText="Allow users to dismiss the broadcast message "
329 ></my-peertube-checkbox>
330 </div>
331
332 <div class="form-group">
333 <label i18n for="broadcastMessageLevel">Broadcast message level</label>
334 <div class="peertube-select-container">
335 <select id="broadcastMessageLevel" formControlName="level" class="form-control">
336 <option value="info">info</option>
337 <option value="warning">warning</option>
338 <option value="error">error</option>
339 </select>
340 </div>
341 <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
342 </div>
343
344 <div class="form-group">
345 <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
346 <my-markdown-textarea
347 name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
348 [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
349 ></my-markdown-textarea>
350 <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
351 </div>
352
353 </ng-container>
354
355 </div>
356 </div>
357
358 <div class="form-row mt-4"> <!-- new users grid -->
359 <div class="form-group col-12 col-lg-4 col-xl-3">
360 <div i18n class="inner-form-title">NEW USERS</div>
361 <div i18n class="inner-for-description">
362 Manage <a routerLink="/admin/users">users</a> to set their quota individually.
363 </div>
364 </div>
365
366 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
367
368 <ng-container formGroupName="signup">
369 <div class="form-group">
370 <my-peertube-checkbox
371 inputName="signupEnabled" formControlName="enabled"
372 i18n-labelText labelText="Enable Signup"
373 >
374 <ng-container ngProjectAs="description">
375 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
376
377 <div class="alert alert-info alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div>
378 </ng-container>
379 <ng-container ngProjectAs="extra">
380 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }"
381 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
382 i18n-labelText labelText="Signup requires email verification"
383 ></my-peertube-checkbox>
384
385 <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3">
386 <label i18n for="signupLimit">Signup limit</label>
387 <div class="number-with-unit">
388 <input
389 type="number" min="-1" id="signupLimit" class="form-control"
390 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
391 >
392 <span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
393 </div>
394 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
395 <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small>
396 </div>
397 </ng-container>
398 </my-peertube-checkbox>
399 </div>
400 </ng-container>
401
402 <ng-container formGroupName="user">
403 <div class="form-group">
404 <label i18n for="userVideoQuota">Default video quota per user</label>
405
406 <my-select-custom-value
407 id="userVideoQuota"
408 [items]="videoQuotaOptions"
409 formControlName="videoQuota"
410 i18n-inputSuffix inputSuffix="bytes" inputType="number"
411 [clearable]="false"
412 ></my-select-custom-value>
413
414 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
415 </div>
416
417 <div class="form-group">
418 <label i18n for="userVideoQuotaDaily">Default daily upload limit per user</label>
419
420 <my-select-custom-value
421 id="userVideoQuotaDaily"
422 [items]="videoQuotaDailyOptions"
423 formControlName="videoQuotaDaily"
424 i18n-inputSuffix inputSuffix="bytes" inputType="number"
425 [clearable]="false"
426 ></my-select-custom-value>
427
428 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
429 </div>
430 </ng-container>
431
432 </div>
433 </div>
434
435 <div class="form-row mt-4"> <!-- videos grid -->
436 <div class="form-group col-12 col-lg-4 col-xl-3">
437 <div i18n class="inner-form-title">VIDEOS</div>
438 </div>
439
440 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
441
442 <ng-container formGroupName="import">
443
444 <ng-container formGroupName="videos">
445
446 <div class="form-group mt-4">
447 <label i18n for="importConcurrency">Import jobs concurrency</label>
448 <span class="text-muted ml-1">
449 <span i18n>allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
450 </span>
451
452 <div class="number-with-unit">
453 <input type="number" name="importConcurrency" formControlName="concurrency" />
454 <span i18n>jobs in parallel</span>
455 </div>
456
457 <div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
458 </div>
459
460 <div class="form-group" formGroupName="http">
461 <my-peertube-checkbox
462 inputName="importVideosHttpEnabled" formControlName="enabled"
463 i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
464 ></my-peertube-checkbox>
465 </div>
466
467 <div class="form-group" formGroupName="torrent">
468 <my-peertube-checkbox
469 inputName="importVideosTorrentEnabled" formControlName="enabled"
470 i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
471 ></my-peertube-checkbox>
472 </div>
473
474 </ng-container>
475 </ng-container>
476
477 <ng-container formGroupName="autoBlacklist">
478 <ng-container formGroupName="videos">
479 <ng-container formGroupName="ofUsers">
480
481 <div class="form-group">
482 <my-peertube-checkbox
483 inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
484 i18n-labelText labelText="Block new videos automatically"
485 >
486 <ng-container ngProjectAs="description">
487 <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
488 </ng-container>
489 </my-peertube-checkbox>
490 </div>
491
492 </ng-container>
493 </ng-container>
494 </ng-container>
495
496 </div>
497 </div>
498
499 <div class="form-row mt-4"> <!-- search grid -->
500 <div class="form-group col-12 col-lg-4 col-xl-3">
501 <div i18n class="inner-form-title">SEARCH</div>
502 </div>
503
504 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
505
506 <ng-container formGroupName="search">
507 <ng-container formGroupName="remoteUri">
508
509 <div class="form-group">
510 <my-peertube-checkbox
511 inputName="searchRemoteUriUsers" formControlName="users"
512 i18n-labelText labelText="Allow users to do remote URI/handle search"
513 >
514 <ng-container ngProjectAs="description">
515 <span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
516 </ng-container>
517 </my-peertube-checkbox>
518 </div>
519
520 <div class="form-group">
521 <my-peertube-checkbox
522 inputName="searchRemoteUriAnonymous" formControlName="anonymous"
523 i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
524 >
525 <ng-container ngProjectAs="description">
526 <span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
527 </ng-container>
528 </my-peertube-checkbox>
529 </div>
530
531 </ng-container>
532
533 <ng-container formGroupName="searchIndex">
534 <div class="form-group">
535 <my-peertube-checkbox
536 inputName="searchIndexEnabled" formControlName="enabled"
537 i18n-labelText labelText="Enable global search"
538 >
539 <ng-container ngProjectAs="description">
540 <p i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</p>
541
542 <span i18n>
543 You should only use moderated search indexes in production, or <a href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
544 </span>
545 </ng-container>
546
547 <ng-container ngProjectAs="extra">
548 <div [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }">
549 <label i18n for="searchIndexUrl">Search index URL</label>
550 <input
551 type="text" id="searchIndexUrl" class="form-control"
552 formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
553 >
554 <div *ngIf="formErrors.search.searchIndex.url" class="form-error">{{ formErrors.search.searchIndex.url }}</div>
555 </div>
556
557 <div class="mt-3">
558 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
559 inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
560 i18n-labelText labelText="Disable local search in search bar"
561 ></my-peertube-checkbox>
562 </div>
563
564 <div class="mt-3">
565 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
566 inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
567 i18n-labelText labelText="Search bar uses the global search index by default"
568 >
569 <ng-container ngProjectAs="description">
570 <span i18n>Otherwise the local search stays used by default</span>
571 </ng-container>
572 </my-peertube-checkbox>
573 </div>
574
575 </ng-container>
576 </my-peertube-checkbox>
577 </div>
578
579 </ng-container>
580
581 </ng-container>
582
583 </div>
584 </div>
585
586 <div class="form-row mt-4"> <!-- federation grid -->
587 <div class="form-group col-12 col-lg-4 col-xl-3">
588 <div i18n class="inner-form-title">FEDERATION</div>
589 <div i18n class="inner-form-description">
590 Manage <a routerLink="/admin/follows">relations</a> with other instances.
591 </div>
592 </div>
593
594 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
595
596 <ng-container formGroupName="followers">
597 <ng-container formGroupName="instance">
598
599 <div class="form-group">
600 <my-peertube-checkbox
601 inputName="followersInstanceEnabled" formControlName="enabled"
602 i18n-labelText labelText="Other instances can follow yours"
603 ></my-peertube-checkbox>
604 </div>
605
606 <div class="form-group">
607 <my-peertube-checkbox
608 inputName="followersInstanceManualApproval" formControlName="manualApproval"
609 i18n-labelText labelText="Manually approve new instance followers"
610 ></my-peertube-checkbox>
611 </div>
612 </ng-container>
613 </ng-container>
614
615 <ng-container formGroupName="followings">
616 <ng-container formGroupName="instance">
617
618 <ng-container formGroupName="autoFollowBack">
619 <div class="form-group">
620 <my-peertube-checkbox
621 inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
622 i18n-labelText labelText="Automatically follow back instances"
623 >
624 <ng-container ngProjectAs="description">
625 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
626 </ng-container>
627 </my-peertube-checkbox>
628 </div>
629 </ng-container>
630
631 <ng-container formGroupName="autoFollowIndex">
632 <div class="form-group">
633 <my-peertube-checkbox
634 inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
635 i18n-labelText labelText="Automatically follow instances of a public index"
636 >
637 <ng-container ngProjectAs="description">
638 <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p>
639
640 <span i18n>
641 You should only follow moderated indexes in production, or <a href="https://framagit.org/framasoft/peertube/instances-peertube#peertube-auto-follow">host your own</a>.
642 </span>
643 </ng-container>
644
645 <ng-container ngProjectAs="extra">
646 <div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
647 <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
648 <input
649 type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
650 formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
651 >
652 <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
653 </div>
654 </ng-container>
655 </my-peertube-checkbox>
656 </div>
657
658 </ng-container>
659 </ng-container>
660 </ng-container>
661
662 </div>
663 </div>
664
665 <div class="form-row mt-4"> <!-- administrators grid -->
666 <div class="form-group col-12 col-lg-4 col-xl-3">
667 <div i18n class="inner-form-title">ADMINISTRATORS</div>
668 </div>
669
670 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
671
672 <div class="form-group" formGroupName="admin">
673 <label i18n for="adminEmail">Admin email</label>
674 <input
675 type="text" id="adminEmail" class="form-control"
676 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
677 >
678 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
679 </div>
680
681 <div class="form-group" formGroupName="contactForm">
682 <my-peertube-checkbox
683 inputName="enableContactForm" formControlName="enabled"
684 i18n-labelText labelText="Enable contact form"
685 ></my-peertube-checkbox>
686 </div>
687
688 </div>
689 </div>
690
691 <div class="form-row mt-4"> <!-- Twitter grid -->
692 <div class="form-group col-12 col-lg-4 col-xl-3">
693 <div i18n class="inner-form-title">TWITTER</div>
694 <div i18n class="inner-form-description">
695 Provide the Twitter account representing your instance to improve link previews.
696 If you don't have a Twitter account, just leave the default value.
697 </div>
698 </div>
699
700 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
701
702 <ng-container formGroupName="services">
703 <ng-container formGroupName="twitter">
704
705 <div class="form-group">
706 <label i18n for="signupLimit">Your Twitter username</label>
707
708 <input
709 type="text" id="servicesTwitterUsername" class="form-control"
710 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
711 >
712 <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
713 </div>
714
715 <div class="form-group">
716 <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
717 <ng-template ptTemplate="label">
718 <ng-container i18n>Instance allowed by Twitter</ng-container>
719 </ng-template>
720
721 <ng-template ptTemplate="help">
722 <ng-container i18n>
723 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
724 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
725 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
726 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
727 to see if you instance is allowed.
728 </ng-container>
729 </ng-template>
730 </my-peertube-checkbox>
731 </div>
732
733 </ng-container>
734 </ng-container>
735
736 </div>
737 </div>
738 </ng-template> 21 </ng-template>
739 </ng-container> 22 </ng-container>
740 23
@@ -742,208 +25,8 @@
742 <a ngbNavLink i18n>VOD Transcoding</a> 25 <a ngbNavLink i18n>VOD Transcoding</a>
743 26
744 <ng-template ngbNavContent> 27 <ng-template ngbNavContent>
745 28 <my-edit-vod-transcoding [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
746 <div class="form-row mt-4"> <!-- transcoding grid --> 29 </my-edit-vod-transcoding>
747 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
748 <div class="form-group form-group-right col-12 col-lg-8">
749
750 <div class="callout callout-info">
751 <span i18n>
752 Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
753 </span>
754 <span i18n>
755 However, you may want to read our guidelines before tweaking the following values.
756 </span>
757
758 <div class="callout-container">
759 <a class="callout-link" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/#/admin-configuration?id=transcoding" i18n>Read guidelines</a>
760 </div>
761 </div>
762
763
764 </div>
765 </div>
766
767 <div class="form-row mt-2"> <!-- transcoding grid -->
768 <div class="form-group col-12 col-lg-4 col-xl-3">
769 <div i18n class="inner-form-title">TRANSCODING</div>
770 <div i18n class="inner-form-description">
771 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
772 resources, this is a critical part of PeerTube, so tread carefully.
773 </div>
774 </div>
775
776 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
777
778 <ng-container formGroupName="transcoding">
779
780 <div class="form-group mb-0 col-12 col-xl-11">
781 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
782 <ng-template ptTemplate="label">
783 <ng-container i18n>Transcoding enabled</ng-container>
784 </ng-template>
785
786 <ng-container ngProjectAs="extra">
787
788 <div class="callout callout-light pt-2 pb-0">
789 <label i18n>Input formats</label>
790
791 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
792 <my-peertube-checkbox
793 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
794 i18n-labelText labelText="Allow additional extensions"
795 >
796 <ng-container ngProjectAs="description">
797 <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, .m2ts, .mxf, or .nut videos.</span>
798 </ng-container>
799 </my-peertube-checkbox>
800 </div>
801
802 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
803 <my-peertube-checkbox
804 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
805 i18n-labelText labelText="Allow audio files upload"
806 >
807 <ng-container ngProjectAs="description">
808 <div i18n>Allows users to upload .mp3, .ogg, .wma, .flac, .aac, or .ac3 audio files.</div>
809 <div i18n>The file will be merged in a still image video with the preview file on upload.</div>
810 </ng-container>
811 </my-peertube-checkbox>
812 </div>
813 </div>
814
815 <div class="callout callout-light pt-2 mt-2 pb-0">
816 <label i18n>Output formats</label>
817
818 <ng-container formGroupName="webtorrent">
819 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
820 <my-peertube-checkbox
821 inputName="transcodingWebTorrentEnabled" formControlName="enabled"
822 i18n-labelText labelText="WebTorrent enabled"
823 >
824 <ng-template ptTemplate="help">
825 <ng-container i18n>
826 <p>If you also enabled HLS support, it will multiply videos storage by 2</p>
827
828 <br />
829
830 <strong>If disabled, breaks federation with PeerTube instances < 2.1</strong>
831 </ng-container>
832 </ng-template>
833 </my-peertube-checkbox>
834 </div>
835 </ng-container>
836
837 <ng-container formGroupName="hls">
838 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
839 <my-peertube-checkbox
840 inputName="transcodingHlsEnabled" formControlName="enabled"
841 i18n-labelText labelText="HLS with P2P support enabled"
842 [recommended]="true"
843 >
844 <ng-template ptTemplate="help">
845 <ng-container i18n>
846 <strong>Requires ffmpeg >= 4.1</strong>
847
848 <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p>
849 <ul>
850 <li>Resolution change is smoother</li>
851 <li>Faster playback especially with long videos</li>
852 <li>More stable playback (less bugs/infinite loading)</li>
853 </ul>
854
855 <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p>
856 </ng-container>
857 </ng-template>
858 </my-peertube-checkbox>
859 </div>
860 </ng-container>
861
862 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
863 <label i18n>Resolutions to generate per enabled format</label>
864
865 <div class="ml-2 mt-2 d-flex flex-column">
866 <ng-container formGroupName="resolutions">
867 <div class="form-group" *ngFor="let resolution of resolutions">
868 <my-peertube-checkbox
869 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
870 labelText="{{ resolution.label }}"
871 >
872 <ng-template *ngIf="resolution.description" ptTemplate="help">
873 <div [innerHTML]="resolution.description"></div>
874 </ng-template>
875 </my-peertube-checkbox>
876 </div>
877
878 <span class="mb-2 text-muted" i18n>
879 The original file resolution will be the default target if no option is selected.
880 </span>
881 </ng-container>
882 </div>
883 </div>
884 </div>
885
886 </ng-container>
887 </my-peertube-checkbox>
888 </div>
889
890 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
891 <label i18n for="transcodingThreads">Transcoding threads</label>
892 <span class="text-muted ml-1">
893 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
894 <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
895 </span>
896
897 <my-select-custom-value
898 id="transcodingThreads"
899 [items]="transcodingThreadOptions"
900 formControlName="threads"
901 [clearable]="false"
902 ></my-select-custom-value>
903
904 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
905 </div>
906
907 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
908 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
909 <span class="text-muted ml-1">
910 <span i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart.</span>
911 </span>
912
913 <div class="number-with-unit">
914 <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
915 <span i18n>jobs in parallel</span>
916 </div>
917
918 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
919 </div>
920
921 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
922 <label i18n for="transcodingProfile">Transcoding profile</label>
923 <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
924
925 <my-select-options
926 id="transcodingProfile"
927 formControlName="profile"
928 [items]="getAvailableTranscodingProfile('vod')"
929 [clearable]="false"
930 >
931 <ng-template ng-option-tmp let-item="item" let-index="index">
932 {{ item }}
933 <ng-container *ngIf="item === 'default'">
934 <br>
935 <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
936 </ng-container>
937 </ng-template>
938 </my-select-options>
939 <div *ngIf="formErrors.transcoding.profile" class="form-error">{{ formErrors.transcoding.profile }}</div>
940 </div>
941
942 </ng-container>
943
944 </div>
945 </div>
946
947 </ng-template> 30 </ng-template>
948 </ng-container> 31 </ng-container>
949 32
@@ -951,160 +34,8 @@
951 <a ngbNavLink i18n>Live streaming</a> 34 <a ngbNavLink i18n>Live streaming</a>
952 35
953 <ng-template ngbNavContent> 36 <ng-template ngbNavContent>
954 37 <my-edit-live-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
955 <div class="form-row mt-5"> 38 </my-edit-live-configuration>
956 <div class="form-group col-12 col-lg-4 col-xl-3">
957 <div i18n class="inner-form-title">LIVE</div>
958 <div i18n class="inner-form-description">
959 Enable users of your instance to stream live.
960 </div>
961 </div>
962
963 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
964
965 <ng-container formGroupName="live">
966
967 <div class="form-group">
968 <my-peertube-checkbox inputName="liveEnabled" formControlName="enabled">
969 <ng-template ptTemplate="label">
970 <ng-container i18n>Allow live streaming</ng-container>
971 </ng-template>
972
973 <ng-container ngProjectAs="description">
974 <div i18n>⚠️ Enabling live streaming requires trust in your users and extra moderation work</div>
975 <div i18n>If enabled, your server needs to accept incoming TCP traffic on port {{ liveRTMPPort }}</div>
976 </ng-container>
977
978 <ng-container ngProjectAs="extra">
979
980 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
981 <my-peertube-checkbox
982 inputName="liveAllowReplay" formControlName="allowReplay"
983 i18n-labelText labelText="Allow your users to automatically publish a replay of their live"
984 >
985 <ng-container ngProjectAs="description" i18n>
986 If the user quota is reached, PeerTube will automatically terminate the live streaming
987 </ng-container>
988 </my-peertube-checkbox>
989 </div>
990
991 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
992 <label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span></label>
993 <div class="number-with-unit">
994 <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
995 <span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
996 </div>
997 </div>
998
999 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
1000 <label i18n for="liveMaxUserLives">Max simultaneous lives created per user <span class="text-muted">(-1 for "unlimited")</span></label>
1001 <div class="number-with-unit">
1002 <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
1003 <span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
1004 </div>
1005 </div>
1006
1007 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
1008 <label i18n for="liveMaxDuration">Max live duration</label>
1009
1010 <my-select-options
1011 labelForId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"
1012 bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
1013 ></my-select-options>
1014 </div>
1015
1016 </ng-container>
1017 </my-peertube-checkbox>
1018 </div>
1019 </ng-container>
1020 </div>
1021 </div>
1022
1023 <div class="form-row"> <!-- transcoding live streams grid -->
1024 <div class="form-group col-12 col-lg-4 col-xl-3">
1025 <div i18n class="inner-form-title">TRANSCODING</div>
1026 <div i18n class="inner-form-description">
1027 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some.
1028 </div>
1029 </div>
1030
1031 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
1032
1033 <ng-container formGroupName="live">
1034 <ng-container formGroupName="transcoding">
1035
1036 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
1037 <my-peertube-checkbox
1038 inputName="liveTranscodingEnabled" formControlName="enabled"
1039 >
1040 <ng-template ptTemplate="label">
1041 <ng-container i18n>Transcoding enabled for live streams</ng-container>
1042 </ng-template>
1043 </my-peertube-checkbox>
1044 </div>
1045
1046 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
1047 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
1048
1049 <div class="ml-2 mt-2 d-flex flex-column">
1050 <ng-container formGroupName="resolutions">
1051 <div class="form-group" *ngFor="let resolution of liveResolutions">
1052 <my-peertube-checkbox
1053 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
1054 labelText="{{resolution.label}}"
1055 >
1056 <ng-template *ngIf="resolution.description" ptTemplate="help">
1057 <div [innerHTML]="resolution.description"></div>
1058 </ng-template>
1059 </my-peertube-checkbox>
1060 </div>
1061 </ng-container>
1062 </div>
1063 </div>
1064
1065 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
1066 <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
1067 <span class="text-muted ml-1">
1068 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
1069 <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
1070 </span>
1071
1072 <my-select-custom-value
1073 id="liveTranscodingThreads"
1074 [items]="transcodingThreadOptions"
1075 formControlName="threads"
1076 [clearable]="false"
1077 ></my-select-custom-value>
1078 <div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div>
1079 </div>
1080
1081 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
1082 <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
1083 <span class="text-muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
1084
1085 <my-select-options
1086 id="liveTranscodingProfile"
1087 formControlName="profile"
1088 [items]="getAvailableTranscodingProfile('live')"
1089 [clearable]="false"
1090 >
1091 <ng-template ng-option-tmp let-item="item" let-index="index">
1092 {{ item }}
1093 <ng-container *ngIf="item === 'default'">
1094 <br>
1095 <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
1096 </ng-container>
1097 </ng-template>
1098 </my-select-options>
1099 <div *ngIf="formErrors.live.transcoding.profile" class="form-error">{{ formErrors.live.transcoding.profile }}</div>
1100 </div>
1101
1102 </ng-container>
1103 </ng-container>
1104
1105 </div>
1106 </div>
1107
1108 </ng-template> 39 </ng-template>
1109 </ng-container> 40 </ng-container>
1110 41
@@ -1112,111 +43,8 @@
1112 <a ngbNavLink i18n>Advanced configuration</a> 43 <a ngbNavLink i18n>Advanced configuration</a>
1113 44
1114 <ng-template ngbNavContent> 45 <ng-template ngbNavContent>
1115 46 <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
1116 <div class="form-row mt-5"> <!-- cache grid --> 47 </my-edit-advanced-configuration>
1117 <div class="form-group col-12 col-lg-4 col-xl-3">
1118 <div i18n class="inner-form-title">CACHE</div>
1119 <div i18n class="inner-form-description">
1120 Some files are not federated, and fetched when necessary. Define their caching policies.
1121 </div>
1122 </div>
1123
1124 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
1125
1126 <ng-container formGroupName="cache">
1127 <div class="form-group" formGroupName="previews">
1128 <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
1129 <div class="number-with-unit">
1130 <input
1131 type="number" min="0" id="cachePreviewsSize" class="form-control"
1132 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
1133 >
1134 <span i18n>{form.value['cache']['previews']['size'], plural, =1 {cached image} other {cached images}}</span>
1135 </div>
1136 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
1137 </div>
1138
1139 <div class="form-group" formGroupName="captions">
1140 <label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
1141 <div class="number-with-unit">
1142 <input
1143 type="number" min="0" id="cacheCaptionsSize" class="form-control"
1144 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
1145 >
1146 <span i18n>{form.value['cache']['captions']['size'], plural, =1 {cached image} other {cached images}}</span>
1147 </div>
1148 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
1149 </div>
1150 </ng-container>
1151
1152 </div>
1153 </div>
1154
1155 <div class="form-row mt-4"> <!-- cache grid -->
1156 <div class="form-group col-12 col-lg-4 col-xl-3">
1157 <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
1158 <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
1159 <div i18n class="inner-form-description">
1160 Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
1161 </div>
1162 </div>
1163
1164 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
1165
1166 <ng-container formGroupName="instance">
1167 <ng-container formGroupName="customizations">
1168 <div class="form-group">
1169 <label i18n for="customizationJavascript">JavaScript</label>
1170 <my-help>
1171 <ng-template ptTemplate="customHtml">
1172 <ng-container i18n>
1173 Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
1174 </ng-container>
1175 </ng-template>
1176 </my-help>
1177
1178 <textarea
1179 id="customizationJavascript" formControlName="javascript" class="form-control"
1180 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
1181 ></textarea>
1182
1183 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
1184 </div>
1185
1186 <div class="form-group">
1187 <label for="customizationCSS">CSS</label>
1188
1189 <my-help>
1190 <ng-template ptTemplate="customHtml">
1191 <ng-container i18n>
1192 Write CSS code directly. Example:<br /><br />
1193 <pre>
1194 #custom-css {{ '{' }}
1195 color: red;
1196 {{ '}' }}
1197 </pre>
1198 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
1199 <pre>
1200 #custom-css .logged-in-email {{ '{' }}
1201 color: red;
1202 {{ '}' }}
1203 </pre>
1204 </ng-container>
1205 </ng-template>
1206 </my-help>
1207
1208 <textarea
1209 id="customizationCSS" formControlName="css" class="form-control"
1210 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
1211 ></textarea>
1212 <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
1213 </div>
1214 </ng-container>
1215 </ng-container>
1216
1217 </div>
1218 </div>
1219
1220 </ng-template> 48 </ng-template>
1221 </ng-container> 49 </ng-container>
1222 </div> 50 </div>
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 29524fdd8..a5eddf6c2 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
@@ -1,8 +1,5 @@
1import { forkJoin } from 'rxjs' 1
2import { pairwise } from 'rxjs/operators' 2import { Component, OnInit } from '@angular/core'
3import { SelectOptionsItem } from 'src/types/select-options-item.model'
4import { ViewportScroller } from '@angular/common'
5import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core'
6import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
7import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
8import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
@@ -22,155 +19,35 @@ import {
22} from '@app/shared/form-validators/custom-config-validators' 19} from '@app/shared/form-validators/custom-config-validators'
23import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' 20import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
24import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 21import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
25import { NgbNav } from '@ng-bootstrap/ng-bootstrap'
26import { CustomConfig, ServerConfig } from '@shared/models' 22import { CustomConfig, ServerConfig } from '@shared/models'
23import { forkJoin } from 'rxjs'
24import { SelectOptionsItem } from 'src/types/select-options-item.model'
25import { EditConfigurationService } from './edit-configuration.service'
27 26
28@Component({ 27@Component({
29 selector: 'my-edit-custom-config', 28 selector: 'my-edit-custom-config',
30 templateUrl: './edit-custom-config.component.html', 29 templateUrl: './edit-custom-config.component.html',
31 styleUrls: [ './edit-custom-config.component.scss' ] 30 styleUrls: [ './edit-custom-config.component.scss' ]
32}) 31})
33export class EditCustomConfigComponent extends FormReactive implements OnInit, AfterViewChecked { 32export class EditCustomConfigComponent extends FormReactive implements OnInit {
34 @ViewChild('nav') nav: NgbNav 33 activeNav: string
35 34
36 initDone = false
37 customConfig: CustomConfig 35 customConfig: CustomConfig
38 36 serverConfig: ServerConfig
39 resolutions: { id: string, label: string, description?: string }[] = []
40 liveResolutions: { id: string, label: string, description?: string }[] = []
41
42 transcodingThreadOptions: SelectOptionsItem[] = []
43 liveMaxDurationOptions: SelectOptionsItem[] = []
44 37
45 languageItems: SelectOptionsItem[] = [] 38 languageItems: SelectOptionsItem[] = []
46 categoryItems: SelectOptionsItem[] = [] 39 categoryItems: SelectOptionsItem[] = []
47 40
48 signupAlertMessage: string
49
50 activeNav: string
51
52 private serverConfig: ServerConfig
53
54 constructor ( 41 constructor (
55 private router: Router, 42 private router: Router,
56 private route: ActivatedRoute, 43 private route: ActivatedRoute,
57 private viewportScroller: ViewportScroller,
58 protected formValidatorService: FormValidatorService, 44 protected formValidatorService: FormValidatorService,
59 private notifier: Notifier, 45 private notifier: Notifier,
60 private configService: ConfigService, 46 private configService: ConfigService,
61 private serverService: ServerService 47 private serverService: ServerService,
48 private editConfigurationService: EditConfigurationService
62 ) { 49 ) {
63 super() 50 super()
64
65 this.resolutions = [
66 {
67 id: '0p',
68 label: $localize`Audio-only`,
69 description: $localize`A <code>.mp4</code> that keeps the original audio track, with no video`
70 },
71 {
72 id: '240p',
73 label: $localize`240p`
74 },
75 {
76 id: '360p',
77 label: $localize`360p`
78 },
79 {
80 id: '480p',
81 label: $localize`480p`
82 },
83 {
84 id: '720p',
85 label: $localize`720p`
86 },
87 {
88 id: '1080p',
89 label: $localize`1080p`
90 },
91 {
92 id: '1440p',
93 label: $localize`1440p`
94 },
95 {
96 id: '2160p',
97 label: $localize`2160p`
98 }
99 ]
100
101 this.liveResolutions = this.resolutions.filter(r => r.id !== '0p')
102
103 this.transcodingThreadOptions = [
104 { id: 0, label: $localize`Auto (via ffmpeg)` },
105 { id: 1, label: '1' },
106 { id: 2, label: '2' },
107 { id: 4, label: '4' },
108 { id: 8, label: '8' },
109 { id: 12, label: '12' },
110 { id: 16, label: '16' },
111 { id: 32, label: '32' }
112 ]
113
114 this.liveMaxDurationOptions = [
115 { id: -1, label: $localize`No limit` },
116 { id: 1000 * 3600, label: $localize`1 hour` },
117 { id: 1000 * 3600 * 3, label: $localize`3 hours` },
118 { id: 1000 * 3600 * 5, label: $localize`5 hours` },
119 { id: 1000 * 3600 * 10, label: $localize`10 hours` }
120 ]
121 }
122
123 get videoQuotaOptions () {
124 return this.configService.videoQuotaOptions
125 }
126
127 get videoQuotaDailyOptions () {
128 return this.configService.videoQuotaDailyOptions
129 }
130
131 get availableThemes () {
132 return this.serverConfig.theme.registered
133 .map(t => t.name)
134 }
135
136 get liveRTMPPort () {
137 return this.serverConfig.live.rtmp.port
138 }
139
140 getAvailableTranscodingProfile (type: 'live' | 'vod') {
141 const profiles = type === 'live'
142 ? this.serverConfig.live.transcoding.availableProfiles
143 : this.serverConfig.transcoding.availableProfiles
144
145 return profiles.map(p => ({ id: p, label: p }))
146 }
147
148 getTotalTranscodingThreads () {
149 const transcodingEnabled = this.form.value['transcoding']['enabled']
150 const transcodingThreads = this.form.value['transcoding']['threads']
151 const liveTranscodingEnabled = this.form.value['live']['transcoding']['enabled']
152 const liveTranscodingThreads = this.form.value['live']['transcoding']['threads']
153
154 // checks whether all enabled method are on fixed values and not on auto (= 0)
155 let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
156 noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
157
158 // count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
159 let value = 0
160 if (transcodingEnabled) value += +transcodingThreads || 1
161 if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
162
163 return {
164 value,
165 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
166 unit: value > 1
167 ? $localize`threads`
168 : $localize`thread`
169 }
170 }
171
172 getResolutionKey (resolution: string) {
173 return 'transcoding.resolutions.' + resolution
174 } 51 }
175 52
176 ngOnInit () { 53 ngOnInit () {
@@ -346,60 +223,24 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
346 } 223 }
347 } 224 }
348 225
349 for (const resolution of this.resolutions) { 226 for (const resolution of this.editConfigurationService.getVODResolutions()) {
350 defaultValues.transcoding.resolutions[resolution.id] = 'false' 227 defaultValues.transcoding.resolutions[resolution.id] = 'false'
351 formGroupData.transcoding.resolutions[resolution.id] = null 228 formGroupData.transcoding.resolutions[resolution.id] = null
352 } 229 }
353 230
354 for (const resolution of this.liveResolutions) { 231 for (const resolution of this.editConfigurationService.getLiveResolutions()) {
355 defaultValues.live.transcoding.resolutions[resolution.id] = 'false' 232 defaultValues.live.transcoding.resolutions[resolution.id] = 'false'
356 formGroupData.live.transcoding.resolutions[resolution.id] = null 233 formGroupData.live.transcoding.resolutions[resolution.id] = null
357 } 234 }
358 235
359 if (this.route.snapshot.fragment) {
360 this.onNavChange(this.route.snapshot.fragment)
361 }
362
363 this.buildForm(formGroupData) 236 this.buildForm(formGroupData)
364 this.loadForm()
365 237
366 this.checkTranscodingFields() 238 if (this.route.snapshot.fragment) {
367 this.checkSignupField() 239 this.onNavChange(this.route.snapshot.fragment)
368 }
369
370 ngAfterViewChecked () {
371 if (!this.initDone) {
372 this.initDone = true
373 this.gotoAnchor()
374 } 240 }
375 }
376
377 isTranscodingEnabled () {
378 return this.form.value['transcoding']['enabled'] === true
379 }
380
381 isLiveEnabled () {
382 return this.form.value['live']['enabled'] === true
383 }
384
385 isLiveTranscodingEnabled () {
386 return this.form.value['live']['transcoding']['enabled'] === true
387 }
388
389 isSignupEnabled () {
390 return this.form.value['signup']['enabled'] === true
391 }
392
393 isSearchIndexEnabled () {
394 return this.form.value['search']['searchIndex']['enabled'] === true
395 }
396 241
397 isAutoFollowIndexEnabled () { 242 this.loadConfigAndUpdateForm()
398 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true 243 this.loadCategoriesAndLanguages()
399 }
400
401 trendingVideosAlgorithmsEnabledIncludes (algorithm: string) {
402 return this.form.value['trending']['videos']['algorithms']['enabled'].find((e: string) => e === algorithm)
403 } 244 }
404 245
405 async formValidated () { 246 async formValidated () {
@@ -422,18 +263,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
422 ) 263 )
423 } 264 }
424 265
425 gotoAnchor () {
426 const hashToNav = {
427 'customizations': 'advanced-configuration'
428 }
429 const hash = window.location.hash.replace('#', '')
430
431 if (hash && Object.keys(hashToNav).includes(hash)) {
432 this.nav.select(hashToNav[hash])
433 setTimeout(() => this.viewportScroller.scrollToAnchor(hash), 100)
434 }
435 }
436
437 hasConsistentOptions () { 266 hasConsistentOptions () {
438 if (this.hasLiveAllowReplayConsistentOptions()) return true 267 if (this.hasLiveAllowReplayConsistentOptions()) return true
439 268
@@ -441,7 +270,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
441 } 270 }
442 271
443 hasLiveAllowReplayConsistentOptions () { 272 hasLiveAllowReplayConsistentOptions () {
444 if (this.isTranscodingEnabled() === false && this.isLiveEnabled() && this.form.value['live']['allowReplay'] === true) { 273 if (
274 this.editConfigurationService.isTranscodingEnabled(this.form) === false &&
275 this.editConfigurationService.isLiveEnabled(this.form) &&
276 this.form.value['live']['allowReplay'] === true
277 ) {
445 return false 278 return false
446 } 279 }
447 280
@@ -458,18 +291,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
458 this.form.patchValue(this.customConfig) 291 this.form.patchValue(this.customConfig)
459 } 292 }
460 293
461 private loadForm () { 294 private loadConfigAndUpdateForm () {
462 forkJoin([ 295 this.configService.getCustomConfig()
463 this.configService.getCustomConfig(), 296 .subscribe(config => {
464 this.serverService.getVideoLanguages(),
465 this.serverService.getVideoCategories()
466 ]).subscribe(
467 ([ config, languages, categories ]) => {
468 this.customConfig = config 297 this.customConfig = config
469 298
470 this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
471 this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
472
473 this.updateForm() 299 this.updateForm()
474 // Force form validation 300 // Force form validation
475 this.forceCheck() 301 this.forceCheck()
@@ -479,53 +305,17 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
479 ) 305 )
480 } 306 }
481 307
482 private checkTranscodingFields () { 308 private loadCategoriesAndLanguages () {
483 const hlsControl = this.form.get('transcoding.hls.enabled') 309 forkJoin([
484 const webtorrentControl = this.form.get('transcoding.webtorrent.enabled') 310 this.serverService.getVideoLanguages(),
485 311 this.serverService.getVideoCategories()
486 webtorrentControl.valueChanges 312 ]).subscribe(
487 .subscribe(newValue => { 313 ([ languages, categories ]) => {
488 if (newValue === false && !hlsControl.disabled) { 314 this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
489 hlsControl.disable() 315 this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
490 } 316 },
491
492 if (newValue === true && !hlsControl.enabled) {
493 hlsControl.enable()
494 }
495 })
496
497 hlsControl.valueChanges
498 .subscribe(newValue => {
499 if (newValue === false && !webtorrentControl.disabled) {
500 webtorrentControl.disable()
501 }
502
503 if (newValue === true && !webtorrentControl.enabled) {
504 webtorrentControl.enable()
505 }
506 })
507 }
508 317
509 private checkSignupField () { 318 err => this.notifier.error(err.message)
510 const signupControl = this.form.get('signup.enabled') 319 )
511
512 signupControl.valueChanges
513 .pipe(pairwise())
514 .subscribe(([ oldValue, newValue ]) => {
515 if (oldValue !== true && newValue === true) {
516 // tslint:disable:max-line-length
517 this.signupAlertMessage = $localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
518
519 this.form.patchValue({
520 autoBlacklist: {
521 videos: {
522 ofUsers: {
523 enabled: true
524 }
525 }
526 }
527 })
528 }
529 })
530 } 320 }
531} 321}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
new file mode 100644
index 000000000..6f19ede0a
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
@@ -0,0 +1,228 @@
1<ng-container [formGroup]="form">
2
3 <ng-container formGroupName="instance">
4
5 <div class="form-row mt-5"> <!-- instance grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE</div>
8 </div>
9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
11
12 <div class="form-group">
13 <label i18n for="instanceName">Name</label>
14 <input
15 type="text" id="instanceName" class="form-control"
16 formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
17 >
18 <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
19 </div>
20
21 <div class="form-group">
22 <label i18n for="instanceShortDescription">Short description</label>
23 <textarea
24 id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
25 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
26 ></textarea>
27 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
28 </div>
29
30 <div class="form-group">
31 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
32 <my-markdown-textarea
33 name="instanceDescription" formControlName="description" textareaMaxWidth="500px"
34 [classes]="{ 'input-error': formErrors['instance.description'] }"
35 ></my-markdown-textarea>
36 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
37 </div>
38
39 <div class="form-group">
40 <label i18n for="instanceCategories">Main instance categories</label>
41
42 <div>
43 <my-select-checkbox
44 id="instanceCategories"
45 formControlName="categories" [availableItems]="categoryItems"
46 [selectableGroup]="false"
47 i18n-placeholder placeholder="Add a new category"
48 >
49 </my-select-checkbox>
50 </div>
51 </div>
52
53 <div class="form-group">
54 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
55
56 <div>
57 <my-select-checkbox
58 id="instanceLanguages"
59 formControlName="languages" [availableItems]="languageItems"
60 [selectableGroup]="false"
61 i18n-placeholder placeholder="Add a new language"
62 >
63 </my-select-checkbox>
64 </div>
65 </div>
66
67 </div>
68 </div>
69
70 <div class="form-row mt-4"> <!-- moderation & nsfw grid -->
71 <div class="form-group col-12 col-lg-4 col-xl-3">
72 <div i18n class="inner-form-title">MODERATION & NSFW</div>
73 <div i18n class="inner-for-description">
74 Manage <a routerLink="/admin/users">users</a> to build a moderation team.
75 </div>
76 </div>
77
78 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
79
80 <div class="form-group">
81 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
82 <ng-template ptTemplate="label">
83 <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
84 </ng-template>
85
86 <ng-template ptTemplate="help">
87 <ng-container i18n>
88 Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
89 Moreover, the NSFW checkbox on video upload will be automatically checked by default.
90 </ng-container>
91 </ng-template>
92 </my-peertube-checkbox>
93 </div>
94
95 <div class="form-group">
96 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
97
98 <my-help>
99 <ng-template ptTemplate="customHtml">
100 <ng-container i18n>
101 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
102 </ng-container>
103 </ng-template>
104 </my-help>
105
106 <div class="peertube-select-container">
107 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy" class="form-control">
108 <option i18n value="undefined" disabled>Policy for sensitive videos</option>
109 <option i18n value="do_not_list">Do not list</option>
110 <option i18n value="blur">Blur thumbnails</option>
111 <option i18n value="display">Display</option>
112 </select>
113 </div>
114 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
115 </div>
116
117 <div class="form-group">
118 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
119 <my-markdown-textarea
120 name="instanceTerms" formControlName="terms" textareaMaxWidth="500px"
121 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
122 ></my-markdown-textarea>
123 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
124 </div>
125
126 <div class="form-group">
127 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
128 <my-markdown-textarea
129 name="instanceCodeOfConduct" formControlName="codeOfConduct" textareaMaxWidth="500px"
130 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
131 ></my-markdown-textarea>
132 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
133 </div>
134
135 <div class="form-group">
136 <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
137 <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
138
139 <my-markdown-textarea
140 name="instanceModerationInformation" formControlName="moderationInformation" textareaMaxWidth="500px"
141 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
142 ></my-markdown-textarea>
143 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
144 </div>
145
146 </div>
147 </div>
148
149 <div class="form-row mt-4"> <!-- you and your instance grid -->
150 <div class="form-group col-12 col-lg-4 col-xl-3">
151 <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
152 </div>
153
154 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
155
156 <div class="form-group">
157 <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
158 <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
159
160 <my-markdown-textarea
161 name="instanceAdministrator" formControlName="administrator" textareaMaxWidth="500px"
162 [classes]="{ 'input-error': formErrors['instance.administrator'] }"
163 ></my-markdown-textarea>
164
165 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
166 </div>
167
168 <div class="form-group">
169 <label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
170 <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
171
172 <my-markdown-textarea
173 name="instanceCreationReason" formControlName="creationReason" textareaMaxWidth="500px"
174 [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
175 ></my-markdown-textarea>
176 <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
177 </div>
178
179 <div class="form-group">
180 <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
181 <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
182
183 <my-markdown-textarea
184 name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" textareaMaxWidth="500px"
185 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
186 ></my-markdown-textarea>
187 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
188 </div>
189
190 <div class="form-group">
191 <label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
192 <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
193
194 <my-markdown-textarea
195 name="instanceBusinessModel" formControlName="businessModel" textareaMaxWidth="500px"
196 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
197 ></my-markdown-textarea>
198 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
199 </div>
200
201 </div>
202 </div>
203
204 <div class="form-row mt-4"> <!-- other information grid -->
205 <div class="form-group col-12 col-lg-4 col-xl-3">
206 <div i18n class="inner-form-title">OTHER INFORMATION</div>
207 </div>
208
209 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
210
211 <div class="form-group">
212 <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
213 <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
214
215 <my-markdown-textarea
216 name="instanceHardwareInformation" formControlName="hardwareInformation" textareaMaxWidth="500px"
217 [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
218 ></my-markdown-textarea>
219
220 <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
221 </div>
222
223 </div>
224 </div>
225
226 </ng-container>
227
228</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.ts
new file mode 100644
index 000000000..26365e707
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.ts
@@ -0,0 +1,16 @@
1import { SelectOptionsItem } from 'src/types/select-options-item.model'
2import { Component, Input } from '@angular/core'
3import { FormGroup } from '@angular/forms'
4
5@Component({
6 selector: 'my-edit-instance-information',
7 templateUrl: './edit-instance-information.component.html',
8 styleUrls: [ './edit-custom-config.component.scss' ]
9})
10export class EditInstanceInformationComponent {
11 @Input() form: FormGroup
12 @Input() formErrors: any
13
14 @Input() languageItems: SelectOptionsItem[] = []
15 @Input() categoryItems: SelectOptionsItem[] = []
16}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
new file mode 100644
index 000000000..4b1a55245
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
@@ -0,0 +1,155 @@
1<ng-container [formGroup]="form">
2
3 <div class="form-row mt-5">
4 <div class="form-group col-12 col-lg-4 col-xl-3">
5 <div i18n class="inner-form-title">LIVE</div>
6 <div i18n class="inner-form-description">
7 Enable users of your instance to stream live.
8 </div>
9 </div>
10
11 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
12
13 <ng-container formGroupName="live">
14
15 <div class="form-group">
16 <my-peertube-checkbox inputName="liveEnabled" formControlName="enabled">
17 <ng-template ptTemplate="label">
18 <ng-container i18n>Allow live streaming</ng-container>
19 </ng-template>
20
21 <ng-container ngProjectAs="description">
22 <div i18n>⚠️ Enabling live streaming requires trust in your users and extra moderation work</div>
23 <div i18n>If enabled, your server needs to accept incoming TCP traffic on port {{ getLiveRTMPPort() }}</div>
24 </ng-container>
25
26 <ng-container ngProjectAs="extra">
27
28 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
29 <my-peertube-checkbox
30 inputName="liveAllowReplay" formControlName="allowReplay"
31 i18n-labelText labelText="Allow your users to automatically publish a replay of their live"
32 >
33 <ng-container ngProjectAs="description" i18n>
34 If the user quota is reached, PeerTube will automatically terminate the live streaming
35 </ng-container>
36 </my-peertube-checkbox>
37 </div>
38
39 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
40 <label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span></label>
41 <div class="number-with-unit">
42 <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
43 <span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
44 </div>
45 </div>
46
47 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
48 <label i18n for="liveMaxUserLives">Max simultaneous lives created per user <span class="text-muted">(-1 for "unlimited")</span></label>
49 <div class="number-with-unit">
50 <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
51 <span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
52 </div>
53 </div>
54
55 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
56 <label i18n for="liveMaxDuration">Max live duration</label>
57
58 <my-select-options
59 labelForId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"
60 bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
61 ></my-select-options>
62 </div>
63
64 </ng-container>
65 </my-peertube-checkbox>
66 </div>
67 </ng-container>
68 </div>
69 </div>
70
71 <div class="form-row"> <!-- transcoding live streams grid -->
72 <div class="form-group col-12 col-lg-4 col-xl-3">
73 <div i18n class="inner-form-title">TRANSCODING</div>
74 <div i18n class="inner-form-description">
75 Same as VOD transcoding, transcoding live streams so that they are in a streamable form that any device can play. Requires a beefy CPU, and then some.
76 </div>
77 </div>
78
79 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
80
81 <ng-container formGroupName="live">
82 <ng-container formGroupName="transcoding">
83
84 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
85 <my-peertube-checkbox
86 inputName="liveTranscodingEnabled" formControlName="enabled"
87 >
88 <ng-template ptTemplate="label">
89 <ng-container i18n>Transcoding enabled for live streams</ng-container>
90 </ng-template>
91 </my-peertube-checkbox>
92 </div>
93
94 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
95 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
96
97 <div class="ml-2 mt-2 d-flex flex-column">
98 <ng-container formGroupName="resolutions">
99 <div class="form-group" *ngFor="let resolution of liveResolutions">
100 <my-peertube-checkbox
101 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
102 labelText="{{resolution.label}}"
103 >
104 <ng-template *ngIf="resolution.description" ptTemplate="help">
105 <div [innerHTML]="resolution.description"></div>
106 </ng-template>
107 </my-peertube-checkbox>
108 </div>
109 </ng-container>
110 </div>
111 </div>
112
113 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
114 <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
115 <span class="text-muted ml-1">
116 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
117 <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
118 </span>
119
120 <my-select-custom-value
121 id="liveTranscodingThreads"
122 [items]="transcodingThreadOptions"
123 formControlName="threads"
124 [clearable]="false"
125 ></my-select-custom-value>
126 <div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div>
127 </div>
128
129 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
130 <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
131 <span class="text-muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
132
133 <my-select-options
134 id="liveTranscodingProfile"
135 formControlName="profile"
136 [items]="getAvailableTranscodingProfile()"
137 [clearable]="false"
138 >
139 <ng-template ng-option-tmp let-item="item" let-index="index">
140 {{ item }}
141 <ng-container *ngIf="item === 'default'">
142 <br>
143 <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
144 </ng-container>
145 </ng-template>
146 </my-select-options>
147 <div *ngIf="formErrors.live.transcoding.profile" class="form-error">{{ formErrors.live.transcoding.profile }}</div>
148 </div>
149
150 </ng-container>
151 </ng-container>
152
153 </div>
154 </div>
155</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts
new file mode 100644
index 000000000..a82a40a84
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts
@@ -0,0 +1,67 @@
1
2import { SelectOptionsItem } from 'src/types/select-options-item.model'
3import { Component, Input, OnInit } from '@angular/core'
4import { FormGroup } from '@angular/forms'
5import { ServerConfig } from '@shared/models'
6import { ConfigService } from '../shared/config.service'
7import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
8
9@Component({
10 selector: 'my-edit-live-configuration',
11 templateUrl: './edit-live-configuration.component.html',
12 styleUrls: [ './edit-custom-config.component.scss' ]
13})
14export class EditLiveConfigurationComponent implements OnInit {
15 @Input() form: FormGroup
16 @Input() formErrors: any
17 @Input() serverConfig: ServerConfig
18
19 transcodingThreadOptions: SelectOptionsItem[] = []
20 liveMaxDurationOptions: SelectOptionsItem[] = []
21 liveResolutions: ResolutionOption[] = []
22
23 constructor (
24 private configService: ConfigService,
25 private editConfigurationService: EditConfigurationService
26 ) { }
27
28 ngOnInit () {
29 this.transcodingThreadOptions = this.configService.transcodingThreadOptions
30
31 this.liveMaxDurationOptions = [
32 { id: -1, label: $localize`No limit` },
33 { id: 1000 * 3600, label: $localize`1 hour` },
34 { id: 1000 * 3600 * 3, label: $localize`3 hours` },
35 { id: 1000 * 3600 * 5, label: $localize`5 hours` },
36 { id: 1000 * 3600 * 10, label: $localize`10 hours` }
37 ]
38
39 this.liveResolutions = this.editConfigurationService.getLiveResolutions()
40 }
41
42 getAvailableTranscodingProfile () {
43 const profiles = this.serverConfig.live.transcoding.availableProfiles
44
45 return profiles.map(p => ({ id: p, label: p }))
46 }
47
48 getResolutionKey (resolution: string) {
49 return 'live.transcoding.resolutions.' + resolution
50 }
51
52 getLiveRTMPPort () {
53 return this.serverConfig.live.rtmp.port
54 }
55
56 isLiveEnabled () {
57 return this.editConfigurationService.isLiveEnabled(this.form)
58 }
59
60 isLiveTranscodingEnabled () {
61 return this.editConfigurationService.isLiveTranscodingEnabled(this.form)
62 }
63
64 getTotalTranscodingThreads () {
65 return this.editConfigurationService.getTotalTranscodingThreads(this.form)
66 }
67}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
new file mode 100644
index 000000000..a51909865
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
@@ -0,0 +1,201 @@
1<ng-container [formGroup]="form">
2
3 <div class="form-row mt-4"> <!-- transcoding grid -->
4 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
5 <div class="form-group form-group-right col-12 col-lg-8">
6
7 <div class="callout callout-info">
8 <span i18n>
9 Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
10 </span>
11 <span i18n>
12 However, you may want to read our guidelines before tweaking the following values.
13 </span>
14
15 <div class="callout-container">
16 <a class="callout-link" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/#/admin-configuration?id=transcoding" i18n>Read guidelines</a>
17 </div>
18 </div>
19 </div>
20 </div>
21
22 <div class="form-row mt-2"> <!-- transcoding grid -->
23 <div class="form-group col-12 col-lg-4 col-xl-3">
24 <div i18n class="inner-form-title">TRANSCODING</div>
25 <div i18n class="inner-form-description">
26 Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
27 resources, this is a critical part of PeerTube, so tread carefully.
28 </div>
29 </div>
30
31 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
32
33 <ng-container formGroupName="transcoding">
34
35 <div class="form-group mb-0 col-12 col-xl-11">
36 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
37 <ng-template ptTemplate="label">
38 <ng-container i18n>Transcoding enabled</ng-container>
39 </ng-template>
40
41 <ng-container ngProjectAs="extra">
42
43 <div class="callout callout-light pt-2 pb-0">
44 <label i18n>Input formats</label>
45
46 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
47 <my-peertube-checkbox
48 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
49 i18n-labelText labelText="Allow additional extensions"
50 >
51 <ng-container ngProjectAs="description">
52 <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, .m2ts, .mxf, or .nut videos.</span>
53 </ng-container>
54 </my-peertube-checkbox>
55 </div>
56
57 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
58 <my-peertube-checkbox
59 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
60 i18n-labelText labelText="Allow audio files upload"
61 >
62 <ng-container ngProjectAs="description">
63 <div i18n>Allows users to upload .mp3, .ogg, .wma, .flac, .aac, or .ac3 audio files.</div>
64 <div i18n>The file will be merged in a still image video with the preview file on upload.</div>
65 </ng-container>
66 </my-peertube-checkbox>
67 </div>
68 </div>
69
70 <div class="callout callout-light pt-2 mt-2 pb-0">
71 <label i18n>Output formats</label>
72
73 <ng-container formGroupName="webtorrent">
74 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
75 <my-peertube-checkbox
76 inputName="transcodingWebTorrentEnabled" formControlName="enabled"
77 i18n-labelText labelText="WebTorrent enabled"
78 >
79 <ng-template ptTemplate="help">
80 <ng-container i18n>
81 <p>If you also enabled HLS support, it will multiply videos storage by 2</p>
82
83 <br />
84
85 <strong>If disabled, breaks federation with PeerTube instances < 2.1</strong>
86 </ng-container>
87 </ng-template>
88 </my-peertube-checkbox>
89 </div>
90 </ng-container>
91
92 <ng-container formGroupName="hls">
93 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
94 <my-peertube-checkbox
95 inputName="transcodingHlsEnabled" formControlName="enabled"
96 i18n-labelText labelText="HLS with P2P support enabled"
97 [recommended]="true"
98 >
99 <ng-template ptTemplate="help">
100 <ng-container i18n>
101 <strong>Requires ffmpeg >= 4.1</strong>
102
103 <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p>
104 <ul>
105 <li>Resolution change is smoother</li>
106 <li>Faster playback especially with long videos</li>
107 <li>More stable playback (less bugs/infinite loading)</li>
108 </ul>
109
110 <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p>
111 </ng-container>
112 </ng-template>
113 </my-peertube-checkbox>
114 </div>
115 </ng-container>
116
117 <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
118 <label i18n>Resolutions to generate per enabled format</label>
119
120 <div class="ml-2 mt-2 d-flex flex-column">
121 <ng-container formGroupName="resolutions">
122 <div class="form-group" *ngFor="let resolution of resolutions">
123 <my-peertube-checkbox
124 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
125 labelText="{{ resolution.label }}"
126 >
127 <ng-template *ngIf="resolution.description" ptTemplate="help">
128 <div [innerHTML]="resolution.description"></div>
129 </ng-template>
130 </my-peertube-checkbox>
131 </div>
132
133 <span class="mb-2 text-muted" i18n>
134 The original file resolution will be the default target if no option is selected.
135 </span>
136 </ng-container>
137 </div>
138 </div>
139 </div>
140
141 </ng-container>
142 </my-peertube-checkbox>
143 </div>
144
145 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
146 <label i18n for="transcodingThreads">Transcoding threads</label>
147 <span class="text-muted ml-1">
148 <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
149 <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
150 </span>
151
152 <my-select-custom-value
153 id="transcodingThreads"
154 [items]="transcodingThreadOptions"
155 formControlName="threads"
156 [clearable]="false"
157 ></my-select-custom-value>
158
159 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
160 </div>
161
162 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
163 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
164 <span class="text-muted ml-1">
165 <span i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart.</span>
166 </span>
167
168 <div class="number-with-unit">
169 <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
170 <span i18n>jobs in parallel</span>
171 </div>
172
173 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
174 </div>
175
176 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
177 <label i18n for="transcodingProfile">Transcoding profile</label>
178 <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
179
180 <my-select-options
181 id="transcodingProfile"
182 formControlName="profile"
183 [items]="getAvailableTranscodingProfile()"
184 [clearable]="false"
185 >
186 <ng-template ng-option-tmp let-item="item" let-index="index">
187 {{ item }}
188 <ng-container *ngIf="item === 'default'">
189 <br>
190 <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
191 </ng-container>
192 </ng-template>
193 </my-select-options>
194 <div *ngIf="formErrors.transcoding.profile" class="form-error">{{ formErrors.transcoding.profile }}</div>
195 </div>
196
197 </ng-container>
198
199 </div>
200 </div>
201</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts
new file mode 100644
index 000000000..d745912a0
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts
@@ -0,0 +1,78 @@
1
2import { SelectOptionsItem } from 'src/types/select-options-item.model'
3import { Component, Input, OnInit } from '@angular/core'
4import { FormGroup } from '@angular/forms'
5import { ServerConfig } from '@shared/models'
6import { ConfigService } from '../shared/config.service'
7import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
8
9@Component({
10 selector: 'my-edit-vod-transcoding',
11 templateUrl: './edit-vod-transcoding.component.html',
12 styleUrls: [ './edit-custom-config.component.scss' ]
13})
14export class EditVODTranscodingComponent implements OnInit {
15 @Input() form: FormGroup
16 @Input() formErrors: any
17 @Input() serverConfig: ServerConfig
18
19 transcodingThreadOptions: SelectOptionsItem[] = []
20 resolutions: ResolutionOption[] = []
21
22 constructor (
23 private configService: ConfigService,
24 private editConfigurationService: EditConfigurationService
25 ) { }
26
27 ngOnInit () {
28 this.transcodingThreadOptions = this.configService.transcodingThreadOptions
29 this.resolutions = this.editConfigurationService.getVODResolutions()
30
31 this.checkTranscodingFields()
32 }
33
34 getAvailableTranscodingProfile () {
35 const profiles = this.serverConfig.transcoding.availableProfiles
36
37 return profiles.map(p => ({ id: p, label: p }))
38 }
39
40 getResolutionKey (resolution: string) {
41 return 'transcoding.resolutions.' + resolution
42 }
43
44 isTranscodingEnabled () {
45 return this.editConfigurationService.isTranscodingEnabled(this.form)
46 }
47
48 getTotalTranscodingThreads () {
49 return this.editConfigurationService.getTotalTranscodingThreads(this.form)
50 }
51
52 private checkTranscodingFields () {
53 const hlsControl = this.form.get('transcoding.hls.enabled')
54 const webtorrentControl = this.form.get('transcoding.webtorrent.enabled')
55
56 webtorrentControl.valueChanges
57 .subscribe(newValue => {
58 if (newValue === false && !hlsControl.disabled) {
59 hlsControl.disable()
60 }
61
62 if (newValue === true && !hlsControl.enabled) {
63 hlsControl.enable()
64 }
65 })
66
67 hlsControl.valueChanges
68 .subscribe(newValue => {
69 if (newValue === false && !webtorrentControl.disabled) {
70 webtorrentControl.disable()
71 }
72
73 if (newValue === true && !webtorrentControl.enabled) {
74 webtorrentControl.enable()
75 }
76 })
77 }
78}
diff --git a/client/src/app/+admin/config/edit-custom-config/index.ts b/client/src/app/+admin/config/edit-custom-config/index.ts
index 1ec12631f..95fcc8f52 100644
--- a/client/src/app/+admin/config/edit-custom-config/index.ts
+++ b/client/src/app/+admin/config/edit-custom-config/index.ts
@@ -1 +1,7 @@
1export * from './edit-advanced-configuration.component'
2export * from './edit-basic-configuration.component'
3export * from './edit-configuration.service'
1export * from './edit-custom-config.component' 4export * from './edit-custom-config.component'
5export * from './edit-instance-information.component'
6export * from './edit-live-configuration.component'
7export * from './edit-vod-transcoding.component'
diff --git a/client/src/app/+admin/config/shared/config.service.ts b/client/src/app/+admin/config/shared/config.service.ts
index d29b752f7..80f495b41 100644
--- a/client/src/app/+admin/config/shared/config.service.ts
+++ b/client/src/app/+admin/config/shared/config.service.ts
@@ -12,11 +12,12 @@ export class ConfigService {
12 12
13 videoQuotaOptions: SelectOptionsItem[] = [] 13 videoQuotaOptions: SelectOptionsItem[] = []
14 videoQuotaDailyOptions: SelectOptionsItem[] = [] 14 videoQuotaDailyOptions: SelectOptionsItem[] = []
15 transcodingThreadOptions: SelectOptionsItem[] = []
15 16
16 constructor ( 17 constructor (
17 private authHttp: HttpClient, 18 private authHttp: HttpClient,
18 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor
19 ) { 20 ) {
20 this.videoQuotaOptions = [ 21 this.videoQuotaOptions = [
21 { id: -1, label: $localize`Unlimited` }, 22 { id: -1, label: $localize`Unlimited` },
22 { id: 0, label: $localize`None - no upload possible` }, 23 { id: 0, label: $localize`None - no upload possible` },
@@ -44,6 +45,17 @@ export class ConfigService {
44 { id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` }, 45 { id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
45 { id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` } 46 { id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` }
46 ] 47 ]
48
49 this.transcodingThreadOptions = [
50 { id: 0, label: $localize`Auto (via ffmpeg)` },
51 { id: 1, label: '1' },
52 { id: 2, label: '2' },
53 { id: 4, label: '4' },
54 { id: 8, label: '8' },
55 { id: 12, label: '12' },
56 { id: 16, label: '16' },
57 { id: 32, label: '32' }
58 ]
47 } 59 }
48 60
49 getCustomConfig () { 61 getCustomConfig () {