<div class="actor-info">
<div class="actor-names">
<div class="actor-display-name">{{ account.displayName }}</div>
- <div class="actor-name">{{ account.nameWithHost }}
-
- <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
- class="btn btn-outline-secondary btn-sm copy-button"
- >
- <span class="glyphicon glyphicon-copy"></span>
- </button>
-
+ <div class="actor-name">
+ <span>{{ account.nameWithHost }}</span>
+ <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
+ class="btn btn-outline-secondary btn-sm copy-button"
+ >
+ <span class="glyphicon glyphicon-copy"></span>
+ </button>
</div>
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
<div class="actor-info">
<div class="actor-names">
<div class="actor-display-name">{{ videoChannel.displayName }}</div>
- <div class="actor-name">{{ videoChannel.nameWithHost }}
+ <div class="actor-name">
+ <span>{{ videoChannel.nameWithHost }}</span>
<button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
class="btn btn-outline-secondary btn-sm copy-button"
>
<span class="glyphicon glyphicon-copy"></span>
</button>
</div>
+ </div>
- <div class="right-buttons">
- <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
- <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
- </div>
+ <div class="right-buttons">
+ <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
+ <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
</div>
- <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
- <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
- <span i18n>Created by {{ videoChannel.ownerBy }}</span>
- <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
- </a>
+ <div class="actor-lower">
+ <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
+
+ <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
+ <span i18n>Created by {{ videoChannel.ownerBy }}</span>
+ <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
+ </a>
+ </div>
</div>
</div>
width: 100%;
}
+ .actor-info {
+ display: grid !important;
+ grid-template-columns: 1fr auto;
+ grid-template-rows: 1fr auto / 1fr auto;
+ grid-template-areas: "name buttons"
+ "lower buttons";
+
+ @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
+ grid-template-areas: "name name"
+ "lower buttons";
+ }
+ }
+
+ .actor-names {
+ grid-area: name;
+ }
+
.actor-name {
flex-grow: 1;
margin-left: auto;
margin-top: 20px;
+ grid-row: buttons-start / span buttons-end;
+ grid-column: buttons-start;
+
a {
@include peertube-button-outline;
line-height: 1.8;
-<my-search-typeahead class="w-100 d-flex justify-content-end">
- <input
- type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
- i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue"
- >
- <span class="icon icon-search"></span>
-</my-search-typeahead>
+<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
<a class="upload-button" routerLink="/videos/upload">
<my-global-icon iconName="upload"></my-global-icon>
margin-right: 15px;
}
-#search-video {
- @include peertube-input-text($search-input-width);
- padding-left: 10px;
- padding-right: 40px; // For the search icon
- font-size: 14px;
-
- &::placeholder {
- color: var(--inputPlaceholderColor);
- }
-}
-
-.icon.icon-search {
- @include icon(25px);
- height: 21px;
-
- background-color: var(--mainForegroundColor);
- mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
-
- // yolo
- position: absolute;
- margin-left: -35px;
- margin-top: 5px;
-}
-
.upload-button {
@include peertube-button-link;
@include orange-button;
-import { Component, OnInit } from '@angular/core'
-import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Component } from '@angular/core'
@Component({
selector: 'my-header',
styleUrls: [ './header.component.scss' ]
})
-export class HeaderComponent implements OnInit {
- searchValue = ''
- ariaLabelTextForSearch = ''
-
- constructor (
- private i18n: I18n
- ) {}
-
- ngOnInit () {
- this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
- }
-}
+export class HeaderComponent {}
-<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper>
- <ng-content></ng-content>
+<div class="d-inline-flex position-relative" id="typeahead-container">
+ <input
+ type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
+ [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
+ >
+ <span class="icon icon-search" (click)="doSearch()"></span>
<div class="position-absolute jump-to-suggestions">
<!-- suggestions -->
- <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions>
+ <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
<!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
<div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
<div class="d-flex justify-content-between">
<label class="small-title" i18n>Global search</label>
<div class="advanced-search-status text-muted">
- <span class="mr-1" i18n>using {{ globalSearchIndex }}</span>
+ <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
<i class="glyphicon glyphicon-globe"></i>
</div>
</div>
</div>
<!-- search instructions, when search input is empty -->
- <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
+ <div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden">
<div class="d-flex justify-content-between">
<label class="small-title" i18n>Advanced search</label>
<div class="advanced-search-status c-help">
- <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText">
- <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span>
- <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
+ <span [ngClass]="anyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
+ <span *ngIf="anyURI" class="mr-1" i18n>any instance</span>
+ <span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span>
+ <i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
</span>
</div>
</div>
@import '_bootstrap-variables';
@import '~bootstrap/scss/mixins/_breakpoints';
+#search-video {
+ @include peertube-input-text($search-input-width);
+ padding-left: 10px;
+ padding-right: 40px; // For the search icon
+ font-size: 14px;
+
+ &::placeholder {
+ color: var(--inputPlaceholderColor);
+ }
+}
+
+.icon.icon-search {
+ @include icon(25px);
+ height: 21px;
+
+ background-color: var(--mainForegroundColor);
+ mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
+
+ // yolo
+ position: absolute;
+ margin-left: -35px;
+ margin-top: 5px;
+}
+
.jump-to-suggestions {
top: 100%;
left: 0;
}
#typeahead-container {
- ::ng-deep input {
+ input {
border: 1px solid var(--mainBackgroundColor) !important;
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
flex-grow: 1;
@media screen and (max-width: $small-view) {
flex: 1;
- ::ng-deep input {
+ input {
width: unset;
}
}
- ::ng-deep span {
+ span {
right: 10px;
}
import {
Component,
- ViewChild,
- ElementRef,
AfterViewInit,
OnInit,
OnDestroy,
- QueryList
+ QueryList,
+ ViewChild,
+ ElementRef
} from '@angular/core'
-import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router'
+import { Router, Params, ActivatedRoute } from '@angular/router'
import { AuthService, ServerService } from '@app/core'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-import { filter, first, tap, map } from 'rxjs/operators'
+import { first, tap } from 'rxjs/operators'
import { ListKeyManager } from '@angular/cdk/a11y'
import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
import { SuggestionComponent, Result } from './suggestion.component'
styleUrls: [ './search-typeahead.component.scss' ]
})
export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
- @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
+ @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
hasChannel = false
inChannel = false
newSearch = true
- searchInput: HTMLInputElement
+ search = ''
serverConfig: ServerConfig
- URIPolicyText: string
- inAllText: string
inThisChannelText: string
- globalSearchIndex = 'https://index.joinpeertube.org'
keyboardEventsManager: ListKeyManager<SuggestionComponent>
results: any[] = []
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
- private serverService: ServerService,
- private i18n: I18n
- ) {
- this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
- this.inAllText = this.i18n('In all PeerTube')
- this.inThisChannelText = this.i18n('In this channel')
- }
+ private serverService: ServerService
+ ) {}
ngOnInit () {
- this.router.events
- .pipe(filter(e => e instanceof NavigationEnd))
- .subscribe((event: NavigationEnd) => {
- this.hasChannel = event.url.startsWith('/videos/watch')
- this.inChannel = event.url.startsWith('/video-channels')
- this.computeResults()
- })
-
- this.router.events
- .pipe(
- filter(e => e instanceof NavigationEnd),
- map(() => getParameterByName('search', window.location.href))
- )
- .subscribe(searchQuery => this.searchInput.value = searchQuery || '')
-
this.serverService.getConfig()
.subscribe(config => this.serverConfig = config)
}
}
ngAfterViewInit () {
- this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
- this.searchInput.addEventListener('input', this.computeResults.bind(this))
- this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
- }
-
- get hasSearch () {
- return !!this.searchInput && !!this.searchInput.value
+ this.search = getParameterByName('search', window.location.href) || ''
}
get activeResult () {
return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
}
+ get showInstructions () {
+ return !this.search
+ }
+
get showHelp () {
- return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
+ return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
}
- get URIPolicy (): 'only-followed' | 'any' {
- return (
- this.authService.isLoggedIn()
- ? this.serverConfig.search.remoteUri.users
- : this.serverConfig.search.remoteUri.anonymous
- )
- ? 'any'
- : 'only-followed'
+ get anyURI () {
+ if (!this.serverConfig) return false
+ return this.authService.isLoggedIn()
+ ? this.serverConfig.search.remoteUri.users
+ : this.serverConfig.search.remoteUri.anonymous
+ }
+
+ onSearchChange () {
+ this.computeResults()
}
computeResults () {
this.newSearch = true
let results: Result[] = []
- if (this.hasSearch) {
+ if (this.search) {
results = [
/* Channel search is still unimplemented. Uncomment when it is.
{
- text: this.searchInput.value,
+ text: this.search,
type: 'search-channel'
},
*/
{
- text: this.searchInput.value,
+ text: this.search,
type: 'search-instance',
default: true
},
/* Global search is still unimplemented. Uncomment when it is.
{
- text: this.searchInput.value,
+ text: this.search,
type: 'search-global'
},
*/
// if we're not in a channel or one of its videos/playlits, show all channel-related results
if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
// if we're in a channel, show all channel-related results except for the channel redirection itself
- if (this.inChannel) return !(result.type === 'channel')
+ if (this.inChannel) return result.type !== 'channel'
+ // all other result types are kept
return true
}
)
Object.assign(queryParams, this.route.snapshot.queryParams)
}
- Object.assign(queryParams, { search: this.searchInput.value })
+ Object.assign(queryParams, { search: this.search })
const o = this.authService.isLoggedIn()
? this.loadUserLanguagesIfNeeded(queryParams)
<div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div>
<div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
- <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText">
- {{ inThisChannelText }}
- </span>
- <span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText">
- {{ inThisInstanceText }}
- </span>
- <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
- {{ inAllText }}
- </span>
+ <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
+ <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
+ <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
<span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
</div>
-import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core'
+import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
import { RouterLink } from '@angular/router'
-import { I18n } from '@ngx-translate/i18n-polyfill'
import { ListKeyManagerOption } from '@angular/cdk/a11y'
export type Result = {
@Component({
selector: 'my-suggestion',
templateUrl: './suggestion.component.html',
- styleUrls: [ './suggestion.component.scss' ]
+ styleUrls: [ './suggestion.component.scss' ],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class SuggestionComponent implements OnInit, ListKeyManagerOption {
@Input() result: Result
@Input() highlight: string
@Output() selected = new EventEmitter()
- inAllText: string
- inThisChannelText: string
- inThisInstanceText: string
-
disabled = false
active = false
- constructor (
- private i18n: I18n
- ) {
- this.inAllText = this.i18n('In the vidiverse')
- this.inThisChannelText = this.i18n('In this channel')
- this.inThisInstanceText = this.i18n('In this instance')
- }
-
getLabel () {
return this.result.text
}
--- /dev/null
+<ul role="listbox" class="p-0 m-0">
+ <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
+ role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
+ <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
+ </li>
+</ul>
\ No newline at end of file
-import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core'
+import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
import { SuggestionComponent } from './suggestion.component'
@Component({
selector: 'my-suggestions',
- template: `
- <ul role="listbox" class="p-0 m-0">
- <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
- role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
- <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
- </li>
- </ul>
- `
+ templateUrl: './suggestions.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class SuggestionsComponent implements AfterViewInit {
@Input() results: any[]
ngAfterViewInit () {
this.listItems.changes.subscribe(
- val => this.init.emit({ items: this.listItems })
+ _ => this.init.emit({ items: this.listItems })
)
}
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
/* use this for single match search */
- static SINGLE_MATCH: string = "Single-Match"
+ static SINGLE_MATCH = 'Single-Match'
/* use this for single match search with a restriction that target should start with search string */
- static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match"
+ static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
/* use this for global search */
- static MULTI_MATCH: string = "Multi-Match"
+ static MULTI_MATCH = 'Multi-Match'
- constructor() {}
- transform(
+ // tslint:disable-next-line:no-empty
+ constructor () {}
+
+ transform (
contentString: string = null,
stringToHighlight: string = null,
- option: string = "Single-And-StartsWith-Match",
- caseSensitive: boolean = false,
- highlightStyleName: string = "search-highlight"
+ option = 'Single-And-StartsWith-Match',
+ caseSensitive = false,
+ highlightStyleName = 'search-highlight'
): SafeHtml {
- if (stringToHighlight && contentString && option) {
- let regex: any = ""
- let caseFlag: string = !caseSensitive ? "i" : ""
- switch (option) {
- case "Single-Match": {
- regex = new RegExp(stringToHighlight, caseFlag)
- break
- }
- case "Single-And-StartsWith-Match": {
- regex = new RegExp("^" + stringToHighlight, caseFlag)
- break
- }
- case "Multi-Match": {
- regex = new RegExp(stringToHighlight, "g" + caseFlag)
- break
- }
- default: {
- // default will be a global case-insensitive match
- regex = new RegExp(stringToHighlight, "gi")
- }
- }
- const replaced = contentString.replace(
- regex,
- (match) => `<span class="${highlightStyleName}">${match}</span>`
- )
- return replaced
- } else {
- return contentString
+ if (stringToHighlight && contentString && option) {
+ let regex: any = ''
+ const caseFlag: string = !caseSensitive ? 'i' : ''
+ switch (option) {
+ case 'Single-Match': {
+ regex = new RegExp(stringToHighlight, caseFlag)
+ break
+ }
+ case 'Single-And-StartsWith-Match': {
+ regex = new RegExp("^" + stringToHighlight, caseFlag)
+ break
+ }
+ case 'Multi-Match': {
+ regex = new RegExp(stringToHighlight, 'g' + caseFlag)
+ break
+ }
+ default: {
+ // default will be a global case-insensitive match
+ regex = new RegExp(stringToHighlight, 'gi')
+ }
}
+ const replaced = contentString.replace(
+ regex,
+ (match) => `<span class="${highlightStyleName}">${match}</span>`
+ )
+ return replaced
+ } else {
+ return contentString
+ }
}
}
$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
+@import '_bootstrap-variables';
@import '_variables';
@import '_mixins';
}
}
-@media screen and (max-width: 1600px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
.main-col {
&.expanded {
.margin-content {
}
}
-@media screen and (max-width: 900px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
.main-col {
&.expanded {
.margin-content {
}
-@media screen and (min-width: 768px) {
+@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
.modal:before {
vertical-align: middle;
content: " ";
md: 768px,
// Large screen / desktop
lg: 900px,
- // Extra large screen / wide desktop
- xl: 1200px
+ // Extra large screens / wide desktops
+ xl: 1200px,
+ xxl: 1600px
);
$container-max-widths: (
@mixin actor-owner {
@include disable-default-a-behaviour;
- display: inline-table;
font-size: 13px;
margin-top: 4px;
color: var(--mainForegroundColor);
.actor-names {
display: flex;
align-items: center;
+ flex-wrap: wrap;
.actor-display-name {
font-size: 23px;
font-weight: $font-bold;
+ margin-right: 7px;
}
.actor-name {
- margin-left: 7px;
position: relative;
top: 3px;
font-size: 14px;
}
}
+ .actor-lower {
+ grid-area: lower;
+ }
+
.actor-followers {
font-size: 15px;
}
margin-bottom: 0;
text-transform: uppercase;
font-weight: 600;
+ font-size: 110%;
+
+ @media screen and (max-width: $mobile-view) {
+ font-size: 130%;
+ }
}
}
}
+@import '_bootstrap-variables';
+
$small-view: 800px;
$mobile-view: 500px;