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