Merge branch 'open-api-clients' into develop
authorChocobozzz <me@florianbigard.com>
Mon, 17 Feb 2020 08:03:49 +0000 (09:03 +0100)
committerChocobozzz <me@florianbigard.com>
Mon, 17 Feb 2020 08:03:49 +0000 (09:03 +0100)
41 files changed:
README.md
client/src/app/+accounts/accounts.component.html
client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss
client/src/app/+signup/+register/register.component.scss
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.scss
client/src/app/app.component.html
client/src/app/app.component.scss
client/src/app/app.module.ts
client/src/app/core/hotkeys/hotkeys.component.scss
client/src/app/core/server/server.service.ts
client/src/app/header/header.component.html
client/src/app/header/header.component.scss
client/src/app/header/header.component.ts
client/src/app/header/index.ts
client/src/app/header/search-typeahead.component.html [new file with mode: 0644]
client/src/app/header/search-typeahead.component.scss [new file with mode: 0644]
client/src/app/header/search-typeahead.component.ts [new file with mode: 0644]
client/src/app/header/suggestion.component.html [new file with mode: 0644]
client/src/app/header/suggestion.component.scss [new file with mode: 0644]
client/src/app/header/suggestion.component.ts [new file with mode: 0644]
client/src/app/header/suggestions.component.html [new file with mode: 0644]
client/src/app/header/suggestions.component.ts [new file with mode: 0644]
client/src/app/menu/menu.component.scss
client/src/app/shared/angular/highlight.pipe.ts [new file with mode: 0644]
client/src/app/shared/instance/instance-features-table.component.html
client/src/app/shared/shared.module.ts
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/include/_bootstrap-variables.scss
client/src/sass/include/_miniature.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss
client/src/sass/loading-bar.scss
client/src/sass/primeng-custom.scss
scripts/build/client.sh
server/controllers/api/config.ts
server/controllers/static.ts
shared/models/server/server-config.model.ts
support/doc/docker.md

index 6219d6f3a3acd0fa3b2329086bdd7483d2b0ddf0..02809db428894b94a8181b53398a22ad8d590ad3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -196,7 +196,7 @@ Quonfucius, IP Solution, \_Laure\_, @lex666, 0x010C, 3dsman, 3rw4n-G3D, aallrd,
 
 ## License
 
-Copyright (C) 2015-2019 PeerTube Contributors
+Copyright (C) 2015-2020 PeerTube Contributors
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as published
index b982fba9a9e299fe755eafa50e2885cbc338ff07..6a76393b91b9c72aca28ddecca3e5ce24fdb08c0 100644 (file)
@@ -7,14 +7,13 @@
       <div class="actor-info">
         <div class="actor-names">
           <div class="actor-display-name">{{ account.displayName }}</div>
-          <div class="actor-name">{{ account.nameWithHost }}
-
-          <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
-                  class="btn btn-outline-secondary btn-sm copy-button"
-          >
-            <span class="glyphicon glyphicon-copy"></span>
-          </button>
-
+          <div class="actor-name">
+            <span>{{ account.nameWithHost }}</span>
+            <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
+                    class="btn btn-outline-secondary btn-sm copy-button"
+            >
+              <span class="glyphicon glyphicon-copy"></span>
+            </button>
           </div>
           <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
           <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
index 20582e478a29afcc5f7ff87e7913c812424af9fa..db0c7f94faf0580f63d2257bbb10d8085ea636c9 100644 (file)
@@ -58,7 +58,7 @@
   margin: 20px 0 50px;
 }
 
-@media screen and (max-width: 800px) {
+@media screen and (max-width: $small-view) {
   .video-channels-header {
     text-align: center;
   }
index 4e4156b22c605a11fbb13fe56dbf8647699f2979..aed3302ba45b37d1489cfac0776f7dd726b0108d 100644 (file)
@@ -43,7 +43,7 @@
   }
 }
 
-@media screen and (max-width: 800px) {
+@media screen and (max-width: $small-view) {
   .video-playlists-header {
     text-align: center;
   }
index 2f62dd59db594a39eb87f44a0aba61ce44edbaf6..e135b5cb4312488b91a7f4bb3845051f01d5c9bb 100644 (file)
@@ -44,7 +44,7 @@
       }
     }
 
-    @media screen and (max-width: 500px) {
+    @media screen and (max-width: $mobile-view) {
       width: auto;
     }
   }
index 735a8f2c8f558dce804703e1d4eba58ab01fa484..1087de113aba42014184af8729ad75e889219861 100644 (file)
@@ -7,25 +7,29 @@
       <div class="actor-info">
         <div class="actor-names">
           <div class="actor-display-name">{{ videoChannel.displayName }}</div>
-          <div class="actor-name">{{ videoChannel.nameWithHost }}
+          <div class="actor-name">
+            <span>{{ videoChannel.nameWithHost }}</span>
             <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
                     class="btn btn-outline-secondary btn-sm copy-button"
             >
               <span class="glyphicon glyphicon-copy"></span>
             </button>
           </div>
+        </div>
 
-          <div class="right-buttons">
-            <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
-            <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
-          </div>
+        <div class="right-buttons">
+          <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
+          <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
         </div>
-        <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
 
