Rewrite infinite scroll
This commit is contained in:
parent
29c6b82944
commit
0cd4344f3c
|
@ -61,7 +61,6 @@
|
||||||
"ngx-bootstrap": "2.0.2",
|
"ngx-bootstrap": "2.0.2",
|
||||||
"ngx-chips": "1.6.3",
|
"ngx-chips": "1.6.3",
|
||||||
"ngx-clipboard": "9.0.1",
|
"ngx-clipboard": "9.0.1",
|
||||||
"ngx-infinite-scroll": "0.7.2",
|
|
||||||
"ngx-pipes": "^2.0.5",
|
"ngx-pipes": "^2.0.5",
|
||||||
"node-sass": "^4.1.1",
|
"node-sass": "^4.1.1",
|
||||||
"npm-font-source-sans-pro": "^1.0.2",
|
"npm-font-source-sans-pro": "^1.0.2",
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
<div *ngIf="pagination.totalItems === 0">No results.</div>
|
<div *ngIf="pagination.totalItems === 0">No results.</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="videos"
|
myInfiniteScroller
|
||||||
infiniteScroll
|
[pageHeight]="pageHeight"
|
||||||
[infiniteScrollDistance]="0.5"
|
(nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
|
||||||
[infiniteScrollUpDistance]="1.5"
|
class="videos" #videoElement
|
||||||
(scrolled)="onNearOfBottom()"
|
|
||||||
(scrolledUp)="onNearOfTop()"
|
|
||||||
>
|
>
|
||||||
<div class="video" *ngFor="let video of videos; let i = index">
|
<div *ngFor="let videos of videoPages; let i = index" class="videos-page">
|
||||||
<div class="checkbox-container">
|
<div class="video" *ngFor="let video of videos; let j = index">
|
||||||
<input [id]="'video-check-' + i" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
|
<div class="checkbox-container">
|
||||||
<label [for]="'video-check-' + i"></label>
|
<input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
|
||||||
</div>
|
<label [for]="'video-check-' + video.id"></label>
|
||||||
|
|
||||||
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
|
||||||
|
|
||||||
<div class="video-info">
|
|
||||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
|
||||||
<span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Display only once -->
|
|
||||||
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
|
|
||||||
<div class="action-selection-mode-child">
|
|
||||||
<span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
|
|
||||||
Cancel
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
|
||||||
<span class="icon icon-delete-white"></span>
|
|
||||||
Delete
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||||
<my-delete-button (click)="deleteVideo(video)"></my-delete-button>
|
|
||||||
|
|
||||||
<my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
|
<div class="video-info">
|
||||||
|
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||||
|
<span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display only once -->
|
||||||
|
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
|
||||||
|
<div class="action-selection-mode-child">
|
||||||
|
<span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
||||||
|
<span class="icon icon-delete-white"></span>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
||||||
|
<my-delete-button (click)="deleteVideo(video)"></my-delete-button>
|
||||||
|
|
||||||
|
<my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,16 +45,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #C6C6C6;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 47px;
|
margin-top: 47px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #C6C6C6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import 'rxjs/add/observable/from'
|
import 'rxjs/add/observable/from'
|
||||||
import 'rxjs/add/operator/concatAll'
|
import 'rxjs/add/operator/concatAll'
|
||||||
|
@ -19,7 +21,9 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
||||||
titlePage = 'My videos'
|
titlePage = 'My videos'
|
||||||
currentRoute = '/account/videos'
|
currentRoute = '/account/videos'
|
||||||
checkedVideos: { [ id: number ]: boolean } = {}
|
checkedVideos: { [ id: number ]: boolean } = {}
|
||||||
pagination = {
|
videoHeight = 155
|
||||||
|
videoWidth = -1
|
||||||
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 10,
|
itemsPerPage: 10,
|
||||||
totalItems: null
|
totalItems: null
|
||||||
|
@ -46,8 +50,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
||||||
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
|
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideosObservable () {
|
getVideosObservable (page: number) {
|
||||||
return this.videoService.getMyVideos(this.pagination, this.sort)
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
|
||||||
|
return this.videoService.getMyVideos(newPagination, this.sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSelectedVideos () {
|
deleteSelectedVideos () {
|
||||||
|
@ -71,9 +77,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
||||||
Observable.from(observables)
|
Observable.from(observables)
|
||||||
.concatAll()
|
.concatAll()
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`),
|
res => {
|
||||||
|
this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
|
||||||
|
this.buildVideoPages()
|
||||||
|
},
|
||||||
|
|
||||||
err => this.notificationsService.error('Error', err.message)
|
err => this.notificationsService.error('Error', err.message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -89,6 +98,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
||||||
status => {
|
status => {
|
||||||
this.notificationsService.success('Success', `Video ${video.name} deleted.`)
|
this.notificationsService.success('Success', `Video ${video.name} deleted.`)
|
||||||
this.spliceVideosById(video.id)
|
this.spliceVideosById(video.id)
|
||||||
|
this.buildVideoPages()
|
||||||
},
|
},
|
||||||
|
|
||||||
error => this.notificationsService.error('Error', error.message)
|
error => this.notificationsService.error('Error', error.message)
|
||||||
|
@ -98,7 +108,14 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
||||||
}
|
}
|
||||||
|
|
||||||
private spliceVideosById (id: number) {
|
private spliceVideosById (id: number) {
|
||||||
const index = this.videos.findIndex(v => v.id === id)
|
for (const key of Object.keys(this.loadedPages)) {
|
||||||
this.videos.splice(index, 1)
|
const videos = this.loadedPages[key]
|
||||||
|
const index = videos.findIndex(v => v.id === id)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
videos.splice(index, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,10 @@ function dateToHuman (date: string) {
|
||||||
return datePipe.transform(date, 'medium')
|
return datePipe.transform(date, 'medium')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function immutableAssign <A, B> (target: A, source: B) {
|
||||||
|
return Object.assign({}, target, source)
|
||||||
|
}
|
||||||
|
|
||||||
function isInSmallView () {
|
function isInSmallView () {
|
||||||
return window.innerWidth < 600
|
return window.innerWidth < 600
|
||||||
}
|
}
|
||||||
|
@ -70,5 +74,6 @@ export {
|
||||||
getAbsoluteAPIUrl,
|
getAbsoluteAPIUrl,
|
||||||
dateToHuman,
|
dateToHuman,
|
||||||
isInSmallView,
|
isInSmallView,
|
||||||
isInMobileView
|
isInMobileView,
|
||||||
|
immutableAssign
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import { NgModule } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
|
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
|
||||||
|
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
import { MarkdownService } from '@app/videos/shared'
|
||||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||||
|
|
||||||
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
|
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
|
||||||
import { ModalModule } from 'ngx-bootstrap/modal'
|
import { ModalModule } from 'ngx-bootstrap/modal'
|
||||||
import { TabsModule } from 'ngx-bootstrap/tabs'
|
import { TabsModule } from 'ngx-bootstrap/tabs'
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll'
|
|
||||||
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
||||||
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ import { VideoService } from './video/video.service'
|
||||||
ModalModule.forRoot(),
|
ModalModule.forRoot(),
|
||||||
|
|
||||||
PrimeSharedModule,
|
PrimeSharedModule,
|
||||||
InfiniteScrollModule,
|
|
||||||
NgPipesModule,
|
NgPipesModule,
|
||||||
TabsModule.forRoot()
|
TabsModule.forRoot()
|
||||||
],
|
],
|
||||||
|
@ -55,7 +54,8 @@ import { VideoService } from './video/video.service'
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
MarkdownTextareaComponent
|
MarkdownTextareaComponent,
|
||||||
|
InfiniteScrollerDirective
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -70,7 +70,6 @@ import { VideoService } from './video/video.service'
|
||||||
BsDropdownModule,
|
BsDropdownModule,
|
||||||
ModalModule,
|
ModalModule,
|
||||||
PrimeSharedModule,
|
PrimeSharedModule,
|
||||||
InfiniteScrollModule,
|
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
KeysPipe,
|
KeysPipe,
|
||||||
|
|
||||||
|
@ -80,6 +79,7 @@ import { VideoService } from './video/video.service'
|
||||||
DeleteButtonComponent,
|
DeleteButtonComponent,
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
MarkdownTextareaComponent,
|
MarkdownTextareaComponent,
|
||||||
|
InfiniteScrollerDirective,
|
||||||
|
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
FromNowPipe
|
FromNowPipe
|
||||||
|
|
|
@ -6,17 +6,17 @@
|
||||||
<div *ngIf="pagination.totalItems === 0">No results.</div>
|
<div *ngIf="pagination.totalItems === 0">No results.</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="videos"
|
myInfiniteScroller
|
||||||
infiniteScroll
|
[pageHeight]="pageHeight"
|
||||||
[infiniteScrollUpDistance]="1.5"
|
(nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
|
||||||
[infiniteScrollDistance]="0.5"
|
class="videos" #videoElement
|
||||||
(scrolled)="onNearOfBottom()"
|
|
||||||
(scrolledUp)="onNearOfTop()"
|
|
||||||
>
|
>
|
||||||
<my-video-miniature
|
<div *ngFor="let videos of videoPages" class="videos-page">
|
||||||
class="ng-animate"
|
<my-video-miniature
|
||||||
*ngFor="let video of videos" [video]="video" [user]="user"
|
class="ng-animate"
|
||||||
>
|
*ngFor="let video of videos" [video]="video" [user]="user"
|
||||||
</my-video-miniature>
|
>
|
||||||
|
</my-video-miniature>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { OnInit } from '@angular/core'
|
import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { isInMobileView, isInSmallView } from '@app/shared/misc/utils'
|
import { isInMobileView } from '@app/shared/misc/utils'
|
||||||
|
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { Observable } from 'rxjs/Observable'
|
import { Observable } from 'rxjs/Observable'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
|
@ -9,30 +10,35 @@ import { SortField } from './sort-field.type'
|
||||||
import { Video } from './video.model'
|
import { Video } from './video.model'
|
||||||
|
|
||||||
export abstract class AbstractVideoList implements OnInit {
|
export abstract class AbstractVideoList implements OnInit {
|
||||||
|
private static LINES_PER_PAGE = 3
|
||||||
|
|
||||||
|
@ViewChild('videoElement') videosElement: ElementRef
|
||||||
|
@ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
|
||||||
|
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 25,
|
itemsPerPage: 10,
|
||||||
totalItems: null
|
totalItems: null
|
||||||
}
|
}
|
||||||
sort: SortField = '-createdAt'
|
sort: SortField = '-createdAt'
|
||||||
defaultSort: SortField = '-createdAt'
|
defaultSort: SortField = '-createdAt'
|
||||||
videos: Video[] = []
|
|
||||||
loadOnInit = true
|
loadOnInit = true
|
||||||
|
pageHeight: number
|
||||||
|
videoWidth = 215
|
||||||
|
videoHeight = 230
|
||||||
|
videoPages: Video[][]
|
||||||
|
|
||||||
protected abstract notificationsService: NotificationsService
|
protected abstract notificationsService: NotificationsService
|
||||||
protected abstract authService: AuthService
|
protected abstract authService: AuthService
|
||||||
protected abstract router: Router
|
protected abstract router: Router
|
||||||
protected abstract route: ActivatedRoute
|
protected abstract route: ActivatedRoute
|
||||||
|
|
||||||
protected abstract currentRoute: string
|
protected abstract currentRoute: string
|
||||||
|
|
||||||
abstract titlePage: string
|
abstract titlePage: string
|
||||||
|
|
||||||
protected otherParams = {}
|
protected loadedPages: { [ id: number ]: Video[] } = {}
|
||||||
|
protected otherRouteParams = {}
|
||||||
|
|
||||||
private loadedPages: { [ id: number ]: boolean } = {}
|
abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
|
||||||
|
|
||||||
abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
|
|
||||||
|
|
||||||
get user () {
|
get user () {
|
||||||
return this.authService.getUser()
|
return this.authService.getUser()
|
||||||
|
@ -45,15 +51,26 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
|
|
||||||
if (isInMobileView()) {
|
if (isInMobileView()) {
|
||||||
this.pagination.itemsPerPage = 5
|
this.pagination.itemsPerPage = 5
|
||||||
|
this.videoWidth = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.loadOnInit === true) this.loadMoreVideos('after')
|
if (this.videoWidth !== -1) {
|
||||||
|
const videosWidth = this.videosElement.nativeElement.offsetWidth
|
||||||
|
this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video takes all the width
|
||||||
|
if (this.videoWidth === -1) {
|
||||||
|
this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
|
||||||
|
} else {
|
||||||
|
this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
onNearOfTop () {
|
onNearOfTop () {
|
||||||
if (this.pagination.currentPage > 1) {
|
this.previousPage()
|
||||||
this.previousPage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNearOfBottom () {
|
onNearOfBottom () {
|
||||||
|
@ -62,16 +79,20 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadVideos () {
|
onPageChanged (page: number) {
|
||||||
this.videos = []
|
this.pagination.currentPage = page
|
||||||
this.loadedPages = {}
|
this.setNewRouteParams()
|
||||||
this.loadMoreVideos('before')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMoreVideos (where: 'before' | 'after') {
|
reloadVideos () {
|
||||||
if (this.loadedPages[this.pagination.currentPage] === true) return
|
this.loadedPages = {}
|
||||||
|
this.loadMoreVideos(this.pagination.currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
const observable = this.getVideosObservable()
|
loadMoreVideos (page: number) {
|
||||||
|
if (this.loadedPages[page] !== undefined) return
|
||||||
|
|
||||||
|
const observable = this.getVideosObservable(page)
|
||||||
|
|
||||||
observable.subscribe(
|
observable.subscribe(
|
||||||
({ videos, totalVideos }) => {
|
({ videos, totalVideos }) => {
|
||||||
|
@ -82,13 +103,14 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
return this.reloadVideos()
|
return this.reloadVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadedPages[this.pagination.currentPage] = true
|
this.loadedPages[page] = videos
|
||||||
|
this.buildVideoPages()
|
||||||
this.pagination.totalItems = totalVideos
|
this.pagination.totalItems = totalVideos
|
||||||
|
|
||||||
if (where === 'before') {
|
// Initialize infinite scroller now we loaded the first page
|
||||||
this.videos = videos.concat(this.videos)
|
if (Object.keys(this.loadedPages).length === 1) {
|
||||||
} else {
|
// Wait elements creation
|
||||||
this.videos = this.videos.concat(videos)
|
setTimeout(() => this.infiniteScroller.initialize(), 500)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => this.notificationsService.error('Error', error.message)
|
error => this.notificationsService.error('Error', error.message)
|
||||||
|
@ -107,17 +129,15 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected previousPage () {
|
protected previousPage () {
|
||||||
this.pagination.currentPage--
|
const min = this.minPageLoaded()
|
||||||
|
|
||||||
this.setNewRouteParams()
|
if (min > 1) {
|
||||||
this.loadMoreVideos('before')
|
this.loadMoreVideos(min - 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected nextPage () {
|
protected nextPage () {
|
||||||
this.pagination.currentPage++
|
this.loadMoreVideos(this.maxPageLoaded() + 1)
|
||||||
|
|
||||||
this.setNewRouteParams()
|
|
||||||
this.loadMoreVideos('after')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildRouteParams () {
|
protected buildRouteParams () {
|
||||||
|
@ -127,7 +147,7 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
page: this.pagination.currentPage
|
page: this.pagination.currentPage
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(params, this.otherParams)
|
return Object.assign(params, this.otherRouteParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
|
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
|
||||||
|
@ -144,4 +164,16 @@ export abstract class AbstractVideoList implements OnInit {
|
||||||
const routeParams = this.buildRouteParams()
|
const routeParams = this.buildRouteParams()
|
||||||
this.router.navigate([ this.currentRoute, routeParams ])
|
this.router.navigate([ this.currentRoute, routeParams ])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected buildVideoPages () {
|
||||||
|
this.videoPages = Object.values(this.loadedPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
private minPageLoaded () {
|
||||||
|
return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private maxPageLoaded () {
|
||||||
|
return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
|
import 'rxjs/add/operator/distinct'
|
||||||
|
import 'rxjs/add/operator/startWith'
|
||||||
|
import { fromEvent } from 'rxjs/observable/fromEvent'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[myInfiniteScroller]'
|
||||||
|
})
|
||||||
|
export class InfiniteScrollerDirective implements OnInit {
|
||||||
|
private static PAGE_VIEW_TOP_MARGIN = 500
|
||||||
|
|
||||||
|
@Input() containerHeight: number
|
||||||
|
@Input() pageHeight: number
|
||||||
|
@Input() percentLimit = 70
|
||||||
|
@Input() autoLoading = false
|
||||||
|
|
||||||
|
@Output() nearOfBottom = new EventEmitter<void>()
|
||||||
|
@Output() nearOfTop = new EventEmitter<void>()
|
||||||
|
@Output() pageChanged = new EventEmitter<number>()
|
||||||
|
|
||||||
|
private decimalLimit = 0
|
||||||
|
private lastCurrentBottom = -1
|
||||||
|
private lastCurrentTop = 0
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.decimalLimit = this.percentLimit / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
if (this.autoLoading === true) return this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize () {
|
||||||
|
const scrollObservable = fromEvent(window, 'scroll')
|
||||||
|
.startWith(true)
|
||||||
|
.map(() => ({ current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight }))
|
||||||
|
|
||||||
|
// Scroll Down
|
||||||
|
scrollObservable
|
||||||
|
// Check we scroll down
|
||||||
|
.filter(({ current }) => {
|
||||||
|
const res = this.lastCurrentBottom < current
|
||||||
|
|
||||||
|
this.lastCurrentBottom = current
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
.filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
|
||||||
|
.debounceTime(200)
|
||||||
|
.distinct()
|
||||||
|
.subscribe(() => this.nearOfBottom.emit())
|
||||||
|
|
||||||
|
// Scroll up
|
||||||
|
scrollObservable
|
||||||
|
// Check we scroll up
|
||||||
|
.filter(({ current }) => {
|
||||||
|
const res = this.lastCurrentTop > current
|
||||||
|
|
||||||
|
this.lastCurrentTop = current
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
.filter(({ current, maximumScroll }) => {
|
||||||
|
return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit
|
||||||
|
})
|
||||||
|
.debounceTime(200)
|
||||||
|
.distinct()
|
||||||
|
.subscribe(() => this.nearOfTop.emit())
|
||||||
|
|
||||||
|
// Page change
|
||||||
|
scrollObservable
|
||||||
|
.debounceTime(500)
|
||||||
|
.distinct()
|
||||||
|
.map(({ current }) => Math.max(1, Math.round((current + InfiniteScrollerDirective.PAGE_VIEW_TOP_MARGIN) / this.pageHeight)))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe(res => this.pageChanged.emit(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,10 +15,9 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="comment-threads"
|
class="comment-threads"
|
||||||
infiniteScroll
|
myInfiniteScroller
|
||||||
[infiniteScrollUpDistance]="1.5"
|
[autoLoading]="true"
|
||||||
[infiniteScrollDistance]="0.5"
|
(nearOfBottom)="onNearOfBottom()"
|
||||||
(scrolled)="onNearOfBottom()"
|
|
||||||
>
|
>
|
||||||
<div *ngFor="let comment of comments">
|
<div *ngFor="let comment of comments">
|
||||||
<my-video-comment
|
<my-video-comment
|
||||||
|
|
|
@ -160,11 +160,8 @@ export class VideoCommentsComponent implements OnChanges {
|
||||||
this.threadComments = {}
|
this.threadComments = {}
|
||||||
this.threadLoading = {}
|
this.threadLoading = {}
|
||||||
this.inReplyToCommentId = undefined
|
this.inReplyToCommentId = undefined
|
||||||
this.componentPagination = {
|
this.componentPagination.currentPage = 1
|
||||||
currentPage: 1,
|
this.componentPagination.totalItems = null
|
||||||
itemsPerPage: 10,
|
|
||||||
totalItems: null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadMoreComments()
|
this.loadMoreComments()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||||
|
@ -28,7 +29,9 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideosObservable () {
|
getVideosObservable (page: number) {
|
||||||
return this.videoService.getVideos(this.pagination, this.sort)
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
|
||||||
|
return this.videoService.getVideos(newPagination, this.sort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { Subscription } from 'rxjs/Subscription'
|
import { Subscription } from 'rxjs/Subscription'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
|
@ -16,7 +17,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
|
||||||
currentRoute = '/videos/search'
|
currentRoute = '/videos/search'
|
||||||
loadOnInit = false
|
loadOnInit = false
|
||||||
|
|
||||||
protected otherParams = {
|
protected otherRouteParams = {
|
||||||
search: ''
|
search: ''
|
||||||
}
|
}
|
||||||
private subActivatedRoute: Subscription
|
private subActivatedRoute: Subscription
|
||||||
|
@ -35,9 +36,9 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
|
||||||
this.subActivatedRoute = this.route.queryParams.subscribe(
|
this.subActivatedRoute = this.route.queryParams.subscribe(
|
||||||
queryParams => {
|
queryParams => {
|
||||||
const querySearch = queryParams['search']
|
const querySearch = queryParams['search']
|
||||||
if (!querySearch || this.otherParams.search === querySearch) return
|
if (!querySearch || this.otherRouteParams.search === querySearch) return
|
||||||
|
|
||||||
this.otherParams.search = querySearch
|
this.otherRouteParams.search = querySearch
|
||||||
this.reloadVideos()
|
this.reloadVideos()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -51,7 +52,8 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideosObservable () {
|
getVideosObservable (page: number) {
|
||||||
return this.videoService.searchVideos(this.otherParams.search, this.pagination, this.sort)
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
return this.videoService.searchVideos(this.otherRouteParams.search, newPagination, this.sort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||||
|
@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideosObservable () {
|
getVideosObservable (page: number) {
|
||||||
return this.videoService.getVideos(this.pagination, this.sort)
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
return this.videoService.getVideos(newPagination, this.sort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4526,10 +4526,6 @@ ngx-clipboard@9.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ngx-window-token "0.0.4"
|
ngx-window-token "0.0.4"
|
||||||
|
|
||||||
ngx-infinite-scroll@0.7.2:
|
|
||||||
version "0.7.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.7.2.tgz#c1f0e7fba4731a55f15557dc6fce2721fd562420"
|
|
||||||
|
|
||||||
ngx-pipes@^2.0.5:
|
ngx-pipes@^2.0.5:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0"
|
resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0"
|
||||||
|
|
Loading…
Reference in New Issue