Redesign account page

pull/3909/head
Chocobozzz 2021-03-26 13:20:37 +01:00 committed by Chocobozzz
parent 60c35932f6
commit 67264e060b
16 changed files with 428 additions and 332 deletions

View File

@ -1,15 +0,0 @@
<h1 class="sr-only" i18n>About</h1>
<div class="margin-content">
<div *ngIf="account" class="row no-gutters">
<div class="block col-md-6 col-sm-12 pr-2">
<h2 i18n class="small-title">DESCRIPTION</h2>
<div class="content" [innerHtml]="getAccountDescription()"></div>
</div>
<div class="block col-md-6 col-sm-12">
<h2 i18n class="small-title">STATS</h2>
<div i18n class="content">Joined {{ account.createdAt | date }}</div>
</div>
</div>
</div>

View File

@ -1,12 +0,0 @@
@import '_variables';
@import '_mixins';
.block {
margin-bottom: 40px;
.small-title {
@include in-content-small-title;
margin-bottom: 20px;
}
}

View File

@ -1,40 +0,0 @@
import { Subscription } from 'rxjs'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { MarkdownService } from '@app/core'
import { Account, AccountService } from '@app/shared/shared-main'
@Component({
selector: 'my-account-about',
templateUrl: './account-about.component.html',
styleUrls: [ './account-about.component.scss' ]
})
export class AccountAboutComponent implements OnInit, OnDestroy {
account: Account
descriptionHTML = ''
private accountSub: Subscription
constructor (
private accountService: AccountService,
private markdownService: MarkdownService
) { }
ngOnInit () {
// Parent get the account for us
this.accountSub = this.accountService.accountLoaded
.subscribe(async account => {
this.account = account
this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true)
})
}
ngOnDestroy () {
if (this.accountSub) this.accountSub.unsubscribe()
}
getAccountDescription () {
if (this.descriptionHTML) return this.descriptionHTML
return $localize`No description`
}
}

View File

@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
} }
updateSearch (value: string) { updateSearch (value: string) {
if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
this.search = value this.search = value
if (!this.search) {
this.router.navigate([ '../videos' ], { relativeTo: this.route })
return
}
this.videos = []
this.reloadVideos() this.reloadVideos()
} }

View File

