]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Split admin conf page
authorChocobozzz <me@florianbigard.com>
Wed, 10 Feb 2021 10:06:32 +0000 (11:06 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 10 Feb 2021 10:36:40 +0000 (11:36 +0100)
16 files changed:
client/src/app/+admin/admin.module.ts
client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-advanced-configuration.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/index.ts
client/src/app/+admin/config/shared/config.service.ts

index 5c0864f48709cc9e1ceb3baa857dd32da564bbd5..fd648a42531c0af7946aa18f76f7c8439542b8be 100644 (file)
@@ -10,7 +10,16 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
 import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
 import { AdminRoutingModule } from './admin-routing.module'
 import { AdminComponent } from './admin.component'
-import { ConfigComponent, EditCustomConfigComponent } from './config'
+import {
+  ConfigComponent,
+  EditAdvancedConfigurationComponent,
+  EditBasicConfigurationComponent,
+  EditConfigurationService,
+  EditCustomConfigComponent,
+  EditInstanceInformationComponent,
+  EditLiveConfigurationComponent,
+  EditVODTranscodingComponent
+} from './config'
 import { ConfigService } from './config/shared/config.service'
 import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
 import { FollowingListComponent } from './follows/following-list/following-list.component'
@@ -81,7 +90,13 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
     DebugComponent,
 
     ConfigComponent,
-    EditCustomConfigComponent
+
+    EditCustomConfigComponent,
+    EditBasicConfigurationComponent,
+    EditVODTranscodingComponent,
+    EditLiveConfigurationComponent,
+    EditAdvancedConfigurationComponent,
+    EditInstanceInformationComponent
   ],
 
   exports: [
@@ -93,7 +108,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
     LogsService,
     DebugService,
     ConfigService,
-    PluginApiService
+    PluginApiService,
+    EditConfigurationService
   ]
 })
 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 (file)
