aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--client/src/app/+accounts/accounts.component.html15
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss2
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss2
-rw-r--r--client/src/app/+signup/+register/register.component.scss2
-rw-r--r--client/src/app/+video-channels/video-channels.component.html24
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss20
-rw-r--r--client/src/app/app.component.html2
-rw-r--r--client/src/app/app.component.scss6
-rw-r--r--client/src/app/app.module.ts5
-rw-r--r--client/src/app/core/hotkeys/hotkeys.component.scss7
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/header/header.component.html6
-rw-r--r--client/src/app/header/header.component.scss49
-rw-r--r--client/src/app/header/header.component.ts60
-rw-r--r--client/src/app/header/index.ts3
-rw-r--r--client/src/app/header/search-typeahead.component.html53
-rw-r--r--client/src/app/header/search-typeahead.component.scss145
-rw-r--r--client/src/app/header/search-typeahead.component.ts186
-rw-r--r--client/src/app/header/suggestion.component.html22
-rw-r--r--client/src/app/header/suggestion.component.scss32
-rw-r--r--client/src/app/header/suggestion.component.ts37
-rw-r--r--client/src/app/header/suggestions.component.html6
-rw-r--r--client/src/app/header/suggestions.component.ts24
-rw-r--r--client/src/app/menu/menu.component.scss3
-rw-r--r--client/src/app/shared/angular/highlight.pipe.ts54
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.html11
-rw-r--r--client/src/app/shared/shared.module.ts3
-rw-r--r--client/src/sass/application.scss12
-rw-r--r--client/src/sass/bootstrap.scss14
-rw-r--r--client/src/sass/include/_bootstrap-variables.scss5
-rw-r--r--client/src/sass/include/_miniature.scss2
-rw-r--r--client/src/sass/include/_mixins.scss18
-rw-r--r--client/src/sass/include/_variables.scss22
-rw-r--r--client/src/sass/loading-bar.scss5
-rw-r--r--client/src/sass/primeng-custom.scss3
-rwxr-xr-xscripts/build/client.sh46
-rw-r--r--server/controllers/api/config.ts6
-rw-r--r--server/controllers/static.ts6
-rw-r--r--shared/models/server/server-config.model.ts7
-rw-r--r--support/doc/docker.md3
41 files changed, 770 insertions, 166 deletions
diff --git a/README.md b/README.md
index 6219d6f3a..02809db42 100644
--- a/README.md
+++ b/README.md
@@ -196,7 +196,7 @@ Quonfucius, IP Solution, \_Laure\_, @lex666, 0x010C, 3dsman, 3rw4n-G3D, aallrd,
196 196
197## License 197## License
198 198
199Copyright (C) 2015-2019 PeerTube Contributors 199Copyright (C) 2015-2020 PeerTube Contributors
200 200
201This program is free software: you can redistribute it and/or modify 201This program is free software: you can redistribute it and/or modify
202it under the terms of the GNU Affero General Public License as published 202it under the terms of the GNU Affero General Public License as published
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index b982fba9a..6a76393b9 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -7,14 +7,13 @@
7 <div class="actor-info"> 7 <div class="actor-info">
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ account.displayName }}</div> 9 <div class="actor-display-name">{{ account.displayName }}</div>
10 <div class="actor-name">{{ account.nameWithHost }} 10 <div class="actor-name">
11 11 <span>{{ account.nameWithHost }}</span>
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
13 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
14 > 14 >
15 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-copy"></span>
16 </button> 16 </button>
17
18 </div> 17 </div>
19 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
20 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 19 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
index 20582e478..db0c7f94f 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
@@ -58,7 +58,7 @@
58 margin: 20px 0 50px; 58 margin: 20px 0 50px;
59} 59}
60 60
61@media screen and (max-width: 800px) { 61@media screen and (max-width: $small-view) {
62 .video-channels-header { 62 .video-channels-header {
63 text-align: center; 63 text-align: center;
64 } 64 }
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss
index 4e4156b22..aed3302ba 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss
@@ -43,7 +43,7 @@
43 } 43 }
44} 44}
45 45
46@media screen and (max-width: 800px) { 46@media screen and (max-width: $small-view) {
47 .video-playlists-header { 47 .video-playlists-header {
48 text-align: center; 48 text-align: center;
49 } 49 }
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 2f62dd59d..e135b5cb4 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -44,7 +44,7 @@
44 } 44 }
45 } 45 }
46 46
47 @media screen and (max-width: 500px) { 47 @media screen and (max-width: $mobile-view) {
48 width: auto; 48 width: auto;
49 } 49 }
50 } 50 }
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 735a8f2c8..1087de113 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -7,25 +7,29 @@
7 <div class="actor-info"> 7 <div class="actor-info">
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ videoChannel.displayName }}</div> 9 <div class="actor-display-name">{{ videoChannel.displayName }}</div>
10 <div class="actor-name">{{ videoChannel.nameWithHost }} 10 <div class="actor-name">
11 <span>{{ videoChannel.nameWithHost }}</span>
11 <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
12 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
13 > 14 >
14 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-copy"></span>
15 </button> 16 </button>
16 </div> 17 </div>
18 </div>
17 19
18 <div class="right-buttons"> 20 <div class="right-buttons">
19 <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> 21 <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
20 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> 22 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
21 </div>
22 </div> 23 </div>
23 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
24 24
25 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> 25 <div class="actor-lower">
26 <span i18n>Created by {{ videoChannel.ownerBy }}</span> 26 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
27 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 27
28 </a> 28 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
29 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
30 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
31 </a>
32 </div>
29 </div> 33 </div>
30 </div> 34 </div>
31 35
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index 50b69e7ac..aa26a7e7b 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -8,6 +8,23 @@
8 width: 100%; 8 width: 100%;
9 } 9 }
10 10
11 .actor-info {
12 display: grid !important;
13 grid-template-columns: 1fr auto;
14 grid-template-rows: 1fr auto / 1fr auto;
15 grid-template-areas: "name buttons"
16 "lower buttons";
17
18 @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
19 grid-template-areas: "name name"
20 "lower buttons";
21 }
22 }
23
24 .actor-names {
25 grid-area: name;
26 }
27
11 .actor-name { 28 .actor-name {
12 flex-grow: 1; 29 flex-grow: 1;
13 30
@@ -25,6 +42,9 @@
25 margin-left: auto; 42 margin-left: auto;
26 margin-top: 20px; 43 margin-top: 20px;
27 44
45 grid-row: buttons-start / span buttons-end;
46 grid-column: buttons-start;
47
28 a { 48 a {
29 @include peertube-button-outline; 49 @include peertube-button-outline;
30 line-height: 1.8; 50 line-height: 1.8;
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 2660c5377..f5a8dbd34 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -15,7 +15,7 @@
15 </div> 15 </div>
16 16
17 <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> 17 <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
18 <my-header></my-header> 18 <my-header class="w-100 d-flex justify-content-end"></my-header>
19 </div> 19 </div>
20 </div> 20 </div>
21 21
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 51a7a3dd1..a7be8e1af 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -16,12 +16,12 @@
16 top: 0; 16 top: 0;
17 width: 100%; 17 width: 100%;
18 background-color: var(--mainBackgroundColor); 18 background-color: var(--mainBackgroundColor);
19 z-index: 1000; 19 z-index: z(header);
20 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); 20 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
21 display: flex; 21 display: flex;
22 22
23 .top-left-block { 23 .top-left-block {
24 z-index: 1001; 24 z-index: z(headerLeft);
25 height: $header-height; 25 height: $header-height;
26 display: flex; 26 display: flex;
27 align-items: center; 27 align-items: center;
@@ -61,7 +61,7 @@
61 } 61 }
62 } 62 }
63 63
64 @media screen and (max-width: 500px) { 64 @media screen and (max-width: $mobile-view) {
65 width: 70px; 65 width: 70px;
66 66
67 .peertube-title { 67 .peertube-title {
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index d11dba34d..9e220a383 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -9,7 +9,7 @@ import 'focus-visible'
9import { AppRoutingModule } from './app-routing.module' 9import { AppRoutingModule } from './app-routing.module'
10import { AppComponent } from './app.component' 10import { AppComponent } from './app.component'
11import { CoreModule } from './core' 11import { CoreModule } from './core'
12import { HeaderComponent } from './header' 12import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header'
13import { LoginModule } from './login' 13import { LoginModule } from './login'
14import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' 14import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
15import { SharedModule } from './shared' 15import { SharedModule } from './shared'
@@ -41,6 +41,9 @@ export function metaFactory (serverService: ServerService): MetaLoader {
41 LanguageChooserComponent, 41 LanguageChooserComponent,
42 AvatarNotificationComponent, 42 AvatarNotificationComponent,
43 HeaderComponent, 43 HeaderComponent,
44 SearchTypeaheadComponent,
45 SuggestionsComponent,
46 SuggestionComponent,
44 47
45 WelcomeModalComponent, 48 WelcomeModalComponent,
46 InstanceConfigWarningModalComponent 49 InstanceConfigWarningModalComponent
diff --git a/client/src/app/core/hotkeys/hotkeys.component.scss b/client/src/app/core/hotkeys/hotkeys.component.scss
index 3aa0b6252..a970260c9 100644
--- a/client/src/app/core/hotkeys/hotkeys.component.scss
+++ b/client/src/app/core/hotkeys/hotkeys.component.scss
@@ -1,3 +1,6 @@
1@import '_variables';
2@import '_mixins';
3
1.cfp-hotkeys-container { 4.cfp-hotkeys-container {
2 display: flex !important; 5 display: flex !important;
3 align-items: center; 6 align-items: center;
@@ -23,7 +26,7 @@
23} 26}
24 27
25.cfp-hotkeys-container.fade.in { 28.cfp-hotkeys-container.fade.in {
26 z-index: 10002; 29 z-index: z(hotkeys);
27 visibility: visible; 30 visibility: visible;
28 opacity: 1; 31 opacity: 1;
29} 32}
@@ -91,7 +94,7 @@
91 cursor: pointer; 94 cursor: pointer;
92} 95}
93 96
94@media all and (max-width: 500px) { 97@media all and (max-width: $mobile-view) {
95 .cfp-hotkeys { 98 .cfp-hotkeys {
96 font-size: 0.8em; 99 font-size: 0.8em;
97 } 100 }
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 1f6cfb596..c0e1f08bb 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -47,6 +47,12 @@ export class ServerService {
47 css: '' 47 css: ''
48 } 48 }
49 }, 49 },
50 search: {
51 remoteUri: {
52 users: true,
53 anonymous: false
54 }
55 },
50 plugin: { 56 plugin: {
51 registered: [] 57 registered: []
52 }, 58 },
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index 4fd18f9bd..49e219187 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,8 +1,4 @@
1<input 1<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
2 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
3 [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
4>
5<span (click)="doSearch()" class="icon icon-search"></span>
6 2
7<a class="upload-button" routerLink="/videos/upload"> 3<a class="upload-button" routerLink="/videos/upload">
8 <my-global-icon iconName="upload"></my-global-icon> 4 <my-global-icon iconName="upload"></my-global-icon>
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss
index 2bbde74bc..91b390773 100644
--- a/client/src/app/header/header.component.scss
+++ b/client/src/app/header/header.component.scss
@@ -1,51 +1,8 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4#search-video { 4my-search-typeahead {
5 @include peertube-input-text($search-input-width);
6 padding-left: 10px;
7 margin-right: 15px; 5 margin-right: 15px;
8 padding-right: 40px; // For the search icon
9 font-size: 14px;
10
11 transition: box-shadow .3s ease;
12
13 /* light border style */
14 border: 1px solid var(--mainBackgroundColor);
15 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
16
17 &:focus {
18 box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
19 }
20
21 &::placeholder {
22 color: var(--inputPlaceholderColor);
23 }
24
25 &:focus::placeholder {
26 opacity: 0 !important;
27 }
28
29 @media screen and (max-width: 800px) {
30 width: calc(100% - 150px);
31 }
32
33 @media screen and (max-width: 600px) {
34 width: calc(100% - 70px);
35 }
36}
37
38.icon.icon-search {
39 @include icon(25px);
40 height: 21px;
41
42 background-color: var(--mainForegroundColor);
43 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
44
45 // yolo
46 position: absolute;
47 margin-left: -50px;
48 margin-top: 5px;
49} 6}
50 7
51.upload-button { 8.upload-button {
@@ -56,10 +13,6 @@
56 color: var(--mainBackgroundColor) !important; 13 color: var(--mainBackgroundColor) !important;
57 margin-right: 25px; 14 margin-right: 25px;
58 15
59 @media screen and (max-width: 800px) {
60 margin-right: 0;
61 }
62
63 @media screen and (max-width: 600px) { 16 @media screen and (max-width: 600px) {
64 margin-right: 10px; 17 margin-right: 10px;
65 padding: 0 10px; 18 padding: 0 10px;
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
index 92a7eded6..cce76b0d1 100644
--- a/client/src/app/header/header.component.ts
+++ b/client/src/app/header/header.component.ts
@@ -1,10 +1,4 @@
1import { filter, first, map, tap } from 'rxjs/operators' 1import { Component } from '@angular/core'
2import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
4import { getParameterByName } from '../shared/misc/utils'
5import { AuthService, Notifier, ServerService } from '@app/core'
6import { of } from 'rxjs'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8 2
9@Component({ 3@Component({
10 selector: 'my-header', 4 selector: 'my-header',
@@ -12,54 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12 styleUrls: [ './header.component.scss' ] 6 styleUrls: [ './header.component.scss' ]
13}) 7})
14 8
15export class HeaderComponent implements OnInit { 9export class HeaderComponent {}
16 searchValue = ''
17 ariaLabelTextForSearch = ''
18
19 constructor (
20 private router: Router,
21 private route: ActivatedRoute,
22 private auth: AuthService,
23 private serverService: ServerService,
24 private authService: AuthService,
25 private notifier: Notifier,
26 private i18n: I18n
27 ) {}
28
29 ngOnInit () {
30 this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
31
32 this.router.events
33 .pipe(
34 filter(e => e instanceof NavigationEnd),
35 map(() => getParameterByName('search', window.location.href))
36 )
37 .subscribe(searchQuery => this.searchValue = searchQuery || '')
38 }
39
40 doSearch () {
41 const queryParams: Params = {}
42
43 if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
44 Object.assign(queryParams, this.route.snapshot.queryParams)
45 }
46
47 Object.assign(queryParams, { search: this.searchValue })
48
49 const o = this.auth.isLoggedIn()
50 ? this.loadUserLanguagesIfNeeded(queryParams)
51 : of(true)
52
53 o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
54 }
55
56 private loadUserLanguagesIfNeeded (queryParams: any) {
57 if (queryParams && queryParams.languageOneOf) return of(queryParams)
58
59 return this.auth.userInformationLoaded
60 .pipe(
61 first(),
62 tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages }))
63 )
64 }
65}
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts
index d98d2d00a..a882d4d1f 100644
--- a/client/src/app/header/index.ts
+++ b/client/src/app/header/index.ts
@@ -1 +1,4 @@
1export * from './header.component' 1export * from './header.component'
2export * from './search-typeahead.component'
3export * from './suggestions.component'
4export * 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
index 000000000..e36809060
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.html
@@ -0,0 +1,53 @@
1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <input
3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
5 >
6 <span class="icon icon-search" (click)="doSearch()"></span>
7
8 <div class="position-absolute jump-to-suggestions">
9 <!-- suggestions -->
10 <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
11
12 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
13 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
14 <ng-container *ngIf="activeResult.type === 'search-global'">
15 <div class="d-flex justify-content-between">
16 <label class="small-title" i18n>Global search</label>
17 <div class="advanced-search-status text-muted">
18 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
19 <i class="glyphicon glyphicon-globe"></i>
20 </div>
21 </div>
22 <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>
23 </ng-container>
24 </div>
25
26 <!-- search instructions, when search input is empty -->
27 <div *ngIf="areInstructionsDisplayed" id="typeahead-instructions" class="overflow-hidden">
28 <div class="d-flex justify-content-between">
29 <label class="small-title" i18n>Advanced search</label>
30 <div class="advanced-search-status c-help">
31 <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.">
32 <span *ngIf="canSearchAnyURI" class="mr-1" i18n>any instance</span>
33 <span *ngIf="!canSearchAnyURI" class="mr-1" i18n>only followed instances</span>
34 <i [ngClass]="canSearchAnyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
35 </span>
36 </div>
37 </div>
38 <ul>
39 <li>
40 <em>@username@domain</em> <span class="flex-auto text-muted" i18n>account or channel</span>
41 </li>
42 <li>
43 <em>URL</em> <span class="text-muted" i18n>account or channel</span>
44 </li>
45 <li>
46 <em>UUID</em> <span class="text-muted" i18n>video</span>
47 </li>
48 </ul>
49 <span class="text-muted" i18n>Any other text will return matching video, channel or account names.</span>
50 </div>
51 </div>
52
53</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
index 000000000..a55e78326
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -0,0 +1,145 @@
1@import '_mixins';
2@import '_variables';
3@import '_bootstrap-variables';
4@import '~bootstrap/scss/mixins/_breakpoints';
5
6#search-video {
7 @include peertube-input-text($search-input-width);
8 padding-left: 10px;
9 padding-right: 40px; // For the search icon
10 font-size: 14px;
11
12 &::placeholder {
13 color: var(--inputPlaceholderColor);
14 }
15}
16
17.icon.icon-search {
18 @include icon(25px);
19 height: 21px;
20
21 background-color: var(--mainForegroundColor);
22 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
23
24 // yolo
25 position: absolute;
26 margin-left: -35px;
27 margin-top: 5px;
28}
29
30.jump-to-suggestions {
31 top: 100%;
32 left: 0;
33 z-index: 35;
34 width: 100%;
35}
36
37#typeahead-help,
38#typeahead-instructions,
39my-suggestions ::ng-deep ul {
40 border: 1px solid var(--mainBackgroundColor);
41 border-bottom-right-radius: 3px;
42 border-bottom-left-radius: 3px;
43 background: var(--mainBackgroundColor);
44 transition: .3s ease;
45 transition-property: box-shadow;
46}
47
48#typeahead-help,
49#typeahead-instructions {
50 margin-top: 10px;
51 width: 100%;
52 padding: .5rem 1rem;
53 white-space: normal;
54
55 ul {
56 list-style: none;
57 padding: 0;
58 margin-bottom: .5rem;
59
60 em {
61 font-weight: 600;
62 margin-right: 0.2rem;
63 font-style: normal;
64 }
65 }
66}
67
68#typeahead-container {
69 input {
70 border: 1px solid var(--mainBackgroundColor) !important;
71 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
72 flex-grow: 1;
73 transition: box-shadow .3s ease, width .2s ease;
74 }
75
76 @media screen and (min-width: $mobile-view) {
77 margin-left: 10px;
78 }
79
80 @media screen and (max-width: $small-view) {
81 flex: 1;
82
83 input {
84 width: unset;
85 }
86 }
87
88 span {
89 right: 10px;
90 }
91
92 & > div:last-child {
93 // we have to switch the display and not the opacity,
94 // to avoid clashing with the rest of the interface.
95 display: none;
96 }
97
98 &:focus,
99 ::ng-deep &:focus-within {
100 & > div:last-child {
101 @media screen and (min-width: $mobile-view) {
102 display: initial !important;
103 }
104
105 #typeahead-help,
106 #typeahead-instructions,
107 my-suggestions ::ng-deep ul {
108 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
109 }
110 }
111
112 ::ng-deep input {
113 box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
114 border-end-start-radius: 0;
115 border-end-end-radius: 0;
116
117 @include media-breakpoint-up(lg) {
118 width: 500px;
119 }
120 }
121 }
122}
123
124.glyphicon {
125 top: 3px;
126}
127
128.advanced-search-status {
129 height: max-content;
130 cursor: default;
131
132 &.c-help {
133 cursor: help;
134 }
135}
136
137.small-title {
138 @include in-content-small-title;
139
140 margin-bottom: .5rem;
141}
142
143::ng-deep my-suggestion {
144 width: 100%;
145}
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts
new file mode 100644
index 000000000..210a1474c
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.ts
@@ -0,0 +1,186 @@
1import {
2 Component,
3 OnInit,
4 OnDestroy,
5 QueryList,
6 ViewChild,
7 ElementRef
8} from '@angular/core'
9import { Router, Params, ActivatedRoute } from '@angular/router'
10import { AuthService, ServerService } from '@app/core'
11import { first, tap } from 'rxjs/operators'
12import { ListKeyManager } from '@angular/cdk/a11y'
13import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
14import { SuggestionComponent, Result } from './suggestion.component'
15import { of } from 'rxjs'
16import { ServerConfig } from '@shared/models'
17
18@Component({
19 selector: 'my-search-typeahead',
20 templateUrl: './search-typeahead.component.html',
21 styleUrls: [ './search-typeahead.component.scss' ]
22})
23export class SearchTypeaheadComponent implements OnInit, OnDestroy {
24 @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
25
26 hasChannel = false
27 inChannel = false
28 newSearch = true
29
30 search = ''
31 serverConfig: ServerConfig
32
33 inThisChannelText: string
34
35 keyboardEventsManager: ListKeyManager<SuggestionComponent>
36 results: Result[] = []
37
38 constructor (
39 private authService: AuthService,
40 private router: Router,
41 private route: ActivatedRoute,
42 private serverService: ServerService
43 ) {}
44
45 ngOnInit () {
46 const query = this.route.snapshot.queryParams
47 if (query['search']) this.search = query['search']
48
49 this.serverService.getConfig()
50 .subscribe(config => this.serverConfig = config)
51 }
52
53 ngOnDestroy () {
54 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
55 }
56
57 get activeResult () {
58 return this.keyboardEventsManager?.activeItem?.result
59 }
60
61 get areInstructionsDisplayed () {
62 return !this.search
63 }
64
65 get showHelp () {
66 return this.search && this.newSearch && this.activeResult?.type === 'search-global'
67 }
68
69 get canSearchAnyURI () {
70 if (!this.serverConfig) return false
71 return this.authService.isLoggedIn()
72 ? this.serverConfig.search.remoteUri.users
73 : this.serverConfig.search.remoteUri.anonymous
74 }
75
76 onSearchChange () {
77 this.computeResults()
78 }
79
80 computeResults () {
81 this.newSearch = true
82 let results: Result[] = []
83
84 if (this.search) {
85 results = [
86 /* Channel search is still unimplemented. Uncomment when it is.
87 {
88 text: this.search,
89 type: 'search-channel'
90 },
91 */
92 {
93 text: this.search,
94 type: 'search-instance',
95 default: true
96 },
97 /* Global search is still unimplemented. Uncomment when it is.
98 {
99 text: this.search,
100 type: 'search-global'
101 },
102 */
103 ...results
104 ]
105 }
106
107 this.results = results.filter(
108 (result: Result) => {
109 // if we're not in a channel or one of its videos/playlits, show all channel-related results
110 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
111 // if we're in a channel, show all channel-related results except for the channel redirection itself
112 if (this.inChannel) return result.type !== 'channel'
113 // all other result types are kept
114 return true
115 }
116 )
117 }
118
119 setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
120 event.items.forEach(e => {
121 if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
122 this.keyboardEventsManager.activeItem.active = true
123 } else {
124 e.active = false
125 }
126 })
127 }
128
129 initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
130 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
131
132 this.keyboardEventsManager = new ListKeyManager(event.items)
133
134 if (event.index !== undefined) {
135 this.keyboardEventsManager.setActiveItem(event.index)
136 } else {
137 this.keyboardEventsManager.setFirstItemActive()
138 }
139
140 this.keyboardEventsManager.change.subscribe(
141 _ => this.setEventItems(event)
142 )
143 }
144
145 handleKeyUp (event: KeyboardEvent) {
146 event.stopImmediatePropagation()
147 if (!this.keyboardEventsManager) return
148
149 switch (event.key) {
150 case "ArrowDown":
151 case "ArrowUp":
152 this.keyboardEventsManager.onKeydown(event)
153 break
154 case "Enter":
155 this.newSearch = false
156 this.doSearch()
157 break
158 }
159 }
160
161 doSearch () {
162 const queryParams: Params = {}
163
164 if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
165 Object.assign(queryParams, this.route.snapshot.queryParams)
166 }
167
168 Object.assign(queryParams, { search: this.search })
169
170 const o = this.authService.isLoggedIn()
171 ? this.loadUserLanguagesIfNeeded(queryParams)
172 : of(true)
173
174 o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
175 }
176
177 private loadUserLanguagesIfNeeded (queryParams: any) {
178 if (queryParams && queryParams.languageOneOf) return of(queryParams)
179
180 return this.authService.userInformationLoaded
181 .pipe(
182 first(),
183 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
184 )
185 }
186}
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
new file mode 100644
index 000000000..d7ae3450a
--- /dev/null
+++ b/client/src/app/header/suggestion.component.html
@@ -0,0 +1,22 @@
1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active">
2 <div class="flex-shrink-0 mr-2 text-center">
3 <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
4 <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
5 </div>
6
7 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
8
9 <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>
10
11 <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
12 <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
13 <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
14 <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
15 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
16 </div>
17
18 <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>
19 Jump to channel
20 <span class="d-inline-block ml-1 v-align-middle">↵</span>
21 </div>
22</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
index 000000000..1de2f43bd
--- /dev/null
+++ b/client/src/app/header/suggestion.component.scss
@@ -0,0 +1,32 @@
1@import '_mixins';
2
3a {
4 @include disable-default-a-behaviour;
5 width: 100%;
6
7 &, &:hover {
8 color: var(--mainForegroundColor);
9
10 &.focus-visible {
11 background-color: var(--mainHoverColor);
12 color: var(--mainBackgroundColor);
13 }
14 }
15}
16
17.bg-gray {
18 background-color: var(--mainBackgroundColor);
19}
20
21.text-gray-light {
22 color: var(--mainForegroundColor);
23}
24
25my-global-icon {
26 width: 17px;
27 position: relative;
28 top: -2px;
29 margin: 5px;
30
31 @include apply-svg-color(var(--mainForegroundColor));
32}
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts
new file mode 100644
index 000000000..69641b511
--- /dev/null
+++ b/client/src/app/header/suggestion.component.ts
@@ -0,0 +1,37 @@
1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
2import { RouterLink } from '@angular/router'
3import { ListKeyManagerOption } from '@angular/cdk/a11y'
4
5export type Result = {
6 text: string
7 type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
8 routerLink?: RouterLink,
9 default?: boolean
10}
11
12@Component({
13 selector: 'my-suggestion',
14 templateUrl: './suggestion.component.html',
15 styleUrls: [ './suggestion.component.scss' ],
16 changeDetection: ChangeDetectionStrategy.OnPush
17})
18export class SuggestionComponent implements OnInit, ListKeyManagerOption {
19 @Input() result: Result
20 @Input() highlight: string
21 @Output() selected = new EventEmitter()
22
23 disabled = false
24 active = false
25
26 getLabel () {
27 return this.result.text
28 }
29
30 ngOnInit () {
31 if (this.result.default) this.active = true
32 }
33
34 selectItem () {
35 this.selected.emit(this.result)
36 }
37}
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html
new file mode 100644
index 000000000..8d017d78d
--- /dev/null
+++ b/client/src/app/header/suggestions.component.html
@@ -0,0 +1,6 @@
1<ul role="listbox" class="p-0 m-0">
2 <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
3 role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
4 <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
5 </li>
6</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
index 000000000..ee3ef73c2
--- /dev/null
+++ b/client/src/app/header/suggestions.component.ts
@@ -0,0 +1,24 @@
1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
2import { SuggestionComponent } from './suggestion.component'
3
4@Component({
5 selector: 'my-suggestions',
6 templateUrl: './suggestions.component.html',
7 changeDetection: ChangeDetectionStrategy.OnPush
8})
9export class SuggestionsComponent implements AfterViewInit {
10 @Input() results: any[]
11 @Input() highlight: string
12 @ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent>
13 @Output() init = new EventEmitter()
14
15 ngAfterViewInit () {
16 this.listItems.changes.subscribe(
17 _ => this.init.emit({ items: this.listItems })
18 )
19 }
20
21 hoverItem (index: number) {
22 this.init.emit({ items: this.listItems, index: index })
23 }
24}
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index dd718a091..cb5f90723 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -6,7 +6,8 @@
6 height: calc(100vh - #{$header-height}); 6 height: calc(100vh - #{$header-height});
7 padding: 0; 7 padding: 0;
8 width: $menu-width; 8 width: $menu-width;
9 z-index: 11000; 9 z-index: z(menu);
10 scrollbar-color: var(--actionButtonColor) var(--menuBackgroundColor);
10} 11}
11 12
12menu { 13menu {
diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts
new file mode 100644
index 000000000..fb6042280
--- /dev/null
+++ b/client/src/app/shared/angular/highlight.pipe.ts
@@ -0,0 +1,54 @@
1import { PipeTransform, Pipe } from '@angular/core'
2import { SafeHtml } from '@angular/platform-browser'
3
4// Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369
5@Pipe({ name: 'highlight' })
6export class HighlightPipe implements PipeTransform {
7 /* use this for single match search */
8 static SINGLE_MATCH = 'Single-Match'
9 /* use this for single match search with a restriction that target should start with search string */
10 static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
11 /* use this for global search */
12 static MULTI_MATCH = 'Multi-Match'
13
14 // tslint:disable-next-line:no-empty
15 constructor () {}
16
17 transform (
18 contentString: string = null,
19 stringToHighlight: string = null,
20 option = 'Single-And-StartsWith-Match',
21 caseSensitive = false,
22 highlightStyleName = 'search-highlight'
23 ): SafeHtml {
24 if (stringToHighlight && contentString && option) {
25 let regex: any = ''
26 const caseFlag: string = !caseSensitive ? 'i' : ''
27 switch (option) {
28 case 'Single-Match': {
29 regex = new RegExp(stringToHighlight, caseFlag)
30 break
31 }
32 case 'Single-And-StartsWith-Match': {
33 regex = new RegExp('^' + stringToHighlight, caseFlag)
34 break
35 }
36 case 'Multi-Match': {
37 regex = new RegExp(stringToHighlight, 'g' + caseFlag)
38 break
39 }
40 default: {
41 // default will be a global case-insensitive match
42 regex = new RegExp(stringToHighlight, 'gi')
43 }
44 }
45 const replaced = contentString.replace(
46 regex,
47 (match) => `<span class="${highlightStyleName}">${match}</span>`
48 )
49 return replaced
50 } else {
51 return contentString
52 }
53 }
54}
diff --git a/client/src/app/shared/instance/instance-features-table.component.html b/client/src/app/shared/instance/instance-features-table.component.html
index fd8b3354f..51a56d414 100644
--- a/client/src/app/shared/instance/instance-features-table.component.html
+++ b/client/src/app/shared/instance/instance-features-table.component.html
@@ -91,5 +91,16 @@
91 <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean> 91 <my-feature-boolean [value]="serverConfig.tracker.enabled"></my-feature-boolean>
92 </td> 92 </td>
93 </tr> 93 </tr>
94
95 <tr>
96 <td i18n class="label" colspan="2">Search</td>
97 </tr>
98
99 <tr>
100 <td i18n class="sub-label">Users can resolve distant content</td>
101 <td>
102 <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean>
103 </td>
104 </tr>
94 </table> 105 </table>
95</div> 106</div>
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 98211c727..30b3ba0c1 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -89,6 +89,7 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
89import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' 89import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
90import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' 90import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
91import { FromNowPipe } from '@app/shared/angular/from-now.pipe' 91import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
92import { HighlightPipe } from '@app/shared/angular/highlight.pipe'
92import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 93import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
93import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' 94import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
94import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' 95import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
@@ -149,6 +150,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
149 NumberFormatterPipe, 150 NumberFormatterPipe,
150 ObjectLengthPipe, 151 ObjectLengthPipe,
151 FromNowPipe, 152 FromNowPipe,
153 HighlightPipe,
152 PeerTubeTemplateDirective, 154 PeerTubeTemplateDirective,
153 VideoDurationPipe, 155 VideoDurationPipe,
154 156
@@ -254,6 +256,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
254 NumberFormatterPipe, 256 NumberFormatterPipe,
255 ObjectLengthPipe, 257 ObjectLengthPipe,
256 FromNowPipe, 258 FromNowPipe,
259 HighlightPipe,
257 PeerTubeTemplateDirective, 260 PeerTubeTemplateDirective,
258 VideoDurationPipe 261 VideoDurationPipe
259 ], 262 ],
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index e4840dd81..560414e90 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -1,5 +1,6 @@
1$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; 1$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
2 2
3@import '_bootstrap-variables';
3@import '_variables'; 4@import '_variables';
4@import '_mixins'; 5@import '_mixins';
5 6
@@ -47,6 +48,11 @@ body {
47 font-size: 14px; 48 font-size: 14px;
48} 49}
49 50
51::selection {
52 color: var(--mainBackgroundColor);
53 background-color: var(--mainHoverColor);
54}
55
50#incompatible-browser { 56#incompatible-browser {
51 display: none; 57 display: none;
52 text-align: center; 58 text-align: center;
@@ -162,7 +168,7 @@ label {
162 color: var(--mainForegroundColor); 168 color: var(--mainForegroundColor);
163 } 169 }
164 170
165 @media screen and (max-width: 500px) { 171 @media screen and (max-width: $mobile-view) {
166 margin-right: 15px; 172 margin-right: 15px;
167 } 173 }
168} 174}
@@ -229,7 +235,7 @@ table {
229 } 235 }
230} 236}
231 237
232@media screen and (max-width: 1600px) { 238@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
233 .main-col { 239 .main-col {
234 &.expanded { 240 &.expanded {
235 .margin-content { 241 .margin-content {
@@ -240,7 +246,7 @@ table {
240 } 246 }
241} 247}
242 248
243@media screen and (max-width: 900px) { 249@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
244 .main-col { 250 .main-col {
245 &.expanded { 251 &.expanded {
246 .margin-content { 252 .margin-content {
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 035270e89..e10b84596 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
9 animation: spin .7s infinite linear; 9 animation: spin .7s infinite linear;
10} 10}
11 11
12.flex-auto {
13 flex: auto;
14}
15
12@keyframes spin { 16@keyframes spin {
13 from { 17 from {
14 transform: scale(1) rotate(0deg); 18 transform: scale(1) rotate(0deg);
@@ -19,7 +23,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
19} 23}
20 24
21.dropdown { 25.dropdown {
22 z-index: 10001 !important; 26 z-index: z(dropdown) !important;
23} 27}
24 28
25.dropdown-menu { 29.dropdown-menu {
@@ -48,7 +52,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
48} 52}
49 53
50 54
51@media screen and (min-width: 768px) { 55@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
52 .modal:before { 56 .modal:before {
53 vertical-align: middle; 57 vertical-align: middle;
54 content: " "; 58 content: " ";
@@ -176,7 +180,11 @@ ngb-tabset.bootstrap {
176} 180}
177 181
178ngb-modal-backdrop { 182ngb-modal-backdrop {
179 z-index: 10000 !important; 183 z-index: z(modal) - 1 !important;
184}
185
186ngb-modal-window {
187 z-index: z(modal) !important;
180} 188}
181 189
182.btn-outline-tertiary { 190.btn-outline-tertiary {
diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss
index 7f413836b..b3ab0eb2b 100644
--- a/client/src/sass/include/_bootstrap-variables.scss
+++ b/client/src/sass/include/_bootstrap-variables.scss
@@ -13,8 +13,9 @@ $grid-breakpoints: (
13 md: 768px, 13 md: 768px,
14 // Large screen / desktop 14 // Large screen / desktop
15 lg: 900px, 15 lg: 900px,
16 // Extra large screen / wide desktop 16 // Extra large screens / wide desktops
17 xl: 1200px 17 xl: 1200px,
18 xxl: 1600px
18); 19);
19 20
20$container-max-widths: ( 21$container-max-widths: (
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index b0e07d61a..c1d1b3c59 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -240,7 +240,7 @@ $play-overlay-width: 18px;
240 width: $video-miniature-width * 2; 240 width: $video-miniature-width * 2;
241 } 241 }
242 242
243 @media screen and (max-width: 500px) { 243 @media screen and (max-width: $mobile-view) {
244 width: auto; 244 width: auto;
245 margin: 0 !important; 245 margin: 0 !important;
246 246
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 317781e0e..4766e4490 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -11,11 +11,6 @@
11 &:focus:not(.focus-visible) { 11 &:focus:not(.focus-visible) {
12 outline: none; 12 outline: none;
13 } 13 }
14
15 &::-moz-focus-inner {
16 border: 0;
17 padding: 0
18 }
19} 14}
20 15
21 16
@@ -450,7 +445,6 @@
450@mixin actor-owner { 445@mixin actor-owner {
451 @include disable-default-a-behaviour; 446 @include disable-default-a-behaviour;
452 447
453 display: inline-table;
454 font-size: 13px; 448 font-size: 13px;
455 margin-top: 4px; 449 margin-top: 4px;
456 color: var(--mainForegroundColor); 450 color: var(--mainForegroundColor);
@@ -493,14 +487,15 @@
493 .actor-names { 487 .actor-names {
494 display: flex; 488 display: flex;
495 align-items: center; 489 align-items: center;
490 flex-wrap: wrap;
496 491
497 .actor-display-name { 492 .actor-display-name {
498 font-size: 23px; 493 font-size: 23px;
499 font-weight: $font-bold; 494 font-weight: $font-bold;
495 margin-right: 7px;
500 } 496 }
501 497
502 .actor-name { 498 .actor-name {
503 margin-left: 7px;
504 position: relative; 499 position: relative;
505 top: 3px; 500 top: 3px;
506 font-size: 14px; 501 font-size: 14px;
@@ -508,6 +503,10 @@
508 } 503 }
509 } 504 }
510 505
506 .actor-lower {
507 grid-area: lower;
508 }
509
511 .actor-followers { 510 .actor-followers {
512 font-size: 15px; 511 font-size: 15px;
513 } 512 }
@@ -527,6 +526,11 @@
527 margin-bottom: 0; 526 margin-bottom: 0;
528 text-transform: uppercase; 527 text-transform: uppercase;
529 font-weight: 600; 528 font-weight: 600;
529 font-size: 110%;
530
531 @media screen and (max-width: $mobile-view) {
532 font-size: 130%;
533 }
530 } 534 }
531 } 535 }
532} 536}
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index e087a2548..d8db3f3f8 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -1,3 +1,5 @@
1@import '_bootstrap-variables';
2
1$small-view: 800px; 3$small-view: 800px;
2$mobile-view: 500px; 4$mobile-view: 500px;
3 5
@@ -93,8 +95,24 @@ $variables: (
93 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) 95 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor)
94); 96);
95 97
96/*** theme helper ***/
97
98@function var($variable) { 98@function var($variable) {
99 @return map-get($variables, $variable); 99 @return map-get($variables, $variable);
100} 100}
101
102/*** z-index groups ***/
103
104$zindex: (
105 header : 1000,
106 /* header context */
107 headerLeft : 10,
108 menu : 11000,
109 dropdown : 12000,
110 loadbar : 13000,
111 modal : 14000,
112 notification : 15000,
113 hotkeys : 16000
114);
115
116@function z($label) {
117 @return map-get($zindex, $label);
118}
diff --git a/client/src/sass/loading-bar.scss b/client/src/sass/loading-bar.scss
index 7d687d479..d584b7c67 100644
--- a/client/src/sass/loading-bar.scss
+++ b/client/src/sass/loading-bar.scss
@@ -1,3 +1,4 @@
1@import '_mixins';
1// Thanks: https://github.com/aitboudad/ngx-loading-bar/blob/master/loading-bar.css 2// Thanks: https://github.com/aitboudad/ngx-loading-bar/blob/master/loading-bar.css
2 3
3/* Make clicks pass-through */ 4/* Make clicks pass-through */
@@ -20,7 +21,7 @@
20 21
21 background: var(--mainColor); 22 background: var(--mainColor);
22 position: fixed; 23 position: fixed;
23 z-index: 10002; 24 z-index: z(loadbar);
24 top: 0; 25 top: 0;
25 left: 0; 26 left: 0;
26 width: 100%; 27 width: 100%;
@@ -50,7 +51,7 @@
50#loading-bar-spinner { 51#loading-bar-spinner {
51 display: block; 52 display: block;
52 position: fixed; 53 position: fixed;
53 z-index: 10002; 54 z-index: z(loadbar);
54 top: 10px; 55 top: 10px;
55 left: 10px; 56 left: 10px;
56} 57}
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index 753fdf5c9..4d2d6cb67 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -383,8 +383,7 @@ p-inputswitch {
383 383
384p-toast { 384p-toast {
385 .ui-toast { 385 .ui-toast {
386 // Modal is 10005 386 z-index: z(notification) !important;
387 z-index: 10010 !important;
388 } 387 }
389 388
390 .ui-toast-message { 389 .ui-toast-message {
diff --git a/scripts/build/client.sh b/scripts/build/client.sh
index 912401faf..e7475f56c 100755
--- a/scripts/build/client.sh
+++ b/scripts/build/client.sh
@@ -39,8 +39,52 @@ post_build_hook
39 39
40# Don't build other languages if --light arg is provided 40# Don't build other languages if --light arg is provided
41if [ -z ${1+x} ] || [ "$1" != "--light" ]; then 41if [ -z ${1+x} ] || [ "$1" != "--light" ]; then
42 if [ ! -z ${1+x} ] && [ "$1" == "--light-fr" ]; then 42 if [ ! -z ${1+x} ] && [ "$1" == "--light-hu" ]; then
43 languages=(["hu"]="hu-HU")
44 elif [ ! -z ${1+x} ] && [ "$1" == "--light-th" ]; then
45 languages=(["th"]="th-TH")
46 elif [ ! -z ${1+x} ] && [ "$1" == "--light-fi" ]; then
47 languages=(["fi"]="fi-FI")
48 elif [ ! -z ${1+x} ] && [ "$1" == "--light-nl" ]; then
49 languages=(["nl"]="nl-NL")
50 elif [ ! -z ${1+x} ] && [ "$1" == "--light-gd" ]; then
51 languages=(["gd"]="gd")
52 elif [ ! -z ${1+x} ] && [ "$1" == "--light-el" ]; then
53 languages=(["el"]="el-GR")
54 elif [ ! -z ${1+x} ] && [ "$1" == "--light-es" ]; then
55 languages=(["es"]="es-ES")
56 elif [ ! -z ${1+x} ] && [ "$1" == "--light-oc" ]; then
57 languages=(["oc"]="oc")
58 elif [ ! -z ${1+x} ] && [ "$1" == "--light-pt" ]; then
59 languages=(["pt"]="pt-BR")
60 elif [ ! -z ${1+x} ] && [ "$1" == "--light-pt-PT" ]; then
61 languages=(["pt-PT"]="pt-PT")
62 elif [ ! -z ${1+x} ] && [ "$1" == "--light-sv" ]; then
63 languages=(["sv"]="sv-SE")
64 elif [ ! -z ${1+x} ] && [ "$1" == "--light-pl" ]; then
65 languages=(["pl"]="pl-PL")
66 elif [ ! -z ${1+x} ] && [ "$1" == "--light-ru" ]; then
67 languages=(["ru"]="ru-RU")
68 elif [ ! -z ${1+x} ] && [ "$1" == "--light-zh-Hans" ]; then
69 languages=(["zh-Hans"]="zh-Hans-CN")
70 elif [ ! -z ${1+x} ] && [ "$1" == "--light-zh-Hant" ]; then
71 languages=(["zh-Hant"]="zh-Hant-TW")
72 elif [ ! -z ${1+x} ] && [ "$1" == "--light-fr" ]; then
43 languages=(["fr"]="fr-FR") 73 languages=(["fr"]="fr-FR")
74 elif [ ! -z ${1+x} ] && [ "$1" == "--light-ja" ]; then
75 languages=(["ja"]="ja-JP")
76 elif [ ! -z ${1+x} ] && [ "$1" == "--light-eu" ]; then
77 languages=(["eu"]="eu-ES")
78 elif [ ! -z ${1+x} ] && [ "$1" == "--light-ca" ]; then
79 languages=(["ca"]="ca-ES")
80 elif [ ! -z ${1+x} ] && [ "$1" == "--light-cs" ]; then
81 languages=(["cs"]="cs-CZ")
82 elif [ ! -z ${1+x} ] && [ "$1" == "--light-eo" ]; then
83 languages=(["eo"]="eo")
84 elif [ ! -z ${1+x} ] && [ "$1" == "--light-de" ]; then
85 languages=(["de"]="de-DE")
86 elif [ ! -z ${1+x} ] && [ "$1" == "--light-it" ]; then
87 languages=(["it"]="it-IT")
44 else 88 else
45 # Supported languages 89 # Supported languages
46 languages=( 90 languages=(
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 69940f395..a383a723f 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -73,6 +73,12 @@ async function getConfig (req: express.Request, res: express.Response) {
73 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 73 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
74 } 74 }
75 }, 75 },
76 search: {
77 remoteUri: {
78 users: CONFIG.SEARCH.REMOTE_URI.USERS,
79 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
80 }
81 },
76 plugin: { 82 plugin: {
77 registered: getRegisteredPlugins() 83 registered: getRegisteredPlugins()
78 }, 84 },
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 4c6cf9597..75d1a816b 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -235,6 +235,12 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
235 nodeName: CONFIG.INSTANCE.NAME, 235 nodeName: CONFIG.INSTANCE.NAME,
236 nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 236 nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
237 nodeConfig: { 237 nodeConfig: {
238 search: {
239 remoteUri: {
240 users: CONFIG.SEARCH.REMOTE_URI.USERS,
241 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
242 }
243 },
238 plugin: { 244 plugin: {
239 registered: getRegisteredPlugins() 245 registered: getRegisteredPlugins()
240 }, 246 },
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 76e0d6f2d..c3976a346 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -28,6 +28,13 @@ export interface ServerConfig {
28 } 28 }
29 } 29 }
30 30
31 search: {
32 remoteUri: {
33 users: boolean
34 anonymous: boolean
35 }
36 }
37
31 plugin: { 38 plugin: {
32 registered: ServerConfigPlugin[] 39 registered: ServerConfigPlugin[]
33 } 40 }
diff --git a/support/doc/docker.md b/support/doc/docker.md
index d7059d285..b251329d0 100644
--- a/support/doc/docker.md
+++ b/support/doc/docker.md
@@ -21,8 +21,7 @@ $ curl "https://raw.githubusercontent.com/chocobozzz/PeerTube/master/support/doc
21$ touch ./docker-volume/traefik/acme.json && chmod 600 ./docker-volume/traefik/acme.json 21$ touch ./docker-volume/traefik/acme.json && chmod 600 ./docker-volume/traefik/acme.json
22$ 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 22$ 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
23``` 23```
24View 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] 24View 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)
25(https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/.env)
26 25
27Update the reverse proxy configuration: 26Update the reverse proxy configuration:
28 27