@ -1,11 +1,10 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core' import { MetaGuard } from '@ngx-meta/core'
import { AccountsComponent } from './accounts.component'
import { AccountVideosComponent } from './account-videos/account-videos.component'
import { AccountAboutComponent } from './account-about/account-about.component'
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
import { AccountSearchComponent } from './account-search/account-search.component' import { AccountSearchComponent } from './account-search/account-search.component'
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
import { AccountVideosComponent } from './account-videos/account-videos.component'
import { AccountsComponent } from './accounts.component'
const accountsRoutes: Routes = [ const accountsRoutes: Routes = [
{ {
@ -31,15 +30,6 @@ const accountsRoutes: Routes = [
} }
} }
}, },
{
path: 'about',
component: AccountAboutComponent,
data: {
meta: {
title: $localize`About account`
}
}
},
{ {
path: 'videos', path: 'videos',
component: AccountVideosComponent, component: AccountVideosComponent,

View File

@ -1,57 +1,89 @@
<div *ngIf="account" class="row"> <div *ngIf="account" class="root">
<div class="sub-menu"> <div class="account-info">
<div class="actor"> <div class="account-avatar-row">
<img [src]="account.avatarUrl" alt="Avatar" /> <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" />
<div class="actor-info"> <div>
<div class="actor-names"> <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
<div class="actor-display-name">{{ account.displayName }}</div>
<div class="actor-name"> <div class="actor-info">
<span>{{ account.nameWithHost }}</span> <div>
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" <div class="actor-display-name">
class="btn btn-outline-secondary btn-sm copy-button" <h1>{{ account.displayName }}</h1>
>
<span class="glyphicon glyphicon-duplicate"></span> <my-user-moderation-dropdown
</button> [prependActions]="prependModerationActions"
buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
></my-user-moderation-dropdown>
<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>
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
</div>
<div class="actor-handle">
<span>@{{ account.nameWithHost }}</span>
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
>
<span class="glyphicon glyphicon-duplicate"></span>
</button>
</div>
<div class="actor-counters">
<span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
<span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
{accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
</span>
</div>
</div> </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>
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
<my-user-moderation-dropdown
[prependActions]="prependModerationActions"
buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
></my-user-moderation-dropdown>
</div> </div>
<div class="actor-followers" [title]="accountFollowerTitle">
{{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
</div>
</div>
<div class="right-buttons">
<a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a>
<my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
</div> </div>
</div> </div>
<div class="links w-100"> <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
<ng-template #linkTemplate let-item="item"> <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
</div>
<simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> <div *ngIf="!accountDescriptionExpanded" class="show-more" role="button"
(click)="accountDescriptionExpanded = !accountDescriptionExpanded"
title="Show the complete description" i18n-title i18n
>
Show more...
</div>
<div class="buttons">
<a *ngIf="isManageable() && !isInSmallView()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
Manage account
</a>
<my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
</div> </div>
</div> </div>
<div class="margin-content"> <div class="links">
<router-outlet (activate)="onOutletLoaded($event)"></router-outlet> <ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
<simple-search-input
[alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
(inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
i18n-iconTitle icon-title="Search account videos"
i18n-placeholder placeholder="Search account videos"
></simple-search-input>
</div> </div>
<router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
</div> </div>
<ng-container *ngIf="prependModerationActions"> <ng-container *ngIf="prependModerationActions">

View File

@ -1,49 +1,26 @@
// Bootstrap grid utilities require functions, variables and mixins
@import 'node_modules/bootstrap/scss/functions';
@import 'node_modules/bootstrap/scss/variables';
@import 'node_modules/bootstrap/scss/mixins';
@import 'node_modules/bootstrap/scss/grid';
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
@import '_actor';
@import '_miniature';
.sub-menu { .root {
@include sub-menu-with-actor; --myGlobalPadding: 60px;
--myImgMargin: 30px;
.actor { --myFontSize: 16px;
width: 100%; --myGreyFontSize: 16px;
}
} }
.margin-content { .section-label {
// margin-content is required, but child views have their own margins @include section-label-responsive;
// that match views outside the scope of accounts, so we only align
// them with the margins of .sub-menu when required.
margin: 0;
} }
.right-buttons { .links {
@include fluid-videos-miniature-layout;
display: flex; display: flex;
height: max-content; justify-content: space-between;
margin-left: auto; align-items: center;
margin-top: 10px; max-width: 800px;
@include media-breakpoint-down(lg) {
flex-flow: column-reverse;
a {
margin-top: 0.25rem;
margin-right: 0 !important;
}
}
a {
@include peertube-button-outline;
}
my-subscribe-button {
min-height: 30px;
}
} }
my-user-moderation-dropdown, my-user-moderation-dropdown,
@ -60,39 +37,98 @@ my-user-moderation-dropdown,
.copy-button { .copy-button {
border: none; border: none;
padding: 5px; }
margin-top: -2px;
.account-info {
display: grid;
grid-template-columns: 1fr min-content;
grid-template-rows: auto auto;
background-color: pvar(--submenuColor);
margin-bottom: 45px;
padding: var(--myGlobalPadding) var(--myGlobalPadding) 0 var(--myGlobalPadding);
font-size: var(--myFontSize);
}
.account-avatar-row {
@include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
}
.description {
grid-column: 1 / 3;
}
.created-at {
margin-top: 15px;
color: pvar(--greyForegroundColor);
padding-bottom: 60px;
}
.show-more {
@include show-more-description;
display: none;
text-align: center;
}
.buttons {
grid-column: 2;
grid-row: 1;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-content: flex-start;
> *:not(:last-child) {
margin-bottom: 15px;
}
}
@media screen and (max-width: $small-view) {
.root {
--myGlobalPadding: 45px;
--myChannelImgMargin: 15px;
}
.account-info {
display: block;
padding-bottom: 60px;
}
.description:not(.expanded) {
max-height: 70px;
@include fade-text(30px, pvar(--submenuColor));
}
.show-more {
display: block;
}
.buttons {
justify-content: center;
}
} }
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {
.sub-menu { .root {
.actor { --myGlobalPadding: 15px;
flex-direction: column; --myFontSize: 14px;
align-items: center; --myGreyFontSize: 13px;
}
img, .account-info {
.actor-info .actor-names .actor-display-name { display: block;
margin-right: 0; padding-bottom: 30px;
} }
.actor-info { .links {
.actor-names { margin: auto !important;
flex-direction: column; width: min-content;
align-items: center; }
}
my-user-moderation-dropdown { .show-more {
margin-left: 0; margin-bottom: 30px;
}
.actor-followers {
text-align: center;
}
}
.right-buttons {
margin-left: 0;
}
}
} }
} }

View File

@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' import {
Account,
AccountService,
DropdownAction,
ListOverflowItem,
VideoChannel,
VideoChannelService,
VideoService
} from '@app/shared/shared-main'
import { AccountReportComponent } from '@app/shared/shared-moderation' import { AccountReportComponent } from '@app/shared/shared-moderation'
import { User, UserRight } from '@shared/models'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { User, UserRight } from '@shared/models'
import { AccountSearchComponent } from './account-search/account-search.component' import { AccountSearchComponent } from './account-search/account-search.component'
@Component({ @Component({
@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
}) })
export class AccountsComponent implements OnInit, OnDestroy { export class AccountsComponent implements OnInit, OnDestroy {
@ViewChild('accountReportModal') accountReportModal: AccountReportComponent @ViewChild('accountReportModal') accountReportModal: AccountReportComponent
accountSearch: AccountSearchComponent accountSearch: AccountSearchComponent
account: Account account: Account
accountUser: User accountUser: User
videoChannels: VideoChannel[] = []
links: ListOverflowItem[] = []
isAccountManageable = false videoChannels: VideoChannel[] = []
links: ListOverflowItem[] = []
hideMenu = false
accountFollowerTitle = '' accountFollowerTitle = ''
accountVideosCount: number
accountDescriptionHTML = ''
accountDescriptionExpanded = false
prependModerationActions: DropdownAction<any>[] prependModerationActions: DropdownAction<any>[]
private routeSub: Subscription private routeSub: Subscription
@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private redirectService: RedirectService, private redirectService: RedirectService,
private authService: AuthService, private authService: AuthService,
private videoService: VideoService,
private markdown: MarkdownService,
private screenService: ScreenService private screenService: ScreenService
) { ) {
} }
@ -63,8 +80,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
this.links = [ this.links = [
{ label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' },
{ label: $localize`VIDEOS`, routerLink: 'videos' }, { label: $localize`VIDEOS`, routerLink: 'videos' }
{ label: $localize`ABOUT`, routerLink: 'about' }
] ]
} }
@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
if (this.routeSub) this.routeSub.unsubscribe() if (this.routeSub) this.routeSub.unsubscribe()
} }
get naiveAggregatedSubscribers () { naiveAggregatedSubscribers () {
return this.videoChannels.reduce( return this.videoChannels.reduce(
(acc, val) => acc + val.followersCount, (acc, val) => acc + val.followersCount,
this.account.followersCount // accumulator starts with the base number of subscribers the account has this.account.followersCount // accumulator starts with the base number of subscribers the account has
) )
} }
get isInSmallView () { isUserLoggedIn () {
return this.authService.isLoggedIn()
}
isInSmallView () {
return this.screenService.isInSmallView() return this.screenService.isInSmallView()
} }
isManageable () {
if (!this.isUserLoggedIn()) return false
return this.account?.userId === this.authService.getUser().id
}
onUserChanged () { onUserChanged () {
this.getUserIfNeeded(this.account) this.loadUserIfNeeded(this.account)
} }
onUserDeleted () { onUserDeleted () {
@ -113,40 +139,30 @@ export class AccountsComponent implements OnInit, OnDestroy {
if (this.accountSearch) this.accountSearch.updateSearch(search) if (this.accountSearch) this.accountSearch.updateSearch(search)
} }
private onAccount (account: Account) { onSearchInputDisplayChanged (displayed: boolean) {
this.hideMenu = this.isInSmallView() && displayed
}
private async onAccount (account: Account) {
this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
this.prependModerationActions = undefined this.prependModerationActions = undefined
this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
// After the markdown renderer to avoid layout changes
this.account = account this.account = account
if (this.authService.isLoggedIn()) { this.updateModerationActions()
this.authService.userInformationLoaded.subscribe( this.loadUserIfNeeded(account)
() => { this.loadAccountVideosCount()
this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
const followers = this.subscribersDisplayFor(account.followersCount)
this.accountFollowerTitle = $localize`${followers} direct account followers`
// It's not our account, we can report it
if (!this.isAccountManageable) {
this.prependModerationActions = [
{
label: $localize`Report this account`,
handler: () => this.showReportModal()
}
]
}
}
)
}
this.getUserIfNeeded(account)
} }
private showReportModal () { private showReportModal () {
this.accountReportModal.show() this.accountReportModal.show()
} }
private getUserIfNeeded (account: Account) { private loadUserIfNeeded (account: Account) {
if (!account.userId || !this.authService.isLoggedIn()) return if (!account.userId || !this.authService.isLoggedIn()) return
const user = this.authService.getUser() const user = this.authService.getUser()
@ -158,4 +174,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
) )
} }
} }
private updateModerationActions () {
if (!this.authService.isLoggedIn()) return
this.authService.userInformationLoaded.subscribe(
() => {
if (this.isManageable()) return
// It's not our account, we can report it
this.prependModerationActions = [
{
label: $localize`Report this account`,
handler: () => this.showReportModal()
}
]
}
)
}
private loadAccountVideosCount () {
this.videoService.getAccountVideos({
account: this.account,
videoPagination: {
currentPage: 1,
itemsPerPage: 0
},
sort: '-publishedAt'
}).subscribe(res => this.accountVideosCount = res.total)
}
} }

