From 489290b8b16bede6ddfb773adad55dee6471ccfd Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 21 Mar 2019 16:49:46 +0100 Subject: Restore videos list components --- .../account-videos/account-videos.component.ts | 9 +- .../src/app/+accounts/accounts-routing.module.ts | 4 + .../video-auto-blacklist-list.component.html | 75 +++---- .../video-auto-blacklist-list.component.ts | 13 +- .../my-account-history.component.html | 16 +- .../my-account-history.component.ts | 9 +- .../app/+my-account/my-account-routing.module.ts | 8 + .../my-account-videos.component.html | 75 +++---- .../my-account-videos.component.ts | 47 ++-- .../video-channel-videos.component.ts | 9 +- .../video-channels-routing.module.ts | 4 + client/src/app/app-routing.module.ts | 9 +- client/src/app/app.component.ts | 131 ++++++++--- .../src/app/core/routing/custom-reuse-strategy.ts | 81 +++++++ .../src/app/core/routing/disable-for-reuse-hook.ts | 7 + .../src/app/shared/video/abstract-video-list.html | 11 +- client/src/app/shared/video/abstract-video-list.ts | 244 +++++---------------- .../shared/video/infinite-scroller.directive.ts | 47 ---- .../app/videos/video-list/video-local.component.ts | 8 +- .../video-list/video-recently-added.component.ts | 10 +- .../videos/video-list/video-trending.component.ts | 7 +- .../video-user-subscriptions.component.ts | 8 +- client/src/app/videos/videos-routing.module.ts | 16 ++ 23 files changed, 393 insertions(+), 455 deletions(-) create mode 100644 client/src/app/core/routing/custom-reuse-strategy.ts create mode 100644 client/src/app/core/routing/disable-for-reuse-hook.ts (limited to 'client/src') diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 13b634a01..7535eef08 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-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-account-videos', @@ -25,7 +24,6 @@ import { Notifier } from '@app/core' export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { titlePage: string marginContent = false // Disable margin - currentRoute = '/accounts/videos' loadOnInit = false private account: Account @@ -33,13 +31,13 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, 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 accountService: AccountService, private videoService: VideoService ) { @@ -55,7 +53,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, this.accountSub = this.accountService.accountLoaded .subscribe(account => { this.account = account - this.currentRoute = '/accounts/' + this.account.nameWithHost + '/videos' this.reloadVideos() this.generateSyndicationList() diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index ffe606b43..531d763c4 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts @@ -23,6 +23,10 @@ const accountsRoutes: Routes = [ data: { meta: { title: 'Account videos' + }, + reuse: { + enabled: true, + key: 'account-videos-list' } } }, diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html index fe579ffd7..961ac51d3 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html @@ -1,49 +1,42 @@
No results.
-
-
-
-
- -
- -
- {{ video.name }} -
{{ video.account.displayName }}
-
{{ video.publishedAt | myFromNow }}
-
Privacy: {{ video.privacy.label }}
-
Sensitve: {{ video.nsfw }}
-
+
+
+
+ +
- -
-
- - Cancel - + - - - Unblacklist - -
-
+
+ {{ video.name }} +
{{ video.account.displayName }}
+
{{ video.publishedAt | myFromNow }}
+
Privacy: {{ video.privacy.label }}
+
Sensitive: {{ video.nsfw }}
+
+ + +
+
+ + Cancel + -
- + + + Unblacklist +
-
\ No newline at end of file +
+ +
+
+
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts index b79f574c9..af68d7e2e 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts @@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { Router, ActivatedRoute } from '@angular/router' import { AbstractVideoList } from '@app/shared/video/abstract-video-list' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { Notifier, AuthService } from '@app/core' +import { Notifier, AuthService, ServerService } from '@app/core' import { Video } from '@shared/models' import { VideoBlacklistService } from '@app/shared' import { immutableAssign } from '@app/shared/misc/utils' @@ -17,7 +17,6 @@ import { ScreenService } from '@app/shared/misc/screen.service' }) export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy { titlePage: string - currentRoute = '/admin/moderation/video-auto-blacklist/list' checkedVideos: { [ id: number ]: boolean } = {} pagination: ComponentPagination = { currentPage: 1, @@ -25,18 +24,15 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement totalItems: null } - protected baseVideoWidth = -1 - protected baseVideoHeight = 155 - constructor ( protected router: Router, protected route: ActivatedRoute, - protected i18n: I18n, protected notifier: Notifier, - protected location: Location, protected authService: AuthService, protected screenService: ScreenService, - private videoBlacklistService: VideoBlacklistService, + protected serverService: ServerService, + private i18n: I18n, + private videoBlacklistService: VideoBlacklistService ) { super() @@ -96,5 +92,4 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement error => this.notifier.error(error.message) ) } - } diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html index 2349f02f5..00ee5fbd1 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.html +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html @@ -13,16 +13,14 @@
You don't have videos history yet.
-
-
-
- +
+
+ -
- {{ video.name }} - {{ video.views | myNumberFormatter }} views - -
+
+ {{ video.name }} + {{ video.views | myNumberFormatter }} views +
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts index 394091bad..73340d21a 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ b/client/src/app/+my-account/my-account-history/my-account-history.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 { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { AuthService } from '../../core/auth' @@ -11,7 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' import { UserHistoryService } from '@app/shared/users/user-history.service' import { UserService } from '@app/shared' -import { Notifier } from '@app/core' +import { Notifier, ServerService } from '@app/core' @Component({ selector: 'my-account-history', @@ -20,7 +19,6 @@ import { Notifier } from '@app/core' }) export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { titlePage: string - currentRoute = '/my-account/history/videos' pagination: ComponentPagination = { currentPage: 1, itemsPerPage: 5, @@ -28,16 +26,13 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn } videosHistoryEnabled: boolean - protected baseVideoWidth = -1 - protected baseVideoHeight = 155 - constructor ( protected router: Router, + protected serverService: ServerService, protected route: ActivatedRoute, protected authService: AuthService, protected userService: UserService, protected notifier: Notifier, - protected location: Location, protected screenService: ScreenService, protected i18n: I18n, private confirmService: ConfirmService, diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 07557a029..018d6f996 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -118,6 +118,10 @@ const myAccountRoutes: Routes = [ data: { meta: { title: 'Account videos' + }, + reuse: { + enabled: true, + key: 'my-account-videos-list' } } }, @@ -172,6 +176,10 @@ const myAccountRoutes: Routes = [ data: { meta: { title: 'Videos history' + }, + reuse: { + enabled: true, + key: 'my-videos-history-list' } } }, diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index b09e845ac..1f3ac0005 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html @@ -1,54 +1,47 @@
No results.
-
-
-
-
- -
+
+
+
+ +
- + -
- {{ video.name }} - {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views -
{{ video.privacy.label }}{{ getStateLabel(video) }}
-
- Blacklisted - {{ video.blacklistedReason }} -
+
+ {{ video.name }} + {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views +
{{ video.privacy.label }}{{ getStateLabel(video) }}
+
+ Blacklisted + {{ video.blacklistedReason }}
+
- -
-
- - Cancel - - - - - 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 } - videoById (index: number, video: Video) { - return video.id + enabledForReuse () { + this.disabled = false } - onNearOfTop () { - this.previousPage() + videoById (index: number, video: Video) { + return video.id } onNearOfBottom () { - if (this.hasMoreVideos()) { - this.nextPage() - } - } + if (this.disabled) return - onPageChanged (page: number) { - this.pagination.currentPage = page - this.setNewRouteParams() - } + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return - reloadVideos () { - this.loadedPages = {} - this.loadMoreVideos(this.pagination.currentPage) - } - - loadMoreVideos (page: number, loadOnTop = false) { - this.adjustVideoPageHeight() + this.pagination.currentPage += 1 - const currentY = window.scrollY + this.setScrollRouteParams() - if (this.loadedPages[page] !== undefined) return - if (this.loadingPage[page] === true) return + this.loadMoreVideos() + } - 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) - } - ) - } - - 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) - } + error => this.notifier.error(error.message) + ) } - protected nextPage () { - this.loadMoreVideos(this.maxPageLoaded() + 1) + reloadVideos () { + this.pagination.currentPage = 1 + this.videos = [] + this.loadMoreVideos() } - 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) + toggleModerationDisplay () { + throw new Error('toggleModerationDisplay is not implemented') } 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 - - 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 } + } - // Rebuild pages because maybe we modified the number of items per page - const videos = [].concat(...this.videoPages) - this.loadedPages = {} + private setScrollRouteParams () { + // Already set + if (this.angularState) return - 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++ - } + this.angularState = 42 - // Re fetch the last page - if (videos.length !== 0) { - this.loadMoreVideos(i) - } else { - this.buildVideoPages() + const queryParams = { + 'a-state': this.angularState, + categoryOneOf: this.categoryOneOf } - console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage) + let path = this.router.url + if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute + + 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' } } }, -- cgit v1.2.3