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