View File

@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedModerationModule } from '@app/shared/shared-moderation'
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
import { AccountAboutComponent } from './account-about/account-about.component' import { AccountSearchComponent } from './account-search/account-search.component'
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
import { AccountVideosComponent } from './account-videos/account-videos.component' import { AccountVideosComponent } from './account-videos/account-videos.component'
import { AccountSearchComponent } from './account-search/account-search.component'
import { AccountsRoutingModule } from './accounts-routing.module' import { AccountsRoutingModule } from './accounts-routing.module'
import { AccountsComponent } from './accounts.component' import { AccountsComponent } from './accounts.component'
@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
AccountsComponent, AccountsComponent,
AccountVideosComponent, AccountVideosComponent,
AccountVideoChannelsComponent, AccountVideoChannelsComponent,
AccountAboutComponent,
AccountSearchComponent AccountSearchComponent
], ],

View File

@ -12,7 +12,7 @@
<ng-template #ownerTemplate> <ng-template #ownerTemplate>
<div class="owner-block"> <div class="owner-block">
<div class="avatar-row"> <div class="avatar-row">
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> <img class="channel-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
<div class="actor-info"> <div class="actor-info">
<h4>{{ videoChannel.ownerAccount.displayName }}</h4> <h4>{{ videoChannel.ownerAccount.displayName }}</h4>

