Improve account channel page

Set it as the default route for account page. The main goal is to better
differentiate the channel page from the account page. With the channel
page set as default, I hope people will better understand they are in
the account page, and that this account could have multiple channels.
pull/1856/head
Chocobozzz 2019-05-29 16:45:59 +02:00
parent 91b6631984
commit c8487f3f63
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
13 changed files with 208 additions and 143 deletions

View File

@ -1,11 +1,17 @@
<div *ngIf="account" class="row">
<a
*ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]"
class="video-channel" i18n-title title="See this video channel"
>
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
<div class="margin-content">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
</a>
</div>
<div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true">
<div class="section channel" *ngFor="let videoChannel of videoChannels">
<div class="section-title">
<a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" i18n-title title="See this video channel">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
<div>{{ videoChannel.displayName }}</div>
<div i18n class="followers">{{ videoChannel.followersCount }} subscribers</div>
</a>
</div>
<my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
</div>
</div>
</div>

View File

@ -1,30 +1,13 @@
@import '_variables';
@import '_mixins';
@import '_miniature';
.row {
justify-content: center;
.margin-content {
@include adapt-margin-content-width;
}
a.video-channel {
@include disable-default-a-behaviour;
.section {
@include miniature-rows;
display: inline-block;
text-align: center;
color: var(--mainForegroundColor);
margin: 10px 30px;
img {
@include avatar(80px);
margin-bottom: 10px;
}
.video-channel-display-name {
font-size: 20px;
font-weight: $font-bold;
}
.video-channel-followers {
font-size: 15px;
}
}
padding-top: 0 !important;
}

View File

@ -3,9 +3,14 @@ import { ActivatedRoute } from '@angular/router'
import { Account } from '@app/shared/account/account.model'
import { AccountService } from '@app/shared/account/account.service'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { flatMap, map, tap } from 'rxjs/operators'
import { Subscription } from 'rxjs'
import { concatMap, map, switchMap, tap } from 'rxjs/operators'
import { from, Subscription } from 'rxjs'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { Video } from '@app/shared/video/video.model'
import { AuthService } from '@app/core'
import { VideoService } from '@app/shared/video/video.service'
import { VideoSortField } from '@app/shared/video/sort-field.type'
import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
@Component({
selector: 'my-account-video-channels',
@ -15,27 +20,73 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
account: Account
videoChannels: VideoChannel[] = []
videos: { [id: number]: Video[] } = {}
channelPagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 2
}
videosPagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 12
}
videosSort: VideoSortField = '-publishedAt'
private accountSub: Subscription
constructor (
protected route: ActivatedRoute,
private route: ActivatedRoute,
private authService: AuthService,
private accountService: AccountService,
private videoChannelService: VideoChannelService
private videoChannelService: VideoChannelService,
private videoService: VideoService
) { }
get user () {
return this.authService.getUser()
}
ngOnInit () {
// Parent get the account for us
this.accountSub = this.accountService.accountLoaded
.pipe(
tap(account => this.account = account),
flatMap(account => this.videoChannelService.listAccountVideoChannels(account)),
map(res => res.data)
)
.subscribe(videoChannels => this.videoChannels = videoChannels)
.subscribe(account => {
this.account = account
this.loadMoreChannels()
})
}
ngOnDestroy () {
if (this.accountSub) this.accountSub.unsubscribe()
}
loadMoreChannels () {
this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination)
.pipe(
tap(res => this.channelPagination.totalItems = res.total),
switchMap(res => from(res.data)),
concatMap(videoChannel => {
return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort)
.pipe(map(data => ({ videoChannel, videos: data.videos })))
})
)
.subscribe(({ videoChannel, videos }) => {
this.videoChannels.push(videoChannel)
this.videos[videoChannel.id] = videos
})
}
getVideosOf (videoChannel: VideoChannel) {
return this.videos[ videoChannel.id ] || []
}
onNearOfBottom () {
if (!hasMoreItems(this.channelPagination)) return
this.channelPagination.currentPage += 1
this.loadMoreChannels()
}
}

View File

