diff options
Diffstat (limited to 'client/src')
35 files changed, 704 insertions, 162 deletions
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' | |||
9 | import { AppRoutingModule } from './app-routing.module' | 9 | import { AppRoutingModule } from './app-routing.module' |
10 | import { AppComponent } from './app.component' | 10 | import { AppComponent } from './app.component' |
11 | import { CoreModule } from './core' | 11 | import { CoreModule } from './core' |
12 | import { HeaderComponent } from './header' | 12 | import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header' |
13 | import { LoginModule } from './login' | 13 | import { LoginModule } from './login' |
14 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' | 14 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
15 | import { SharedModule } from './shared' | 15 | import { 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 { | 4 | my-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 @@ | |||
1 | import { filter, first, map, tap } from 'rxjs/operators' | 1 | import { Component } from '@angular/core' |
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router' | ||
4 | import { getParameterByName } from '../shared/misc/utils' | ||
5 | import { AuthService, Notifier, ServerService } from '@app/core' | ||
6 | import { of } from 'rxjs' | ||
7 | import { 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 | ||
15 | export class HeaderComponent implements OnInit { | 9 | export 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 @@ | |||
1 | export * from './header.component' | 1 | export * from './header.component' |
2 | export * from './search-typeahead.component' | ||
3 | export * from './suggestions.component' | ||
4 | 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 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, | ||
39 | my-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 @@ | |||
1 | import { | ||
2 | Component, | ||
3 | OnInit, | ||
4 | OnDestroy, | ||
5 | QueryList, | ||
6 | ViewChild, | ||
7 | ElementRef | ||
8 | } from '@angular/core' | ||
9 | import { Router, Params, ActivatedRoute } from '@angular/router' | ||
10 | import { AuthService, ServerService } from '@app/core' | ||
11 | import { first, tap } from 'rxjs/operators' | ||
12 | import { ListKeyManager } from '@angular/cdk/a11y' | ||
13 | import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' | ||
14 | import { SuggestionComponent, Result } from './suggestion.component' | ||
15 | import { of } from 'rxjs' | ||
16 | import { 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 | }) | ||
23 | export 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 | |||
3 | a { | ||
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 | |||
25 | my-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 @@ | |||
1 | import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' | ||
2 | import { RouterLink } from '@angular/router' | ||
3 | import { ListKeyManagerOption } from '@angular/cdk/a11y' | ||
4 | |||
5 | export 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 | }) | ||
18 | export 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 @@ | |||
1 | import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' | ||
2 | import { SuggestionComponent } from './suggestion.component' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-suggestions', | ||
6 | templateUrl: './suggestions.component.html', | ||
7 | changeDetection: ChangeDetectionStrategy.OnPush | ||
8 | }) | ||
9 | export 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 | ||
12 | menu { | 13 | 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 index 000000000..fb6042280 --- /dev/null +++ b/client/src/app/shared/angular/highlight.pipe.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { PipeTransform, Pipe } from '@angular/core' | ||
2 | import { SafeHtml } from '@angular/platform-browser' | ||
3 | |||
4 | // Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369 | ||
5 | @Pipe({ name: 'highlight' }) | ||
6 | export 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' | |||
89 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' | 89 | import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' |
90 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' | 90 | import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' |
91 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' | 91 | import { FromNowPipe } from '@app/shared/angular/from-now.pipe' |
92 | import { HighlightPipe } from '@app/shared/angular/highlight.pipe' | ||
92 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' | 93 | import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' |
93 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' | 94 | import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component' |
94 | import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' | 95 | import { 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 | ||
178 | ngb-modal-backdrop { | 182 | ngb-modal-backdrop { |
179 | z-index: 10000 !important; | 183 | z-index: z(modal) - 1 !important; |
184 | } | ||
185 | |||
186 | ngb-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 | ||
384 | p-toast { | 384 | p-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 { |