View File

@ -1,5 +1,6 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
@import '_actor';
@import '_miniature'; @import '_miniature';
.root { .root {
@ -11,11 +12,7 @@
} }
.section-label { .section-label {
color: pvar(--mainColor); @include section-label-responsive;
font-size: 12px;
margin-bottom: 15px;
font-weight: $font-bold;
letter-spacing: 2.5px;
} }
.links { .links {
@ -34,48 +31,7 @@
} }
.channel-avatar-row { .channel-avatar-row {
display: flex; @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
grid-column: 1;
margin-bottom: 30px;
img {
@include channel-avatar(120px);
}
> div {
margin-left: var(--myChannelImgMargin);
}
.actor-info {
display: flex;
> div:first-child {
flex-grow: 1;
}
}
.actor-display-name {
display: flex;
flex-wrap: wrap;
}
h1 {
font-size: 28px;
font-weight: $font-bold;
margin: 0;
}
.actor-handle,
.actor-counters {
color: pvar(--greyForegroundColor);
font-size: var(--myGreyChannelFontSize);
}
.actor-counters > *:not(:last-child)::after {
content: '';
margin: 0 10px;
color: pvar(--mainColor);
}
} }
.channel-description { .channel-description {
@ -83,12 +39,10 @@
} }
.show-more { .show-more {
display: none; @include show-more-description;
color: pvar(--mainColor);
cursor: pointer;
margin: 10px auto 45px auto;
}
display: none;
}
.channel-buttons { .channel-buttons {
display: flex; display: flex;
@ -280,24 +234,6 @@
width: min-content; width: min-content;
} }
.section-label {
font-size: 10px;
letter-spacing: 2.1px;
margin-bottom: 5px;
}
.channel-avatar-row {
margin-bottom: 15px;
h1 {
font-size: 22px;
}
img {
@include channel-avatar(80px);
}
}
.show-more { .show-more {
margin-bottom: 30px; margin-bottom: 30px;
} }

View File

