diff options
author | Chocobozzz <me@florianbigard.com> | 2018-02-13 14:11:05 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-02-13 14:20:46 +0100 |
commit | 0cd4344f3cf529b15308fcf3eb7d7eb07726df56 (patch) | |
tree | 276f6e8cfe72d583114d82fd5db981550a395488 | |
parent | 29c6b829446a6fb29dffc6b7b638079ce60f3771 (diff) | |
download | PeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.tar.gz PeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.tar.zst PeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.zip |
Rewrite infinite scroll
15 files changed, 239 insertions, 113 deletions
diff --git a/client/package.json b/client/package.json index 4cb0b002c..fe8bdd1f9 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -61,7 +61,6 @@ | |||
61 | "ngx-bootstrap": "2.0.2", | 61 | "ngx-bootstrap": "2.0.2", |
62 | "ngx-chips": "1.6.3", | 62 | "ngx-chips": "1.6.3", |
63 | "ngx-clipboard": "9.0.1", | 63 | "ngx-clipboard": "9.0.1", |
64 | "ngx-infinite-scroll": "0.7.2", | ||
65 | "ngx-pipes": "^2.0.5", | 64 | "ngx-pipes": "^2.0.5", |
66 | "node-sass": "^4.1.1", | 65 | "node-sass": "^4.1.1", |
67 | "npm-font-source-sans-pro": "^1.0.2", | 66 | "npm-font-source-sans-pro": "^1.0.2", |
diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html index 0755158b9..3d8c656ee 100644 --- a/client/src/app/account/account-videos/account-videos.component.html +++ b/client/src/app/account/account-videos/account-videos.component.html | |||
@@ -1,44 +1,44 @@ | |||
1 | <div *ngIf="pagination.totalItems === 0">No results.</div> | 1 | <div *ngIf="pagination.totalItems === 0">No results.</div> |
2 | 2 | ||
3 | <div | 3 | <div |
4 | class="videos" | 4 | myInfiniteScroller |
5 | infiniteScroll | 5 | [pageHeight]="pageHeight" |
6 | [infiniteScrollDistance]="0.5" | 6 | (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)" |
7 | [infiniteScrollUpDistance]="1.5" | 7 | class="videos" #videoElement |
8 | (scrolled)="onNearOfBottom()" | ||
9 | (scrolledUp)="onNearOfTop()" | ||
10 | > | 8 | > |
11 | <div class="video" *ngFor="let video of videos; let i = index"> | 9 | <div *ngFor="let videos of videoPages; let i = index" class="videos-page"> |
12 | <div class="checkbox-container"> | 10 | <div class="video" *ngFor="let video of videos; let j = index"> |
13 | <input [id]="'video-check-' + i" type="checkbox" [(ngModel)]="checkedVideos[video.id]" /> | 11 | <div class="checkbox-container"> |
14 | <label [for]="'video-check-' + i"></label> | 12 | <input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" /> |
15 | </div> | 13 | <label [for]="'video-check-' + video.id"></label> |
14 | </div> | ||
16 | 15 | ||
17 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | 16 | <my-video-thumbnail [video]="video"></my-video-thumbnail> |
18 | 17 | ||
19 | <div class="video-info"> | 18 | <div class="video-info"> |
20 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | 19 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> |
21 | <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | 20 | <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> |
22 | </div> | 21 | </div> |
23 | 22 | ||
24 | <!-- Display only once --> | 23 | <!-- Display only once --> |
25 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> | 24 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0"> |
26 | <div class="action-selection-mode-child"> | 25 | <div class="action-selection-mode-child"> |
27 | <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> | 26 | <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> |
28 | Cancel | 27 | Cancel |
29 | </span> | 28 | </span> |
30 | 29 | ||
31 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | 30 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> |
32 | <span class="icon icon-delete-white"></span> | 31 | <span class="icon icon-delete-white"></span> |
33 | Delete | 32 | Delete |
34 | </span> | 33 | </span> |
34 | </div> | ||
35 | </div> | 35 | </div> |
36 | </div> | ||
37 | 36 | ||
38 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> | 37 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> |
39 | <my-delete-button (click)="deleteVideo(video)"></my-delete-button> | 38 | <my-delete-button (click)="deleteVideo(video)"></my-delete-button> |
40 | 39 | ||
41 | <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button> | 40 | <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button> |
41 | </div> | ||
42 | </div> | 42 | </div> |
43 | </div> | 43 | </div> |
44 | </div> | 44 | </div> |
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss index 707bd66ad..449cc6af4 100644 --- a/client/src/app/account/account-videos/account-videos.component.scss +++ b/client/src/app/account/account-videos/account-videos.component.scss | |||
@@ -45,16 +45,13 @@ | |||
45 | display: flex; | 45 | display: flex; |
46 | min-height: 130px; | 46 | min-height: 130px; |
47 | padding-bottom: 20px; | 47 | padding-bottom: 20px; |
48 | margin-bottom: 20px; | ||
49 | border-bottom: 1px solid #C6C6C6; | ||
48 | 50 | ||
49 | &:first-child { | 51 | &:first-child { |
50 | margin-top: 47px; | 52 | margin-top: 47px; |
51 | } | 53 | } |
52 | 54 | ||
53 | &:not(:last-child) { | ||
54 | margin-bottom: 20px; | ||
55 | border-bottom: 1px solid #C6C6C6; | ||
56 | } | ||
57 | |||
58 | .checkbox-container { | 55 | .checkbox-container { |
59 | display: flex; | 56 | display: flex; |
60 | align-items: center; | 57 | align-items: center; |
diff --git a/client/src/app/account/account-videos/account-videos.component.ts b/client/src/app/account/account-videos/account-videos.component.ts index bce135557..e9d044dbf 100644 --- a/client/src/app/account/account-videos/account-videos.component.ts +++ b/client/src/app/account/account-videos/account-videos.component.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | ||
4 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
3 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
4 | import 'rxjs/add/observable/from' | 6 | import 'rxjs/add/observable/from' |
5 | import 'rxjs/add/operator/concatAll' | 7 | import 'rxjs/add/operator/concatAll' |
@@ -19,7 +21,9 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit | |||
19 | titlePage = 'My videos' | 21 | titlePage = 'My videos' |
20 | currentRoute = '/account/videos' | 22 | currentRoute = '/account/videos' |
21 | checkedVideos: { [ id: number ]: boolean } = {} | 23 | checkedVideos: { [ id: number ]: boolean } = {} |
22 | pagination = { | 24 | videoHeight = 155 |
25 | videoWidth = -1 | ||
26 | pagination: ComponentPagination = { | ||
23 | currentPage: 1, | 27 | currentPage: 1, |
24 | itemsPerPage: 10, | 28 | itemsPerPage: 10, |
25 | totalItems: null | 29 | totalItems: null |
@@ -46,8 +50,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit | |||
46 | return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) | 50 | return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) |
47 | } | 51 | } |
48 | 52 | ||
49 | getVideosObservable () { | 53 | getVideosObservable (page: number) { |
50 | return this.videoService.getMyVideos(this.pagination, this.sort) | 54 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
55 | |||
56 | return this.videoService.getMyVideos(newPagination, this.sort) | ||
51 | } | 57 | } |
52 | 58 | ||
53 | deleteSelectedVideos () { | 59 | deleteSelectedVideos () { |
@@ -71,9 +77,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit | |||
71 | Observable.from(observables) | 77 | Observable.from(observables) |
72 | .concatAll() | 78 | .concatAll() |
73 | .subscribe( | 79 | .subscribe( |
74 | res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`), | 80 | res => { |
81 | this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`) | ||
82 | this.buildVideoPages() | ||
83 | }, | ||
75 | 84 | ||
76 | err => this.notificationsService.error('Error', err.message) | 85 | err => this.notificationsService.error('Error', err.message) |
77 | ) | 86 | ) |
78 | } | 87 | } |
79 | ) | 88 | ) |
@@ -89,6 +98,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit | |||
89 | status => { | 98 | status => { |
90 | this.notificationsService.success('Success', `Video ${video.name} deleted.`) | 99 | this.notificationsService.success('Success', `Video ${video.name} deleted.`) |
91 | this.spliceVideosById(video.id) | 100 | this.spliceVideosById(video.id) |
101 | this.buildVideoPages() | ||
92 | }, | 102 | }, |
93 | 103 | ||
94 | error => this.notificationsService.error('Error', error.message) | 104 | error => this.notificationsService.error('Error', error.message) |
@@ -98,7 +108,14 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit | |||
98 | } | 108 | } |
99 | 109 | ||
100 | private spliceVideosById (id: number) { | 110 | private spliceVideosById (id: number) { |
101 | const index = this.videos.findIndex(v => v.id === id) | 111 | for (const key of Object.keys(this.loadedPages)) { |
102 | this.videos.splice(index, 1) | 112 | const videos = this.loadedPages[key] |
113 | const index = videos.findIndex(v => v.id === id) | ||
114 | |||
115 | if (index !== -1) { | ||
116 | videos.splice(index, 1) | ||
117 | return | ||
118 | } | ||
119 | } | ||
103 | } | 120 | } |
104 | } | 121 | } |
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 6620ac973..e6a697098 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -55,6 +55,10 @@ function dateToHuman (date: string) { | |||
55 | return datePipe.transform(date, 'medium') | 55 | return datePipe.transform(date, 'medium') |
56 | } | 56 | } |
57 | 57 | ||
58 | function immutableAssign <A, B> (target: A, source: B) { | ||
59 | return Object.assign({}, target, source) | ||
60 | } | ||
61 | |||
58 | function isInSmallView () { | 62 | function isInSmallView () { |
59 | return window.innerWidth < 600 | 63 | return window.innerWidth < 600 |
60 | } | 64 | } |
@@ -70,5 +74,6 @@ export { | |||
70 | getAbsoluteAPIUrl, | 74 | getAbsoluteAPIUrl, |
71 | dateToHuman, | 75 | dateToHuman, |
72 | isInSmallView, | 76 | isInSmallView, |
73 | isInMobileView | 77 | isInMobileView, |
78 | immutableAssign | ||
74 | } | 79 | } |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index d8f98bdf6..330a0ba84 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -4,13 +4,13 @@ import { NgModule } from '@angular/core' | |||
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
5 | import { RouterModule } from '@angular/router' | 5 | import { RouterModule } from '@angular/router' |
6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' | 6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' |
7 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | ||
7 | import { MarkdownService } from '@app/videos/shared' | 8 | import { MarkdownService } from '@app/videos/shared' |
8 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 9 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
9 | 10 | ||
10 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' | 11 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' |
11 | import { ModalModule } from 'ngx-bootstrap/modal' | 12 | import { ModalModule } from 'ngx-bootstrap/modal' |
12 | import { TabsModule } from 'ngx-bootstrap/tabs' | 13 | import { TabsModule } from 'ngx-bootstrap/tabs' |
13 | import { InfiniteScrollModule } from 'ngx-infinite-scroll' | ||
14 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 14 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
15 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 15 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' |
16 | 16 | ||
@@ -42,7 +42,6 @@ import { VideoService } from './video/video.service' | |||
42 | ModalModule.forRoot(), | 42 | ModalModule.forRoot(), |
43 | 43 | ||
44 | PrimeSharedModule, | 44 | PrimeSharedModule, |
45 | InfiniteScrollModule, | ||
46 | NgPipesModule, | 45 | NgPipesModule, |
47 | TabsModule.forRoot() | 46 | TabsModule.forRoot() |
48 | ], | 47 | ], |
@@ -55,7 +54,8 @@ import { VideoService } from './video/video.service' | |||
55 | EditButtonComponent, | 54 | EditButtonComponent, |
56 | NumberFormatterPipe, | 55 | NumberFormatterPipe, |
57 | FromNowPipe, | 56 | FromNowPipe, |
58 | MarkdownTextareaComponent | 57 | MarkdownTextareaComponent, |
58 | InfiniteScrollerDirective | ||
59 | ], | 59 | ], |
60 | 60 | ||
61 | exports: [ | 61 | exports: [ |
@@ -70,7 +70,6 @@ import { VideoService } from './video/video.service' | |||
70 | BsDropdownModule, | 70 | BsDropdownModule, |
71 | ModalModule, | 71 | ModalModule, |
72 | PrimeSharedModule, | 72 | PrimeSharedModule, |
73 | InfiniteScrollModule, | ||
74 | BytesPipe, | 73 | BytesPipe, |
75 | KeysPipe, | 74 | KeysPipe, |
76 | 75 | ||
@@ -80,6 +79,7 @@ import { VideoService } from './video/video.service' | |||
80 | DeleteButtonComponent, | 79 | DeleteButtonComponent, |
81 | EditButtonComponent, | 80 | EditButtonComponent, |
82 | MarkdownTextareaComponent, | 81 | MarkdownTextareaComponent, |
82 | InfiniteScrollerDirective, | ||
83 | 83 | ||
84 | NumberFormatterPipe, | 84 | NumberFormatterPipe, |
85 | FromNowPipe | 85 | FromNowPipe |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 60a2563b3..fb7f86852 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -6,17 +6,17 @@ | |||
6 | <div *ngIf="pagination.totalItems === 0">No results.</div> | 6 | <div *ngIf="pagination.totalItems === 0">No results.</div> |
7 | 7 | ||
8 | <div | 8 | <div |
9 | class="videos" | 9 | myInfiniteScroller |
10 | infiniteScroll | 10 | [pageHeight]="pageHeight" |
11 | [infiniteScrollUpDistance]="1.5" | 11 | (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)" |
12 | [infiniteScrollDistance]="0.5" | 12 | class="videos" #videoElement |
13 | (scrolled)="onNearOfBottom()" | ||
14 | (scrolledUp)="onNearOfTop()" | ||
15 | > | 13 | > |
16 | <my-video-miniature | 14 | <div *ngFor="let videos of videoPages" class="videos-page"> |
17 | class="ng-animate" | 15 | <my-video-miniature |
18 | *ngFor="let video of videos" [video]="video" [user]="user" | 16 | class="ng-animate" |
19 | > | 17 | *ngFor="let video of videos" [video]="video" [user]="user" |
20 | </my-video-miniature> | 18 | > |
19 | </my-video-miniature> | ||
20 | </div> | ||
21 | </div> | 21 | </div> |
22 | </div> | 22 | </div> |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index a25fc532c..034d0d879 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { OnInit } from '@angular/core' | 1 | import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { isInMobileView, isInSmallView } from '@app/shared/misc/utils' | 3 | import { isInMobileView } from '@app/shared/misc/utils' |
4 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | ||
4 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
5 | import { Observable } from 'rxjs/Observable' | 6 | import { Observable } from 'rxjs/Observable' |
6 | import { AuthService } from '../../core/auth' | 7 | import { AuthService } from '../../core/auth' |
@@ -9,30 +10,35 @@ import { SortField } from './sort-field.type' | |||
9 | import { Video } from './video.model' | 10 | import { Video } from './video.model' |
10 | 11 | ||
11 | export abstract class AbstractVideoList implements OnInit { | 12 | export abstract class AbstractVideoList implements OnInit { |
13 | private static LINES_PER_PAGE = 3 | ||
14 | |||
15 | @ViewChild('videoElement') videosElement: ElementRef | ||
16 | @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective | ||
17 | |||
12 | pagination: ComponentPagination = { | 18 | pagination: ComponentPagination = { |
13 | currentPage: 1, | 19 | currentPage: 1, |
14 | itemsPerPage: 25, | 20 | itemsPerPage: 10, |
15 | totalItems: null | 21 | totalItems: null |
16 | } | 22 | } |
17 | sort: SortField = '-createdAt' | 23 | sort: SortField = '-createdAt' |
18 | defaultSort: SortField = '-createdAt' | 24 | defaultSort: SortField = '-createdAt' |
19 | videos: Video[] = [] | ||
20 | loadOnInit = true | 25 | loadOnInit = true |
26 | pageHeight: number | ||
27 | videoWidth = 215 | ||
28 | videoHeight = 230 | ||
29 | videoPages: Video[][] | ||
21 | 30 | ||
22 | protected abstract notificationsService: NotificationsService | 31 | protected abstract notificationsService: NotificationsService |
23 | protected abstract authService: AuthService | 32 | protected abstract authService: AuthService |
24 | protected abstract router: Router | 33 | protected abstract router: Router |
25 | protected abstract route: ActivatedRoute | 34 | protected abstract route: ActivatedRoute |
26 | |||
27 | protected abstract currentRoute: string | 35 | protected abstract currentRoute: string |
28 | |||
29 | abstract titlePage: string | 36 | abstract titlePage: string |
30 | 37 | ||
31 | protected otherParams = {} | 38 | protected loadedPages: { [ id: number ]: Video[] } = {} |
32 | 39 | protected otherRouteParams = {} | |
33 | private loadedPages: { [ id: number ]: boolean } = {} | ||
34 | 40 | ||
35 | abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> | 41 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> |
36 | 42 | ||
37 | get user () { | 43 | get user () { |
38 | return this.authService.getUser() | 44 | return this.authService.getUser() |
@@ -45,15 +51,26 @@ export abstract class AbstractVideoList implements OnInit { | |||
45 | 51 | ||
46 | if (isInMobileView()) { | 52 | if (isInMobileView()) { |
47 | this.pagination.itemsPerPage = 5 | 53 | this.pagination.itemsPerPage = 5 |
54 | this.videoWidth = -1 | ||
55 | } | ||
56 | |||
57 | if (this.videoWidth !== -1) { | ||
58 | const videosWidth = this.videosElement.nativeElement.offsetWidth | ||
59 | this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE | ||
60 | } | ||
61 | |||
62 | // Video takes all the width | ||
63 | if (this.videoWidth === -1) { | ||
64 | this.pageHeight = this.pagination.itemsPerPage * this.videoHeight | ||
65 | } else { | ||
66 | this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE | ||
48 | } | 67 | } |
49 | 68 | ||
50 | if (this.loadOnInit === true) this.loadMoreVideos('after') | 69 | if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage) |
51 | } | 70 | } |
52 | 71 | ||
53 | onNearOfTop () { | 72 | onNearOfTop () { |
54 | if (this.pagination.currentPage > 1) { | 73 | this.previousPage() |
55 | this.previousPage() | ||
56 | } | ||
57 | } | 74 | } |
58 | 75 | ||
59 | onNearOfBottom () { | 76 | onNearOfBottom () { |
@@ -62,16 +79,20 @@ export abstract class AbstractVideoList implements OnInit { | |||
62 | } | 79 | } |
63 | } | 80 | } |
64 | 81 | ||
82 | onPageChanged (page: number) { | ||
83 | this.pagination.currentPage = page | ||
84 | this.setNewRouteParams() | ||
85 | } | ||
86 | |||
65 | reloadVideos () { | 87 | reloadVideos () { |
66 | this.videos = [] | ||
67 | this.loadedPages = {} | 88 | this.loadedPages = {} |
68 | this.loadMoreVideos('before') | 89 | this.loadMoreVideos(this.pagination.currentPage) |
69 | } | 90 | } |
70 | 91 | ||
71 | loadMoreVideos (where: 'before' | 'after') { | 92 | loadMoreVideos (page: number) { |
72 | if (this.loadedPages[this.pagination.currentPage] === true) return | 93 | if (this.loadedPages[page] !== undefined) return |
73 | 94 | ||
74 | const observable = this.getVideosObservable() | 95 | const observable = this.getVideosObservable(page) |
75 | 96 | ||
76 | observable.subscribe( | 97 | observable.subscribe( |
77 | ({ videos, totalVideos }) => { | 98 | ({ videos, totalVideos }) => { |
@@ -82,13 +103,14 @@ export abstract class AbstractVideoList implements OnInit { | |||
82 | return this.reloadVideos() | 103 | return this.reloadVideos() |
83 | } | 104 | } |
84 | 105 | ||
85 | this.loadedPages[this.pagination.currentPage] = true | 106 | this.loadedPages[page] = videos |
107 | this.buildVideoPages() | ||
86 | this.pagination.totalItems = totalVideos | 108 | this.pagination.totalItems = totalVideos |
87 | 109 | ||
88 | if (where === 'before') { | 110 | // Initialize infinite scroller now we loaded the first page |
89 | this.videos = videos.concat(this.videos) | 111 | if (Object.keys(this.loadedPages).length === 1) { |
90 | } else { | 112 | // Wait elements creation |
91 | this.videos = this.videos.concat(videos) | 113 | setTimeout(() => this.infiniteScroller.initialize(), 500) |
92 | } | 114 | } |
93 | }, | 115 | }, |
94 | error => this.notificationsService.error('Error', error.message) | 116 | error => this.notificationsService.error('Error', error.message) |
@@ -107,17 +129,15 @@ export abstract class AbstractVideoList implements OnInit { | |||
107 | } | 129 | } |
108 | 130 | ||
109 | protected previousPage () { | 131 | protected previousPage () { |
110 | this.pagination.currentPage-- | 132 | const min = this.minPageLoaded() |
111 | 133 | ||
112 | this.setNewRouteParams() | 134 | if (min > 1) { |
113 | this.loadMoreVideos('before') | 135 | this.loadMoreVideos(min - 1) |
136 | } | ||
114 | } | 137 | } |
115 | 138 | ||
116 | protected nextPage () { | 139 | protected nextPage () { |
117 | this.pagination.currentPage++ | 140 | this.loadMoreVideos(this.maxPageLoaded() + 1) |
118 | |||
119 | this.setNewRouteParams() | ||
120 | this.loadMoreVideos('after') | ||
121 | } | 141 | } |
122 | 142 | ||
123 | protected buildRouteParams () { | 143 | protected buildRouteParams () { |
@@ -127,7 +147,7 @@ export abstract class AbstractVideoList implements OnInit { | |||
127 | page: this.pagination.currentPage | 147 | page: this.pagination.currentPage |
128 | } | 148 | } |
129 | 149 | ||
130 | return Object.assign(params, this.otherParams) | 150 | return Object.assign(params, this.otherRouteParams) |
131 | } | 151 | } |
132 | 152 | ||
133 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | 153 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { |
@@ -144,4 +164,16 @@ export abstract class AbstractVideoList implements OnInit { | |||
144 | const routeParams = this.buildRouteParams() | 164 | const routeParams = this.buildRouteParams() |
145 | this.router.navigate([ this.currentRoute, routeParams ]) | 165 | this.router.navigate([ this.currentRoute, routeParams ]) |
146 | } | 166 | } |
167 | |||
168 | protected buildVideoPages () { | ||
169 | this.videoPages = Object.values(this.loadedPages) | ||
170 | } | ||
171 | |||
172 | private minPageLoaded () { | ||
173 | return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10))) | ||
174 | } | ||
175 | |||
176 | private maxPageLoaded () { | ||
177 | return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10))) | ||
178 | } | ||
147 | } | 179 | } |
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts new file mode 100644 index 000000000..43e014cbd --- /dev/null +++ b/client/src/app/shared/video/infinite-scroller.directive.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import 'rxjs/add/operator/distinct' | ||
3 | import 'rxjs/add/operator/startWith' | ||
4 | import { fromEvent } from 'rxjs/observable/fromEvent' | ||
5 | |||
6 | @Directive({ | ||
7 | selector: '[myInfiniteScroller]' | ||
8 | }) | ||
9 | export class InfiniteScrollerDirective implements OnInit { | ||
10 | private static PAGE_VIEW_TOP_MARGIN = 500 | ||
11 | |||
12 | @Input() containerHeight: number | ||
13 | @Input() pageHeight: number | ||
14 | @Input() percentLimit = 70 | ||
15 | @Input() autoLoading = false | ||
16 | |||
17 | @Output() nearOfBottom = new EventEmitter<void>() | ||
18 | @Output() nearOfTop = new EventEmitter<void>() | ||
19 | @Output() pageChanged = new EventEmitter<number>() | ||
20 | |||
21 | private decimalLimit = 0 | ||
22 | private lastCurrentBottom = -1 | ||
23 | private lastCurrentTop = 0 | ||
24 | |||
25 | constructor () { | ||
26 | this.decimalLimit = this.percentLimit / 100 | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | if (this.autoLoading === true) return this.initialize() | ||
31 | } | ||
32 | |||
33 | initialize () { | ||
34 | const scrollObservable = fromEvent(window, 'scroll') | ||
35 | .startWith(true) | ||
36 | .map(() => ({ current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight })) | ||
37 | |||
38 | // Scroll Down | ||
39 | scrollObservable | ||
40 | // Check we scroll down | ||
41 | .filter(({ current }) => { | ||
42 | const res = this.lastCurrentBottom < current | ||
43 | |||
44 | this.lastCurrentBottom = current | ||
45 | return res | ||
46 | }) | ||
47 | .filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit) | ||
48 | .debounceTime(200) | ||
49 | .distinct() | ||
50 | .subscribe(() => this.nearOfBottom.emit()) | ||
51 | |||
52 | // Scroll up | ||
53 | scrollObservable | ||
54 | // Check we scroll up | ||
55 | .filter(({ current }) => { | ||
56 | const res = this.lastCurrentTop > current | ||
57 | |||
58 | this.lastCurrentTop = current | ||
59 | return res | ||
60 | }) | ||
61 | .filter(({ current, maximumScroll }) => { | ||
62 | return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit | ||
63 | }) | ||
64 | .debounceTime(200) | ||
65 | .distinct() | ||
66 | .subscribe(() => this.nearOfTop.emit()) | ||
67 | |||
68 | // Page change | ||
69 | scrollObservable | ||
70 | .debounceTime(500) | ||
71 | .distinct() | ||
72 | .map(({ current }) => Math.max(1, Math.round((current + InfiniteScrollerDirective.PAGE_VIEW_TOP_MARGIN) / this.pageHeight))) | ||
73 | .distinctUntilChanged() | ||
74 | .subscribe(res => this.pageChanged.emit(res)) | ||
75 | } | ||
76 | |||
77 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html index 80b200931..7f2e96e93 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html | |||
@@ -15,10 +15,9 @@ | |||
15 | 15 | ||
16 | <div | 16 | <div |
17 | class="comment-threads" | 17 | class="comment-threads" |
18 | infiniteScroll | 18 | myInfiniteScroller |
19 | [infiniteScrollUpDistance]="1.5" | 19 | [autoLoading]="true" |
20 | [infiniteScrollDistance]="0.5" | 20 | (nearOfBottom)="onNearOfBottom()" |
21 | (scrolled)="onNearOfBottom()" | ||
22 | > | 21 | > |
23 | <div *ngFor="let comment of comments"> | 22 | <div *ngFor="let comment of comments"> |
24 | <my-video-comment | 23 | <my-video-comment |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index 6025256de..7ca3bafb5 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts | |||
@@ -160,11 +160,8 @@ export class VideoCommentsComponent implements OnChanges { | |||
160 | this.threadComments = {} | 160 | this.threadComments = {} |
161 | this.threadLoading = {} | 161 | this.threadLoading = {} |
162 | this.inReplyToCommentId = undefined | 162 | this.inReplyToCommentId = undefined |
163 | this.componentPagination = { | 163 | this.componentPagination.currentPage = 1 |
164 | currentPage: 1, | 164 | this.componentPagination.totalItems = null |
165 | itemsPerPage: 10, | ||
166 | totalItems: null | ||
167 | } | ||
168 | 165 | ||
169 | this.loadMoreComments() | 166 | this.loadMoreComments() |
170 | } | 167 | } |
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index 3020b8c30..f150e38da 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | ||
3 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
4 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -28,7 +29,9 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On | |||
28 | super.ngOnInit() | 29 | super.ngOnInit() |
29 | } | 30 | } |
30 | 31 | ||
31 | getVideosObservable () { | 32 | getVideosObservable (page: number) { |
32 | return this.videoService.getVideos(this.pagination, this.sort) | 33 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
34 | |||
35 | return this.videoService.getVideos(newPagination, this.sort) | ||
33 | } | 36 | } |
34 | } | 37 | } |
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts index 67726afdc..241b97bc7 100644 --- a/client/src/app/videos/video-list/video-search.component.ts +++ b/client/src/app/videos/video-list/video-search.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | ||
3 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
4 | import { Subscription } from 'rxjs/Subscription' | 5 | import { Subscription } from 'rxjs/Subscription' |
5 | import { AuthService } from '../../core/auth' | 6 | import { AuthService } from '../../core/auth' |
@@ -16,7 +17,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O | |||
16 | currentRoute = '/videos/search' | 17 | currentRoute = '/videos/search' |
17 | loadOnInit = false | 18 | loadOnInit = false |
18 | 19 | ||
19 | protected otherParams = { | 20 | protected otherRouteParams = { |
20 | search: '' | 21 | search: '' |
21 | } | 22 | } |
22 | private subActivatedRoute: Subscription | 23 | private subActivatedRoute: Subscription |
@@ -35,9 +36,9 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O | |||
35 | this.subActivatedRoute = this.route.queryParams.subscribe( | 36 | this.subActivatedRoute = this.route.queryParams.subscribe( |
36 | queryParams => { | 37 | queryParams => { |
37 | const querySearch = queryParams['search'] | 38 | const querySearch = queryParams['search'] |
38 | if (!querySearch || this.otherParams.search === querySearch) return | 39 | if (!querySearch || this.otherRouteParams.search === querySearch) return |
39 | 40 | ||
40 | this.otherParams.search = querySearch | 41 | this.otherRouteParams.search = querySearch |
41 | this.reloadVideos() | 42 | this.reloadVideos() |
42 | }, | 43 | }, |
43 | 44 | ||
@@ -51,7 +52,8 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O | |||
51 | } | 52 | } |
52 | } | 53 | } |
53 | 54 | ||
54 | getVideosObservable () { | 55 | getVideosObservable (page: number) { |
55 | return this.videoService.searchVideos(this.otherParams.search, this.pagination, this.sort) | 56 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
57 | return this.videoService.searchVideos(this.otherRouteParams.search, newPagination, this.sort) | ||
56 | } | 58 | } |
57 | } | 59 | } |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index fc48086d6..a42457273 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | ||
3 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
4 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit | |||
28 | super.ngOnInit() | 29 | super.ngOnInit() |
29 | } | 30 | } |
30 | 31 | ||
31 | getVideosObservable () { | 32 | getVideosObservable (page: number) { |
32 | return this.videoService.getVideos(this.pagination, this.sort) | 33 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
34 | return this.videoService.getVideos(newPagination, this.sort) | ||
33 | } | 35 | } |
34 | } | 36 | } |
diff --git a/client/yarn.lock b/client/yarn.lock index cd4492150..dd0a6bb9c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -4526,10 +4526,6 @@ ngx-clipboard@9.0.1: | |||
4526 | dependencies: | 4526 | dependencies: |
4527 | ngx-window-token "0.0.4" | 4527 | ngx-window-token "0.0.4" |
4528 | 4528 | ||
4529 | ngx-infinite-scroll@0.7.2: | ||
4530 | version "0.7.2" | ||
4531 | resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.7.2.tgz#c1f0e7fba4731a55f15557dc6fce2721fd562420" | ||
4532 | |||
4533 | ngx-pipes@^2.0.5: | 4529 | ngx-pipes@^2.0.5: |
4534 | version "2.1.0" | 4530 | version "2.1.0" |
4535 | resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0" | 4531 | resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0" |