@ -41,8 +41,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
private videoService: VideoService
) {
super()
this.titlePage = this.i18n('Published videos')
}
ngOnInit () {

View File

@ -14,7 +14,7 @@ const accountsRoutes: Routes = [
children: [
{
path: '',
redirectTo: 'videos',
redirectTo: 'video-channels',
pathMatch: 'full'
},
{

View File

@ -26,10 +26,10 @@
</div>
<div class="links">
<a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
<a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a>
<a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
<a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
</div>
</div>

View File

@ -5,7 +5,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { Subscription } from 'rxjs'
import { Notifier } from '@app/core'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
@Component({
@ -46,8 +46,7 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
}
onNearOfBottom () {
// Last page
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
if (!hasMoreItems(this.pagination)) return
this.pagination.currentPage += 1
this.loadVideoPlaylists()

View File

@ -2,7 +2,7 @@ import { catchError, map, tap } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { Observable, ReplaySubject } from 'rxjs'
import { RestExtractor } from '../rest/rest-extractor.service'
import { HttpClient } from '@angular/common/http'
import { HttpClient, HttpParams } from '@angular/common/http'
import { VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '../../../../../shared/models/videos'
import { AccountService } from '../account/account.service'
import { ResultList } from '../../../../../shared'
@ -10,6 +10,8 @@ import { VideoChannel } from './video-channel.model'
import { environment } from '../../../environments/environment'
import { Account } from '@app/shared/account/account.model'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { RestService } from '@app/shared/rest'
@Injectable()
export class VideoChannelService {
@ -29,6 +31,7 @@ export class VideoChannelService {
constructor (
private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor
) { }
@ -41,8 +44,16 @@ export class VideoChannelService {
)
}
listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> {
return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels')
listAccountVideoChannels (account: Account, componentPagination?: ComponentPagination): Observable<ResultList<VideoChannel>> {
const pagination = componentPagination
? this.restService.componentPaginationToRestPagination(componentPagination)
: { start: 0, count: 20 }
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination)
const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels'
return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params })
.pipe(
map(res => VideoChannelService.extractVideoChannels(res)),
catchError(err => this.restExtractor.handleError(err))

View File

@ -6,7 +6,7 @@
</div>
</div>
<my-feed [syndicationItems]="syndicationItems"></my-feed>
<my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed>
<div class="moderation-block" *ngIf="displayModerationBlock">
<my-peertube-checkbox

View File

@ -25,32 +25,6 @@
}
.margin-content {
width: $video-miniature-width * 6;
margin: auto !important;
@media screen and (max-width: 1800px) {
width: $video-miniature-width * 5;
}
@media screen and (max-width: 1800px - $video-miniature-width) {
width: $video-miniature-width * 4;
}
@media screen and (max-width: 1800px - (2* $video-miniature-width)) {
width: $video-miniature-width * 3;
}
@media screen and (max-width: 1800px - (3* $video-miniature-width)) {
width: $video-miniature-width * 2;
}
@media screen and (max-width: 500px) {
width: auto;
margin: 0 !important;
.videos {
@include video-miniature-small-screen;
}
}
@include adapt-margin-content-width;
}

View File

@ -3,7 +3,7 @@
<div class="no-results" i18n *ngIf="notResults">No results.</div>
<div class="section" *ngFor="let object of overview.categories">
<div class="section-title" i18n>
<div class="section-title">
<a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
</div>
@ -11,7 +11,7 @@
</div>
<div class="section" *ngFor="let object of overview.tags">
<div class="section-title" i18n>
<div class="section-title">
<a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
</div>
@ -19,7 +19,7 @@
</div>
<div class="section channel" *ngFor="let object of overview.channels">
<div class="section-title" i18n>
<div class="section-title">
<a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
<img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />

View File

@ -2,62 +2,10 @@
@import '_mixins';
@import '_miniature';
.margin-content {
@include adapt-margin-content-width;
}
.section {
max-height: 500px; // 2 rows max
overflow: hidden;
padding-top: 10px;
&:first-child {
padding-top: 30px;
}
my-video-miniature {
text-align: left;
}
}
.section-title {
font-size: 24px;
font-weight: $font-semibold;
margin-bottom: 10px;
a {
&:hover, &:focus:not(.focus-visible), &:active {
text-decoration: none;
outline: none;
}
color: var(--mainForegroundColor);
}
}
.channel {
.section-title a {
display: flex;
width: fit-content;
align-items: center;
img {
@include avatar(28px);
margin-right: 8px;
}
}
}
@media screen and (max-width: 500px) {
.margin-content {
margin: 0 !important;
}
.section-title {
font-size: 17px;
}
.section {
max-height: initial;
overflow: initial;
@include video-miniature-small-screen;
}
@include miniature-rows;
}

View File

@ -138,3 +138,98 @@ $play-overlay-width: 18px;
}
}
}
@mixin miniature-rows {
max-height: 540px; // 2 rows max
overflow: hidden;
padding-top: 10px;
&:first-child {
padding-top: 30px;
}
my-video-miniature {
text-align: left;
}
.section-title {
font-size: 24px;
font-weight: $font-semibold;
margin-bottom: 30px;
a {
&:hover, &:focus:not(.focus-visible), &:active {
text-decoration: none;
outline: none;
}
color: var(--mainForegroundColor);
}
}
&.channel {
.section-title {
a {
display: flex;
width: fit-content;
align-items: center;
img {
@include avatar(28px);
margin-right: 8px;
}
}
.followers {
color: $grey-foreground-color;
font-weight: normal;
font-size: 14px;
margin-left: 10px;
position: relative;
top: 2px;
}
}
}
@media screen and (max-width: $mobile-view) {
max-height: initial;
overflow: initial;
@include video-miniature-small-screen;
.section-title {
font-size: 17px;
}
}
}
@mixin adapt-margin-content-width {
width: $video-miniature-width * 6;
margin: auto !important;
@media screen and (max-width: 1800px) {
width: $video-miniature-width * 5;
}
@media screen and (max-width: 1800px - $video-miniature-width) {
width: $video-miniature-width * 4;
}
@media screen and (max-width: 1800px - (2* $video-miniature-width)) {
width: $video-miniature-width * 3;
}
@media screen and (max-width: 1800px - (3* $video-miniature-width)) {
width: $video-miniature-width * 2;
}
@media screen and (max-width: 500px) {
width: auto;
margin: 0 !important;
.videos {
@include video-miniature-small-screen;
}
}
}