index 0000000..db3036c
--- /dev/null
@@ -0,0 +1,107 @@
+<ng-container [formGroup]="form">
+
+  <div class="form-row mt-5"> <!-- cache grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">CACHE</div>
+      <div i18n class="inner-form-description">
+        Some files are not federated, and fetched when necessary. Define their caching policies.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="cache">
+        <div class="form-group" formGroupName="previews">
+          <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
+          <div class="number-with-unit">
+            <input
+              type="number" min="0" id="cachePreviewsSize" class="form-control"
+              formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
+            >
+            <span i18n>{form.value['cache']['previews']['size'], plural, =1 {cached image} other {cached images}}</span>
+          </div>
+          <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
+        </div>
+
+        <div class="form-group" formGroupName="captions">
+          <label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
+          <div class="number-with-unit">
+            <input
+              type="number" min="0" id="cacheCaptionsSize" class="form-control"
+              formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
+            >
+            <span i18n>{form.value['cache']['captions']['size'], plural, =1 {cached image} other {cached images}}</span>
+          </div>
+          <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
+        </div>
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- cache grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
+      <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
+      <div i18n class="inner-form-description">
+        Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="instance">
+        <ng-container formGroupName="customizations">
+          <div class="form-group">
+            <label i18n for="customizationJavascript">JavaScript</label>
+            <my-help>
+              <ng-template ptTemplate="customHtml">
+                <ng-container i18n>
+                  Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
+                </ng-container>
+              </ng-template>
+            </my-help>
+
+            <textarea
+              id="customizationJavascript" formControlName="javascript" class="form-control"
+              [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
+            ></textarea>
+
+            <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
+          </div>
+
+          <div class="form-group">
+            <label for="customizationCSS">CSS</label>
+
+            <my-help>
+              <ng-template ptTemplate="customHtml">
+                <ng-container i18n>
+                  Write CSS code directly. Example:<br /><br />
+<pre>
+#custom-css {{ '{' }}
+color: red;
+{{ '}' }}
+</pre>
+                  Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
+<pre>
+#custom-css .logged-in-email {{ '{' }}
+color: red;
+{{ '}' }}
+</pre>
+                </ng-container>
+              </ng-template>
+            </my-help>
+
+            <textarea
+              id="customizationCSS" formControlName="css" class="form-control"
+              [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
+            ></textarea>
+            <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
+          </div>
+        </ng-container>
+      </ng-container>
+
+    </div>
+  </div>
+
+</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 (file)
index 0000000..a37b7b7
--- /dev/null
@@ -0,0 +1,14 @@
+
+import { SelectOptionsItem } from 'src/types/select-options-item.model'
+import { Component, Input } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+
+@Component({
+  selector: 'my-edit-advanced-configuration',
+  templateUrl: './edit-advanced-configuration.component.html',
+  styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditAdvancedConfigurationComponent {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+}
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 (file)
index 0000000..ac1a11b
--- /dev/null
@@ -0,0 +1,500 @@
+<ng-container [formGroup]="form">
+  <div class="form-row mt-5"> <!-- appearance grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">APPEARANCE</div>
+      <div i18n class="inner-form-description">
+        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>.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="theme">
+        <div class="form-group">
+          <label i18n for="themeDefault">Theme</label>
+
+          <div class="peertube-select-container">
+            <select formControlName="default" id="themeDefault" class="form-control">
+              <option i18n value="default">default</option>
+
+              <option *ngFor="let theme of getAvailableThemes()" [value]="theme">{{ theme }}</option>
+            </select>
+          </div>
+        </div>
+      </ng-container>
+
+      <div class="form-group" formGroupName="instance">
+        <label i18n for="instanceDefaultClientRoute">Landing page</label>
+        <div class="peertube-select-container">
+          <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
+            <option i18n value="/videos/overview">Discover videos</option>
+
+            <optgroup i18n-label label="Trending pages">
+              <option i18n value="/videos/trending">Default trending page</option>
+              <option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
+              <option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
+              <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
+              <option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
+            </optgroup>
+
+            <option i18n value="/videos/recently-added">Recently added videos</option>
+            <option i18n value="/videos/local">Local videos</option>
+          </select>
+        </div>
+        <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
+      </div>
+
+      <div class="form-group" formGroupName="trending">
+        <ng-container formGroupName="videos">
+          <ng-container formGroupName="algorithms">
+            <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
+            <div class="peertube-select-container">
+              <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
+                <option i18n value="best">Best videos</option>
+                <option i18n value="hot">Hot videos</option>
+                <option i18n value="most-viewed">Most viewed videos</option>
+                <option i18n value="most-liked">Most liked videos</option>
+              </select>
+            </div>
+            <div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
+          </ng-container>
+        </ng-container>
+      </div>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- broadcast grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
+      <div i18n class="inner-for-description">
+        Display a message on your instance
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="broadcastMessage">
+
+        <div class="form-group">
+          <my-peertube-checkbox
+            inputName="broadcastMessageEnabled" formControlName="enabled"
+            i18n-labelText labelText="Enable broadcast message"
+          ></my-peertube-checkbox>
+        </div>
+
+        <div class="form-group">
+          <my-peertube-checkbox
+            inputName="broadcastMessageDismissable" formControlName="dismissable"
+            i18n-labelText labelText="Allow users to dismiss the broadcast message "
+          ></my-peertube-checkbox>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="broadcastMessageLevel">Broadcast message level</label>
+          <div class="peertube-select-container">
+            <select id="broadcastMessageLevel" formControlName="level" class="form-control">
+              <option value="info">info</option>
+              <option value="warning">warning</option>
+              <option value="error">error</option>
+            </select>
+          </div>
+          <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
+          <my-markdown-textarea
+            name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
+            [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
+        </div>
+
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- new users grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">NEW USERS</div>
+      <div i18n class="inner-for-description">
+        Manage <a routerLink="/admin/users">users</a> to set their quota individually.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="signup">
+        <div class="form-group">
+          <my-peertube-checkbox
+            inputName="signupEnabled" formControlName="enabled"
+            i18n-labelText labelText="Enable Signup"
+          >
+            <ng-container ngProjectAs="description">
+              <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
+
+              <div class="alert alert-info alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div>
+            </ng-container>
+
+            <ng-container ngProjectAs="extra">
+              <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }"
+                inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
+                i18n-labelText labelText="Signup requires email verification"
+              ></my-peertube-checkbox>
+
+              <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3">
+                <label i18n for="signupLimit">Signup limit</label>
+                <div class="number-with-unit">
+                  <input
+                    type="number" min="-1" id="signupLimit" class="form-control"
+                    formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
+                  >
+                  <span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
+                </div>
+                <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
+                <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small>
+              </div>
+            </ng-container>
+          </my-peertube-checkbox>
+        </div>
+      </ng-container>
+
+      <ng-container formGroupName="user">
+        <div class="form-group">
+          <label i18n for="userVideoQuota">Default video quota per user</label>
+
+          <my-select-custom-value
+            id="userVideoQuota"
+            [items]="getVideoQuotaOptions()"
+            formControlName="videoQuota"
+            i18n-inputSuffix inputSuffix="bytes" inputType="number"
+            [clearable]="false"
+          ></my-select-custom-value>
+
+          <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="userVideoQuotaDaily">Default daily upload limit per user</label>
+
+          <my-select-custom-value
+            id="userVideoQuotaDaily"
+            [items]="getVideoQuotaDailyOptions()"
+            formControlName="videoQuotaDaily"
+            i18n-inputSuffix inputSuffix="bytes" inputType="number"
+            [clearable]="false"
+          ></my-select-custom-value>
+
+          <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
+        </div>
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- videos grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">VIDEOS</div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="import">
+
+        <ng-container formGroupName="videos">
+
+          <div class="form-group mt-4">
+            <label i18n for="importConcurrency">Import jobs concurrency</label>
+            <span class="text-muted ml-1">
+              <span i18n>allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
+            </span>
+
+            <div class="number-with-unit">
+              <input type="number" name="importConcurrency" formControlName="concurrency" />
+              <span i18n>jobs in parallel</span>
+            </div>
+
+            <div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
+          </div>
+
+          <div class="form-group" formGroupName="http">
+            <my-peertube-checkbox
+              inputName="importVideosHttpEnabled" formControlName="enabled"
+              i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
+            ></my-peertube-checkbox>
+          </div>
+
+          <div class="form-group" formGroupName="torrent">
+            <my-peertube-checkbox
+              inputName="importVideosTorrentEnabled" formControlName="enabled"
+              i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
+            ></my-peertube-checkbox>
+          </div>
+
+        </ng-container>
+      </ng-container>
+
+      <ng-container formGroupName="autoBlacklist">
+        <ng-container formGroupName="videos">
+          <ng-container formGroupName="ofUsers">
+
+            <div class="form-group">
+              <my-peertube-checkbox
+                inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
+                i18n-labelText labelText="Block new videos automatically"
+              >
+              <ng-container ngProjectAs="description">
+                <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
+              </ng-container>
+            </my-peertube-checkbox>
+            </div>
+
+          </ng-container>
+        </ng-container>
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- search grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">SEARCH</div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="search">
+        <ng-container formGroupName="remoteUri">
+
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="searchRemoteUriUsers" formControlName="users"
+              i18n-labelText labelText="Allow users to do remote URI/handle search"
+            >
+              <ng-container ngProjectAs="description">
+                <span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
+              </ng-container>
+            </my-peertube-checkbox>
+          </div>
+
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="searchRemoteUriAnonymous" formControlName="anonymous"
+              i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
+            >
+              <ng-container ngProjectAs="description">
+                <span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
+              </ng-container>
+            </my-peertube-checkbox>
+          </div>
+
+        </ng-container>
+
+        <ng-container formGroupName="searchIndex">
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="searchIndexEnabled" formControlName="enabled"
+              i18n-labelText labelText="Enable global search"
+            >
+              <ng-container ngProjectAs="description">
+                <p i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</p>
+
+                <span i18n>
+                  You should only use moderated search indexes in production, or <a href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
+                </span>
+              </ng-container>
+
+              <ng-container ngProjectAs="extra">
+                <div [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }">
+                  <label i18n for="searchIndexUrl">Search index URL</label>
+                  <input
+                    type="text"  id="searchIndexUrl" class="form-control"
+                    formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
+                  >
+                  <div *ngIf="formErrors.search.searchIndex.url" class="form-error">{{ formErrors.search.searchIndex.url }}</div>
+                </div>
+
+                <div class="mt-3">
+                  <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
+                    inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
+                    i18n-labelText labelText="Disable local search in search bar"
+                  ></my-peertube-checkbox>
+                </div>
+
+                <div class="mt-3">
+                  <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
+                    inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
+                    i18n-labelText labelText="Search bar uses the global search index by default"
+                  >
+                    <ng-container ngProjectAs="description">
+                      <span i18n>Otherwise the local search stays used by default</span>
+                    </ng-container>
+                  </my-peertube-checkbox>
+                </div>
+
+              </ng-container>
+            </my-peertube-checkbox>
+          </div>
+
+        </ng-container>
+
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- federation grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">FEDERATION</div>
+      <div i18n class="inner-form-description">
+        Manage <a routerLink="/admin/follows">relations</a> with other instances.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="followers">
+        <ng-container formGroupName="instance">
+
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="followersInstanceEnabled" formControlName="enabled"
+              i18n-labelText labelText="Other instances can follow yours"
+            ></my-peertube-checkbox>
+          </div>
+
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="followersInstanceManualApproval" formControlName="manualApproval"
+              i18n-labelText labelText="Manually approve new instance followers"
+            ></my-peertube-checkbox>
+          </div>
+        </ng-container>
+      </ng-container>
+
+      <ng-container formGroupName="followings">
+        <ng-container formGroupName="instance">
+
+          <ng-container formGroupName="autoFollowBack">
+            <div class="form-group">
+              <my-peertube-checkbox
+                inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
+                i18n-labelText labelText="Automatically follow back instances"
+              >
+                <ng-container ngProjectAs="description">
+                  <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
+                </ng-container>
+              </my-peertube-checkbox>
+            </div>
+          </ng-container>
+
+          <ng-container formGroupName="autoFollowIndex">
+            <div class="form-group">
+              <my-peertube-checkbox
+                inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
+                i18n-labelText labelText="Automatically follow instances of a public index"
+              >
+                <ng-container ngProjectAs="description">
+                  <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p>
+
+                  <span i18n>
+                    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>.
+                  </span>
+                </ng-container>
+
+                <ng-container ngProjectAs="extra">
+                  <div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
+                    <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
+                    <input
+                      type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
+                      formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
+                    >
+                    <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
+                  </div>
+                </ng-container>
+              </my-peertube-checkbox>
+            </div>
+
+          </ng-container>
+        </ng-container>
+      </ng-container>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- administrators grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">ADMINISTRATORS</div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <div class="form-group" formGroupName="admin">
+        <label i18n for="adminEmail">Admin email</label>
+        <input
+          type="text" id="adminEmail" class="form-control"
+          formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
+        >
+        <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
+      </div>
+
+      <div class="form-group" formGroupName="contactForm">
+        <my-peertube-checkbox
+          inputName="enableContactForm" formControlName="enabled"
+          i18n-labelText labelText="Enable contact form"
+        ></my-peertube-checkbox>
+      </div>
+
+    </div>
+  </div>
+
+  <div class="form-row mt-4"> <!-- Twitter grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">TWITTER</div>
+      <div i18n class="inner-form-description">
+        Provide the Twitter account representing your instance to improve link previews.
+        If you don't have a Twitter account, just leave the default value.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="services">
+        <ng-container formGroupName="twitter">
+
+          <div class="form-group">
+            <label i18n for="signupLimit">Your Twitter username</label>
+
+            <input
+              type="text" id="servicesTwitterUsername" class="form-control"
+              formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
+            >
+            <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
+          </div>
+
+          <div class="form-group">
+            <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
+              <ng-template ptTemplate="label">
+                <ng-container i18n>Instance allowed by Twitter</ng-container>
+              </ng-template>
+
+              <ng-template ptTemplate="help">
+                <ng-container i18n>
+                  If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
+                  If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
+                  Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
+                  <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
+                  to see if you instance is allowed.
+                </ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
+          </div>
+
+        </ng-container>
+      </ng-container>
+
+    </div>
+  </div>
+</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 (file)
index 0000000..9a19c29
--- /dev/null
@@ -0,0 +1,83 @@
+
+import { pairwise } from 'rxjs/operators'
+import { Component, Input, OnInit } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+import { ServerConfig } from '@shared/models'
+import { ConfigService } from '../shared/config.service'
+
+@Component({
+  selector: 'my-edit-basic-configuration',
+  templateUrl: './edit-basic-configuration.component.html',
+  styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditBasicConfigurationComponent implements OnInit {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+
+  @Input() serverConfig: ServerConfig
+
+  signupAlertMessage: string
+
+  constructor (
+    private configService: ConfigService
+  ) { }
+
+  ngOnInit () {
+    this.checkSignupField()
+  }
+
+  getVideoQuotaOptions () {
+    return this.configService.videoQuotaOptions
+  }
+
+  getVideoQuotaDailyOptions () {
+    return this.configService.videoQuotaDailyOptions
+  }
+
+  getAvailableThemes () {
+    return this.serverConfig.theme.registered
+      .map(t => t.name)
+  }
+
+  doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
+    const enabled = this.form.value['trending']['videos']['algorithms']['enabled']
+    if (!Array.isArray(enabled)) return false
+
+    return !!enabled.find((e: string) => e === algorithm)
+  }
+
+  isSignupEnabled () {
+    return this.form.value['signup']['enabled'] === true
+  }
+
+  isSearchIndexEnabled () {
+    return this.form.value['search']['searchIndex']['enabled'] === true
+  }
+
+  isAutoFollowIndexEnabled () {
+    return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
+  }
+
+  private checkSignupField () {
+    const signupControl = this.form.get('signup.enabled')
+
+    signupControl.valueChanges
+      .pipe(pairwise())
+      .subscribe(([ oldValue, newValue ]) => {
+        if (oldValue !== true && newValue === true) {
+          // tslint:disable:max-line-length
+          this.signupAlertMessage = $localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
+
+          this.form.patchValue({
+            autoBlacklist: {
+              videos: {
+                ofUsers: {
+                  enabled: true
+                }
+              }
+            }
+          })
+        }
+      })
+  }
+}
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 (file)
index 0000000..63e0343
--- /dev/null
@@ -0,0 +1,90 @@
+import { Injectable } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+
+export type ResolutionOption = {
+  id: string
+  label: string
+  description?: string
+}
+
+@Injectable()
+export class EditConfigurationService {
+
+  getVODResolutions () {
+    return [
+      {
+        id: '0p',
+        label: $localize`Audio-only`,
+        description: $localize`A <code>.mp4</code> that keeps the original audio track, with no video`
+      },
+      {
+        id: '240p',
+        label: $localize`240p`
+      },
+      {
+        id: '360p',
+        label: $localize`360p`
+      },
+      {
+        id: '480p',
+        label: $localize`480p`
+      },
+      {
+        id: '720p',
+        label: $localize`720p`
+      },
+      {
+        id: '1080p',
+        label: $localize`1080p`
+      },
+      {
+        id: '1440p',
+        label: $localize`1440p`
+      },
+      {
+        id: '2160p',
+        label: $localize`2160p`
+      }
+    ]
+  }
+
+  getLiveResolutions () {
+    return this.getVODResolutions().filter(r => r.id !== '0p')
+  }
+
+  isTranscodingEnabled (form: FormGroup) {
+    return form.value['transcoding']['enabled'] === true
+  }
+
+  isLiveEnabled (form: FormGroup) {
+    return form.value['live']['enabled'] === true
+  }
+
+  isLiveTranscodingEnabled (form: FormGroup) {
+    return form.value['live']['transcoding']['enabled'] === true
+  }
+
+  getTotalTranscodingThreads (form: FormGroup) {
+    const transcodingEnabled = form.value['transcoding']['enabled']
+    const transcodingThreads = form.value['transcoding']['threads']
+    const liveTranscodingEnabled = form.value['live']['transcoding']['enabled']
+    const liveTranscodingThreads = form.value['live']['transcoding']['threads']
+
+    // checks whether all enabled method are on fixed values and not on auto (= 0)
+    let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
+    noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
+
+    // count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
+    let value = 0
+    if (transcodingEnabled) value += +transcodingThreads || 1
+    if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
+
+    return {
+      value,
+      atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
+      unit: value > 1
+        ? $localize`threads`
+        : $localize`thread`
+    }
+  }
+}
index 1352761922d7814190e6f07016a39aa7678f2565..534b03517b4810b12d3490b88c5ddb8129eda6c0 100644 (file)
@@ -7,231 +7,8 @@
       <a ngbNavLink i18n>Instance information</a>
 
       <ng-template ngbNavContent>
-
-        <ng-container formGroupName="instance">
-
-          <div class="form-row mt-5"> <!-- instance grid -->
-            <div class="form-group col-12 col-lg-4 col-xl-3">
-              <div i18n class="inner-form-title">INSTANCE</div>
-            </div>
-
-            <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-              <div class="form-group">
-                <label i18n for="instanceName">Name</label>
-                <input
-                  type="text" id="instanceName" class="form-control"
-                  formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
-                >
-                <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceShortDescription">Short description</label>
-                <textarea
-                  id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
-                  [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
-                ></textarea>
-                <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
-                <my-markdown-textarea
-                  name="instanceDescription" formControlName="description" textareaMaxWidth="500px"
-                  [classes]="{ 'input-error': formErrors['instance.description'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceCategories">Main instance categories</label>
-
-                <div>
-                  <my-select-checkbox
-                    id="instanceCategories"
-                    formControlName="categories" [availableItems]="categoryItems"
-                    [selectableGroup]="false"
-                    i18n-placeholder placeholder="Add a new category"
-                  >
-                  </my-select-checkbox>
-                </div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
-
-                <div>
-                  <my-select-checkbox
-                    id="instanceLanguages"
-                    formControlName="languages" [availableItems]="languageItems"
-                    [selectableGroup]="false"
-                    i18n-placeholder placeholder="Add a new language"
-                  >
-                  </my-select-checkbox>
-                </div>
-              </div>
-
-            </div>
-          </div>
-
-          <div class="form-row mt-4"> <!-- moderation & nsfw grid -->
-            <div class="form-group col-12 col-lg-4 col-xl-3">
-              <div i18n class="inner-form-title">MODERATION & NSFW</div>
-              <div i18n class="inner-for-description">
-                Manage <a routerLink="/admin/users">users</a> to build a moderation team.
-              </div>
-            </div>
-
-            <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-              <div class="form-group">
-                <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
-                  <ng-template ptTemplate="label">
-                    <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
-                  </ng-template>
-
-                  <ng-template ptTemplate="help">
-                    <ng-container i18n>
-                      Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
-                      Moreover, the NSFW checkbox on video upload will be automatically checked by default.
-                    </ng-container>
-                  </ng-template>
-                </my-peertube-checkbox>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
-
-                <my-help>
-                  <ng-template ptTemplate="customHtml">
-                    <ng-container i18n>
-                      With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
-                    </ng-container>
-                  </ng-template>
-                </my-help>
-
-                <div class="peertube-select-container">
-                  <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy" class="form-control">
-                    <option i18n value="undefined" disabled>Policy for sensitive videos</option>
-                    <option i18n value="do_not_list">Do not list</option>
-                    <option i18n value="blur">Blur thumbnails</option>
-                    <option i18n value="display">Display</option>
-                  </select>
-                </div>
-                <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
-                <my-markdown-textarea
-                  name="instanceTerms" formControlName="terms" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
-                <my-markdown-textarea
-                  name="instanceCodeOfConduct" formControlName="codeOfConduct" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
-                <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
-
-                <my-markdown-textarea
-                  name="instanceModerationInformation" formControlName="moderationInformation" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
-              </div>
-
-            </div>
-          </div>
-
-          <div class="form-row mt-4"> <!-- you and your instance grid -->
-            <div class="form-group col-12 col-lg-4 col-xl-3">
-              <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
-            </div>
-
-            <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-              <div class="form-group">
-                <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
-                <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
-
-                <my-markdown-textarea
-                  name="instanceAdministrator" formControlName="administrator" textareaMaxWidth="500px"
-                  [classes]="{ 'input-error': formErrors['instance.administrator'] }"
-                ></my-markdown-textarea>
-
-                <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
-                <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
-
-                <my-markdown-textarea
-                  name="instanceCreationReason" formControlName="creationReason" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
-                <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
-
-                <my-markdown-textarea
-                  name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
-                <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
-
-                <my-markdown-textarea
-                  name="instanceBusinessModel" formControlName="businessModel" textareaMaxWidth="500px"
-                  [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
-              </div>
-
-            </div>
-          </div>
-
-          <div class="form-row mt-4"> <!-- other information grid -->
-            <div class="form-group col-12 col-lg-4 col-xl-3">
-              <div i18n class="inner-form-title">OTHER INFORMATION</div>
-            </div>
-
-            <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-              <div class="form-group">
-                <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
-                <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
-
-                <my-markdown-textarea
-                  name="instanceHardwareInformation" formControlName="hardwareInformation" textareaMaxWidth="500px"
-                  [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
-                ></my-markdown-textarea>
-
-                <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
-              </div>
-
-            </div>
-          </div>
-
-        </ng-container>
+        <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
+        </my-edit-instance-information>
       </ng-template>
     </ng-container>
 
       <a ngbNavLink i18n>Basic configuration</a>
 
       <ng-template ngbNavContent>
-
-        <div class="form-row mt-5"> <!-- appearance grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">APPEARANCE</div>
-            <div i18n class="inner-form-description">
-              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>.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="theme">
-              <div class="form-group">
-                <label i18n for="themeDefault">Theme</label>
-
-                <div class="peertube-select-container">
-                  <select formControlName="default" id="themeDefault" class="form-control">
-                    <option i18n value="default">default</option>
-
-                    <option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
-                  </select>
-                </div>
-              </div>
-            </ng-container>
-
-            <div class="form-group" formGroupName="instance">
-              <label i18n for="instanceDefaultClientRoute">Landing page</label>
-              <div class="peertube-select-container">
-                <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
-                  <option i18n value="/videos/overview">Discover videos</option>
-                  <optgroup i18n-label label="Trending pages">
-                    <option i18n value="/videos/trending">Default trending page</option>
-                    <option i18n value="/videos/trending?alg=best" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('best')">Best videos</option>
-                    <option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
-                    <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
-                    <option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
-                  </optgroup>
-                  <option i18n value="/videos/recently-added">Recently added videos</option>
-                  <option i18n value="/videos/local">Local videos</option>
-                </select>
-              </div>
-              <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
-            </div>
-
-            <div class="form-group" formGroupName="trending">
-              <ng-container formGroupName="videos">
-                <ng-container formGroupName="algorithms">
-                  <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
-                  <div class="peertube-select-container">
-                    <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
-                      <option i18n value="best">Best videos</option>
-                      <option i18n value="hot">Hot videos</option>
-                      <option i18n value="most-viewed">Most viewed videos</option>
-                      <option i18n value="most-liked">Most liked videos</option>
-                    </select>
-                  </div>
-                  <div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
-                </ng-container>
-              </ng-container>
-            </div>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- broadcast grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
-            <div i18n class="inner-for-description">
-              Display a message on your instance
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="broadcastMessage">
-
-              <div class="form-group">
-                <my-peertube-checkbox
-                  inputName="broadcastMessageEnabled" formControlName="enabled"
-                  i18n-labelText labelText="Enable broadcast message"
-                ></my-peertube-checkbox>
-              </div>
-
-              <div class="form-group">
-                <my-peertube-checkbox
-                  inputName="broadcastMessageDismissable" formControlName="dismissable"
-                  i18n-labelText labelText="Allow users to dismiss the broadcast message "
-                ></my-peertube-checkbox>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="broadcastMessageLevel">Broadcast message level</label>
-                <div class="peertube-select-container">
-                  <select id="broadcastMessageLevel" formControlName="level" class="form-control">
-                    <option value="info">info</option>
-                    <option value="warning">warning</option>
-                    <option value="error">error</option>
-                  </select>
-                </div>
-                <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
-                <my-markdown-textarea
-                  name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
-                  [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
-                ></my-markdown-textarea>
-                <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
-              </div>
-
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- new users grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">NEW USERS</div>
-            <div i18n class="inner-for-description">
-              Manage <a routerLink="/admin/users">users</a> to set their quota individually.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="signup">
-              <div class="form-group">
-                <my-peertube-checkbox
-                  inputName="signupEnabled" formControlName="enabled"
-                  i18n-labelText labelText="Enable Signup"
-                >
-                  <ng-container ngProjectAs="description">
-                    <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
-
-                    <div class="alert alert-info alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</div>
-                  </ng-container>
-                  <ng-container ngProjectAs="extra">
-                    <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }"
-                      inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
-                      i18n-labelText labelText="Signup requires email verification"
-                    ></my-peertube-checkbox>
-
-                    <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3">
-                      <label i18n for="signupLimit">Signup limit</label>
-                      <div class="number-with-unit">
-                        <input
-                          type="number" min="-1" id="signupLimit" class="form-control"
-                          formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
-                        >
-                        <span i18n>{form.value['signup']['limit'], plural, =1 {user} other {users}}</span>
-                      </div>
-                      <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
-                      <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small>
-                    </div>
-                  </ng-container>
-                </my-peertube-checkbox>
-              </div>
-            </ng-container>
-
-            <ng-container formGroupName="user">
-              <div class="form-group">
-                <label i18n for="userVideoQuota">Default video quota per user</label>
-
-                <my-select-custom-value
-                  id="userVideoQuota"
-                  [items]="videoQuotaOptions"
-                  formControlName="videoQuota"
-                  i18n-inputSuffix inputSuffix="bytes" inputType="number"
-                  [clearable]="false"
-                ></my-select-custom-value>
-
-                <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
-              </div>
-
-              <div class="form-group">
-                <label i18n for="userVideoQuotaDaily">Default daily upload limit per user</label>
-
-                <my-select-custom-value
-                  id="userVideoQuotaDaily"
-                  [items]="videoQuotaDailyOptions"
-                  formControlName="videoQuotaDaily"
-                  i18n-inputSuffix inputSuffix="bytes" inputType="number"
-                  [clearable]="false"
-                ></my-select-custom-value>
-
-                <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
-              </div>
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- videos grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">VIDEOS</div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="import">
-
-              <ng-container formGroupName="videos">
-
-                <div class="form-group mt-4">
-                  <label i18n for="importConcurrency">Import jobs concurrency</label>
-                  <span class="text-muted ml-1">
-                    <span i18n>allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
-                  </span>
-
-                  <div class="number-with-unit">
-                    <input type="number" name="importConcurrency" formControlName="concurrency" />
-                    <span i18n>jobs in parallel</span>
-                  </div>
-
-                  <div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
-                </div>
-
-                <div class="form-group" formGroupName="http">
-                  <my-peertube-checkbox
-                    inputName="importVideosHttpEnabled" formControlName="enabled"
-                    i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
-                  ></my-peertube-checkbox>
-                </div>
-
-                <div class="form-group" formGroupName="torrent">
-                  <my-peertube-checkbox
-                    inputName="importVideosTorrentEnabled" formControlName="enabled"
-                    i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
-                  ></my-peertube-checkbox>
-                </div>
-
-              </ng-container>
-            </ng-container>
-
-            <ng-container formGroupName="autoBlacklist">
-              <ng-container formGroupName="videos">
-                <ng-container formGroupName="ofUsers">
-
-                  <div class="form-group">
-                    <my-peertube-checkbox
-                      inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
-                      i18n-labelText labelText="Block new videos automatically"
-                    >
-                    <ng-container ngProjectAs="description">
-                      <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
-                    </ng-container>
-                  </my-peertube-checkbox>
-                  </div>
-
-                </ng-container>
-              </ng-container>
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- search grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">SEARCH</div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="search">
-              <ng-container formGroupName="remoteUri">
-
-                <div class="form-group">
-                  <my-peertube-checkbox
-                    inputName="searchRemoteUriUsers" formControlName="users"
-                    i18n-labelText labelText="Allow users to do remote URI/handle search"
-                  >
-                    <ng-container ngProjectAs="description">
-                      <span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
-                    </ng-container>
-                  </my-peertube-checkbox>
-                </div>
-
-                <div class="form-group">
-                  <my-peertube-checkbox
-                    inputName="searchRemoteUriAnonymous" formControlName="anonymous"
-                    i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
-                  >
-                    <ng-container ngProjectAs="description">
-                      <span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
-                    </ng-container>
-                  </my-peertube-checkbox>
-                </div>
-
-              </ng-container>
-
-              <ng-container formGroupName="searchIndex">
-                <div class="form-group">
-                  <my-peertube-checkbox
-                    inputName="searchIndexEnabled" formControlName="enabled"
-                    i18n-labelText labelText="Enable global search"
-                  >
-                    <ng-container ngProjectAs="description">
-                      <p i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</p>
-
-                      <span i18n>
-                        You should only use moderated search indexes in production, or <a href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
-                      </span>
-                    </ng-container>
-
-                    <ng-container ngProjectAs="extra">
-                      <div [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }">
-                        <label i18n for="searchIndexUrl">Search index URL</label>
-                        <input
-                          type="text"  id="searchIndexUrl" class="form-control"
-                          formControlName="url" [ngClass]="{ 'input-error': formErrors['search.searchIndex.url'] }"
-                        >
-                        <div *ngIf="formErrors.search.searchIndex.url" class="form-error">{{ formErrors.search.searchIndex.url }}</div>
-                      </div>
-
-                      <div class="mt-3">
-                        <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
-                          inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
-                          i18n-labelText labelText="Disable local search in search bar"
-                        ></my-peertube-checkbox>
-                      </div>
-
-                      <div class="mt-3">
-                        <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSearchIndexEnabled() }"
-                          inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
-                          i18n-labelText labelText="Search bar uses the global search index by default"
-                        >
-                          <ng-container ngProjectAs="description">
-                            <span i18n>Otherwise the local search stays used by default</span>
-                          </ng-container>
-                        </my-peertube-checkbox>
-                      </div>
-
-                    </ng-container>
-                  </my-peertube-checkbox>
-                </div>
-
-              </ng-container>
-
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- federation grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">FEDERATION</div>
-            <div i18n class="inner-form-description">
-              Manage <a routerLink="/admin/follows">relations</a> with other instances.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="followers">
-              <ng-container formGroupName="instance">
-
-                <div class="form-group">
-                  <my-peertube-checkbox
-                    inputName="followersInstanceEnabled" formControlName="enabled"
-                    i18n-labelText labelText="Other instances can follow yours"
-                  ></my-peertube-checkbox>
-                </div>
-
-                <div class="form-group">
-                  <my-peertube-checkbox
-                    inputName="followersInstanceManualApproval" formControlName="manualApproval"
-                    i18n-labelText labelText="Manually approve new instance followers"
-                  ></my-peertube-checkbox>
-                </div>
-              </ng-container>
-            </ng-container>
-
-            <ng-container formGroupName="followings">
-              <ng-container formGroupName="instance">
-
-                <ng-container formGroupName="autoFollowBack">
-                  <div class="form-group">
-                    <my-peertube-checkbox
-                      inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
-                      i18n-labelText labelText="Automatically follow back instances"
-                    >
-                      <ng-container ngProjectAs="description">
-                        <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
-                      </ng-container>
-                    </my-peertube-checkbox>
-                  </div>
-                </ng-container>
-
-                <ng-container formGroupName="autoFollowIndex">
-                  <div class="form-group">
-                    <my-peertube-checkbox
-                      inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
-                      i18n-labelText labelText="Automatically follow instances of a public index"
-                    >
-                      <ng-container ngProjectAs="description">
-                        <p i18n>⚠️ This functionality requires a lot of attention and extra moderation.</p>
-
-                        <span i18n>
-                          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>.
-                        </span>
-                      </ng-container>
-
-                      <ng-container ngProjectAs="extra">
-                        <div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
-                          <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
-                          <input
-                            type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
-                            formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
-                          >
-                          <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
-                        </div>
-                      </ng-container>
-                    </my-peertube-checkbox>
-                  </div>
-
-                </ng-container>
-              </ng-container>
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- administrators grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">ADMINISTRATORS</div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <div class="form-group" formGroupName="admin">
-              <label i18n for="adminEmail">Admin email</label>
-              <input
-                type="text" id="adminEmail" class="form-control"
-                formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
-              >
-              <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
-            </div>
-
-            <div class="form-group" formGroupName="contactForm">
-              <my-peertube-checkbox
-                inputName="enableContactForm" formControlName="enabled"
-                i18n-labelText labelText="Enable contact form"
-              ></my-peertube-checkbox>
-            </div>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- Twitter grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">TWITTER</div>
-            <div i18n class="inner-form-description">
-              Provide the Twitter account representing your instance to improve link previews.
-              If you don't have a Twitter account, just leave the default value.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="services">
-              <ng-container formGroupName="twitter">
-
-                <div class="form-group">
-                  <label i18n for="signupLimit">Your Twitter username</label>
-
-                  <input
-                    type="text" id="servicesTwitterUsername" class="form-control"
-                    formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
-                  >
-                  <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
-                </div>
-
-                <div class="form-group">
-                  <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
-                    <ng-template ptTemplate="label">
-                      <ng-container i18n>Instance allowed by Twitter</ng-container>
-                    </ng-template>
-
-                    <ng-template ptTemplate="help">
-                      <ng-container i18n>
-                        If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
-                        If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
-                        Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
-                        <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
-                        to see if you instance is allowed.
-                      </ng-container>
-                    </ng-template>
-                  </my-peertube-checkbox>
-                </div>
-
-              </ng-container>
-            </ng-container>
-
-          </div>
-        </div>
+        <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
+        </my-edit-basic-configuration>
       </ng-template>
     </ng-container>
 
       <a ngbNavLink i18n>VOD Transcoding</a>
 
       <ng-template ngbNavContent>
-
-        <div class="form-row mt-4"> <!-- transcoding grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3"></div>
-          <div class="form-group form-group-right col-12 col-lg-8">
-
-            <div class="callout callout-info">
-              <span i18n>
-                Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
-              </span>
-              <span i18n>
-                However, you may want to read our guidelines before tweaking the following values.
-              </span>
-
-              <div class="callout-container">
-                <a class="callout-link" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/#/admin-configuration?id=transcoding" i18n>Read guidelines</a>
-              </div>
-            </div>
-
-
-          </div>
-        </div>
-
-        <div class="form-row mt-2"> <!-- transcoding grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">TRANSCODING</div>
-            <div i18n class="inner-form-description">
-              Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
-              resources, this is a critical part of PeerTube, so tread carefully.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="transcoding">
-
-              <div class="form-group mb-0 col-12 col-xl-11">
-                <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
-                  <ng-template ptTemplate="label">
-                    <ng-container i18n>Transcoding enabled</ng-container>
-                  </ng-template>
-
-                  <ng-container ngProjectAs="extra">
-
-                    <div class="callout callout-light pt-2 pb-0">
-                      <label i18n>Input formats</label>
-
-                      <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                        <my-peertube-checkbox
-                          inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
-                          i18n-labelText labelText="Allow additional extensions"
-                        >
-                          <ng-container ngProjectAs="description">
-                            <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, .m2ts, .mxf, or .nut videos.</span>
-                          </ng-container>
-                        </my-peertube-checkbox>
-                      </div>
-
-                      <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                        <my-peertube-checkbox
-                          inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
-                          i18n-labelText labelText="Allow audio files upload"
-                        >
-                          <ng-container ngProjectAs="description">
-                            <div i18n>Allows users to upload .mp3, .ogg, .wma, .flac, .aac, or .ac3 audio files.</div>
-                            <div i18n>The file will be merged in a still image video with the preview file on upload.</div>
-                          </ng-container>
-                        </my-peertube-checkbox>
-                      </div>
-                    </div>
-
-                    <div class="callout callout-light pt-2 mt-2 pb-0">
-                      <label i18n>Output formats</label>
-
-                      <ng-container formGroupName="webtorrent">
-                        <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                          <my-peertube-checkbox
-                            inputName="transcodingWebTorrentEnabled" formControlName="enabled"
-                            i18n-labelText labelText="WebTorrent enabled"
-                          >
-                            <ng-template ptTemplate="help">
-                              <ng-container i18n>
-                                <p>If you also enabled HLS support, it will multiply videos storage by 2</p>
-
-                                <br />
-
-                                <strong>If disabled, breaks federation with PeerTube instances < 2.1</strong>
-                              </ng-container>
-                            </ng-template>
-                          </my-peertube-checkbox>
-                        </div>
-                      </ng-container>
-
-                      <ng-container formGroupName="hls">
-                        <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                          <my-peertube-checkbox
-                            inputName="transcodingHlsEnabled" formControlName="enabled"
-                            i18n-labelText labelText="HLS with P2P support enabled"
-                            [recommended]="true"
-                          >
-                            <ng-template ptTemplate="help">
-                              <ng-container i18n>
-                                <strong>Requires ffmpeg >= 4.1</strong>
-
-                                <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p>
-                                <ul>
-                                  <li>Resolution change is smoother</li>
-                                  <li>Faster playback especially with long videos</li>
-                                  <li>More stable playback (less bugs/infinite loading)</li>
-                                </ul>
-
-                                <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p>
-                              </ng-container>
-                            </ng-template>
-                          </my-peertube-checkbox>
-                        </div>
-                      </ng-container>
-
-                      <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                        <label i18n>Resolutions to generate per enabled format</label>
-
-                        <div class="ml-2 mt-2 d-flex flex-column">
-                          <ng-container formGroupName="resolutions">
-                            <div class="form-group" *ngFor="let resolution of resolutions">
-                              <my-peertube-checkbox
-                                [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
-                                labelText="{{ resolution.label }}"
-                              >
-                                <ng-template *ngIf="resolution.description" ptTemplate="help">
-                                  <div [innerHTML]="resolution.description"></div>
-                                </ng-template>
-                              </my-peertube-checkbox>
-                            </div>
-
-                            <span class="mb-2 text-muted" i18n>
-                              The original file resolution will be the default target if no option is selected.
-                            </span>
-                          </ng-container>
-                        </div>
-                      </div>
-                    </div>
-
-                  </ng-container>
-                </my-peertube-checkbox>
-              </div>
-
-              <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                <label i18n for="transcodingThreads">Transcoding threads</label>
-                <span class="text-muted ml-1">
-                  <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
-                  <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
-                </span>
-
-                <my-select-custom-value
-                  id="transcodingThreads"
-                  [items]="transcodingThreadOptions"
-                  formControlName="threads"
-                  [clearable]="false"
-                ></my-select-custom-value>
-
-                <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
-              </div>
-
-              <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
-                <span class="text-muted ml-1">
-                  <span i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart.</span>
-                </span>
-
-                <div class="number-with-unit">
-                  <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
-                  <span i18n>jobs in parallel</span>
-                </div>
-
-                <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
-              </div>
-
-              <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
-                <label i18n for="transcodingProfile">Transcoding profile</label>
-                <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
-
-                <my-select-options
-                  id="transcodingProfile"
-                  formControlName="profile"
-                  [items]="getAvailableTranscodingProfile('vod')"
-                  [clearable]="false"
-                >
-                  <ng-template ng-option-tmp let-item="item" let-index="index">
-                    {{ item }}
-                    <ng-container *ngIf="item === 'default'">
-                      <br>
-                      <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
-                    </ng-container>
-                  </ng-template>
-                </my-select-options>
-                <div *ngIf="formErrors.transcoding.profile" class="form-error">{{ formErrors.transcoding.profile }}</div>
-              </div>
-
-            </ng-container>
-
-          </div>
-        </div>
-
+        <my-edit-vod-transcoding [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
+        </my-edit-vod-transcoding>
       </ng-template>
     </ng-container>
 
       <a ngbNavLink i18n>Live streaming</a>
 
       <ng-template ngbNavContent>
-
-        <div class="form-row mt-5">
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">LIVE</div>
-            <div i18n class="inner-form-description">
-              Enable users of your instance to stream live.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="live">
-
-              <div class="form-group">
-                <my-peertube-checkbox inputName="liveEnabled" formControlName="enabled">
-                  <ng-template ptTemplate="label">
-                    <ng-container i18n>Allow live streaming</ng-container>
-                  </ng-template>
-
-                  <ng-container ngProjectAs="description">
-                    <div i18n>⚠️ Enabling live streaming requires trust in your users and extra moderation work</div>
-                    <div i18n>If enabled, your server needs to accept incoming TCP traffic on port {{ liveRTMPPort }}</div>
-                  </ng-container>
-
-                  <ng-container ngProjectAs="extra">
-
-                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
-                      <my-peertube-checkbox
-                        inputName="liveAllowReplay" formControlName="allowReplay"
-                        i18n-labelText labelText="Allow your users to automatically publish a replay of their live"
-                      >
-                        <ng-container ngProjectAs="description" i18n>
-                          If the user quota is reached, PeerTube will automatically terminate the live streaming
-                        </ng-container>
-                      </my-peertube-checkbox>
-                    </div>
-
-                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
-                      <label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span></label>
-                      <div class="number-with-unit">
-                        <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
-                        <span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
-                      </div>
-                    </div>
-
-                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
-                      <label i18n for="liveMaxUserLives">Max simultaneous lives created per user <span class="text-muted">(-1 for "unlimited")</span></label>
-                      <div class="number-with-unit">
-                        <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
-                        <span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
-                      </div>
-                    </div>
-
-                    <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
-                      <label i18n for="liveMaxDuration">Max live duration</label>
-
-                      <my-select-options
-                        labelForId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"
-                        bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
-                      ></my-select-options>
-                    </div>
-
-                  </ng-container>
-                </my-peertube-checkbox>
-              </div>
-            </ng-container>
-          </div>
-        </div>
-
-        <div class="form-row"> <!-- transcoding live streams grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">TRANSCODING</div>
-            <div i18n class="inner-form-description">
-              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.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="live">
-              <ng-container formGroupName="transcoding">
-
-                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
-                  <my-peertube-checkbox
-                    inputName="liveTranscodingEnabled" formControlName="enabled"
-                  >
-                    <ng-template ptTemplate="label">
-                      <ng-container i18n>Transcoding enabled for live streams</ng-container>
-                    </ng-template>
-                  </my-peertube-checkbox>
-                </div>
-
-                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
-                  <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
-
-                  <div class="ml-2 mt-2 d-flex flex-column">
-                    <ng-container formGroupName="resolutions">
-                      <div class="form-group" *ngFor="let resolution of liveResolutions">
-                        <my-peertube-checkbox
-                          [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
-                          labelText="{{resolution.label}}"
-                        >
-                          <ng-template *ngIf="resolution.description" ptTemplate="help">
-                            <div [innerHTML]="resolution.description"></div>
-                          </ng-template>
-                        </my-peertube-checkbox>
-                      </div>
-                    </ng-container>
-                  </div>
-                </div>
-
-                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
-                  <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
-                  <span class="text-muted ml-1">
-                    <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
-                    <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
-                  </span>
-
-                  <my-select-custom-value
-                    id="liveTranscodingThreads"
-                    [items]="transcodingThreadOptions"
-                    formControlName="threads"
-                    [clearable]="false"
-                  ></my-select-custom-value>
-                  <div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div>
-                </div>
-
-                <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
-                  <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
-                  <span class="text-muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
-
-                  <my-select-options
-                    id="liveTranscodingProfile"
-                    formControlName="profile"
-                    [items]="getAvailableTranscodingProfile('live')"
-                    [clearable]="false"
-                  >
-                    <ng-template ng-option-tmp let-item="item" let-index="index">
-                      {{ item }}
-                      <ng-container *ngIf="item === 'default'">
-                        <br>
-                        <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
-                      </ng-container>
-                    </ng-template>
-                  </my-select-options>
-                  <div *ngIf="formErrors.live.transcoding.profile" class="form-error">{{ formErrors.live.transcoding.profile }}</div>
-                </div>
-
-              </ng-container>
-            </ng-container>
-
-          </div>
-        </div>
-
+        <my-edit-live-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
+        </my-edit-live-configuration>
       </ng-template>
     </ng-container>
 
       <a ngbNavLink i18n>Advanced configuration</a>
 
       <ng-template ngbNavContent>
-
-        <div class="form-row mt-5"> <!-- cache grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div i18n class="inner-form-title">CACHE</div>
-            <div i18n class="inner-form-description">
-              Some files are not federated, and fetched when necessary. Define their caching policies.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="cache">
-              <div class="form-group" formGroupName="previews">
-                <label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
-                <div class="number-with-unit">
-                  <input
-                    type="number" min="0" id="cachePreviewsSize" class="form-control"
-                    formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
-                  >
-                  <span i18n>{form.value['cache']['previews']['size'], plural, =1 {cached image} other {cached images}}</span>
-                </div>
-                <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
-              </div>
-
-              <div class="form-group" formGroupName="captions">
-                <label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
-                <div class="number-with-unit">
-                  <input
-                    type="number" min="0" id="cacheCaptionsSize" class="form-control"
-                    formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
-                  >
-                  <span i18n>{form.value['cache']['captions']['size'], plural, =1 {cached image} other {cached images}}</span>
-                </div>
-                <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
-              </div>
-            </ng-container>
-
-          </div>
-        </div>
-
-        <div class="form-row mt-4"> <!-- cache grid -->
-          <div class="form-group col-12 col-lg-4 col-xl-3">
-            <div class="anchor" id="customizations"></div> <!-- customizations anchor -->
-            <div i18n class="inner-form-title">CUSTOMIZATIONS</div>
-            <div i18n class="inner-form-description">
-              Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
-            </div>
-          </div>
-
-          <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
-
-            <ng-container formGroupName="instance">
-              <ng-container formGroupName="customizations">
-                <div class="form-group">
-                  <label i18n for="customizationJavascript">JavaScript</label>
-                  <my-help>
-                    <ng-template ptTemplate="customHtml">
-                      <ng-container i18n>
-                        Write JavaScript code directly.<br />Example: <pre>console.log('my instance is amazing');</pre>
-                      </ng-container>
-                    </ng-template>
-                  </my-help>
-
-                  <textarea
-                    id="customizationJavascript" formControlName="javascript" class="form-control"
-                    [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
-                  ></textarea>
-
-                  <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
-                </div>
-
-                <div class="form-group">
-                  <label for="customizationCSS">CSS</label>
-
-                  <my-help>
-                    <ng-template ptTemplate="customHtml">
-                      <ng-container i18n>
-                        Write CSS code directly. Example:<br /><br />
-    <pre>
-    #custom-css {{ '{' }}
-      color: red;
-    {{ '}' }}
-    </pre>
-                        Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
-    <pre>
-    #custom-css .logged-in-email {{ '{' }}
-      color: red;
-    {{ '}' }}
-    </pre>
-                      </ng-container>
-                    </ng-template>
-                  </my-help>
-
-                  <textarea
-                    id="customizationCSS" formControlName="css" class="form-control"
-                    [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
-                  ></textarea>
-                  <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
-                </div>
-              </ng-container>
-            </ng-container>
-
-          </div>
-        </div>
-
+        <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
+        </my-edit-advanced-configuration>
       </ng-template>
     </ng-container>
   </div>
index 29524fdd8c704e0ec93e9e60df0ebad76cf96829..a5eddf6c2b49a7dbf5400874febe5c5a15cb00cd 100644 (file)
@@ -1,8 +1,5 @@
-import { forkJoin } from 'rxjs'
-import { pairwise } from 'rxjs/operators'
-import { SelectOptionsItem } from 'src/types/select-options-item.model'
-import { ViewportScroller } from '@angular/common'
-import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core'
+
+import { Component, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { ConfigService } from '@app/+admin/config/shared/config.service'
 import { Notifier } from '@app/core'
@@ -22,155 +19,35 @@ import {
 } from '@app/shared/form-validators/custom-config-validators'
 import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
-import { NgbNav } from '@ng-bootstrap/ng-bootstrap'
 import { CustomConfig, ServerConfig } from '@shared/models'
+import { forkJoin } from 'rxjs'
+import { SelectOptionsItem } from 'src/types/select-options-item.model'
+import { EditConfigurationService } from './edit-configuration.service'
 
 @Component({
   selector: 'my-edit-custom-config',
   templateUrl: './edit-custom-config.component.html',
   styleUrls: [ './edit-custom-config.component.scss' ]
 })
-export class EditCustomConfigComponent extends FormReactive implements OnInit, AfterViewChecked {
-  @ViewChild('nav') nav: NgbNav
+export class EditCustomConfigComponent extends FormReactive implements OnInit {
+  activeNav: string
 
-  initDone = false
   customConfig: CustomConfig
-
-  resolutions: { id: string, label: string, description?: string }[] = []
-  liveResolutions: { id: string, label: string, description?: string }[] = []
-
-  transcodingThreadOptions: SelectOptionsItem[] = []
-  liveMaxDurationOptions: SelectOptionsItem[] = []
+  serverConfig: ServerConfig
 
   languageItems: SelectOptionsItem[] = []
   categoryItems: SelectOptionsItem[] = []
 
-  signupAlertMessage: string
-
-  activeNav: string
-
-  private serverConfig: ServerConfig
-
   constructor (
     private router: Router,
     private route: ActivatedRoute,
-    private viewportScroller: ViewportScroller,
     protected formValidatorService: FormValidatorService,
     private notifier: Notifier,
     private configService: ConfigService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private editConfigurationService: EditConfigurationService
   ) {
     super()
-
-    this.resolutions = [
-      {
-        id: '0p',
-        label: $localize`Audio-only`,
-        description: $localize`A <code>.mp4</code> that keeps the original audio track, with no video`
-      },
-      {
-        id: '240p',
-        label: $localize`240p`
-      },
-      {
-        id: '360p',
-        label: $localize`360p`
-      },
-      {
-        id: '480p',
-        label: $localize`480p`
-      },
-      {
-        id: '720p',
-        label: $localize`720p`
-      },
-      {
-        id: '1080p',
-        label: $localize`1080p`
-      },
-      {
-        id: '1440p',
-        label: $localize`1440p`
-      },
-      {
-        id: '2160p',
-        label: $localize`2160p`
-      }
-    ]
-
-    this.liveResolutions = this.resolutions.filter(r => r.id !== '0p')
-
-    this.transcodingThreadOptions = [
-      { id: 0, label: $localize`Auto (via ffmpeg)` },
-      { id: 1, label: '1' },
-      { id: 2, label: '2' },
-      { id: 4, label: '4' },
-      { id: 8, label: '8' },
-      { id: 12, label: '12' },
-      { id: 16, label: '16' },
-      { id: 32, label: '32' }
-    ]
-
-    this.liveMaxDurationOptions = [
-      { id: -1, label: $localize`No limit` },
-      { id: 1000 * 3600, label: $localize`1 hour` },
-      { id: 1000 * 3600 * 3, label: $localize`3 hours` },
-      { id: 1000 * 3600 * 5, label: $localize`5 hours` },
-      { id: 1000 * 3600 * 10, label: $localize`10 hours` }
-    ]
-  }
-
-  get videoQuotaOptions () {
-    return this.configService.videoQuotaOptions
-  }
-
-  get videoQuotaDailyOptions () {
-    return this.configService.videoQuotaDailyOptions
-  }
-
-  get availableThemes () {
-    return this.serverConfig.theme.registered
-      .map(t => t.name)
-  }
-
-  get liveRTMPPort () {
-    return this.serverConfig.live.rtmp.port
-  }
-
-  getAvailableTranscodingProfile (type: 'live' | 'vod') {
-    const profiles = type === 'live'
-      ? this.serverConfig.live.transcoding.availableProfiles
-      : this.serverConfig.transcoding.availableProfiles
-
-    return profiles.map(p => ({ id: p, label: p }))
-  }
-
-  getTotalTranscodingThreads () {
-    const transcodingEnabled = this.form.value['transcoding']['enabled']
-    const transcodingThreads = this.form.value['transcoding']['threads']
-    const liveTranscodingEnabled = this.form.value['live']['transcoding']['enabled']
-    const liveTranscodingThreads = this.form.value['live']['transcoding']['threads']
-
-    // checks whether all enabled method are on fixed values and not on auto (= 0)
-    let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
-    noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
-
-    // count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
-    let value = 0
-    if (transcodingEnabled) value += +transcodingThreads || 1
-    if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
-
-    return {
-      value,
-      atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
-      unit: value > 1
-        ? $localize`threads`
-        : $localize`thread`
-    }
-  }
-
-  getResolutionKey (resolution: string) {
-    return 'transcoding.resolutions.' + resolution
   }
 
   ngOnInit () {
@@ -346,60 +223,24 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
       }
     }
 
-    for (const resolution of this.resolutions) {
+    for (const resolution of this.editConfigurationService.getVODResolutions()) {
       defaultValues.transcoding.resolutions[resolution.id] = 'false'
       formGroupData.transcoding.resolutions[resolution.id] = null
     }
 
-    for (const resolution of this.liveResolutions) {
+    for (const resolution of this.editConfigurationService.getLiveResolutions()) {
       defaultValues.live.transcoding.resolutions[resolution.id] = 'false'
       formGroupData.live.transcoding.resolutions[resolution.id] = null
     }
 
-    if (this.route.snapshot.fragment) {
-      this.onNavChange(this.route.snapshot.fragment)
-    }
-
     this.buildForm(formGroupData)
-    this.loadForm()
 
-    this.checkTranscodingFields()
-    this.checkSignupField()
-  }
-
-  ngAfterViewChecked () {
-    if (!this.initDone) {
-      this.initDone = true
-      this.gotoAnchor()
+    if (this.route.snapshot.fragment) {
+      this.onNavChange(this.route.snapshot.fragment)
     }
-  }
-
-  isTranscodingEnabled () {
-    return this.form.value['transcoding']['enabled'] === true
-  }
-
-  isLiveEnabled () {
-    return this.form.value['live']['enabled'] === true
-  }
-
-  isLiveTranscodingEnabled () {
-    return this.form.value['live']['transcoding']['enabled'] === true
-  }
-
-  isSignupEnabled () {
-    return this.form.value['signup']['enabled'] === true
-  }
-
-  isSearchIndexEnabled () {
-    return this.form.value['search']['searchIndex']['enabled'] === true
-  }
 
-  isAutoFollowIndexEnabled () {
-    return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
-  }
-
-  trendingVideosAlgorithmsEnabledIncludes (algorithm: string) {
-    return this.form.value['trending']['videos']['algorithms']['enabled'].find((e: string) => e === algorithm)
+    this.loadConfigAndUpdateForm()
+    this.loadCategoriesAndLanguages()
   }
 
   async formValidated () {
@@ -422,18 +263,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
       )
   }
 
-  gotoAnchor () {
-    const hashToNav = {
-      'customizations': 'advanced-configuration'
-    }
-    const hash = window.location.hash.replace('#', '')
-
-    if (hash && Object.keys(hashToNav).includes(hash)) {
-      this.nav.select(hashToNav[hash])
-      setTimeout(() => this.viewportScroller.scrollToAnchor(hash), 100)
-    }
-  }
-
   hasConsistentOptions () {
     if (this.hasLiveAllowReplayConsistentOptions()) return true
 
@@ -441,7 +270,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
   }
 
   hasLiveAllowReplayConsistentOptions () {
-    if (this.isTranscodingEnabled() === false && this.isLiveEnabled() && this.form.value['live']['allowReplay'] === true) {
+    if (
+      this.editConfigurationService.isTranscodingEnabled(this.form) === false &&
+      this.editConfigurationService.isLiveEnabled(this.form) &&
+      this.form.value['live']['allowReplay'] === true
+    ) {
       return false
     }
 
@@ -458,18 +291,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
     this.form.patchValue(this.customConfig)
   }
 
-  private loadForm () {
-    forkJoin([
-      this.configService.getCustomConfig(),
-      this.serverService.getVideoLanguages(),
-      this.serverService.getVideoCategories()
-    ]).subscribe(
-      ([ config, languages, categories ]) => {
+  private loadConfigAndUpdateForm () {
+    this.configService.getCustomConfig()
+      .subscribe(config => {
         this.customConfig = config
 
-        this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
-        this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
-
         this.updateForm()
         // Force form validation
         this.forceCheck()
@@ -479,53 +305,17 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
     )
   }
 
-  private checkTranscodingFields () {
-    const hlsControl = this.form.get('transcoding.hls.enabled')
-    const webtorrentControl = this.form.get('transcoding.webtorrent.enabled')
-
-    webtorrentControl.valueChanges
-                     .subscribe(newValue => {
-                       if (newValue === false && !hlsControl.disabled) {
-                         hlsControl.disable()
-                       }
-
-                       if (newValue === true && !hlsControl.enabled) {
-                         hlsControl.enable()
-                       }
-                     })
-
-    hlsControl.valueChanges
-              .subscribe(newValue => {
-                if (newValue === false && !webtorrentControl.disabled) {
-                  webtorrentControl.disable()
-                }
-
-                if (newValue === true && !webtorrentControl.enabled) {
-                  webtorrentControl.enable()
-                }
-              })
-  }
+  private loadCategoriesAndLanguages () {
+    forkJoin([
+      this.serverService.getVideoLanguages(),
+      this.serverService.getVideoCategories()
+    ]).subscribe(
+      ([ languages, categories ]) => {
+        this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
+        this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
+      },
 
-  private checkSignupField () {
-    const signupControl = this.form.get('signup.enabled')
-
-    signupControl.valueChanges
-      .pipe(pairwise())
-      .subscribe(([ oldValue, newValue ]) => {
-        if (oldValue !== true && newValue === true) {
-          // tslint:disable:max-line-length
-          this.signupAlertMessage = $localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
-
-          this.form.patchValue({
-            autoBlacklist: {
-              videos: {
-                ofUsers: {
-                  enabled: true
-                }
-              }
-            }
-          })
-        }
-      })
+      err => this.notifier.error(err.message)
+    )
   }
 }
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 (file)
index 0000000..6f19ede
--- /dev/null
@@ -0,0 +1,228 @@
+<ng-container [formGroup]="form">
+
+  <ng-container formGroupName="instance">
+
+    <div class="form-row mt-5"> <!-- instance grid -->
+      <div class="form-group col-12 col-lg-4 col-xl-3">
+        <div i18n class="inner-form-title">INSTANCE</div>
+      </div>
+
+      <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+        <div class="form-group">
+          <label i18n for="instanceName">Name</label>
+          <input
+            type="text" id="instanceName" class="form-control"
+            formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
+          >
+          <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceShortDescription">Short description</label>
+          <textarea
+            id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
+            [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
+          ></textarea>
+          <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
+          <my-markdown-textarea
+            name="instanceDescription" formControlName="description" textareaMaxWidth="500px"
+            [classes]="{ 'input-error': formErrors['instance.description'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceCategories">Main instance categories</label>
+
+          <div>
+            <my-select-checkbox
+              id="instanceCategories"
+              formControlName="categories" [availableItems]="categoryItems"
+              [selectableGroup]="false"
+              i18n-placeholder placeholder="Add a new category"
+            >
+            </my-select-checkbox>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
+
+          <div>
+            <my-select-checkbox
+              id="instanceLanguages"
+              formControlName="languages" [availableItems]="languageItems"
+              [selectableGroup]="false"
+              i18n-placeholder placeholder="Add a new language"
+            >
+            </my-select-checkbox>
+          </div>
+        </div>
+
+      </div>
+    </div>
+
+    <div class="form-row mt-4"> <!-- moderation & nsfw grid -->
+      <div class="form-group col-12 col-lg-4 col-xl-3">
+        <div i18n class="inner-form-title">MODERATION & NSFW</div>
+        <div i18n class="inner-for-description">
+          Manage <a routerLink="/admin/users">users</a> to build a moderation team.
+        </div>
+      </div>
+
+      <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+        <div class="form-group">
+          <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
+            <ng-template ptTemplate="label">
+              <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
+            </ng-template>
+
+            <ng-template ptTemplate="help">
+              <ng-container i18n>
+                Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
+                Moreover, the NSFW checkbox on video upload will be automatically checked by default.
+              </ng-container>
+            </ng-template>
+          </my-peertube-checkbox>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
+
+          <my-help>
+            <ng-template ptTemplate="customHtml">
+              <ng-container i18n>
+                With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
+              </ng-container>
+            </ng-template>
+          </my-help>
+
+          <div class="peertube-select-container">
+            <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy" class="form-control">
+              <option i18n value="undefined" disabled>Policy for sensitive videos</option>
+              <option i18n value="do_not_list">Do not list</option>
+              <option i18n value="blur">Blur thumbnails</option>
+              <option i18n value="display">Display</option>
+            </select>
+          </div>
+          <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
+          <my-markdown-textarea
+            name="instanceTerms" formControlName="terms" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
+          <my-markdown-textarea
+            name="instanceCodeOfConduct" formControlName="codeOfConduct" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
+          <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
+
+          <my-markdown-textarea
+            name="instanceModerationInformation" formControlName="moderationInformation" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
+        </div>
+
+      </div>
+    </div>
+
+    <div class="form-row mt-4"> <!-- you and your instance grid -->
+      <div class="form-group col-12 col-lg-4 col-xl-3">
+        <div i18n class="inner-form-title">YOU AND YOUR INSTANCE</div>
+      </div>
+
+      <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+        <div class="form-group">
+          <label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
+          <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
+
+          <my-markdown-textarea
+            name="instanceAdministrator" formControlName="administrator" textareaMaxWidth="500px"
+            [classes]="{ 'input-error': formErrors['instance.administrator'] }"
+          ></my-markdown-textarea>
+
+          <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
+          <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
+
+          <my-markdown-textarea
+            name="instanceCreationReason" formControlName="creationReason" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
+          <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
+
+          <my-markdown-textarea
+            name="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
+        </div>
+
+        <div class="form-group">
+          <label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
+          <div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
+
+          <my-markdown-textarea
+            name="instanceBusinessModel" formControlName="businessModel" textareaMaxWidth="500px"
+            [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
+          ></my-markdown-textarea>
+          <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
+        </div>
+
+      </div>
+    </div>
+
+    <div class="form-row mt-4"> <!-- other information grid -->
+      <div class="form-group col-12 col-lg-4 col-xl-3">
+        <div i18n class="inner-form-title">OTHER INFORMATION</div>
+      </div>
+
+      <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+        <div class="form-group">
+          <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
+          <div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
+
+          <my-markdown-textarea
+            name="instanceHardwareInformation" formControlName="hardwareInformation" textareaMaxWidth="500px"
+            [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
+          ></my-markdown-textarea>
+
+          <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
+        </div>
+
+      </div>
+    </div>
+
+  </ng-container>
+
+</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 (file)
index 0000000..26365e7
--- /dev/null
@@ -0,0 +1,16 @@
+import { SelectOptionsItem } from 'src/types/select-options-item.model'
+import { Component, Input } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+
+@Component({
+  selector: 'my-edit-instance-information',
+  templateUrl: './edit-instance-information.component.html',
+  styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditInstanceInformationComponent {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+
+  @Input() languageItems: SelectOptionsItem[] = []
+  @Input() categoryItems: SelectOptionsItem[] = []
+}
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 (file)
index 0000000..4b1a552
--- /dev/null
@@ -0,0 +1,155 @@
+<ng-container [formGroup]="form">
+
+  <div class="form-row mt-5">
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">LIVE</div>
+      <div i18n class="inner-form-description">
+        Enable users of your instance to stream live.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="live">
+
+        <div class="form-group">
+          <my-peertube-checkbox inputName="liveEnabled" formControlName="enabled">
+            <ng-template ptTemplate="label">
+              <ng-container i18n>Allow live streaming</ng-container>
+            </ng-template>
+
+            <ng-container ngProjectAs="description">
+              <div i18n>⚠️ Enabling live streaming requires trust in your users and extra moderation work</div>
+              <div i18n>If enabled, your server needs to accept incoming TCP traffic on port {{ getLiveRTMPPort() }}</div>
+            </ng-container>
+
+            <ng-container ngProjectAs="extra">
+
+              <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                <my-peertube-checkbox
+                  inputName="liveAllowReplay" formControlName="allowReplay"
+                  i18n-labelText labelText="Allow your users to automatically publish a replay of their live"
+                >
+                  <ng-container ngProjectAs="description" i18n>
+                    If the user quota is reached, PeerTube will automatically terminate the live streaming
+                  </ng-container>
+                </my-peertube-checkbox>
+              </div>
+
+              <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                <label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance <span class="text-muted">(-1 for "unlimited")</span></label>
+                <div class="number-with-unit">
+                  <input type="number" name="liveMaxInstanceLives" formControlName="maxInstanceLives" />
+                  <span i18n>{form.value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
+                </div>
+              </div>
+
+              <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                <label i18n for="liveMaxUserLives">Max simultaneous lives created per user <span class="text-muted">(-1 for "unlimited")</span></label>
+                <div class="number-with-unit">
+                  <input type="number" name="liveMaxUserLives" formControlName="maxUserLives" />
+                  <span i18n>{form.value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
+                </div>
+              </div>
+
+              <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+                <label i18n for="liveMaxDuration">Max live duration</label>
+
+                <my-select-options
+                  labelForId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"
+                  bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
+                ></my-select-options>
+              </div>
+
+            </ng-container>
+          </my-peertube-checkbox>
+        </div>
+      </ng-container>
+    </div>
+  </div>
+
+  <div class="form-row"> <!-- transcoding live streams grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">TRANSCODING</div>
+      <div i18n class="inner-form-description">
+        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.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="live">
+        <ng-container formGroupName="transcoding">
+
+          <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }">
+            <my-peertube-checkbox
+              inputName="liveTranscodingEnabled" formControlName="enabled"
+            >
+              <ng-template ptTemplate="label">
+                <ng-container i18n>Transcoding enabled for live streams</ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
+          </div>
+
+          <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
+            <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
+
+            <div class="ml-2 mt-2 d-flex flex-column">
+              <ng-container formGroupName="resolutions">
+                <div class="form-group" *ngFor="let resolution of liveResolutions">
+                  <my-peertube-checkbox
+                    [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
+                    labelText="{{resolution.label}}"
+                  >
+                    <ng-template *ngIf="resolution.description" ptTemplate="help">
+                      <div [innerHTML]="resolution.description"></div>
+                    </ng-template>
+                  </my-peertube-checkbox>
+                </div>
+              </ng-container>
+            </div>
+          </div>
+
+          <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
+            <label i18n for="liveTranscodingThreads">Live transcoding threads</label>
+            <span class="text-muted ml-1">
+              <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
+              <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with VOD transcoding</ng-container>
+            </span>
+
+            <my-select-custom-value
+              id="liveTranscodingThreads"
+              [items]="transcodingThreadOptions"
+              formControlName="threads"
+              [clearable]="false"
+            ></my-select-custom-value>
+            <div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div>
+          </div>
+
+          <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }">
+            <label i18n for="liveTranscodingProfile">Live transcoding profile</label>
+            <span class="text-muted ml-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
+
+            <my-select-options
+              id="liveTranscodingProfile"
+              formControlName="profile"
+              [items]="getAvailableTranscodingProfile()"
+              [clearable]="false"
+            >
+              <ng-template ng-option-tmp let-item="item" let-index="index">
+                {{ item }}
+                <ng-container *ngIf="item === 'default'">
+                  <br>
+                  <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
+                </ng-container>
+              </ng-template>
+            </my-select-options>
+            <div *ngIf="formErrors.live.transcoding.profile" class="form-error">{{ formErrors.live.transcoding.profile }}</div>
+          </div>
+
+        </ng-container>
+      </ng-container>
+
+    </div>
+  </div>
+</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 (file)
index 0000000..a82a40a
--- /dev/null
@@ -0,0 +1,67 @@
+
+import { SelectOptionsItem } from 'src/types/select-options-item.model'
+import { Component, Input, OnInit } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+import { ServerConfig } from '@shared/models'
+import { ConfigService } from '../shared/config.service'
+import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
+
+@Component({
+  selector: 'my-edit-live-configuration',
+  templateUrl: './edit-live-configuration.component.html',
+  styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditLiveConfigurationComponent implements OnInit {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+  @Input() serverConfig: ServerConfig
+
+  transcodingThreadOptions: SelectOptionsItem[] = []
+  liveMaxDurationOptions: SelectOptionsItem[] = []
+  liveResolutions: ResolutionOption[] = []
+
+  constructor (
+    private configService: ConfigService,
+    private editConfigurationService: EditConfigurationService
+  ) { }
+
+  ngOnInit () {
+    this.transcodingThreadOptions = this.configService.transcodingThreadOptions
+
+    this.liveMaxDurationOptions = [
+      { id: -1, label: $localize`No limit` },
+      { id: 1000 * 3600, label: $localize`1 hour` },
+      { id: 1000 * 3600 * 3, label: $localize`3 hours` },
+      { id: 1000 * 3600 * 5, label: $localize`5 hours` },
+      { id: 1000 * 3600 * 10, label: $localize`10 hours` }
+    ]
+
+    this.liveResolutions = this.editConfigurationService.getLiveResolutions()
+  }
+
+  getAvailableTranscodingProfile () {
+    const profiles = this.serverConfig.live.transcoding.availableProfiles
+
+    return profiles.map(p => ({ id: p, label: p }))
+  }
+
+  getResolutionKey (resolution: string) {
+    return 'live.transcoding.resolutions.' + resolution
+  }
+
+  getLiveRTMPPort () {
+    return this.serverConfig.live.rtmp.port
+  }
+
+  isLiveEnabled () {
+    return this.editConfigurationService.isLiveEnabled(this.form)
+  }
+
+  isLiveTranscodingEnabled () {
+    return this.editConfigurationService.isLiveTranscodingEnabled(this.form)
+  }
+
+  getTotalTranscodingThreads () {
+    return this.editConfigurationService.getTotalTranscodingThreads(this.form)
+  }
+}
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 (file)
index 0000000..a519098
--- /dev/null
@@ -0,0 +1,201 @@
+<ng-container [formGroup]="form">
+
+  <div class="form-row mt-4"> <!-- transcoding grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3"></div>
+    <div class="form-group form-group-right col-12 col-lg-8">
+
+      <div class="callout callout-info">
+        <span i18n>
+          Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
+        </span>
+        <span i18n>
+          However, you may want to read our guidelines before tweaking the following values.
+        </span>
+
+        <div class="callout-container">
+          <a class="callout-link" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/#/admin-configuration?id=transcoding" i18n>Read guidelines</a>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="form-row mt-2"> <!-- transcoding grid -->
+    <div class="form-group col-12 col-lg-4 col-xl-3">
+      <div i18n class="inner-form-title">TRANSCODING</div>
+      <div i18n class="inner-form-description">
+        Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
+        resources, this is a critical part of PeerTube, so tread carefully.
+      </div>
+    </div>
+
+    <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
+
+      <ng-container formGroupName="transcoding">
+
+        <div class="form-group mb-0 col-12 col-xl-11">
+          <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled" [recommended]="true">
+            <ng-template ptTemplate="label">
+              <ng-container i18n>Transcoding enabled</ng-container>
+            </ng-template>
+
+            <ng-container ngProjectAs="extra">
+
+              <div class="callout callout-light pt-2 pb-0">
+                <label i18n>Input formats</label>
+
+                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+                  <my-peertube-checkbox
+                    inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
+                    i18n-labelText labelText="Allow additional extensions"
+                  >
+                    <ng-container ngProjectAs="description">
+                      <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, .m2ts, .mxf, or .nut videos.</span>
+                    </ng-container>
+                  </my-peertube-checkbox>
+                </div>
+
+                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+                  <my-peertube-checkbox
+                    inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
+                    i18n-labelText labelText="Allow audio files upload"
+                  >
+                    <ng-container ngProjectAs="description">
+                      <div i18n>Allows users to upload .mp3, .ogg, .wma, .flac, .aac, or .ac3 audio files.</div>
+                      <div i18n>The file will be merged in a still image video with the preview file on upload.</div>
+                    </ng-container>
+                  </my-peertube-checkbox>
+                </div>
+              </div>
+
+              <div class="callout callout-light pt-2 mt-2 pb-0">
+                <label i18n>Output formats</label>
+
+                <ng-container formGroupName="webtorrent">
+                  <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+                    <my-peertube-checkbox
+                      inputName="transcodingWebTorrentEnabled" formControlName="enabled"
+                      i18n-labelText labelText="WebTorrent enabled"
+                    >
+                      <ng-template ptTemplate="help">
+                        <ng-container i18n>
+                          <p>If you also enabled HLS support, it will multiply videos storage by 2</p>
+
+                          <br />
+
+                          <strong>If disabled, breaks federation with PeerTube instances < 2.1</strong>
+                        </ng-container>
+                      </ng-template>
+                    </my-peertube-checkbox>
+                  </div>
+                </ng-container>
+
+                <ng-container formGroupName="hls">
+                  <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+                    <my-peertube-checkbox
+                      inputName="transcodingHlsEnabled" formControlName="enabled"
+                      i18n-labelText labelText="HLS with P2P support enabled"
+                      [recommended]="true"
+                    >
+                      <ng-template ptTemplate="help">
+                        <ng-container i18n>
+                          <strong>Requires ffmpeg >= 4.1</strong>
+
+                          <p>Generate HLS playlists and fragmented MP4 files resulting in a better playback than with plain WebTorrent:</p>
+                          <ul>
+                            <li>Resolution change is smoother</li>
+                            <li>Faster playback especially with long videos</li>
+                            <li>More stable playback (less bugs/infinite loading)</li>
+                          </ul>
+
+                          <p>If you also enabled WebTorrent support, it will multiply videos storage by 2</p>
+                        </ng-container>
+                      </ng-template>
+                    </my-peertube-checkbox>
+                  </div>
+                </ng-container>
+
+                <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+                  <label i18n>Resolutions to generate per enabled format</label>
+
+                  <div class="ml-2 mt-2 d-flex flex-column">
+                    <ng-container formGroupName="resolutions">
+                      <div class="form-group" *ngFor="let resolution of resolutions">
+                        <my-peertube-checkbox
+                          [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
+                          labelText="{{ resolution.label }}"
+                        >
+                          <ng-template *ngIf="resolution.description" ptTemplate="help">
+                            <div [innerHTML]="resolution.description"></div>
+                          </ng-template>
+                        </my-peertube-checkbox>
+                      </div>
+
+                      <span class="mb-2 text-muted" i18n>
+                        The original file resolution will be the default target if no option is selected.
+                      </span>
+                    </ng-container>
+                  </div>
+                </div>
+              </div>
+
+            </ng-container>
+          </my-peertube-checkbox>
+        </div>
+
+        <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+          <label i18n for="transcodingThreads">Transcoding threads</label>
+          <span class="text-muted ml-1">
+            <ng-container *ngIf="getTotalTranscodingThreads().atMost" i18n>will claim at most {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
+            <ng-container *ngIf="!getTotalTranscodingThreads().atMost" i18n>will claim at least {{ getTotalTranscodingThreads().value }} {{ getTotalTranscodingThreads().unit }} with live transcoding</ng-container>
+          </span>
+
+          <my-select-custom-value
+            id="transcodingThreads"
+            [items]="transcodingThreadOptions"
+            formControlName="threads"
+            [clearable]="false"
+          ></my-select-custom-value>
+
+          <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
+        </div>
+
+        <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+          <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
+          <span class="text-muted ml-1">
+            <span i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart.</span>
+          </span>
+
+          <div class="number-with-unit">
+            <input type="number" name="transcodingConcurrency" formControlName="concurrency" />
+            <span i18n>jobs in parallel</span>
+          </div>
+
+          <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
+        </div>
+
+        <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
+          <label i18n for="transcodingProfile">Transcoding profile</label>
+          <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
+
+          <my-select-options
+            id="transcodingProfile"
+            formControlName="profile"
+            [items]="getAvailableTranscodingProfile()"
+            [clearable]="false"
+          >
+            <ng-template ng-option-tmp let-item="item" let-index="index">
+              {{ item }}
+              <ng-container *ngIf="item === 'default'">
+                <br>
+                <span class="text-muted" i18n>x264, targeting maximum device compatibility</span>
+              </ng-container>
+            </ng-template>
+          </my-select-options>
+          <div *ngIf="formErrors.transcoding.profile" class="form-error">{{ formErrors.transcoding.profile }}</div>
+        </div>
+
+      </ng-container>
+
+    </div>
+  </div>
+</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 (file)
index 0000000..d745912
--- /dev/null
@@ -0,0 +1,78 @@
+
+import { SelectOptionsItem } from 'src/types/select-options-item.model'
+import { Component, Input, OnInit } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+import { ServerConfig } from '@shared/models'
+import { ConfigService } from '../shared/config.service'
+import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
+
+@Component({
+  selector: 'my-edit-vod-transcoding',
+  templateUrl: './edit-vod-transcoding.component.html',
+  styleUrls: [ './edit-custom-config.component.scss' ]
+})
+export class EditVODTranscodingComponent implements OnInit {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+  @Input() serverConfig: ServerConfig
+
+  transcodingThreadOptions: SelectOptionsItem[] = []
+  resolutions: ResolutionOption[] = []
+
+  constructor (
+    private configService: ConfigService,
+    private editConfigurationService: EditConfigurationService
+  ) { }
+
+  ngOnInit () {
+    this.transcodingThreadOptions = this.configService.transcodingThreadOptions
+    this.resolutions = this.editConfigurationService.getVODResolutions()
+
+    this.checkTranscodingFields()
+  }
+
+  getAvailableTranscodingProfile () {
+    const profiles = this.serverConfig.transcoding.availableProfiles
+
+    return profiles.map(p => ({ id: p, label: p }))
+  }
+
+  getResolutionKey (resolution: string) {
+    return 'transcoding.resolutions.' + resolution
+  }
+
+  isTranscodingEnabled () {
+    return this.editConfigurationService.isTranscodingEnabled(this.form)
+  }
+
+  getTotalTranscodingThreads () {
+    return this.editConfigurationService.getTotalTranscodingThreads(this.form)
+  }
+
+  private checkTranscodingFields () {
+    const hlsControl = this.form.get('transcoding.hls.enabled')
+    const webtorrentControl = this.form.get('transcoding.webtorrent.enabled')
+
+    webtorrentControl.valueChanges
+                     .subscribe(newValue => {
+                       if (newValue === false && !hlsControl.disabled) {
+                         hlsControl.disable()
+                       }
+
+                       if (newValue === true && !hlsControl.enabled) {
+                         hlsControl.enable()
+                       }
+                     })
+
+    hlsControl.valueChanges
+              .subscribe(newValue => {
+                if (newValue === false && !webtorrentControl.disabled) {
+                  webtorrentControl.disable()
+                }
+
+                if (newValue === true && !webtorrentControl.enabled) {
+                  webtorrentControl.enable()
+                }
+              })
+  }
+}
index 1ec12631f5e3c5b1940007cfaabef7cf20895363..95fcc8f52677fb097c826604f97fdb1be1303bb2 100644 (file)
@@ -1 +1,7 @@
+export * from './edit-advanced-configuration.component'
+export * from './edit-basic-configuration.component'
+export * from './edit-configuration.service'
 export * from './edit-custom-config.component'
+export * from './edit-instance-information.component'
+export * from './edit-live-configuration.component'
+export * from './edit-vod-transcoding.component'
index d29b752f70f4988bbbccbaca86e1cb1a846ef57d..80f495b41b782258742555fd8885f685847efcd2 100644 (file)
@@ -12,11 +12,12 @@ export class ConfigService {
 
   videoQuotaOptions: SelectOptionsItem[] = []
   videoQuotaDailyOptions: SelectOptionsItem[] = []
+  transcodingThreadOptions: SelectOptionsItem[] = []
 
   constructor (
     private authHttp: HttpClient,
     private restExtractor: RestExtractor
-    ) {
+  ) {
     this.videoQuotaOptions = [
       { id: -1, label: $localize`Unlimited` },
       { id: 0, label: $localize`None - no upload possible` },
@@ -44,6 +45,17 @@ export class ConfigService {
       { id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
       { id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` }
     ]
+
+    this.transcodingThreadOptions = [
+      { id: 0, label: $localize`Auto (via ffmpeg)` },
+      { id: 1, label: '1' },
+      { id: 2, label: '2' },
+      { id: 4, label: '4' },
+      { id: 8, label: '8' },
+      { id: 12, label: '12' },
+      { id: 16, label: '16' },
+      { id: 32, label: '32' }
+    ]
   }
 
   getCustomConfig () {