Redesign channel page

This commit is contained in:
Chocobozzz 2021-03-25 13:42:55 +01:00 committed by Chocobozzz
parent 4097c6d66c
commit 60c35932f6
11 changed files with 508 additions and 197 deletions

View File

@ -1,22 +0,0 @@
<div class="margin-content">
<div *ngIf="videoChannel" class="row no-gutters">
<div class="description col-md-6 col-sm-12 pr-2">
<div class="block">
<div i18n class="small-title">DESCRIPTION</div>
<div class="content" [innerHtml]="getVideoChannelDescription()"></div>
</div>
<div class="block" *ngIf="supportHTML">
<div i18n class="small-title">SUPPORT THIS CHANNEL</div>
<div class="content" [innerHtml]="supportHTML"></div>
</div>
</div>
<div class="stats col-md-6 col-sm-12">
<div class="block">
<div i18n class="small-title">STATS</div>
<div i18n class="content">Created {{ videoChannel.createdAt | date }}</div>
</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,43 +0,0 @@
import { Subscription } from 'rxjs'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { MarkdownService } from '@app/core'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
@Component({
selector: 'my-video-channel-about',
templateUrl: './video-channel-about.component.html',
styleUrls: [ './video-channel-about.component.scss' ]
})
export class VideoChannelAboutComponent implements OnInit, OnDestroy {
videoChannel: VideoChannel
descriptionHTML = ''
supportHTML = ''
private videoChannelSub: Subscription
constructor (
private videoChannelService: VideoChannelService,
private markdownService: MarkdownService
) { }
ngOnInit () {
// Parent get the video channel for us
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.subscribe(async videoChannel => {
this.videoChannel = videoChannel
this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.videoChannel.description)
this.supportHTML = await this.markdownService.enhancedMarkdownToHTML(this.videoChannel.support)
})
}
ngOnDestroy () {
if (this.videoChannelSub) this.videoChannelSub.unsubscribe()
}
getVideoChannelDescription () {
if (this.descriptionHTML) return this.descriptionHTML
return $localize`No description`
}
}

View File

@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core'
import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelsComponent } from './video-channels.component'
@ -38,15 +37,6 @@ const videoChannelsRoutes: Routes = [
title: $localize`Video channel playlists`
}
}
},
{
path: 'about',
component: VideoChannelAboutComponent,
data: {
meta: {
title: $localize`About video channel`
}
}
}
]
}

View File

@ -1,50 +1,114 @@
<div *ngIf="videoChannel" class="row">
<div class="sub-menu">
<div class="root" *ngIf="videoChannel">
<div class="channel-info">
<div class="actor">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
<ng-template #buttonsTemplate>
<a *ngIf="isManageable() && !isInSmallView()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
Manage channel
</a>
<div class="actor-info">
<div class="actor-names">
<div class="actor-display-name">{{ videoChannel.displayName }}</div>
<div class="actor-name">
<span>{{ videoChannel.nameWithHost }}</span>
<button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
class="btn btn-outline-secondary btn-sm copy-button"
>
<span class="glyphicon glyphicon-duplicate"></span>
</button>
<my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
</ng-template>
<ng-template #ownerTemplate>
<div class="owner-block">
<div class="avatar-row">
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
<div class="actor-info">
<h4>{{ videoChannel.ownerAccount.displayName }}</h4>
<div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
</div>
</div>
<div class="right-buttons">
<a *ngIf="isChannelManageable && !isInSmallView" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>
Manage channel
</a>
<my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
<div class="owner-description">
<div class="description-html" [innerHTML]="ownerDescriptionHTML"></div>
</div>
<div class="actor-lower">
<div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
<a class="view-account short" [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n>
View account
</a>
<a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
<span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span>
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
</a>
<a class="view-account complete" [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n>
View owner account
</a>
</div>
</ng-template>
<div class="channel-avatar-row">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
<div>
<div class="section-label" i18n>VIDEO CHANNEL</div>
<div class="actor-info">
<div>
<div class="actor-display-name">
<h1>{{ videoChannel.displayName }}</h1>
</div>
<div class="actor-handle">
<span>@{{ videoChannel.nameWithHost }}</span>
<button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title
>
<span class="glyphicon glyphicon-duplicate"></span>
</button>
</div>
<div class="actor-counters">
<span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
<span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n>
{channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}}
</span>
</div>
</div>
<div class="channel-buttons right">
<ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
</div>
</div>
</div>
</div>
<div class="links w-100">
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }">
<div class="description-html" [innerHTML]="channelDescriptionHTML"></div>
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
<div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div>
</div>
<div *ngIf="!channelDescriptionExpanded" class="show-more" role="button"
(click)="channelDescriptionExpanded = !channelDescriptionExpanded"
title="Show the complete description" i18n-title i18n
>
Show more...
</div>
<div class="channel-buttons bottom">
<ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
</div>
<div class="owner-card">
<div class="section-label" i18n>OWNER ACCOUNT</div>
<ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
</div>
</div>
<div class="margin-content">
<router-outlet></router-outlet>
<div class="bottom-owner">
<div class="section-label" i18n>OWNER ACCOUNT</div>
<ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
</div>
<div class="links">
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
</div>
<router-outlet></router-outlet>
</div>

