"ngx-bootstrap": "2.0.2",
"ngx-chips": "1.6.3",
"ngx-clipboard": "9.0.1",
- "ngx-infinite-scroll": "0.7.2",
"ngx-pipes": "^2.0.5",
"node-sass": "^4.1.1",
"npm-font-source-sans-pro": "^1.0.2",
<div *ngIf="pagination.totalItems === 0">No results.</div>
<div
- class="videos"
- infiniteScroll
- [infiniteScrollDistance]="0.5"
- [infiniteScrollUpDistance]="1.5"
- (scrolled)="onNearOfBottom()"
- (scrolledUp)="onNearOfTop()"
+ myInfiniteScroller
+ [pageHeight]="pageHeight"
+ (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
+ class="videos" #videoElement
>
- <div class="video" *ngFor="let video of videos; let i = index">
- <div class="checkbox-container">
- <input [id]="'video-check-' + i" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
- <label [for]="'video-check-' + i"></label>
- </div>
+ <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
+ <div class="video" *ngFor="let video of videos; let j = index">
+ <div class="checkbox-container">
+ <input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
+ <label [for]="'video-check-' + video.id"></label>
+ </div>
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
+ <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>
+ <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>
+ <!-- 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>
+ <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-delete-button (click)="deleteVideo(video)"></my-delete-button>
+ <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>
+ <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
+ </div>
</div>
</div>
</div>
display: flex;
min-height: 130px;
padding-bottom: 20px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #C6C6C6;
&:first-child {
margin-top: 47px;
}
- &:not(:last-child) {
- margin-bottom: 20px;
- border-bottom: 1px solid #C6C6C6;
- }
-
.checkbox-container {
display: flex;
align-items: center;
import { Component, OnInit } from '@angular/core'
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 'rxjs/add/observable/from'
import 'rxjs/add/operator/concatAll'
titlePage = 'My videos'
currentRoute = '/account/videos'
checkedVideos: { [ id: number ]: boolean } = {}
- pagination = {
+ videoHeight = 155
+ videoWidth = -1
+ pagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 10,
totalItems: null
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
}
- getVideosObservable () {
- return this.videoService.getMyVideos(this.pagination, this.sort)
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.videoService.getMyVideos(newPagination, this.sort)
}
deleteSelectedVideos () {
Observable.from(observables)
.concatAll()
.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)
)
}
)
status => {
this.notificationsService.success('Success', `Video ${video.name} deleted.`)
this.spliceVideosById(video.id)
+ this.buildVideoPages()
},
error => this.notificationsService.error('Error', error.message)
}
private spliceVideosById (id: number) {
- const index = this.videos.findIndex(v => v.id === id)
- this.videos.splice(index, 1)
+ for (const key of Object.keys(this.loadedPages)) {
+ const videos = this.loadedPages[key]
+ const index = videos.findIndex(v => v.id === id)
+
+ if (index !== -1) {
+ videos.splice(index, 1)
+ return
+ }
+ }
}
}
return datePipe.transform(date, 'medium')
}
+function immutableAssign <A, B> (target: A, source: B) {
+ return Object.assign({}, target, source)
+}
+
function isInSmallView () {
return window.innerWidth < 600
}
getAbsoluteAPIUrl,
dateToHuman,
isInSmallView,
- isInMobileView
+ isInMobileView,
+ immutableAssign
}
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
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 { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
import { ModalModule } from 'ngx-bootstrap/modal'
import { TabsModule } from 'ngx-bootstrap/tabs'
-import { InfiniteScrollModule } from 'ngx-infinite-scroll'
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
ModalModule.forRoot(),
PrimeSharedModule,
- InfiniteScrollModule,
NgPipesModule,
TabsModule.forRoot()
],
EditButtonComponent,
NumberFormatterPipe,
FromNowPipe,
- MarkdownTextareaComponent
+ MarkdownTextareaComponent,
+ InfiniteScrollerDirective
],
exports: [
BsDropdownModule,
ModalModule,
PrimeSharedModule,
- InfiniteScrollModule,
BytesPipe,
KeysPipe,
DeleteButtonComponent,
EditButtonComponent,
MarkdownTextareaComponent,
+ InfiniteScrollerDirective,
NumberFormatterPipe,
FromNowPipe
<div *ngIf="pagination.totalItems === 0">No results.</div>
<div
- class="videos"
- infiniteScroll
- [infiniteScrollUpDistance]="1.5"
- [infiniteScrollDistance]="0.5"
- (scrolled)="onNearOfBottom()"
- (scrolledUp)="onNearOfTop()"
+ myInfiniteScroller
+ [pageHeight]="pageHeight"
+ (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
+ class="videos" #videoElement
>
- <my-video-miniature
- class="ng-animate"
- *ngFor="let video of videos" [video]="video" [user]="user"
- >
- </my-video-miniature>
+ <div *ngFor="let videos of videoPages" class="videos-page">
+ <my-video-miniature
+ class="ng-animate"
+ *ngFor="let video of videos" [video]="video" [user]="user"
+ >
+ </my-video-miniature>
+ </div>
</div>
</div>
-import { OnInit } from '@angular/core'
+import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core'
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 { Observable } from 'rxjs/Observable'
import { AuthService } from '../../core/auth'
import { Video } from './video.model'
export abstract class AbstractVideoList implements OnInit {
+ private static LINES_PER_PAGE = 3
+
+ @ViewChild('videoElement') videosElement: ElementRef
+ @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
+
pagination: ComponentPagination = {
currentPage: 1,
- itemsPerPage: 25,
+ itemsPerPage: 10,
totalItems: null
}
sort: SortField = '-createdAt'
defaultSort: SortField = '-createdAt'
- videos: Video[] = []
loadOnInit = true
+ pageHeight: number
+ videoWidth = 215
+ videoHeight = 230
+ videoPages: Video[][]
protected abstract notificationsService: NotificationsService
protected abstract authService: AuthService
protected abstract router: Router
protected abstract route: ActivatedRoute
-
protected abstract currentRoute: string
-
abstract titlePage: string
- protected otherParams = {}
-
- private loadedPages: { [ id: number ]: boolean } = {}
+ protected loadedPages: { [ id: number ]: Video[] } = {}
+ protected otherRouteParams = {}
- abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
+ abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
get user () {
return this.authService.getUser()
if (isInMobileView()) {
this.pagination.itemsPerPage = 5
+ this.videoWidth = -1
+ }
+
+ 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('after')
+ if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
}
onNearOfTop () {
- if (this.pagination.currentPage > 1) {
- this.previousPage()
- }
+ this.previousPage()
}
onNearOfBottom () {
}
}
+ onPageChanged (page: number) {
+ this.pagination.currentPage = page
+ this.setNewRouteParams()
+ }
+
reloadVideos () {
- this.videos = []
this.loadedPages = {}
- this.loadMoreVideos('before')
+ this.loadMoreVideos(this.pagination.currentPage)
}
- loadMoreVideos (where: 'before' | 'after') {
- if (this.loadedPages[this.pagination.currentPage] === true) return
+ loadMoreVideos (page: number) {
+ if (this.loadedPages[page] !== undefined) return
- const observable = this.getVideosObservable()
+ const observable = this.getVideosObservable(page)
observable.subscribe(
({ videos, totalVideos }) => {
return this.reloadVideos()
}
- this.loadedPages[this.pagination.currentPage] = true
+ this.loadedPages[page] = videos
+ this.buildVideoPages()
this.pagination.totalItems = totalVideos
- if (where === 'before') {
- this.videos = videos.concat(this.videos)
- } else {
- this.videos = this.videos.concat(videos)
+ // Initialize infinite scroller now we loaded the first page
+ if (Object.keys(this.loadedPages).length === 1) {
+ // Wait elements creation
+ setTimeout(() => this.infiniteScroller.initialize(), 500)
}
},
error => this.notificationsService.error('Error', error.message)
}
protected previousPage () {
- this.pagination.currentPage--
+ const min = this.minPageLoaded()
- this.setNewRouteParams()
- this.loadMoreVideos('before')
+ if (min > 1) {
+ this.loadMoreVideos(min - 1)
+ }
}
protected nextPage () {
- this.pagination.currentPage++
-
- this.setNewRouteParams()
- this.loadMoreVideos('after')
+ this.loadMoreVideos(this.maxPageLoaded() + 1)
}
protected buildRouteParams () {
page: this.pagination.currentPage
}
- return Object.assign(params, this.otherParams)
+ return Object.assign(params, this.otherRouteParams)
}
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
const routeParams = this.buildRouteParams()
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)))
+ }
}
--- /dev/null
+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))
+ }
+
+}
<div
class="comment-threads"
- infiniteScroll
- [infiniteScrollUpDistance]="1.5"
- [infiniteScrollDistance]="0.5"
- (scrolled)="onNearOfBottom()"
+ myInfiniteScroller
+ [autoLoading]="true"
+ (nearOfBottom)="onNearOfBottom()"
>
<div *ngFor="let comment of comments">
<my-video-comment
this.threadComments = {}
this.threadLoading = {}
this.inReplyToCommentId = undefined
- this.componentPagination = {
- currentPage: 1,
- itemsPerPage: 10,
- totalItems: null
- }
+ this.componentPagination.currentPage = 1
+ this.componentPagination.totalItems = null
this.loadMoreComments()
}
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
+import { immutableAssign } from '@app/shared/misc/utils'
import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
super.ngOnInit()
}
- getVideosObservable () {
- return this.videoService.getVideos(this.pagination, this.sort)
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.videoService.getVideos(newPagination, this.sort)
}
}
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
+import { immutableAssign } from '@app/shared/misc/utils'
import { NotificationsService } from 'angular2-notifications'
import { Subscription } from 'rxjs/Subscription'
import { AuthService } from '../../core/auth'
currentRoute = '/videos/search'
loadOnInit = false
- protected otherParams = {
+ protected otherRouteParams = {
search: ''
}
private subActivatedRoute: Subscription
this.subActivatedRoute = this.route.queryParams.subscribe(
queryParams => {
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()
},
}
}
- getVideosObservable () {
- return this.videoService.searchVideos(this.otherParams.search, this.pagination, this.sort)
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+ return this.videoService.searchVideos(this.otherRouteParams.search, newPagination, this.sort)
}
}
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
+import { immutableAssign } from '@app/shared/misc/utils'
import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
super.ngOnInit()
}
- getVideosObservable () {
- return this.videoService.getVideos(this.pagination, this.sort)
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+ return this.videoService.getVideos(newPagination, this.sort)
}
}
dependencies:
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:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0"