aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/misc/utils.ts7
-rw-r--r--client/src/app/shared/shared.module.ts8
-rw-r--r--client/src/app/shared/video/abstract-video-list.html22
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts96
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts77
5 files changed, 162 insertions, 48 deletions
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
58function immutableAssign <A, B> (target: A, source: B) {
59 return Object.assign({}, target, source)
60}
61
58function isInSmallView () { 62function 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'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 4import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { RouterModule } from '@angular/router' 5import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
7import { MarkdownService } from '@app/videos/shared' 8import { MarkdownService } from '@app/videos/shared'
8import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 9import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
9 10
10import { BsDropdownModule } from 'ngx-bootstrap/dropdown' 11import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
11import { ModalModule } from 'ngx-bootstrap/modal' 12import { ModalModule } from 'ngx-bootstrap/modal'
12import { TabsModule } from 'ngx-bootstrap/tabs' 13import { TabsModule } from 'ngx-bootstrap/tabs'
13import { InfiniteScrollModule } from 'ngx-infinite-scroll'
14import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 14import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
15import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 15import { 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 @@
1import { OnInit } from '@angular/core' 1import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { isInMobileView, isInSmallView } from '@app/shared/misc/utils' 3import { isInMobileView } from '@app/shared/misc/utils'
4import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
4import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
5import { Observable } from 'rxjs/Observable' 6import { Observable } from 'rxjs/Observable'
6import { AuthService } from '../../core/auth' 7import { AuthService } from '../../core/auth'
@@ -9,30 +10,35 @@ import { SortField } from './sort-field.type'
9import { Video } from './video.model' 10import { Video } from './video.model'
10 11
11export abstract class AbstractVideoList implements OnInit { 12export 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 @@
1import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import 'rxjs/add/operator/distinct'
3import 'rxjs/add/operator/startWith'
4import { fromEvent } from 'rxjs/observable/fromEvent'
5
6@Directive({
7 selector: '[myInfiniteScroller]'
8})
9export 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}