Handle sort in rss
[oweals/peertube.git] / client / src / app / shared / video / abstract-video-list.ts
1 import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
2 import { ActivatedRoute, Router } from '@angular/router'
3 import { isInMobileView } from '@app/shared/misc/utils'
4 import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
5 import { NotificationsService } from 'angular2-notifications'
6 import 'rxjs/add/operator/debounceTime'
7 import { Observable } from 'rxjs/Observable'
8 import { fromEvent } from 'rxjs/observable/fromEvent'
9 import { Subscription } from 'rxjs/Subscription'
10 import { AuthService } from '../../core/auth'
11 import { ComponentPagination } from '../rest/component-pagination.model'
12 import { VideoSortField } from './sort-field.type'
13 import { Video } from './video.model'
14
15 export abstract class AbstractVideoList implements OnInit, OnDestroy {
16   private static LINES_PER_PAGE = 4
17
18   @ViewChild('videosElement') videosElement: ElementRef
19   @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
20
21   pagination: ComponentPagination = {
22     currentPage: 1,
23     itemsPerPage: 10,
24     totalItems: null
25   }
26   sort: VideoSortField = '-createdAt'
27   defaultSort: VideoSortField = '-createdAt'
28   syndicationItems = []
29
30   loadOnInit = true
31   pageHeight: number
32   videoWidth: number
33   videoHeight: number
34   videoPages: Video[][] = []
35
36   protected baseVideoWidth = 215
37   protected baseVideoHeight = 230
38
39   protected abstract notificationsService: NotificationsService
40   protected abstract authService: AuthService
41   protected abstract router: Router
42   protected abstract route: ActivatedRoute
43   protected abstract currentRoute: string
44   abstract titlePage: string
45
46   protected loadedPages: { [ id: number ]: Video[] } = {}
47   protected otherRouteParams = {}
48
49   private resizeSubscription: Subscription
50
51   abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
52   abstract generateSyndicationList ()
53
54   get user () {
55     return this.authService.getUser()
56   }
57
58   ngOnInit () {
59     // Subscribe to route changes
60     const routeParams = this.route.snapshot.queryParams
61     this.loadRouteParams(routeParams)
62
63     this.resizeSubscription = fromEvent(window, 'resize')
64       .debounceTime(500)
65       .subscribe(() => this.calcPageSizes())
66
67     this.calcPageSizes()
68     if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
69   }
70
71   ngOnDestroy () {
72     if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
73   }
74
75   onNearOfTop () {
76     this.previousPage()
77   }
78
79   onNearOfBottom () {
80     if (this.hasMoreVideos()) {
81       this.nextPage()
82     }
83   }
84
85   onPageChanged (page: number) {
86     this.pagination.currentPage = page
87     this.setNewRouteParams()
88   }
89
90   reloadVideos () {
91     this.loadedPages = {}
92     this.loadMoreVideos(this.pagination.currentPage)
93   }
94
95   loadMoreVideos (page: number) {
96     if (this.loadedPages[page] !== undefined) return
97
98     const observable = this.getVideosObservable(page)
99
100     observable.subscribe(
101       ({ videos, totalVideos }) => {
102         // Paging is too high, return to the first one
103         if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
104           this.pagination.currentPage = 1
105           this.setNewRouteParams()
106           return this.reloadVideos()
107         }
108
109         this.loadedPages[page] = videos
110         this.buildVideoPages()
111         this.pagination.totalItems = totalVideos
112
113         // Initialize infinite scroller now we loaded the first page
114         if (Object.keys(this.loadedPages).length === 1) {
115           // Wait elements creation
116           setTimeout(() => this.infiniteScroller.initialize(), 500)
117         }
118       },
119       error => this.notificationsService.error('Error', error.message)
120     )
121   }
122
123   protected hasMoreVideos () {
124     // No results
125     if (this.pagination.totalItems === 0) return false
126
127     // Not loaded yet
128     if (!this.pagination.totalItems) return true
129
130     const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
131     return maxPage > this.maxPageLoaded()
132   }
133
134   protected previousPage () {
135     const min = this.minPageLoaded()
136
137     if (min > 1) {
138       this.loadMoreVideos(min - 1)
139     }
140   }
141
142   protected nextPage () {
143     this.loadMoreVideos(this.maxPageLoaded() + 1)
144   }
145
146   protected buildRouteParams () {
147     // There is always a sort and a current page
148     const params = {
149       sort: this.sort,
150       page: this.pagination.currentPage
151     }
152
153     return Object.assign(params, this.otherRouteParams)
154   }
155
156   protected loadRouteParams (routeParams: { [ key: string ]: any }) {
157     this.sort = routeParams['sort'] as VideoSortField || this.defaultSort
158
159     if (routeParams['page'] !== undefined) {
160       this.pagination.currentPage = parseInt(routeParams['page'], 10)
161     } else {
162       this.pagination.currentPage = 1
163     }
164   }
165
166   protected setNewRouteParams () {
167     const routeParams = this.buildRouteParams()
168     this.router.navigate([ this.currentRoute ], { queryParams: routeParams })
169   }
170
171   protected buildVideoPages () {
172     this.videoPages = Object.values(this.loadedPages)
173   }
174
175   protected buildVideoHeight () {
176     // Same ratios than base width/height
177     return this.videosElement.nativeElement.offsetWidth * (this.baseVideoHeight / this.baseVideoWidth)
178   }
179
180   private minPageLoaded () {
181     return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
182   }
183
184   private maxPageLoaded () {
185     return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
186   }
187
188   private calcPageSizes () {
189     if (isInMobileView() || this.baseVideoWidth === -1) {
190       this.pagination.itemsPerPage = 5
191
192       // Video takes all the width
193       this.videoWidth = -1
194       this.videoHeight = this.buildVideoHeight()
195       this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
196     } else {
197       this.videoWidth = this.baseVideoWidth
198       this.videoHeight = this.baseVideoHeight
199
200       const videosWidth = this.videosElement.nativeElement.offsetWidth
201       this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
202       this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
203     }
204
205     // Rebuild pages because maybe we modified the number of items per page
206     const videos = [].concat(...this.videoPages)
207     this.loadedPages = {}
208
209     let i = 1
210     // Don't include the last page if it not complete
211     while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop
212       this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage)
213       i++
214     }
215
216     // Re fetch the last page
217     if (videos.length !== 0) {
218       this.loadMoreVideos(i)
219     } else {
220       this.buildVideoPages()
221     }
222
223     console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage)
224   }
225 }