-        <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
-          <span i18n>Created by {{ videoChannel.ownerBy }}</span>
-          <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
-        </a>
+        <div class="actor-lower">
+          <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
+  
+          <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
+            <span i18n>Created by {{ videoChannel.ownerBy }}</span>
+            <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
+          </a>
+        </div>
       </div>
     </div>
 
index 50b69e7acf7e1489d523b3904db63aa57a125a4c..aa26a7e7b7fc4b977e3d09a8b46131b349c46f5b 100644 (file)
@@ -8,6 +8,23 @@
     width: 100%;
   }
 
+  .actor-info {
+    display: grid !important;
+    grid-template-columns: 1fr auto;
+    grid-template-rows: 1fr auto / 1fr auto;
+    grid-template-areas: "name buttons"
+                         "lower buttons";
+
+    @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
+      grid-template-areas: "name name"
+                           "lower buttons";
+    }
+  }
+
+  .actor-names {
+    grid-area: name;
+  }
+
   .actor-name {
     flex-grow: 1;
 
@@ -25,6 +42,9 @@
   margin-left: auto;
   margin-top: 20px;
 
+  grid-row: buttons-start / span buttons-end;
+  grid-column: buttons-start;
+
   a {
     @include peertube-button-outline;
     line-height: 1.8;
index 2660c537761018e38f572bf59e4d5f9d4d44f507..f5a8dbd34659ce99a9d43967c5638beb9c94798a 100644 (file)
@@ -15,7 +15,7 @@
     </div>
 
     <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
-      <my-header></my-header>
+      <my-header class="w-100 d-flex justify-content-end"></my-header>
     </div>
   </div>
 
index 51a7a3dd1df1dddbb02fbc72407b2e3ae18aab65..a7be8e1afa6af7ca9239e148fb6cce4f839282c9 100644 (file)
   top: 0;
   width: 100%;
   background-color: var(--mainBackgroundColor);
-  z-index: 1000;
+  z-index: z(header);
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
   display: flex;
 
   .top-left-block {
-    z-index: 1001;
+    z-index: z(headerLeft);
     height: $header-height;
     display: flex;
     align-items: center;
@@ -61,7 +61,7 @@
       }
     }
 
-    @media screen and (max-width: 500px) {
+    @media screen and (max-width: $mobile-view) {
       width: 70px;
 
       .peertube-title {
index d11dba34d3febf4150b5bca3e1af1f3116835949..9e220a3836d83d516ef4cc0c974e260e5af9e070 100644 (file)
@@ -9,7 +9,7 @@ import 'focus-visible'
 import { AppRoutingModule } from './app-routing.module'
 import { AppComponent } from './app.component'
 import { CoreModule } from './core'
-import { HeaderComponent } from './header'
+import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header'
 import { LoginModule } from './login'
 import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
 import { SharedModule } from './shared'
@@ -41,6 +41,9 @@ export function metaFactory (serverService: ServerService): MetaLoader {
     LanguageChooserComponent,
     AvatarNotificationComponent,
     HeaderComponent,
+    SearchTypeaheadComponent,
+    SuggestionsComponent,
+    SuggestionComponent,
 
     WelcomeModalComponent,
     InstanceConfigWarningModalComponent
index 3aa0b625266e4531260bf06b513ae938051f29e7..a970260c9294546bc8a4f6e5498e3e393b187614 100644 (file)
@@ -1,3 +1,6 @@
+@import '_variables';
+@import '_mixins';
+
 .cfp-hotkeys-container {
   display: flex !important;
   align-items: center;
@@ -23,7 +26,7 @@
 }
 
 .cfp-hotkeys-container.fade.in {
-  z-index: 10002;
+  z-index: z(hotkeys);
   visibility: visible;
   opacity: 1;
 }
@@ -91,7 +94,7 @@
   cursor: pointer;
 }
 
-@media all and (max-width: 500px) {
+@media all and (max-width: $mobile-view) {
   .cfp-hotkeys {
     font-size: 0.8em;
   }
index 1f6cfb596aa180fa940544c2a5d838b05c582421..c0e1f08bb1d0ef6c71dcd2edfd5cc1708d03c960 100644 (file)
@@ -47,6 +47,12 @@ export class ServerService {
         css: ''
       }
     },
+    search: {
+      remoteUri: {
+        users: true,
+        anonymous: false
+      }
+    },
     plugin: {
       registered: []
     },
index 4fd18f9bdf20b8d1f9beb64d9558aa59d59edb55..49e219187405c398ee903cd12036604955e9a88c 100644 (file)
@@ -1,8 +1,4 @@
-<input
-  type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
-  [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
->
-<span (click)="doSearch()" class="icon icon-search"></span>
+<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
 
 <a class="upload-button" routerLink="/videos/upload">
   <my-global-icon iconName="upload"></my-global-icon>
index 2bbde74bc7d2dbe659f2e5559915726ad0b7eff4..91b39077367ba2b638ea0c2de687a21a824afdff 100644 (file)
@@ -1,51 +1,8 @@
 @import '_variables';
 @import '_mixins';
 
-#search-video {
-  @include peertube-input-text($search-input-width);
-  padding-left: 10px;
+my-search-typeahead {
   margin-right: 15px;
-  padding-right: 40px; // For the search icon
-  font-size: 14px;
-
-  transition: box-shadow .3s ease;
-
-  /* light border style */
-  border: 1px solid var(--mainBackgroundColor);
-  box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
-
-  &:focus {
-    box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
-  }
-
-  &::placeholder {
-    color: var(--inputPlaceholderColor);
-  }
-
-  &:focus::placeholder {
-    opacity: 0 !important;
-  }
-
-  @media screen and (max-width: 800px) {
-    width: calc(100% - 150px);
-  }
-
-  @media screen and (max-width: 600px) {
-    width: calc(100% - 70px);
-  }
-}
-
-.icon.icon-search {
-  @include icon(25px);
-  height: 21px;
-
-  background-color: var(--mainForegroundColor);
-  mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
-
-  // yolo
-  position: absolute;
-  margin-left: -50px;
-  margin-top: 5px;
 }
 
 .upload-button {
   color: var(--mainBackgroundColor) !important;
   margin-right: 25px;
 
-  @media screen and (max-width: 800px) {
-    margin-right: 0;
-  }
-
   @media screen and (max-width: 600px) {
     margin-right: 10px;
     padding: 0 10px;
index 92a7eded651eea650993ca6243ec7930cf433f34..cce76b0d1272c82eb72ae8349b9835d1ea4c693b 100644 (file)
@@ -1,10 +1,4 @@
-import { filter, first, map, tap } from 'rxjs/operators'
-import { Component, OnInit } from '@angular/core'
-import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
-import { getParameterByName } from '../shared/misc/utils'
-import { AuthService, Notifier, ServerService } from '@app/core'
-import { of } from 'rxjs'
-import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Component } from '@angular/core'
 
 @Component({
   selector: 'my-header',
@@ -12,54 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './header.component.scss' ]
 })
 
-export class HeaderComponent implements OnInit {
-  searchValue = ''
-  ariaLabelTextForSearch = ''
-
-  constructor (
-    private router: Router,
-    private route: ActivatedRoute,
-    private auth: AuthService,
-    private serverService: ServerService,
-    private authService: AuthService,
-    private notifier: Notifier,
-    private i18n: I18n
-  ) {}
-
-  ngOnInit () {
-    this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
-
-    this.router.events
-        .pipe(
-          filter(e => e instanceof NavigationEnd),
-          map(() => getParameterByName('search', window.location.href))
-        )
-        .subscribe(searchQuery => this.searchValue = searchQuery || '')
-  }
-
-  doSearch () {
-    const queryParams: Params = {}
-
-    if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
-      Object.assign(queryParams, this.route.snapshot.queryParams)
-    }
-
-    Object.assign(queryParams, { search: this.searchValue })
-
-    const o = this.auth.isLoggedIn()
-      ? this.loadUserLanguagesIfNeeded(queryParams)
-      : of(true)
-
-    o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
-  }
-
-  private loadUserLanguagesIfNeeded (queryParams: any) {
-    if (queryParams && queryParams.languageOneOf) return of(queryParams)
-
-    return this.auth.userInformationLoaded
-               .pipe(
-                 first(),
-                 tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages }))
-               )
-  }
-}
+export class HeaderComponent {}
index d98d2d00a7180f10b990f748eaea89f5082829a9..a882d4d1faaf82877afdaa1facc0ff1cf17f7d2e 100644 (file)
@@ -1 +1,4 @@
 export * from './header.component'
+export * from './search-typeahead.component'
+export * from './suggestions.component'
+export * from './suggestion.component'
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
new file mode 100644 (file)
index 0000000..e368090
--- /dev/null
@@ -0,0 +1,53 @@
+<div class="d-inline-flex position-relative" id="typeahead-container">
+  <input
+    type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
+    [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
+  >
+  <span class="icon icon-search" (click)="doSearch()"></span>
+
+  <div class="position-absolute jump-to-suggestions">
+    <!-- suggestions -->
+    <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
+
+    <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
+    <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
+      <ng-container *ngIf="activeResult.type === 'search-global'">
+        <div class="d-flex justify-content-between">
+          <label class="small-title" i18n>Global search</label>
+          <div class="advanced-search-status text-muted">
+            <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
+            <i class="glyphicon glyphicon-globe"></i>
+          </div>
+        </div>
+        <div class="text-muted" i18n>Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.</div>
+      </ng-container>
+    </div>
+
+    <!-- search instructions, when search input is empty -->
+    <div *ngIf="areInstructionsDisplayed" id="typeahead-instructions" class="overflow-hidden">
+      <div class="d-flex justify-content-between">
+        <label class="small-title" i18n>Advanced search</label>
+        <div class="advanced-search-status c-help">
+          <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
+            <span *ngIf="canSearchAnyURI" class="mr-1" i18n>any instance</span>
+            <span *ngIf="!canSearchAnyURI" class="mr-1" i18n>only followed instances</span>
+            <i [ngClass]="canSearchAnyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
+          </span>
+        </div>
+      </div>
+      <ul>
+        <li>
+          <em>@username@domain</em> <span class="flex-auto text-muted" i18n>account or channel</span>
+        </li>
+        <li>
+          <em>URL</em> <span class="text-muted" i18n>account or channel</span>
+        </li>
+        <li>
+          <em>UUID</em> <span class="text-muted" i18n>video</span>
+        </li>
+      </ul>
+      <span class="text-muted" i18n>Any other text will return matching video, channel or account names.</span>
+    </div>
+  </div>
+
+</div>
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
new file mode 100644 (file)
index 0000000..a55e783
--- /dev/null
@@ -0,0 +1,145 @@
+@import '_mixins';
+@import '_variables';
+@import '_bootstrap-variables';
+@import '~bootstrap/scss/mixins/_breakpoints';
+
+#search-video {
+  @include peertube-input-text($search-input-width);
+  padding-left: 10px;
+  padding-right: 40px; // For the search icon
+  font-size: 14px;
+
+  &::placeholder {
+    color: var(--inputPlaceholderColor);
+  }
+}
+
+.icon.icon-search {
+  @include icon(25px);
+  height: 21px;
+
+  background-color: var(--mainForegroundColor);
+  mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
+
+  // yolo
+  position: absolute;
+  margin-left: -35px;
+  margin-top: 5px;
+}
+
+.jump-to-suggestions {
+  top: 100%;
+  left: 0;
+  z-index: 35;
+  width: 100%;
+}
+
+#typeahead-help,
+#typeahead-instructions,
+my-suggestions ::ng-deep ul {
+  border: 1px solid var(--mainBackgroundColor);
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+  background: var(--mainBackgroundColor);
+  transition: .3s ease;
+  transition-property: box-shadow;
+}
+
+#typeahead-help,
+#typeahead-instructions {
+  margin-top: 10px;
+  width: 100%;
+  padding: .5rem 1rem;
+  white-space: normal;
+
+  ul {
+    list-style: none;
+    padding: 0;
+    margin-bottom: .5rem;
+
+    em {
+      font-weight: 600;
+      margin-right: 0.2rem;
+      font-style: normal;
+    }
+  }
+}
+
+#typeahead-container {
+  input {
+    border: 1px solid var(--mainBackgroundColor) !important;
+    box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
+    flex-grow: 1;
+    transition: box-shadow .3s ease, width .2s ease;
+  }
+
+  @media screen and (min-width: $mobile-view) {
+    margin-left: 10px;
+  }
+
+  @media screen and (max-width: $small-view) {
+    flex: 1;
+
+    input {
+      width: unset;
+    }
+  }
+
+  span {
+    right: 10px;
+  }
+
+  & > div:last-child {
+    // we have to switch the display and not the opacity, 
+    // to avoid clashing with the rest of the interface.
+    display: none;
+  }
+
+  &:focus,
+  ::ng-deep &:focus-within {
+    & > div:last-child {
+      @media screen and (min-width: $mobile-view) {
+        display: initial !important;
+      }
+      
+      #typeahead-help,
+      #typeahead-instructions,
+      my-suggestions ::ng-deep ul {
+        box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
+      }
+    }
+
+    ::ng-deep input {
+      box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
+      border-end-start-radius: 0;
+      border-end-end-radius: 0;
+
+      @include media-breakpoint-up(lg) {
+        width: 500px;
+      }
+    }
+  }
+}
+
+.glyphicon {
+  top: 3px;
+}
+
+.advanced-search-status {
+  height: max-content;
+  cursor: default;
+
+  &.c-help {
+    cursor: help;
+  }
+}
+
+.small-title {
+  @include in-content-small-title;
+
+  margin-bottom: .5rem;
+}
+
+::ng-deep my-suggestion {
+  width: 100%;
+}
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts
new file mode 100644 (file)
index 0000000..210a147
--- /dev/null
@@ -0,0 +1,186 @@
+import {
+  Component,
+  OnInit,
+  OnDestroy,
+  QueryList,
+  ViewChild,
+  ElementRef
+} from '@angular/core'
+import { Router, Params, ActivatedRoute } from '@angular/router'
+import { AuthService, ServerService } from '@app/core'
+import { first, tap } from 'rxjs/operators'
+import { ListKeyManager } from '@angular/cdk/a11y'
+import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
+import { SuggestionComponent, Result } from './suggestion.component'
+import { of } from 'rxjs'
+import { ServerConfig } from '@shared/models'
+
+@Component({
+  selector: 'my-search-typeahead',
+  templateUrl: './search-typeahead.component.html',
+  styleUrls: [ './search-typeahead.component.scss' ]
+})
+export class SearchTypeaheadComponent implements OnInit, OnDestroy {
+  @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
+
+  hasChannel = false
+  inChannel = false
+  newSearch = true
+
+  search = ''
+  serverConfig: ServerConfig
+
+  inThisChannelText: string
+
+  keyboardEventsManager: ListKeyManager<SuggestionComponent>
+  results: Result[] = []
+
+  constructor (
+    private authService: AuthService,
+    private router: Router,
+    private route: ActivatedRoute,
+    private serverService: ServerService
+  ) {}
+
+  ngOnInit () {
+    const query = this.route.snapshot.queryParams
+    if (query['search']) this.search = query['search']
+
+    this.serverService.getConfig()
+      .subscribe(config => this.serverConfig = config)
+  }
+
+  ngOnDestroy () {
+    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
+  }
+
+  get activeResult () {
+    return this.keyboardEventsManager?.activeItem?.result
+  }
+
+  get areInstructionsDisplayed () {
+    return !this.search
+  }
+
+  get showHelp () {
+    return this.search && this.newSearch && this.activeResult?.type === 'search-global'
+  }
+
+  get canSearchAnyURI () {
+    if (!this.serverConfig) return false
+    return this.authService.isLoggedIn()
+      ? this.serverConfig.search.remoteUri.users
+      : this.serverConfig.search.remoteUri.anonymous
+  }
+
+  onSearchChange () {
+    this.computeResults()
+  }
+
+  computeResults () {
+    this.newSearch = true
+    let results: Result[] = []
+
+    if (this.search) {
+      results = [
+        /* Channel search is still unimplemented. Uncomment when it is.
+        {
+          text: this.search,
+          type: 'search-channel'
+        },
+        */
+        {
+          text: this.search,
+          type: 'search-instance',
+          default: true
+        },
+        /* Global search is still unimplemented. Uncomment when it is.
+        {
+          text: this.search,
+          type: 'search-global'
+        },
+        */
+        ...results
+      ]
+    }
+
+    this.results = results.filter(
+      (result: Result) => {
+        // if we're not in a channel or one of its videos/playlits, show all channel-related results
+        if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
+        // if we're in a channel, show all channel-related results except for the channel redirection itself
+        if (this.inChannel) return result.type !== 'channel'
+        // all other result types are kept
+        return true
+      }
+    )
+  }
+
+  setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
+    event.items.forEach(e => {
+      if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
+        this.keyboardEventsManager.activeItem.active = true
+      } else {
+        e.active = false
+      }
+    })
+  }
+
+  initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
+    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
+
+    this.keyboardEventsManager = new ListKeyManager(event.items)
+
+    if (event.index !== undefined) {
+      this.keyboardEventsManager.setActiveItem(event.index)
+    } else {
+      this.keyboardEventsManager.setFirstItemActive()
+    }
+
+    this.keyboardEventsManager.change.subscribe(
+      _ => this.setEventItems(event)
+    )
+  }
+
+  handleKeyUp (event: KeyboardEvent) {
+    event.stopImmediatePropagation()
+    if (!this.keyboardEventsManager) return
+    
+    switch (event.key) {
+      case "ArrowDown":
+      case "ArrowUp":
+        this.keyboardEventsManager.onKeydown(event)
+        break
+      case "Enter":
+        this.newSearch = false
+        this.doSearch()
+        break
+    }
+  }
+
+  doSearch () {
+    const queryParams: Params = {}
+
+    if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
+      Object.assign(queryParams, this.route.snapshot.queryParams)
+    }
+
+    Object.assign(queryParams, { search: this.search })
+
+    const o = this.authService.isLoggedIn()
+      ? this.loadUserLanguagesIfNeeded(queryParams)
+      : of(true)
+
+    o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
+  }
+
+  private loadUserLanguagesIfNeeded (queryParams: any) {
+    if (queryParams && queryParams.languageOneOf) return of(queryParams)
+
+    return this.authService.userInformationLoaded
+               .pipe(
+                 first(),
+                 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
+               )
+  }
+}
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
new file mode 100644 (file)
index 0000000..d7ae345
--- /dev/null
@@ -0,0 +1,22 @@
+<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active">
+  <div class="flex-shrink-0 mr-2 text-center">
+    <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
+    <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
+  </div>
+
+  <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
+
+  <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div>
+
+  <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
+    <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
+    <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
+    <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
+    <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
+  </div>
+
+  <div *ngIf="result.type === 'channel'" aria-hidden="true" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6 d-on-nav-focus" i18n>
+    Jump to channel
+    <span class="d-inline-block ml-1 v-align-middle">↵</span>
+  </div>
+</a>
\ No newline at end of file
diff --git a/client/src/app/header/suggestion.component.scss b/client/src/app/header/suggestion.component.scss
new file mode 100644 (file)
index 0000000..1de2f43
--- /dev/null
@@ -0,0 +1,32 @@
+@import '_mixins';
+
+a {
+  @include disable-default-a-behaviour;
+  width: 100%;
+
+  &, &:hover {
+    color: var(--mainForegroundColor);
+
+    &.focus-visible {
+      background-color: var(--mainHoverColor);
+      color: var(--mainBackgroundColor);
+    }
+  }
+}
+
+.bg-gray {
+  background-color: var(--mainBackgroundColor);
+}
+
+.text-gray-light {
+  color: var(--mainForegroundColor);
+}
+
+my-global-icon {
+  width: 17px;
+  position: relative;
+  top: -2px;
+  margin: 5px;
+
+  @include apply-svg-color(var(--mainForegroundColor));
+}
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts
new file mode 100644 (file)
index 0000000..69641b5
--- /dev/null
@@ -0,0 +1,37 @@
+import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
+import { RouterLink } from '@angular/router'
+import { ListKeyManagerOption } from '@angular/cdk/a11y'
+
+export type Result = {
+  text: string
+  type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
+  routerLink?: RouterLink,
+  default?: boolean
+}
+
+@Component({
+  selector: 'my-suggestion',
+  templateUrl: './suggestion.component.html',
+  styleUrls: [ './suggestion.component.scss' ],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SuggestionComponent implements OnInit, ListKeyManagerOption {
+  @Input() result: Result
+  @Input() highlight: string
+  @Output() selected = new EventEmitter()
+
+  disabled = false
+  active = false
+
+  getLabel () {
+    return this.result.text
+  }
+
+  ngOnInit () {
+    if (this.result.default) this.active = true
+  }
+
+  selectItem () {
+    this.selected.emit(this.result)
+  }
+}
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html
new file mode 100644 (file)
index 0000000..8d017d7
--- /dev/null
@@ -0,0 +1,6 @@
+<ul role="listbox" class="p-0 m-0">
+  <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
+      role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
+    <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
+  </li>
+</ul>
\ No newline at end of file
diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts
new file mode 100644 (file)
index 0000000..ee3ef73
--- /dev/null
@@ -0,0 +1,24 @@
+import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
+import { SuggestionComponent } from './suggestion.component'
+
+@Component({
+  selector: 'my-suggestions',
+  templateUrl: './suggestions.component.html',
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SuggestionsComponent implements AfterViewInit {
+  @Input() results: any[]
+  @Input() highlight: string
+  @ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent>
+  @Output() init = new EventEmitter()
+
+  ngAfterViewInit () {
+    this.listItems.changes.subscribe(
+      _ => this.init.emit({ items: this.listItems })
+    )
+  }
+
+  hoverItem (index: number) {
+    this.init.emit({ items: this.listItems, index: index })
+  }
+}
index dd718a091879b5af317035b3e72064dc4b48c1c0..cb5f907232f04ccb1155574d3f4395fc8ee647e5 100644 (file)
@@ -6,7 +6,8 @@
   height: calc(100vh - #{$header-height});
   padding: 0;
   width: $menu-width;
-  z-index: 11000;
+  z-index: z(menu);
+  scrollbar-color: var(--actionButtonColor) var(--menuBackgroundColor);
 }
 
 menu {
diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts
new file mode 100644 (file)
index 0000000..fb60422
--- /dev/null
@@ -0,0 +1,54 @@
+import { PipeTransform, Pipe } from '@angular/core'
+import { SafeHtml } from '@angular/platform-browser'
+
+// Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369
+@Pipe({ name: 'highlight' })
+export class HighlightPipe implements PipeTransform {
+  /* use this for single match search */
+  static SINGLE_MATCH = 'Single-Match'
+  /* use this for single match search with a restriction that target should start with search string */
+  static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
+  /* use this for global search */
+  static MULTI_MATCH = 'Multi-Match'
+
+  // tslint:disable-next-line:no-empty
+  constructor () {}
+
+  transform (
+      contentString: string = null,
+      stringToHighlight: string = null,
+      option = 'Single-And-StartsWith-Match',
+      caseSensitive = false,
+      highlightStyleName = 'search-highlight'
+  ): SafeHtml {
+    if (stringToHighlight && contentString && option) {
+      let regex: any = ''
+      const caseFlag: string = !caseSensitive ? 'i' : ''
+      switch (option) {
+        case 'Single-Match': {
+          regex = new RegExp(stringToHighlight, caseFlag)
+          break
+        }
+        case 'Single-And-StartsWith-Match': {
+          regex = new RegExp('^' + stringToHighlight, caseFlag)
+          break
+        }
+        case 'Multi-Match': {
+          regex = new RegExp(stringToHighlight, 'g' + caseFlag)
+          break
+        }
+        default: {
+          // default will be a global case-insensitive match
+          regex = new RegExp(stringToHighlight, 'gi')
+        }
+      }
+      const replaced = contentString.replace(
+          regex,
+          (match) => `<span class="${highlightStyleName}">${match}</span>`
+      )
+      return replaced
+    } else {
+      return contentString
+    }
+  }
+}
index fd8b3354f1043ce9abc42c1c6fcf02a92f26460f..51a56d414499044cd808ac4a60528463583fc05c 100644 (file)
         <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean>
       </td>
     </tr>
+
+    <tr>
+      <td i18n class="label" colspan="2">Search</td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">Users can resolve distant content</td>
+      <td>
+        <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean>
+      </td>
+    </tr>
   </table>
 </div>
index 98211c7279891434d13539a1a57db65042696346..30b3ba0c12761c804ccdb19aa55ba0f434b04f8f 100644 (file)
@@ -89,6 +89,7 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
 import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
 import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
 import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
+import { HighlightPipe } from '@app/shared/angular/highlight.pipe'
 import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
 import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
 import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
@@ -149,6 +150,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
     NumberFormatterPipe,
     ObjectLengthPipe,
     FromNowPipe,
+    HighlightPipe,
     PeerTubeTemplateDirective,
     VideoDurationPipe,
 
@@ -254,6 +256,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
     NumberFormatterPipe,
     ObjectLengthPipe,
     FromNowPipe,
+    HighlightPipe,
     PeerTubeTemplateDirective,
     VideoDurationPipe
   ],
index e4840dd81fdaa2d0560c3e46dd5485c86c921b03..560414e90fa60200356f4afb2c4b2321adf8afd1 100644 (file)
@@ -1,5 +1,6 @@
 $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
+@import '_bootstrap-variables';
 @import '_variables';
 @import '_mixins';
 
@@ -47,6 +48,11 @@ body {
   font-size: 14px;
 }
 
+::selection {
+  color: var(--mainBackgroundColor);
+  background-color: var(--mainHoverColor);
+}
+
 #incompatible-browser {
   display: none;
   text-align: center;
@@ -162,7 +168,7 @@ label {
     color: var(--mainForegroundColor);
   }
 
-  @media screen and (max-width: 500px) {
+  @media screen and (max-width: $mobile-view) {
     margin-right: 15px;
   }
 }
@@ -229,7 +235,7 @@ table {
   }
 }
 
-@media screen and (max-width: 1600px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
   .main-col {
     &.expanded {
       .margin-content {
@@ -240,7 +246,7 @@ table {
   }
 }
 
-@media screen and (max-width: 900px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
   .main-col {
     &.expanded {
       .margin-content {
index 035270e89f08f08a340bbbafde03e876e43f9803..e10b8459635dc69938cc01ad8dd448ec0f7d4018 100644 (file)
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   animation: spin .7s infinite linear;
 }
 
+.flex-auto {
+  flex: auto;
+}
+
 @keyframes spin {
   from {
     transform: scale(1) rotate(0deg);
@@ -19,7 +23,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 .dropdown {
-  z-index: 10001 !important;
+  z-index: z(dropdown) !important;
 }
 
 .dropdown-menu {
@@ -48,7 +52,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 
-@media screen and (min-width: 768px) {
+@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
   .modal:before {
     vertical-align: middle;
     content: " ";
@@ -176,7 +180,11 @@ ngb-tabset.bootstrap {
 }
 
 ngb-modal-backdrop {
-  z-index: 10000 !important;
+  z-index: z(modal) - 1 !important;
+}
+
+ngb-modal-window {
+  z-index: z(modal) !important;
 }
 
 .btn-outline-tertiary {
index 7f413836b56388cf32d93582f55c4bcfa5a4c749..b3ab0eb2b6b3586f3be437727502dad14a88e22f 100644 (file)
@@ -13,8 +13,9 @@ $grid-breakpoints: (
   md: 768px,
   // Large screen / desktop
   lg: 900px,
-  // Extra large screen / wide desktop
-  xl: 1200px
+  // Extra large screens / wide desktops
+  xl: 1200px,
+  xxl: 1600px
 );
 
 $container-max-widths: (
index b0e07d61a1823da78d8f152b895e4cfa78afa396..c1d1b3c590afc82fd372ad53aa3d321174efff7f 100644 (file)
@@ -240,7 +240,7 @@ $play-overlay-width: 18px;
     width: $video-miniature-width * 2;
   }
 
-  @media screen and (max-width: 500px) {
+  @media screen and (max-width: $mobile-view) {
     width: auto;
     margin: 0 !important;
 
index 317781e0e9755ad077db1f27d205a1f1a6e3cd1a..4766e449085f77657c8f51ec22d904387d9c48b7 100644 (file)
   &:focus:not(.focus-visible) {
     outline: none;
   }
-
-  &::-moz-focus-inner {
-    border: 0;
-    padding: 0
-  }
 }
 
 
 @mixin actor-owner {
   @include disable-default-a-behaviour;
 
-  display: inline-table;
   font-size: 13px;
   margin-top: 4px;
   color: var(--mainForegroundColor);
       .actor-names {
         display: flex;
         align-items: center;
+        flex-wrap: wrap;
 
         .actor-display-name {
           font-size: 23px;
           font-weight: $font-bold;
+          margin-right: 7px;
         }
 
         .actor-name {
-          margin-left: 7px;
           position: relative;
           top: 3px;
           font-size: 14px;
         }
       }
 
+      .actor-lower {
+        grid-area: lower;
+      }
+
       .actor-followers {
         font-size: 15px;
       }
       margin-bottom: 0;
       text-transform: uppercase;
       font-weight: 600;
+      font-size: 110%;
+
+      @media screen and (max-width: $mobile-view) {
+        font-size: 130%;
+      }
     }
   }
 }
index e087a25482e654e5181afb4b52969345ff23aa1d..d8db3f3f84de1ca80a21be5b4fc275e501cb074b 100644 (file)
@@ -1,3 +1,5 @@
+@import '_bootstrap-variables';
+
 $small-view: 800px;
 $mobile-view: 500px;
 
@@ -93,8 +95,24 @@ $variables: (
   --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor)
 );
 
-/*** theme helper ***/
-
 @function var($variable) {
   @return map-get($variables, $variable);
 }
+
+/*** z-index groups ***/
+
+$zindex: (
+  header       :  1000,
+    /* header context */
+    headerLeft :    10,
+  menu         : 11000,
+  dropdown     : 12000,
+  loadbar      : 13000,
+  modal        : 14000,
+  notification : 15000,
+  hotkeys      : 16000
+);
+
+@function z($label) {
+  @return map-get($zindex, $label);
+}
index 7d687d479c81bb942fc83aad2eef5c5cd035b401..d584b7c67bf3301ef20840c2d1b4abd5d9378d31 100644 (file)
@@ -1,3 +1,4 @@
+@import '_mixins';
 // Thanks: https://github.com/aitboudad/ngx-loading-bar/blob/master/loading-bar.css
 
 /* Make clicks pass-through */
@@ -20,7 +21,7 @@
 
   background: var(--mainColor);
   position: fixed;
-  z-index: 10002;
+  z-index: z(loadbar);
   top: 0;
   left: 0;
   width: 100%;
@@ -50,7 +51,7 @@
 #loading-bar-spinner {
   display: block;
   position: fixed;
-  z-index: 10002;
+  z-index: z(loadbar);
   top: 10px;
   left: 10px;
 }
index 753fdf5c97298a7670f9b9a2ed51878c51c8f7ff..4d2d6cb67669391d14d5d3f63157973b318d042b 100644 (file)
@@ -383,8 +383,7 @@ p-inputswitch {
 
 p-toast {
   .ui-toast {
-    // Modal is 10005
-    z-index: 10010 !important;
+    z-index: z(notification) !important;
   }
 
   .ui-toast-message {
index 912401faf6e99be08a41af25105d01a94f5431e4..e7475f56cec4c126aae6b1cafe3282e3d08f5338 100755 (executable)
@@ -39,8 +39,52 @@ post_build_hook
 
 # Don't build other languages if --light arg is provided
 if [ -z ${1+x} ] || [ "$1" != "--light" ]; then
-    if [ ! -z ${1+x} ] && [ "$1" == "--light-fr" ]; then
+    if [ ! -z ${1+x} ] && [ "$1" == "--light-hu" ]; then
+        languages=(["hu"]="hu-HU")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-th" ]; then
+        languages=(["th"]="th-TH")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-fi" ]; then
+        languages=(["fi"]="fi-FI")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-nl" ]; then
+        languages=(["nl"]="nl-NL")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-gd" ]; then
+        languages=(["gd"]="gd")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-el" ]; then
+        languages=(["el"]="el-GR")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-es" ]; then
+        languages=(["es"]="es-ES")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-oc" ]; then
+        languages=(["oc"]="oc")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-pt" ]; then
+        languages=(["pt"]="pt-BR")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-pt-PT" ]; then
+        languages=(["pt-PT"]="pt-PT")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-sv" ]; then
+        languages=(["sv"]="sv-SE")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-pl" ]; then
+        languages=(["pl"]="pl-PL")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-ru" ]; then
+        languages=(["ru"]="ru-RU")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-zh-Hans" ]; then
+        languages=(["zh-Hans"]="zh-Hans-CN")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-zh-Hant" ]; then
+        languages=(["zh-Hant"]="zh-Hant-TW")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-fr" ]; then
         languages=(["fr"]="fr-FR")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-ja" ]; then
+        languages=(["ja"]="ja-JP")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-eu" ]; then
+        languages=(["eu"]="eu-ES")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-ca" ]; then
+        languages=(["ca"]="ca-ES")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-cs" ]; then
+        languages=(["cs"]="cs-CZ")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-eo" ]; then
+        languages=(["eo"]="eo")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-de" ]; then
+        languages=(["de"]="de-DE")
+    elif [ ! -z ${1+x} ] && [ "$1" == "--light-it" ]; then
+        languages=(["it"]="it-IT")
     else
         # Supported languages
         languages=(
index 69940f3952f6c3eb1f47e5bb8f63b4682f23e759..a383a723f8e42581015dad775763672eb536887d 100644 (file)
@@ -73,6 +73,12 @@ async function getConfig (req: express.Request, res: express.Response) {
         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
       }
     },
+    search: {
+      remoteUri: {
+        users: CONFIG.SEARCH.REMOTE_URI.USERS,
+        anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
+      }
+    },
     plugin: {
       registered: getRegisteredPlugins()
     },
index 4c6cf9597174b22558f3940faaa998790c3dc06b..75d1a816bb7bbd339ae33ac3dfc4764b4875665b 100644 (file)
@@ -235,6 +235,12 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
         nodeName: CONFIG.INSTANCE.NAME,
         nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
         nodeConfig: {
+          search: {
+            remoteUri: {
+              users: CONFIG.SEARCH.REMOTE_URI.USERS,
+              anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
+            }
+          },
           plugin: {
             registered: getRegisteredPlugins()
           },
index 76e0d6f2ddfa562ae42b4a8efd720a4636d8f22f..c3976a3461c35375ccfb0411bc98a3e644f0d996 100644 (file)
@@ -28,6 +28,13 @@ export interface ServerConfig {
     }
   }
 
+  search: {
+    remoteUri: {
+      users: boolean
+      anonymous: boolean
+    }
+  }
+
   plugin: {
     registered: ServerConfigPlugin[]
   }
index d7059d285b5aee0235fc39cd81b5d2e8e588752e..b251329d0130deec31a583edfab2cb74aececfca 100644 (file)
@@ -21,8 +21,7 @@ $ curl "https://raw.githubusercontent.com/chocobozzz/PeerTube/master/support/doc
 $ touch ./docker-volume/traefik/acme.json && chmod 600 ./docker-volume/traefik/acme.json
 $ curl -s "https://raw.githubusercontent.com/chocobozzz/PeerTube/master/support/docker/production/docker-compose.yml" -o docker-compose.yml "https://raw.githubusercontent.com/Chocobozzz/PeerTube/master/support/docker/production/.env" -o .env
 ```
-View the source of the files you're about to download: [docker-compose.yml](https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/docker-compose.yml) and the [traefik.toml](https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/traefik.toml) and the [.env]
-(https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/.env)
+View the source of the files you're about to download: [docker-compose.yml](https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/docker-compose.yml) and the [traefik.toml](https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/traefik.toml) and the [.env](https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/.env)
 
 Update the reverse proxy configuration: