Improve account channel page
authorChocobozzz <me@florianbigard.com>
Wed, 29 May 2019 14:45:59 +0000 (16:45 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 29 May 2019 14:45:59 +0000 (16:45 +0200)
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.

13 files changed:
client/src/app/+accounts/account-video-channels/account-video-channels.component.html
client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
client/src/app/+accounts/account-videos/account-videos.component.ts
client/src/app/+accounts/accounts-routing.module.ts
client/src/app/+accounts/accounts.component.html
client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
client/src/app/shared/video-channel/video-channel.service.ts
client/src/app/shared/video/abstract-video-list.html
client/src/app/shared/video/abstract-video-list.scss
client/src/app/videos/video-list/video-overview.component.html
client/src/app/videos/video-list/video-overview.component.scss
client/src/sass/include/_miniature.scss

index c3ef1d894ba8a5d2bd5cf10ee06f121100ec240d..63f0514fd0b5137e0e84e2ba8244e37fcfd7d9d4 100644 (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>
\ No newline at end of file
+  <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>
index 0c6de2efa42abfce52c4031f5c4f584f2fcdaa6f..f2604684e3b4cff986e5d16dfc9e848c2f27ee0e 100644 (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;
-  }
-}
\ No newline at end of file
+  padding-top: 0 !important;
+}
index 44f5626bb810f3961329285190cc059fb49a2ed3..ee3b5f8e4b6cc3dfe878868312256f5cbda6b78c 100644 (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()
+  }
 }
index 0d579fa0c7c053f6ae8228deae8afe4a601b8fc6..6d26a4322684f175c04d0834145a65267dd2fb84 100644 (file)
@@ -41,8 +41,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
     private videoService: VideoService
   ) {
     super()
-
-    this.titlePage = this.i18n('Published videos')
   }
 
   ngOnInit () {
index 531d763c4ba5ac1ebdafc4b6758b1367b482df1a..55bce351a58692f9362ed2b98d80a432447d2e55 100644 (file)
@@ -14,7 +14,7 @@ const accountsRoutes: Routes = [
     children: [
       {
         path: '',
-        redirectTo: 'videos',
+        redirectTo: 'video-channels',
         pathMatch: 'full'
       },
       {
index c1377c1ea5cfceb0aa81be14a4da8de2dbf60e94..038e18c4bd5413878237e2888a96ca5dcc94848c 100644 (file)
     </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>
index 907aefae13136256ed6d55eb9761e27034627b18..7990044a2baababf4c830c895eeb2552c05e9b12 100644 (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()
index d0bec649a30c025e3abf0603316b839d93e8f3ef..0168d37d9422b56c993ddd60d851676a96502631 100644 (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))
index 2686779779bd8e28ef52d6eb8aceb4ed322f9fc0..14f48b54bc43ea6456f5cd27a528afe53f08175b 100644 (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
index 9d481d6e4023f0d8446f54a284781da5bab75f02..d94596113a025b3046b441c7e10ab6e66c22d0ee 100644 (file)
 }
 
 .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;
 }
 
index b644dd7982fdb7e3f793c2513d05d2bb9df5e048..f59de584a1bcc49554b42bc0422c29119ab70657 100644 (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" />
 
index a24766783e198faffe3739357d0b0320e8421572..ade6f53b7d20e99551962e54c9695e3554d4bdb9 100644 (file)
@@ -2,62 +2,10 @@
 @import '_mixins';
 @import '_miniature';
 
-.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);
-  }
+.margin-content {
+  @include adapt-margin-content-width;
 }
 
-.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;
-  }
+.section {
+  @include miniature-rows;
 }
index b62187fd2ef69c0fca178947646bef738677f7a9..3afcca310fc883a7c2def8f0a2c491eabe4c4a72 100644 (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;
+    }
+  }
+}