View File

@ -1,89 +1,345 @@
// 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 '_mixins';
@import '_miniature';
.sub-menu {
@include sub-menu-with-actor;
.root {
--myGlobalPadding: 60px;
--myChannelImgMargin: 30px;
--myFontSize: 16px;
--myGreyChannelFontSize: 16px;
--myGreyOwnerFontSize: 14px;
}
.actor, .actor-info {
width: 100%;
.section-label {
color: pvar(--mainColor);
font-size: 12px;
margin-bottom: 15px;
font-weight: $font-bold;
letter-spacing: 2.5px;
}
.links {
@include fluid-videos-miniature-layout;
}
.channel-info {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
background-color: pvar(--channelBackgroundColor);
margin-bottom: 45px;
padding: var(--myGlobalPadding) var(--myGlobalPadding) 0 var(--myGlobalPadding);
font-size: var(--myFontSize);
}
.channel-avatar-row {
display: flex;
grid-column: 1;
margin-bottom: 30px;
img {
@include channel-avatar(120px);
}
> div {
margin-left: var(--myChannelImgMargin);
}
.actor-info {
display: grid !important;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto / 1fr auto;
grid-template-areas: "name buttons" "lower buttons";
display: flex;
@include media-breakpoint-down(lg) {
grid-template-areas: "name name" "lower buttons";
> div:first-child {
flex-grow: 1;
}
}
.actor-names {
grid-area: name;
.actor-display-name {
display: flex;
flex-wrap: wrap;
}
.actor-name {
flex-grow: 1;
h1 {
font-size: 28px;
font-weight: $font-bold;
margin: 0;
}
.copy-button {
border: none;
padding: 5px;
margin-top: -2px;
}
.actor-handle,
.actor-counters {
color: pvar(--greyForegroundColor);
font-size: var(--myGreyChannelFontSize);
}
.actor-counters > *:not(:last-child)::after {
content: '';
margin: 0 10px;
color: pvar(--mainColor);
}
}
.margin-content {
// margin-content is required, but child views have their own margins
// that match views outside the scope of accounts, so we only align
// them with the margins of .sub-menu when required.
margin: 0;
.channel-description {
grid-column: 1;
}
.right-buttons {
.show-more {
display: none;
color: pvar(--mainColor);
cursor: pointer;
margin: 10px auto 45px auto;
}
.channel-buttons {
display: flex;
height: max-content;
margin-left: auto;
margin-top: 10px;
flex-wrap: wrap;
grid-row: buttons-start / span buttons-end;
grid-column: buttons-start;
> *:not(:last-child) {
margin-right: 15px;
}
}
@include media-breakpoint-down(lg) {
flex-flow: column-reverse;
.channel-buttons.right {
margin-left: 45px;
}
a {
margin-top: 0.25rem;
margin-right: 0 !important;
// Only used by mobile
.channel-buttons.bottom {
display: none;
}
.created-at {
margin-top: 15px;
color: pvar(--greyForegroundColor);
padding-bottom: 60px;
}
.owner-card {
margin-left: 105px;
grid-column: 2;
// Takes all the column
grid-row: 1 / 3;
place-self: end;
}
// Only used on mobile
.bottom-owner {
display: none;
}
.owner-block {
background-color: pvar(--mainBackgroundColor);
padding: 30px;
width: 300px;
font-size: var(--myFontSize);
.avatar-row {
display: flex;
margin-bottom: 15px;
img {
@include avatar(48px);
}
.actor-info {
margin-left: 15px;
}
h4 {
font-size: 18px;
margin: 0;
}
.actor-handle {
font-size: var(--myGreyOwnerFontSize);
color: pvar(--greyForegroundColor);
}
}
a {
@include peertube-button-outline;
line-height: 1.8;
.owner-description {
height: 140px;
@include fade-text(120px, pvar(--mainBackgroundColor));
}
}
.view-account.short {
@include peertube-button-link;
@include orange-button-inverted;
margin-top: 30px;
}
.view-account.complete {
display: none;
}
.copy-button {
border: none;
}
@media screen and (max-width: 1400px) {
// Takes all the row width
.channel-avatar-row {
grid-column: 1 / 3;
}
my-subscribe-button {
height: min-content;
.owner-card {
grid-row: 2;
margin-left: 60px;
}
}
@media screen and (max-width: 1100px) {
.root {
--myGlobalPadding: 45px;
--myChannelImgMargin: 15px;
}
.channel-info {
display: flex;
flex-direction: column;
margin-bottom: 0;
}
.channel-description:not(.expanded) {
max-height: 70px;
@include fade-text(30px, pvar(--channelBackgroundColor));
}
.show-more {
display: inline-block;
}
.channel-buttons.bottom {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.channel-buttons.right {
display: none;
}
.owner-card {
display: none;
}
.bottom-owner {
display: block;
width: 100%;
border-bottom: 2px solid $separator-border-color;
padding: var(--myGlobalPadding) 45px;
margin-bottom: 60px;
}
.owner-block {
display: grid;
width: 100%;
padding: 0;
.avatar-row {
grid-column: 1;
margin-right: 30px;
}
.owner-description {
grid-column: 2;
max-height: 70px;
@include fade-text(30px, pvar(--mainBackgroundColor));
}
.view-account {
grid-column: 2;
}
}
.view-account.complete {
display: inline-block;
margin-top: 10px;
color: pvar(--mainColor);
}
.view-account.short {
display: none;
}
}
@media screen and (max-width: $mobile-view) {
.sub-menu {
.actor {
flex-direction: column;
.root {
--myGlobalPadding: 15px;
--myFontSize: 14px;
--myGreyChannelFontSize: 13px;
--myGreyOwnerFontSize: 13px;
}
.actor-info .actor-names {
flex-direction: column;
align-items: normal;
.links {
margin: auto !important;
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 {
margin-bottom: 30px;
}
.bottom-owner {
padding: 15px;
margin-bottom: 30px;
.section-label {
display: none;
}
}
.owner-block {
display: block;
.avatar-row {
display: flex;
flex-direction: row-reverse;
margin: 0;
h4 {
font-size: 16px;
}
.actor-info {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
margin-top: -5px;
}
img {
@include channel-avatar(64px);
margin: -30px 0 0 15px;
}
}
.owner-description {
display: none;
}
}
}

View File

@ -3,8 +3,8 @@ import { Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core'
import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
@ -20,6 +20,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
links: ListOverflowItem[] = []
isChannelManageable = false
channelVideosCount: number
ownerDescriptionHTML = ''
channelDescriptionHTML = ''
channelDescriptionExpanded = false
private routeSub: Subscription
constructor (
@ -27,9 +32,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
private notifier: Notifier,
private authService: AuthService,
private videoChannelService: VideoChannelService,
private videoService: VideoService,
private restExtractor: RestExtractor,
private hotkeysService: HotkeysService,
private screenService: ScreenService
private screenService: ScreenService,
private markdown: MarkdownService
) { }
ngOnInit () {
@ -43,16 +50,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
HttpStatusCode.NOT_FOUND_404
]))
)
.subscribe(videoChannel => {
.subscribe(async videoChannel => {
this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description)
this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description)
// After the markdown renderer to avoid layout changes
this.videoChannel = videoChannel
if (this.authService.isLoggedIn()) {
this.authService.userInformationLoaded
.subscribe(() => {
const channelUserId = this.videoChannel.ownerAccount.userId
this.isChannelManageable = channelUserId && channelUserId === this.authService.getUser().id
})
}
this.loadChannelVideosCount()
})
this.hotkeys = [
@ -67,8 +72,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
this.links = [
{ label: $localize`VIDEOS`, routerLink: 'videos' },
{ label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' },
{ label: $localize`ABOUT`, routerLink: 'about' }
{ label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }
]
}
@ -79,7 +83,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys)
}
get isInSmallView () {
isInSmallView () {
return this.screenService.isInSmallView()
}
@ -87,12 +91,24 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn()
}
get isManageable () {
isManageable () {
if (!this.isUserLoggedIn()) return false
return this.videoChannel.ownerAccount.userId === this.authService.getUser().id
}
activateCopiedMessage () {
this.notifier.success($localize`Username copied`)
}
private loadChannelVideosCount () {
this.videoService.getVideoChannelVideos({
videoChannel: this.videoChannel,
videoPagination: {
currentPage: 1,
itemsPerPage: 0
},
sort: '-publishedAt'
}).subscribe(res => this.channelVideosCount = res.total)
}
}

View File

@ -5,7 +5,6 @@ import { SharedMainModule } from '@app/shared/shared-main'
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelsRoutingModule } from './video-channels-routing.module'
@ -26,7 +25,6 @@ import { VideoChannelsComponent } from './video-channels.component'
declarations: [
VideoChannelsComponent,
VideoChannelVideosComponent,
VideoChannelAboutComponent,
VideoChannelPlaylistsComponent
],

View File

@ -36,7 +36,9 @@ body {
--menuBackgroundColor: #{$menu-background};
--menuForegroundColor: #{$menu-color};
--submenuColor: #{$sub-menu-color};
--channelBackgroundColor: #{$channel-background-color};
--inputForegroundColor: #{$input-foreground-color};
--inputBackgroundColor: #{$input-background-color};
@ -277,11 +279,6 @@ my-input-toggle-hidden ::ng-deep input {
font-weight: bold;
}
@keyframes spin {
from { transform: scale(1) rotate(0deg);}
to { transform: scale(1) rotate(360deg);}
}
// In tables, don't have a hover different background
table {
.action-button-edit, .action-button-delete {
@ -468,3 +465,21 @@ ngx-loading-bar {
}
}
}
// Utils
.peertube-button {
@include peertube-button;
}
.peertube-button-link {
@include peertube-button-link;
}
.orange-button {
@include orange-button;
}
.orange-button-inverted {
@include orange-button-inverted;
}

View File

@ -31,9 +31,19 @@
text-overflow: ellipsis;
}
@mixin prefix($property, $parameters...) {
@each $prefix in -webkit-, -moz-, -ms-, -o-, "" {
#{$prefix}#{$property}: $parameters;
@mixin fade-text ($fade-after, $background-color) {
position: relative;
overflow: hidden;
&:after {
content: '';
pointer-events: none;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: linear-gradient(transparent $fade-after, $background-color);
}
}
@ -138,6 +148,33 @@
}
}
@mixin orange-button-inverted {
@include button-focus(pvar(--mainColorLightest));
border: 2px solid pvar(--mainColor);
font-weight: $font-regular;
&, &:active, &:focus {
color: pvar(--mainColor);
background-color: pvar(--mainBackgroundColor);
}
&:hover {
color: pvar(--mainColor);
background-color: pvar(--mainColorLightest);
}
&[disabled], &.disabled {
cursor: default;
color: pvar(--mainColor);
background-color: #C6C6C6;
}
my-global-icon {
@include apply-svg-color(pvar(--mainColor))
}
}
@mixin tertiary-button {
@include button-focus($grey-button-outline-color);
@ -509,6 +546,13 @@
min-height: $size;
}
@mixin channel-avatar ($size) {
width: $size;
height: $size;
min-width: $size;
min-height: $size;
}
@mixin chevron ($size, $border-width) {
border-style: solid;
border-width: $border-width $border-width 0 0;

View File

@ -16,9 +16,10 @@ $grey-foreground-hover-color: #303030;
$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%);
$main-color: hsl(24, 90%, 50%);
$main-hover-color: lighten($main-color, 5%);
$main-color-lighter: lighten($main-color, 10%);
$main-color-lightest: lighten($main-color, 40%);
$main-hover-color: lighten($main-color, 5%);
$secondary-color: hsl(187, 77%, 34%);
$support-button: inherit;
@ -50,6 +51,8 @@ $menu-lateral-padding: 26px;
$sub-menu-color: #F7F7F7;
$sub-menu-height: 81px;
$channel-background-color: #f6ede8;
$footer-height: 30px;
$footer-margin: 30px;
@ -98,7 +101,9 @@ $variables: (
--menuBackgroundColor: var(--menuBackgroundColor),
--menuForegroundColor: var(--menuForegroundColor),
--submenuColor: var(--submenuColor),
--channelBackgroundColor: var(--channelBackgroundColor),
--inputForegroundColor: var(--inputForegroundColor),
--inputBackgroundColor: var(--inputBackgroundColor),