diff options
author | Chocobozzz <me@florianbigard.com> | 2021-02-10 11:06:32 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-02-10 11:36:40 +0100 |
commit | 5f46d28ccac4a20fcbb12c96a047a84a08e485ae (patch) | |
tree | 6529920195176736d187b93bcac8f3ad6920853d | |
parent | 53e4e201797aa1b581209ffd775bf05197efa78f (diff) | |
download | PeerTube-5f46d28ccac4a20fcbb12c96a047a84a08e485ae.tar.gz PeerTube-5f46d28ccac4a20fcbb12c96a047a84a08e485ae.tar.zst PeerTube-5f46d28ccac4a20fcbb12c96a047a84a08e485ae.zip |
Split admin conf page
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' | |||
10 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | 10 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' |
11 | import { AdminRoutingModule } from './admin-routing.module' | 11 | import { AdminRoutingModule } from './admin-routing.module' |
12 | import { AdminComponent } from './admin.component' | 12 | import { AdminComponent } from './admin.component' |
13 | import { ConfigComponent, EditCustomConfigComponent } from './config' | 13 | import { |
14 | ConfigComponent, | ||
15 | EditAdvancedConfigurationComponent, | ||
16 | EditBasicConfigurationComponent, | ||
17 | EditConfigurationService, | ||
18 | EditCustomConfigComponent, | ||
19 | EditInstanceInformationComponent, | ||
20 | EditLiveConfigurationComponent, | ||
21 | EditVODTranscodingComponent | ||
22 | } from './config' | ||
14 | import { ConfigService } from './config/shared/config.service' | 23 | import { ConfigService } from './config/shared/config.service' |
15 | import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' | 24 | import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' |
16 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 25 | import { 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 | }) |
99 | export class AdminModule { } | 115 | export 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 {{ '{' }} | ||
82 | color: 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 {{ '{' }} | ||
88 | color: 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 | |||
2 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
3 | import { Component, Input } from '@angular/core' | ||
4 | import { 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 | }) | ||
11 | export 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 | |||
2 | import { pairwise } from 'rxjs/operators' | ||
3 | import { Component, Input, OnInit } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | ||
5 | import { ServerConfig } from '@shared/models' | ||
6 | import { 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 | }) | ||
13 | export 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 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { FormGroup } from '@angular/forms' | ||
3 | |||
4 | export type ResolutionOption = { | ||
5 | id: string | ||
6 | label: string | ||
7 | description?: string | ||
8 | } | ||
9 | |||
10 | @Injectable() | ||
11 | export 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 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | |
2 | import { pairwise } from 'rxjs/operators' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
4 | import { ViewportScroller } from '@angular/common' | ||
5 | import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core' | ||
6 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
7 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
8 | import { Notifier } from '@app/core' | 5 | import { 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' |
23 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' | 20 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' |
24 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 21 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
25 | import { NgbNav } from '@ng-bootstrap/ng-bootstrap' | ||
26 | import { CustomConfig, ServerConfig } from '@shared/models' | 22 | import { CustomConfig, ServerConfig } from '@shared/models' |
23 | import { forkJoin } from 'rxjs' | ||
24 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
25 | import { 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 | }) |
33 | export class EditCustomConfigComponent extends FormReactive implements OnInit, AfterViewChecked { | 32 | export 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 @@ | |||
1 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
2 | import { Component, Input } from '@angular/core' | ||
3 | import { 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 | }) | ||
10 | export 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 | |||
2 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
3 | import { Component, Input, OnInit } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | ||
5 | import { ServerConfig } from '@shared/models' | ||
6 | import { ConfigService } from '../shared/config.service' | ||
7 | import { 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 | }) | ||
14 | export 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 | |||
2 | import { SelectOptionsItem } from 'src/types/select-options-item.model' | ||
3 | import { Component, Input, OnInit } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | ||
5 | import { ServerConfig } from '@shared/models' | ||
6 | import { ConfigService } from '../shared/config.service' | ||
7 | import { 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 | }) | ||
14 | export 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 @@ | |||
1 | export * from './edit-advanced-configuration.component' | ||
2 | export * from './edit-basic-configuration.component' | ||
3 | export * from './edit-configuration.service' | ||
1 | export * from './edit-custom-config.component' | 4 | export * from './edit-custom-config.component' |
5 | export * from './edit-instance-information.component' | ||
6 | export * from './edit-live-configuration.component' | ||
7 | export * 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 () { |