aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/video
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/video')
-rw-r--r--client/src/app/shared/video/abstract-video-list.html11
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts244
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts47
3 files changed, 59 insertions, 243 deletions
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index 1f97bc389..e134654a3 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -19,13 +19,10 @@
19 19
20 <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> 20 <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
21 <div 21 <div
22 myInfiniteScroller 22 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
23 [pageHeight]="pageHeight" [firstLoadedPage]="firstLoadedPage" 23 class="videos"
24 (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
25 class="videos" #videosElement
26 > 24 >
27 <div *ngFor="let videos of videoPages; trackBy: pageByVideoId" class="videos-page"> 25 <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType">
28 <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"></my-video-miniature> 26 </my-video-miniature>
29 </div>
30 </div> 27 </div>
31</div> 28</div>
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 2cd5bc393..467f629ea 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -1,66 +1,52 @@
1import { debounceTime } from 'rxjs/operators' 1import { debounceTime } from 'rxjs/operators'
2import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' 2import { OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Location } from '@angular/common'
5import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
6import { fromEvent, Observable, Subscription } from 'rxjs' 4import { fromEvent, Observable, Subscription } from 'rxjs'
7import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
8import { ComponentPagination } from '../rest/component-pagination.model' 6import { ComponentPagination } from '../rest/component-pagination.model'
9import { VideoSortField } from './sort-field.type' 7import { VideoSortField } from './sort-field.type'
10import { Video } from './video.model' 8import { Video } from './video.model'
11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service' 9import { ScreenService } from '@app/shared/misc/screen.service'
13import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 10import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
14import { Syndication } from '@app/shared/video/syndication.model' 11import { Syndication } from '@app/shared/video/syndication.model'
15import { Notifier } from '@app/core' 12import { Notifier, ServerService } from '@app/core'
16 13import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
17export abstract class AbstractVideoList implements OnInit, OnDestroy {
18 private static LINES_PER_PAGE = 4
19
20 @ViewChild('videosElement') videosElement: ElementRef
21 @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
22 14
15export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
23 pagination: ComponentPagination = { 16 pagination: ComponentPagination = {
24 currentPage: 1, 17 currentPage: 1,
25 itemsPerPage: 10, 18 itemsPerPage: 25,
26 totalItems: null 19 totalItems: null
27 } 20 }
28 sort: VideoSortField = '-publishedAt' 21 sort: VideoSortField = '-publishedAt'
22
29 categoryOneOf?: number 23 categoryOneOf?: number
30 defaultSort: VideoSortField = '-publishedAt' 24 defaultSort: VideoSortField = '-publishedAt'
25
31 syndicationItems: Syndication[] = [] 26 syndicationItems: Syndication[] = []
32 27
33 loadOnInit = true 28 loadOnInit = true
34 marginContent = true 29 marginContent = true
35 pageHeight: number 30 videos: Video[] = []
36 videoWidth: number
37 videoHeight: number
38 videoPages: Video[][] = []
39 ownerDisplayType: OwnerDisplayType = 'account' 31 ownerDisplayType: OwnerDisplayType = 'account'
40 firstLoadedPage: number
41 displayModerationBlock = false 32 displayModerationBlock = false
42 titleTooltip: string 33 titleTooltip: string
43 34
44 protected baseVideoWidth = 238 35 disabled = false
45 protected baseVideoHeight = 225
46 36
47 protected abstract notifier: Notifier 37 protected abstract notifier: Notifier
48 protected abstract authService: AuthService 38 protected abstract authService: AuthService
49 protected abstract router: Router
50 protected abstract route: ActivatedRoute 39 protected abstract route: ActivatedRoute
40 protected abstract serverService: ServerService
51 protected abstract screenService: ScreenService 41 protected abstract screenService: ScreenService
52 protected abstract i18n: I18n 42 protected abstract router: Router
53 protected abstract location: Location
54 protected abstract currentRoute: string
55 abstract titlePage: string 43 abstract titlePage: string
56 44
57 protected loadedPages: { [ id: number ]: Video[] } = {}
58 protected loadingPage: { [ id: number ]: boolean } = {}
59 protected otherRouteParams = {}
60
61 private resizeSubscription: Subscription 45 private resizeSubscription: Subscription
46 private angularState: number
47
48 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
62 49
63 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
64 abstract generateSyndicationList (): void 50 abstract generateSyndicationList (): void
65 51
66 get user () { 52 get user () {
@@ -77,207 +63,87 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
77 .subscribe(() => this.calcPageSizes()) 63 .subscribe(() => this.calcPageSizes())
78 64
79 this.calcPageSizes() 65 this.calcPageSizes()
80 if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage) 66 if (this.loadOnInit === true) this.loadMoreVideos()
81 } 67 }
82 68
83 ngOnDestroy () { 69 ngOnDestroy () {
84 if (this.resizeSubscription) this.resizeSubscription.unsubscribe() 70 if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
85 } 71 }
86 72
87 pageByVideoId (index: number, page: Video[]) { 73 disableForReuse () {
88 // Video are unique in all pages 74 this.disabled = true
89 return page.length !== 0 ? page[0].id : 0
90 } 75 }
91 76
92 videoById (index: number, video: Video) { 77 enabledForReuse () {
93 return video.id 78 this.disabled = false
94 } 79 }
95 80
96 onNearOfTop () { 81 videoById (index: number, video: Video) {
97 this.previousPage() 82 return video.id
98 } 83 }
99 84
100 onNearOfBottom () { 85 onNearOfBottom () {
101 if (this.hasMoreVideos()) { 86 if (this.disabled) return
102 this.nextPage()
103 }
104 }
105 87
106 onPageChanged (page: number) { 88 // Last page
107 this.pagination.currentPage = page 89 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
108 this.setNewRouteParams()
109 }
110 90
111 reloadVideos () { 91 this.pagination.currentPage += 1
112 this.loadedPages = {}
113 this.loadMoreVideos(this.pagination.currentPage)
114 }
115
116 loadMoreVideos (page: number, loadOnTop = false) {
117 this.adjustVideoPageHeight()
118 92
119 const currentY = window.scrollY 93 this.setScrollRouteParams()
120 94
121 if (this.loadedPages[page] !== undefined) return 95 this.loadMoreVideos()
122 if (this.loadingPage[page] === true) return 96 }
123 97
124 this.loadingPage[page] = true 98 loadMoreVideos () {
125 const observable = this.getVideosObservable(page) 99 const observable = this.getVideosObservable(this.pagination.currentPage)
126 100
127 observable.subscribe( 101 observable.subscribe(
128 ({ videos, totalVideos }) => { 102 ({ videos, totalVideos }) => {
129 this.loadingPage[page] = false
130
131 if (this.firstLoadedPage === undefined || this.firstLoadedPage > page) this.firstLoadedPage = page
132
133 // Paging is too high, return to the first one
134 if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
135 this.pagination.currentPage = 1
136 this.setNewRouteParams()
137 return this.reloadVideos()
138 }
139
140 this.loadedPages[page] = videos
141 this.buildVideoPages()
142 this.pagination.totalItems = totalVideos 103 this.pagination.totalItems = totalVideos
143 104 this.videos = this.videos.concat(videos)
144 // Initialize infinite scroller now we loaded the first page
145 if (Object.keys(this.loadedPages).length === 1) {
146 // Wait elements creation
147 setTimeout(() => {
148 this.infiniteScroller.initialize()
149
150 // At our first load, we did not load the first page
151 // Load the previous page so the user can move on the top (and browser previous pages)
152 if (this.pagination.currentPage > 1) this.loadMoreVideos(this.pagination.currentPage - 1, true)
153 }, 500)
154 }
155
156 // Insert elements on the top but keep the scroll in the previous position
157 if (loadOnTop) setTimeout(() => { window.scrollTo(0, currentY + this.pageHeight) }, 0)
158 }, 105 },
159 error => {
160 this.loadingPage[page] = false
161 this.notifier.error(error.message)
162 }
163 )
164 }
165
166 toggleModerationDisplay () {
167 throw new Error('toggleModerationDisplay is not implemented')
168 }
169 106
170 protected hasMoreVideos () { 107 error => this.notifier.error(error.message)
171 // No results 108 )
172 if (this.pagination.totalItems === 0) return false
173
174 // Not loaded yet
175 if (!this.pagination.totalItems) return true
176
177 const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
178 return maxPage > this.maxPageLoaded()
179 }
180
181 protected previousPage () {
182 const min = this.minPageLoaded()
183
184 if (min > 1) {
185 this.loadMoreVideos(min - 1, true)
186 }
187 } 109 }
188 110
189 protected nextPage () { 111 reloadVideos () {
190 this.loadMoreVideos(this.maxPageLoaded() + 1) 112 this.pagination.currentPage = 1
113 this.videos = []
114 this.loadMoreVideos()
191 } 115 }
192 116
193 protected buildRouteParams () { 117 toggleModerationDisplay () {
194 // There is always a sort and a current page 118 throw new Error('toggleModerationDisplay is not implemented')
195 const params = {
196 sort: this.sort,
197 page: this.pagination.currentPage
198 }
199
200 return Object.assign(params, this.otherRouteParams)
201 } 119 }
202 120
203 protected loadRouteParams (routeParams: { [ key: string ]: any }) { 121 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
204 this.sort = routeParams['sort'] as VideoSortField || this.defaultSort 122 this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort
205 this.categoryOneOf = routeParams['categoryOneOf'] 123 this.categoryOneOf = routeParams[ 'categoryOneOf' ]
206 if (routeParams['page'] !== undefined) { 124 this.angularState = routeParams[ 'a-state' ]
207 this.pagination.currentPage = parseInt(routeParams['page'], 10)
208 } else {
209 this.pagination.currentPage = 1
210 }
211 }
212
213 protected setNewRouteParams () {
214 const paramsObject = this.buildRouteParams()
215
216 const queryParams = Object.keys(paramsObject)
217 .map(p => p + '=' + paramsObject[p])
218 .join('&')
219 this.location.replaceState(this.currentRoute, queryParams)
220 }
221
222 protected buildVideoPages () {
223 this.videoPages = Object.values(this.loadedPages)
224 }
225
226 protected adjustVideoPageHeight () {
227 const numberOfPagesLoaded = Object.keys(this.loadedPages).length
228 if (!numberOfPagesLoaded) return
229
230 this.pageHeight = this.videosElement.nativeElement.offsetHeight / numberOfPagesLoaded
231 }
232
233 protected buildVideoHeight () {
234 // Same ratios than base width/height
235 return this.videosElement.nativeElement.offsetWidth * (this.baseVideoHeight / this.baseVideoWidth)
236 }
237
238 private minPageLoaded () {
239 return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
240 }
241
242 private maxPageLoaded () {
243 return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
244 } 125 }
245 126
246 private calcPageSizes () { 127 private calcPageSizes () {
247 if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) { 128 if (this.screenService.isInMobileView()) {
248 this.pagination.itemsPerPage = 5 129 this.pagination.itemsPerPage = 5
249
250 // Video takes all the width
251 this.videoWidth = -1
252 this.videoHeight = this.buildVideoHeight()
253 this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
254 } else {
255 this.videoWidth = this.baseVideoWidth
256 this.videoHeight = this.baseVideoHeight
257
258 const videosWidth = this.videosElement.nativeElement.offsetWidth
259 this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
260 this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
261 } 130 }
131 }
262 132
263 // Rebuild pages because maybe we modified the number of items per page 133 private setScrollRouteParams () {
264 const videos = [].concat(...this.videoPages) 134 // Already set
265 this.loadedPages = {} 135 if (this.angularState) return
266 136
267 let i = 1 137 this.angularState = 42
268 // Don't include the last page if it not complete
269 while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop
270 this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage)
271 i++
272 }
273 138
274 // Re fetch the last page 139 const queryParams = {
275 if (videos.length !== 0) { 140 'a-state': this.angularState,
276 this.loadMoreVideos(i) 141 categoryOneOf: this.categoryOneOf
277 } else {
278 this.buildVideoPages()
279 } 142 }
280 143
281 console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage) 144 let path = this.router.url
145 if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute
146
147 this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' })
282 } 148 }
283} 149}
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts
index a9e75007c..5f8a1dd6e 100644
--- a/client/src/app/shared/video/infinite-scroller.directive.ts
+++ b/client/src/app/shared/video/infinite-scroller.directive.ts
@@ -6,24 +6,15 @@ import { fromEvent, Subscription } from 'rxjs'
6 selector: '[myInfiniteScroller]' 6 selector: '[myInfiniteScroller]'
7}) 7})
8export class InfiniteScrollerDirective implements OnInit, OnDestroy { 8export class InfiniteScrollerDirective implements OnInit, OnDestroy {
9 @Input() containerHeight: number
10 @Input() pageHeight: number
11 @Input() firstLoadedPage = 1
12 @Input() percentLimit = 70 9 @Input() percentLimit = 70
13 @Input() autoInit = false 10 @Input() autoInit = false
14 @Input() onItself = false 11 @Input() onItself = false
15 12
16 @Output() nearOfBottom = new EventEmitter<void>() 13 @Output() nearOfBottom = new EventEmitter<void>()
17 @Output() nearOfTop = new EventEmitter<void>()
18 @Output() pageChanged = new EventEmitter<number>()
19 14
20 private decimalLimit = 0 15 private decimalLimit = 0
21 private lastCurrentBottom = -1 16 private lastCurrentBottom = -1
22 private lastCurrentTop = 0
23 private scrollDownSub: Subscription 17 private scrollDownSub: Subscription
24 private scrollUpSub: Subscription
25 private pageChangeSub: Subscription
26 private middleScreen: number
27 private container: HTMLElement 18 private container: HTMLElement
28 19
29 constructor (private el: ElementRef) { 20 constructor (private el: ElementRef) {
@@ -36,8 +27,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
36 27
37 ngOnDestroy () { 28 ngOnDestroy () {
38 if (this.scrollDownSub) this.scrollDownSub.unsubscribe() 29 if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
39 if (this.scrollUpSub) this.scrollUpSub.unsubscribe()
40 if (this.pageChangeSub) this.pageChangeSub.unsubscribe()
41 } 30 }
42 31
43 initialize () { 32 initialize () {
@@ -45,8 +34,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
45 this.container = this.el.nativeElement 34 this.container = this.el.nativeElement
46 } 35 }
47 36
48 this.middleScreen = window.innerHeight / 2
49
50 // Emit the last value 37 // Emit the last value
51 const throttleOptions = { leading: true, trailing: true } 38 const throttleOptions = { leading: true, trailing: true }
52 39
@@ -72,40 +59,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
72 filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit) 59 filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
73 ) 60 )
74 .subscribe(() => this.nearOfBottom.emit()) 61 .subscribe(() => this.nearOfBottom.emit())
75
76 // Scroll up
77 this.scrollUpSub = scrollObservable
78 .pipe(
79 // Check we scroll up
80 filter(({ current }) => {
81 const res = this.lastCurrentTop > current
82
83 this.lastCurrentTop = current
84 return res
85 }),
86 filter(({ current, maximumScroll }) => {
87 return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit
88 })
89 )
90 .subscribe(() => this.nearOfTop.emit())
91
92 // Page change
93 this.pageChangeSub = scrollObservable
94 .pipe(
95 distinct(),
96 map(({ current }) => this.calculateCurrentPage(current)),
97 distinctUntilChanged()
98 )
99 .subscribe(res => this.pageChanged.emit(res))
100 }
101
102 private calculateCurrentPage (current: number) {
103 const scrollY = current + this.middleScreen
104
105 const page = Math.max(1, Math.ceil(scrollY / this.pageHeight))
106
107 // Offset page
108 return page + (this.firstLoadedPage - 1)
109 } 62 }
110 63
111 private getScrollInfo () { 64 private getScrollInfo () {