@ -94,7 +94,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
isManageable () { isManageable () {
if (!this.isUserLoggedIn()) return false if (!this.isUserLoggedIn()) return false
return this.videoChannel.ownerAccount.userId === this.authService.getUser().id return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
} }
activateCopiedMessage () { activateCopiedMessage () {

View File

@ -1,14 +1,15 @@
<span> <div class="root">
<my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
<input <input
#ref #ref
type="text" type="text"
[(ngModel)]="value" [(ngModel)]="value"
(focusout)="focusLost()"
(keyup.enter)="searchChange()" (keyup.enter)="searchChange()"
[hidden]="!shown" [hidden]="!inputShown"
[name]="name" [name]="name"
[placeholder]="placeholder" [placeholder]="placeholder"
> >
</span>
<my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
<my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
</div>

View File

@ -1,29 +1,29 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
span { .root {
opacity: .6; display: flex;
&:focus-within {
opacity: 1;
}
} }
my-global-icon { my-global-icon {
height: 18px; height: 26px;
position: relative; width: 26px;
top: -2px; margin-left: 10px;
cursor: pointer;
&:hover {
color: pvar(--mainHoverColor);
}
&[iconName=search] {
color: pvar(--mainColor);
}
&[iconName=cross] {
color: pvar(--mainForegroundColor);
}
} }
input { input {
@include peertube-input-text(150px); @include peertube-input-text(200px);
height: 22px; // maximum height for the account/video-channels links
padding-left: 10px;
background-color: transparent;
border: none;
&::placeholder {
font-size: 15px;
}
} }

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators' import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
@Component({ @Component({
selector: 'simple-search-input', selector: 'simple-search-input',
@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
@Input() name = 'search' @Input() name = 'search'
@Input() placeholder = $localize`Search` @Input() placeholder = $localize`Search`
@Input() iconTitle = $localize`Search`
@Input() alwaysShow = true
@Output() searchChanged = new EventEmitter<string>() @Output() searchChanged = new EventEmitter<string>()
@Output() inputDisplayChanged = new EventEmitter<boolean>()
value = '' value = ''
shown: boolean inputShown: boolean
private searchSubject = new Subject<string>() private searchSubject = new Subject<string>()
@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
.subscribe(value => this.searchChanged.emit(value)) .subscribe(value => this.searchChanged.emit(value))
this.searchSubject.next(this.value) this.searchSubject.next(this.value)
if (this.isInputShown()) this.showInput(false)
} }
showInput () { isInputShown () {
this.shown = true if (this.alwaysShow) return true
setTimeout(() => this.input.nativeElement.focus())
return this.inputShown
}
onIconClick () {
if (!this.isInputShown()) {
this.showInput()
return
}
this.searchChange()
}
showInput (focus = true) {
this.inputShown = true
this.inputDisplayChanged.emit(this.inputShown)
if (focus) {
setTimeout(() => this.input.nativeElement.focus())
}
}
hideInput () {
this.inputShown = false
if (this.isInputShown() === false) {
this.inputDisplayChanged.emit(this.inputShown)
}
} }
focusLost () { focusLost () {
if (this.value !== '') return if (this.value) return
this.shown = false
this.hideInput()
} }
searchChange () { searchChange () {
this.router.navigate(['./search'], { relativeTo: this.route }) this.router.navigate([ './search' ], { relativeTo: this.route })
this.searchSubject.next(this.value) this.searchSubject.next(this.value)
} }
} }

View File

@ -0,0 +1,86 @@
@import '_variables';
@mixin section-label-responsive {
color: pvar(--mainColor);
font-size: 12px;
margin-bottom: 15px;
font-weight: $font-bold;
letter-spacing: 2.5px;
@media screen and (max-width: $mobile-view) {
font-size: 10px;
letter-spacing: 2.1px;
margin-bottom: 5px;
}
}
@mixin show-more-description {
color: pvar(--mainColor);
cursor: pointer;
margin: 10px auto 45px auto;
}
@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
display: flex;
grid-column: 1;
margin-bottom: 30px;
.channel-avatar {
@include channel-avatar(120px);
}
.account-avatar {
@include avatar(120px);
}
> div {
margin-left: $img-margin;
}
.actor-info {
display: flex;
> div:first-child {
flex-grow: 1;
}
}
.actor-display-name {
display: flex;
flex-wrap: wrap;
}
h1 {
font-size: 28px;
font-weight: $font-bold;
margin: 0;
}
.actor-handle,
.actor-counters {
color: pvar(--greyForegroundColor);
font-size: $grey-font-size;
}
.actor-counters > *:not(:last-child)::after {
content: '';
margin: 0 10px;
color: pvar(--mainColor);
}
@media screen and (max-width: $mobile-view) {
margin-bottom: 15px;
h1 {
font-size: 22px;
}
.channel-avatar {
@include channel-avatar(80px);
}
.account-avatar {
@include avatar(120px);
}
}
}