-
+
+
+
+
+
+
+
+
+
+
{{ video.name }}
+
{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
+
{{ video.privacy.label }}{{ getStateLabel(video) }}
+
+ Blacklisted
+ {{ video.blacklistedReason }}
+
-
+
+
+
+
+ Cancel
+
-
-
{{ video.name }}
-
{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
-
{{ video.privacy.label }}{{ getStateLabel(video) }}
-
- Blacklisted
- {{ video.blacklistedReason }}
-
+
+
+ Delete
+
+
-
-
-
-
- Cancel
-
+
+
-
-
- Delete
-
-
-
+
-
-
-
-
-
-
-
+
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index 41608f796..eb5096a5e 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -1,11 +1,10 @@
-import { from as observableFrom, Observable } from 'rxjs'
-import { concatAll, tap } from 'rxjs/operators'
-import { Component, OnDestroy, OnInit, Inject, LOCALE_ID, ViewChild } from '@angular/core'
+import { concat, Observable } from 'rxjs'
+import { tap, toArray } from 'rxjs/operators'
+import { Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -22,8 +21,9 @@ import { VideoChangeOwnershipComponent } from './video-change-ownership/video-ch
styleUrls: [ './my-account-videos.component.scss' ]
})
export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+ @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
+
titlePage: string
- currentRoute = '/my-account/videos'
checkedVideos: { [ id: number ]: boolean } = {}
pagination: ComponentPagination = {
currentPage: 1,
@@ -31,19 +31,14 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
totalItems: null
}
- protected baseVideoWidth = -1
- protected baseVideoHeight = 155
-
- @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
-
constructor (
protected router: Router,
+ protected serverService: ServerService,
protected route: ActivatedRoute,
protected authService: AuthService,
protected notifier: Notifier,
- protected location: Location,
protected screenService: ScreenService,
- protected i18n: I18n,
+ private i18n: I18n,
private confirmService: ConfirmService,
private videoService: VideoService,
@Inject(LOCALE_ID) private localeId: string
@@ -93,19 +88,18 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
const observables: Observable
[] = []
for (const videoId of toDeleteVideosIds) {
const o = this.videoService.removeVideo(videoId)
- .pipe(tap(() => this.spliceVideosById(videoId)))
+ .pipe(tap(() => this.removeVideoFromArray(videoId)))
observables.push(o)
}
- observableFrom(observables)
- .pipe(concatAll())
+ concat(...observables)
+ .pipe(toArray())
.subscribe(
- res => {
+ () => {
this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }))
this.abortSelectionMode()
- this.reloadVideos()
},
err => this.notifier.error(err.message)
@@ -156,20 +150,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
return ' - ' + suffix
}
- protected buildVideoHeight () {
- // In account videos, the video height is fixed
- return this.baseVideoHeight
- }
-
- private spliceVideosById (id: number) {
- for (const key of Object.keys(this.loadedPages)) {
- const videos: Video[] = this.loadedPages[ key ]
- const index = videos.findIndex(v => v.id === id)
-
- if (index !== -1) {
- videos.splice(index, 1)
- return
- }
- }
+ private removeVideoFromArray (id: number) {
+ this.videos = this.videos.filter(v => v.id !== id)
}
}
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index dea378a6e..8af31000e 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -1,6 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
@@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { ScreenService } from '@app/shared/misc/screen.service'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-video-channel-videos',
@@ -25,7 +24,6 @@ import { Notifier } from '@app/core'
export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
marginContent = false // Disable margin
- currentRoute = '/video-channels/videos'
loadOnInit = false
private videoChannel: VideoChannel
@@ -33,13 +31,13 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
constructor (
protected router: Router,
+ protected serverService: ServerService,
protected route: ActivatedRoute,
protected authService: AuthService,
protected notifier: Notifier,
protected confirmService: ConfirmService,
- protected location: Location,
protected screenService: ScreenService,
- protected i18n: I18n,
+ private i18n: I18n,
private videoChannelService: VideoChannelService,
private videoService: VideoService
) {
@@ -55,7 +53,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.subscribe(videoChannel => {
this.videoChannel = videoChannel
- this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
this.reloadVideos()
this.generateSyndicationList()
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index cedd07d39..d4872a0a5 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -23,6 +23,10 @@ const videoChannelsRoutes: Routes = [
data: {
meta: {
title: 'Video channel videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'video-channel-videos-list'
}
}
},
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index cff37a7d6..db8888dba 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -1,8 +1,9 @@
import { NgModule } from '@angular/core'
-import { RouterModule, Routes } from '@angular/router'
+import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
import { PreloadSelectedModulesList } from './core'
import { AppComponent } from '@app/app.component'
+import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
const routes: Routes = [
{
@@ -43,12 +44,14 @@ const routes: Routes = [
imports: [
RouterModule.forRoot(routes, {
useHash: Boolean(history.pushState) === false,
+ scrollPositionRestoration: 'disabled',
preloadingStrategy: PreloadSelectedModulesList,
- anchorScrolling: 'enabled'
+ anchorScrolling: 'disabled'
})
],
providers: [
- PreloadSelectedModulesList
+ PreloadSelectedModulesList,
+ { provide: RouteReuseStrategy, useClass: CustomReuseStrategy }
],
exports: [ RouterModule ]
})
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index c5c5a8f66..ad0588b99 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,13 +1,14 @@
import { Component, OnInit } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
-import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
+import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
import { is18nPath } from '../../../shared/models/i18n'
import { ScreenService } from '@app/shared/misc/screen.service'
-import { skip, debounceTime } from 'rxjs/operators'
-import { HotkeysService, Hotkey } from 'angular2-hotkeys'
+import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
+import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { fromEvent } from 'rxjs'
+import { ViewportScroller } from '@angular/common'
@Component({
selector: 'my-app',
@@ -22,6 +23,7 @@ export class AppComponent implements OnInit {
constructor (
private i18n: I18n,
+ private viewportScroller: ViewportScroller,
private router: Router,
private authService: AuthService,
private serverService: ServerService,
@@ -52,15 +54,6 @@ export class AppComponent implements OnInit {
ngOnInit () {
document.getElementById('incompatible-browser').className += ' browser-ok'
- this.router.events.subscribe(e => {
- if (e instanceof NavigationEnd) {
- const pathname = window.location.pathname
- if (!pathname || pathname === '/' || is18nPath(pathname)) {
- this.redirectService.redirectToHomepage(true)
- }
- }
- })
-
this.authService.loadClientCredentials()
if (this.isUserLoggedIn()) {
@@ -81,15 +74,94 @@ export class AppComponent implements OnInit {
this.isMenuDisplayed = false
}
- this.router.events.subscribe(
- e => {
- // User clicked on a link in the menu, change the page
- if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
- this.isMenuDisplayed = false
- }
- }
- )
+ this.initRouteEvents()
+ this.injectJS()
+ this.injectCSS()
+ this.initHotkeys()
+
+ fromEvent(window, 'resize')
+ .pipe(debounceTime(200))
+ .subscribe(() => this.onResize())
+ }
+
+ isUserLoggedIn () {
+ return this.authService.isLoggedIn()
+ }
+
+ toggleMenu () {
+ this.isMenuDisplayed = !this.isMenuDisplayed
+ this.isMenuChangedByUser = true
+ }
+
+ onResize () {
+ this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
+ }
+
+ private initRouteEvents () {
+ let resetScroll = true
+ const eventsObs = this.router.events
+
+ const scrollEvent = eventsObs.pipe(filter((e: Event): e is Scroll => e instanceof Scroll))
+ const navigationEndEvent = eventsObs.pipe(filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd))
+
+ scrollEvent.subscribe(e => {
+ if (e.position) {
+ return this.viewportScroller.scrollToPosition(e.position)
+ }
+
+ if (e.anchor) {
+ return this.viewportScroller.scrollToAnchor(e.anchor)
+ }
+
+ if (resetScroll) {
+ return this.viewportScroller.scrollToPosition([ 0, 0 ])
+ }
+ })
+
+ // When we add the a-state parameter, we don't want to alter the scroll
+ navigationEndEvent.pipe(pairwise())
+ .subscribe(([ e1, e2 ]) => {
+ try {
+ resetScroll = false
+
+ const previousUrl = new URL(window.location.origin + e1.url)
+ const nextUrl = new URL(window.location.origin + e2.url)
+
+ if (previousUrl.pathname !== nextUrl.pathname) {
+ resetScroll = true
+ return
+ }
+
+ const nextSearchParams = nextUrl.searchParams
+ nextSearchParams.delete('a-state')
+
+ const previousSearchParams = previousUrl.searchParams
+
+ nextSearchParams.sort()
+ previousSearchParams.sort()
+
+ if (nextSearchParams.toString() !== previousSearchParams.toString()) {
+ resetScroll = true
+ }
+ } catch (e) {
+ console.error('Cannot parse URL to check next scroll.', e)
+ resetScroll = true
+ }
+ })
+
+ navigationEndEvent.pipe(
+ map(() => window.location.pathname),
+ filter(pathname => !pathname || pathname === '/' || is18nPath(pathname))
+ ).subscribe(() => this.redirectService.redirectToHomepage(true))
+
+ eventsObs.pipe(
+ filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
+ filter(() => this.screenService.isInSmallView())
+ ).subscribe(() => this.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
+ }
+
+ private injectJS () {
// Inject JS
this.serverService.configLoaded
.subscribe(() => {
@@ -104,7 +176,9 @@ export class AppComponent implements OnInit {
}
}
})
+ }
+ private injectCSS () {
// Inject CSS if modified (admin config settings)
this.serverService.configLoaded
.pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server
@@ -120,7 +194,9 @@ export class AppComponent implements OnInit {
this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
}
})
+ }
+ private initHotkeys () {
this.hotkeysService.add([
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
document.getElementById('search-video').focus()
@@ -155,22 +231,5 @@ export class AppComponent implements OnInit {
return false
}, undefined, this.i18n('Toggle Dark theme'))
])
-
- fromEvent(window, 'resize')
- .pipe(debounceTime(200))
- .subscribe(() => this.onResize())
- }
-
- isUserLoggedIn () {
- return this.authService.isLoggedIn()
- }
-
- toggleMenu () {
- this.isMenuDisplayed = !this.isMenuDisplayed
- this.isMenuChangedByUser = true
- }
-
- onResize () {
- this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
}
}
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts
new file mode 100644
index 000000000..a9f61acec
--- /dev/null
+++ b/client/src/app/core/routing/custom-reuse-strategy.ts
@@ -0,0 +1,81 @@
+import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'
+
+export class CustomReuseStrategy implements RouteReuseStrategy {
+ storedRouteHandles = new Map()
+ recentlyUsed: string
+
+ private readonly MAX_SIZE = 2
+
+ // Decides if the route should be stored
+ shouldDetach (route: ActivatedRouteSnapshot): boolean {
+ return this.isReuseEnabled(route)
+ }
+
+ // Store the information for the route we're destructing
+ store (route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
+ if (!handle) return
+
+ const key = this.generateKey(route)
+ this.recentlyUsed = key
+
+ console.log('Storing component %s to reuse later.', key);
+
+ (handle as any).componentRef.instance.disableForReuse()
+
+ this.storedRouteHandles.set(key, handle)
+
+ this.gb()
+ }
+
+ // Return true if we have a stored route object for the next route
+ shouldAttach (route: ActivatedRouteSnapshot): boolean {
+ const key = this.generateKey(route)
+ return this.isReuseEnabled(route) && this.storedRouteHandles.has(key)
+ }
+
+ // If we returned true in shouldAttach(), now return the actual route data for restoration
+ retrieve (route: ActivatedRouteSnapshot): DetachedRouteHandle {
+ if (!this.isReuseEnabled(route)) return undefined
+
+ const key = this.generateKey(route)
+ this.recentlyUsed = key
+
+ console.log('Reusing component %s.', key)
+
+ const handle = this.storedRouteHandles.get(key)
+ if (!handle) return handle;
+
+ (handle as any).componentRef.instance.enabledForReuse()
+
+ return handle
+ }
+
+ // Reuse the route if we're going to and from the same route
+ shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
+ return future.routeConfig === curr.routeConfig
+ }
+
+ private gb () {
+ if (this.storedRouteHandles.size >= this.MAX_SIZE) {
+ this.storedRouteHandles.forEach((r, key) => {
+ if (key === this.recentlyUsed) return
+
+ console.log('Removing stored component %s.', key);
+
+ (r as any).componentRef.destroy()
+ this.storedRouteHandles.delete(key)
+ })
+ }
+ }
+
+ private generateKey (route: ActivatedRouteSnapshot) {
+ const reuse = route.data.reuse
+ if (!reuse) return undefined
+
+ return reuse.key + JSON.stringify(route.queryParams)
+ }
+
+ private isReuseEnabled (route: ActivatedRouteSnapshot) {
+ return route.data.reuse && route.data.reuse.enabled && route.queryParams['a-state']
+ }
+}
diff --git a/client/src/app/core/routing/disable-for-reuse-hook.ts b/client/src/app/core/routing/disable-for-reuse-hook.ts
new file mode 100644
index 000000000..c5eb5c578
--- /dev/null
+++ b/client/src/app/core/routing/disable-for-reuse-hook.ts
@@ -0,0 +1,7 @@
+export interface DisableForReuseHook {
+
+ disableForReuse (): void
+
+ enabledForReuse (): void
+
+}
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 @@
No results.
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 @@
import { debounceTime } from 'rxjs/operators'
-import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
-import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
import { fromEvent, Observable, Subscription } from 'rxjs'
import { AuthService } from '../../core/auth'
import { ComponentPagination } from '../rest/component-pagination.model'
import { VideoSortField } from './sort-field.type'
import { Video } from './video.model'
-import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
import { Syndication } from '@app/shared/video/syndication.model'
-import { Notifier } from '@app/core'
-
-export abstract class AbstractVideoList implements OnInit, OnDestroy {
- private static LINES_PER_PAGE = 4
-
- @ViewChild('videosElement') videosElement: ElementRef
- @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
+import { Notifier, ServerService } from '@app/core'
+import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
+export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
pagination: ComponentPagination = {
currentPage: 1,
- itemsPerPage: 10,
+ itemsPerPage: 25,
totalItems: null
}
sort: VideoSortField = '-publishedAt'
+
categoryOneOf?: number
defaultSort: VideoSortField = '-publishedAt'
+
syndicationItems: Syndication[] = []
loadOnInit = true
marginContent = true
- pageHeight: number
- videoWidth: number
- videoHeight: number
- videoPages: Video[][] = []
+ videos: Video[] = []
ownerDisplayType: OwnerDisplayType = 'account'
- firstLoadedPage: number
displayModerationBlock = false
titleTooltip: string
- protected baseVideoWidth = 238
- protected baseVideoHeight = 225
+ disabled = false
protected abstract notifier: Notifier
protected abstract authService: AuthService
- protected abstract router: Router
protected abstract route: ActivatedRoute
+ protected abstract serverService: ServerService
protected abstract screenService: ScreenService
- protected abstract i18n: I18n
- protected abstract location: Location
- protected abstract currentRoute: string
+ protected abstract router: Router
abstract titlePage: string
- protected loadedPages: { [ id: number ]: Video[] } = {}
- protected loadingPage: { [ id: number ]: boolean } = {}
- protected otherRouteParams = {}
-
private resizeSubscription: Subscription
+ private angularState: number
+
+ abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
- abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
abstract generateSyndicationList (): void
get user () {
@@ -77,207 +63,87 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
.subscribe(() => this.calcPageSizes())
this.calcPageSizes()
- if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
+ if (this.loadOnInit === true) this.loadMoreVideos()
}
ngOnDestroy () {
if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
}
- pageByVideoId (index: number, page: Video[]) {
- // Video are unique in all pages
- return page.length !== 0 ? page[0].id : 0
+ disableForReuse () {
+ this.disabled = true
+ }
+
+ enabledForReuse () {
+ this.disabled = false
}
videoById (index: number, video: Video) {
return video.id
}
- onNearOfTop () {
- this.previousPage()
- }
-
onNearOfBottom () {
- if (this.hasMoreVideos()) {
- this.nextPage()
- }
+ if (this.disabled) return
+
+ // Last page
+ if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
+
+ this.pagination.currentPage += 1
+
+ this.setScrollRouteParams()
+
+ this.loadMoreVideos()
}
- onPageChanged (page: number) {
- this.pagination.currentPage = page
- this.setNewRouteParams()
- }
-
- reloadVideos () {
- this.loadedPages = {}
- this.loadMoreVideos(this.pagination.currentPage)
- }
-
- loadMoreVideos (page: number, loadOnTop = false) {
- this.adjustVideoPageHeight()
-
- const currentY = window.scrollY
-
- if (this.loadedPages[page] !== undefined) return
- if (this.loadingPage[page] === true) return
-
- this.loadingPage[page] = true
- const observable = this.getVideosObservable(page)
+ loadMoreVideos () {
+ const observable = this.getVideosObservable(this.pagination.currentPage)
observable.subscribe(
({ videos, totalVideos }) => {
- this.loadingPage[page] = false
-
- if (this.firstLoadedPage === undefined || this.firstLoadedPage > page) this.firstLoadedPage = page
-
- // Paging is too high, return to the first one
- if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
- this.pagination.currentPage = 1
- this.setNewRouteParams()
- return this.reloadVideos()
- }
-
- this.loadedPages[page] = videos
- this.buildVideoPages()
this.pagination.totalItems = totalVideos
-
- // Initialize infinite scroller now we loaded the first page
- if (Object.keys(this.loadedPages).length === 1) {
- // Wait elements creation
- setTimeout(() => {
- this.infiniteScroller.initialize()
-
- // At our first load, we did not load the first page
- // Load the previous page so the user can move on the top (and browser previous pages)
- if (this.pagination.currentPage > 1) this.loadMoreVideos(this.pagination.currentPage - 1, true)
- }, 500)
- }
-
- // Insert elements on the top but keep the scroll in the previous position
- if (loadOnTop) setTimeout(() => { window.scrollTo(0, currentY + this.pageHeight) }, 0)
+ this.videos = this.videos.concat(videos)
},
- error => {
- this.loadingPage[page] = false
- this.notifier.error(error.message)
- }
+
+ error => this.notifier.error(error.message)
)
}
+ reloadVideos () {
+ this.pagination.currentPage = 1
+ this.videos = []
+ this.loadMoreVideos()
+ }
+
toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}
- protected hasMoreVideos () {
- // No results
- if (this.pagination.totalItems === 0) return false
-
- // Not loaded yet
- if (!this.pagination.totalItems) return true
-
- const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
- return maxPage > this.maxPageLoaded()
- }
-
- protected previousPage () {
- const min = this.minPageLoaded()
-
- if (min > 1) {
- this.loadMoreVideos(min - 1, true)
- }
- }
-
- protected nextPage () {
- this.loadMoreVideos(this.maxPageLoaded() + 1)
- }
-
- protected buildRouteParams () {
- // There is always a sort and a current page
- const params = {
- sort: this.sort,
- page: this.pagination.currentPage
- }
-
- return Object.assign(params, this.otherRouteParams)
- }
-
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
- this.sort = routeParams['sort'] as VideoSortField || this.defaultSort
- this.categoryOneOf = routeParams['categoryOneOf']
- if (routeParams['page'] !== undefined) {
- this.pagination.currentPage = parseInt(routeParams['page'], 10)
- } else {
- this.pagination.currentPage = 1
- }
- }
-
- protected setNewRouteParams () {
- const paramsObject = this.buildRouteParams()
-
- const queryParams = Object.keys(paramsObject)
- .map(p => p + '=' + paramsObject[p])
- .join('&')
- this.location.replaceState(this.currentRoute, queryParams)
- }
-
- protected buildVideoPages () {
- this.videoPages = Object.values(this.loadedPages)
- }
-
- protected adjustVideoPageHeight () {
- const numberOfPagesLoaded = Object.keys(this.loadedPages).length
- if (!numberOfPagesLoaded) return
-
- this.pageHeight = this.videosElement.nativeElement.offsetHeight / numberOfPagesLoaded
- }
-
- protected buildVideoHeight () {
- // Same ratios than base width/height
- return this.videosElement.nativeElement.offsetWidth * (this.baseVideoHeight / this.baseVideoWidth)
- }
-
- private minPageLoaded () {
- return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
- }
-
- private maxPageLoaded () {
- return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
+ this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort
+ this.categoryOneOf = routeParams[ 'categoryOneOf' ]
+ this.angularState = routeParams[ 'a-state' ]
}
private calcPageSizes () {
- if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
+ if (this.screenService.isInMobileView()) {
this.pagination.itemsPerPage = 5
+ }
+ }
- // Video takes all the width
- this.videoWidth = -1
- this.videoHeight = this.buildVideoHeight()
- this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
- } else {
- this.videoWidth = this.baseVideoWidth
- this.videoHeight = this.baseVideoHeight
+ private setScrollRouteParams () {
+ // Already set
+ if (this.angularState) return
- const videosWidth = this.videosElement.nativeElement.offsetWidth
- this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
- this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
+ this.angularState = 42
+
+ const queryParams = {
+ 'a-state': this.angularState,
+ categoryOneOf: this.categoryOneOf
}
- // Rebuild pages because maybe we modified the number of items per page
- const videos = [].concat(...this.videoPages)
- this.loadedPages = {}
+ let path = this.router.url
+ if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute
- let i = 1
- // Don't include the last page if it not complete
- while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop
- this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage)
- i++
- }
-
- // Re fetch the last page
- if (videos.length !== 0) {
- this.loadMoreVideos(i)
- } else {
- this.buildVideoPages()
- }
-
- console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage)
+ this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' })
}
}
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'
selector: '[myInfiniteScroller]'
})
export class InfiniteScrollerDirective implements OnInit, OnDestroy {
- @Input() containerHeight: number
- @Input() pageHeight: number
- @Input() firstLoadedPage = 1
@Input() percentLimit = 70
@Input() autoInit = false
@Input() onItself = false
@Output() nearOfBottom = new EventEmitter
()
- @Output() nearOfTop = new EventEmitter()
- @Output() pageChanged = new EventEmitter()
private decimalLimit = 0
private lastCurrentBottom = -1
- private lastCurrentTop = 0
private scrollDownSub: Subscription
- private scrollUpSub: Subscription
- private pageChangeSub: Subscription
- private middleScreen: number
private container: HTMLElement
constructor (private el: ElementRef) {
@@ -36,8 +27,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
ngOnDestroy () {
if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
- if (this.scrollUpSub) this.scrollUpSub.unsubscribe()
- if (this.pageChangeSub) this.pageChangeSub.unsubscribe()
}
initialize () {
@@ -45,8 +34,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
this.container = this.el.nativeElement
}
- this.middleScreen = window.innerHeight / 2
-
// Emit the last value
const throttleOptions = { leading: true, trailing: true }
@@ -72,40 +59,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
)
.subscribe(() => this.nearOfBottom.emit())
-
- // Scroll up
- this.scrollUpSub = scrollObservable
- .pipe(
- // Check we scroll up
- filter(({ current }) => {
- const res = this.lastCurrentTop > current
-
- this.lastCurrentTop = current
- return res
- }),
- filter(({ current, maximumScroll }) => {
- return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit
- })
- )
- .subscribe(() => this.nearOfTop.emit())
-
- // Page change
- this.pageChangeSub = scrollObservable
- .pipe(
- distinct(),
- map(({ current }) => this.calculateCurrentPage(current)),
- distinctUntilChanged()
- )
- .subscribe(res => this.pageChanged.emit(res))
- }
-
- private calculateCurrentPage (current: number) {
- const scrollY = current + this.middleScreen
-
- const page = Math.max(1, Math.ceil(scrollY / this.pageHeight))
-
- // Offset page
- return page + (this.firstLoadedPage - 1)
}
private getScrollInfo () {
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index c0be4b885..13d4023c2 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -1,7 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { immutableAssign } from '@app/shared/misc/utils'
-import { Location } from '@angular/common'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,7 +9,7 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { UserRight } from '../../../../../shared/models/users'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-videos-local',
@@ -19,18 +18,17 @@ import { Notifier } from '@app/core'
})
export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
- currentRoute = '/videos/local'
sort = '-publishedAt' as VideoSortField
filter: VideoFilter = 'local'
constructor (
protected router: Router,
+ protected serverService: ServerService,
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
- protected location: Location,
- protected i18n: I18n,
protected screenService: ScreenService,
+ private i18n: I18n,
private videoService: VideoService
) {
super()
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index f99c8abb6..80cef813e 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -1,6 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -8,7 +7,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-videos-recently-added',
@@ -17,17 +16,16 @@ import { Notifier } from '@app/core'
})
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
- currentRoute = '/videos/recently-added'
sort: VideoSortField = '-publishedAt'
constructor (
- protected router: Router,
protected route: ActivatedRoute,
- protected location: Location,
+ protected serverService: ServerService,
+ protected router: Router,
protected notifier: Notifier,
protected authService: AuthService,
- protected i18n: I18n,
protected screenService: ScreenService,
+ private i18n: I18n,
private videoService: VideoService
) {
super()
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index a66a0f97c..e2ad95bc4 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -1,6 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -17,18 +16,16 @@ import { Notifier, ServerService } from '@app/core'
})
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
- currentRoute = '/videos/trending'
defaultSort: VideoSortField = '-trending'
constructor (
protected router: Router,
+ protected serverService: ServerService,
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
- protected location: Location,
protected screenService: ScreenService,
- private serverService: ServerService,
- protected i18n: I18n,
+ private i18n: I18n,
private videoService: VideoService
) {
super()
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index bee828e12..2f0685ccc 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -1,7 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { immutableAssign } from '@app/shared/misc/utils'
-import { Location } from '@angular/common'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -9,7 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-videos-user-subscriptions',
@@ -18,18 +17,17 @@ import { Notifier } from '@app/core'
})
export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
- currentRoute = '/videos/subscriptions'
sort = '-publishedAt' as VideoSortField
ownerDisplayType: OwnerDisplayType = 'auto'
constructor (
protected router: Router,
+ protected serverService: ServerService,
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
- protected location: Location,
- protected i18n: I18n,
protected screenService: ScreenService,
+ private i18n: I18n,
private videoService: VideoService
) {
super()
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts
index 69a9232ce..505173a5b 100644
--- a/client/src/app/videos/videos-routing.module.ts
+++ b/client/src/app/videos/videos-routing.module.ts
@@ -29,6 +29,10 @@ const videosRoutes: Routes = [
data: {
meta: {
title: 'Trending videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'trending-videos-list'
}
}
},
@@ -38,6 +42,10 @@ const videosRoutes: Routes = [
data: {
meta: {
title: 'Recently added videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'recently-added-videos-list'
}
}
},
@@ -47,6 +55,10 @@ const videosRoutes: Routes = [
data: {
meta: {
title: 'Subscriptions'
+ },
+ reuse: {
+ enabled: true,
+ key: 'subscription-videos-list'
}
}
},
@@ -56,6 +68,10 @@ const videosRoutes: Routes = [
data: {
meta: {
title: 'Local videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'local-videos-list'
}
}
},
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index e733138c1..93d84c6fc 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,5 +1,4 @@
import * as Sequelize from 'sequelize'
-import { Op } from 'sequelize'
import {
AllowNull,
BeforeDestroy,
@@ -458,7 +457,7 @@ export class VideoCommentModel extends Model {
const query = {
where: {
updatedAt: {
- [Op.lt]: beforeUpdatedAt
+ [Sequelize.Op.lt]: beforeUpdatedAt
},
videoId
}
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index fb52b35d9..399081564 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -1,5 +1,4 @@
import * as Sequelize from 'sequelize'
-import { Op } from 'sequelize'
import * as Bluebird from 'bluebird'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -206,7 +205,7 @@ export class VideoShareModel extends Model {
const query = {
where: {
updatedAt: {
- [Op.lt]: beforeUpdatedAt
+ [Sequelize.Op.lt]: beforeUpdatedAt
},
videoId
}