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'
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',
export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
marginContent = false // Disable margin
- currentRoute = '/accounts/videos'
loadOnInit = false
private account: Account
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
) {
this.accountSub = this.accountService.accountLoaded
.subscribe(account => {
this.account = account
- this.currentRoute = '/accounts/' + this.account.nameWithHost + '/videos'
this.reloadVideos()
this.generateSyndicationList()
data: {
meta: {
title: 'Account videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'account-videos-list'
}
}
},
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
-<div
- myInfiniteScroller
- [pageHeight]="pageHeight"
- (nearOfTop)="onNearOfTop()"
- (nearOfBottom)="onNearOfBottom()"
- (pageChanged)="onPageChanged($event)"
- class="videos" #videosElement
->
- <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
- <div class="video" *ngFor="let video of videos; let j = index">
- <div class="checkbox-container">
- <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
- </div>
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
- <div class="video-info">
- <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
- <div>{{ video.account.displayName }}</div>
- <div>{{ video.publishedAt | myFromNow }}</div>
- <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
- <div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
- </div>
+<div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="videos">
+ <div class="video" *ngFor="let video of videos; let i = index">
+ <div class="checkbox-container">
+ <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
+ </div>
- <!-- Display only once -->
- <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
- <div class="action-selection-mode-child">
- <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
- Cancel
- </span>
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
- <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
- <my-global-icon iconName="tick"></my-global-icon>
- <ng-container i18n>Unblacklist</ng-container>
- </span>
- </div>
- </div>
+ <div class="video-info">
+ <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <div>{{ video.account.displayName }}</div>
+ <div>{{ video.publishedAt | myFromNow }}</div>
+ <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
+ <div><span i18n>Sensitive: </span><span> {{ video.nsfw }}</span></div>
+ </div>
+
+ <!-- Display only once -->
+ <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
+ <div class="action-selection-mode-child">
+ <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+ Cancel
+ </span>
- <div class="video-buttons" *ngIf="isInSelectionMode() === false">
- <my-button
- i18n-label
- label="Unblacklist"
- icon="tick"
- (click)="removeVideoFromBlacklist(video)"
- ></my-button>
+ <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
+ <my-global-icon iconName="tick"></my-global-icon>
+ <ng-container i18n>Unblacklist</ng-container>
+ </span>
</div>
</div>
-</div>
\ No newline at end of file
+ <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+ <my-button
+ i18n-label
+ label="Unblacklist"
+ icon="tick"
+ (click)="removeVideoFromBlacklist(video)"
+ ></my-button>
+ </div>
+ </div>
+</div>
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'
})
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,
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()
error => this.notifier.error(error.message)
)
}
-
}
<div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
-<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
- <div *ngFor="let videos of videoPages;" class="videos-page">
- <div class="video" *ngFor="let video of videos">
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
+<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
+ <div class="video" *ngFor="let video of videos">
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
- <div class="video-info">
- <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
- <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
- <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
- </div>
+ <div class="video-info">
+ <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
+ <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
</div>
</div>
</div>
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'
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',
})
export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
- currentRoute = '/my-account/history/videos'
pagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 5,
}
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,
data: {
meta: {
title: 'Account videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'my-account-videos-list'
}
}
},
data: {
meta: {
title: 'Videos history'
+ },
+ reuse: {
+ enabled: true,
+ key: 'my-videos-history-list'
}
}
},
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
-<div
- myInfiniteScroller
- [pageHeight]="pageHeight"
- (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
- class="videos" #videosElement
->
- <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
- <div class="video" *ngFor="let video of videos; let j = index">
- <div class="checkbox-container">
- <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
- </div>
+<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
+ <div class="video" *ngFor="let video of videos; let i = index">
+ <div class="checkbox-container">
+ <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
+ </div>
- <my-video-thumbnail [video]="video"></my-video-thumbnail>
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
- <div class="video-info">
- <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
- <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
- <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
- <div *ngIf="video.blacklisted" class="video-info-blacklisted">
- <span class="blacklisted-label" i18n>Blacklisted</span>
- <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
- </div>
+ <div class="video-info">
+ <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
+ <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
+ <div *ngIf="video.blacklisted" class="video-info-blacklisted">
+ <span class="blacklisted-label" i18n>Blacklisted</span>
+ <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
</div>
+ </div>
- <!-- Display only once -->
- <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
- <div class="action-selection-mode-child">
- <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
- Cancel
- </span>
-
- <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
- <my-global-icon iconName="delete"></my-global-icon>
- <ng-container i18n>Delete</ng-container>
- </span>
- </div>
+ <!-- Display only once -->
+ <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
+ <div class="action-selection-mode-child">
+ <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+ Cancel
+ </span>
+
+ <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
+ <my-global-icon iconName="delete"></my-global-icon>
+ <ng-container i18n>Delete</ng-container>
+ </span>
</div>
+ </div>
- <div class="video-buttons" *ngIf="isInSelectionMode() === false">
- <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
+ <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+ <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
- <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
+ <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
- <my-button i18n-label label="Change ownership"
- className="action-button-change-ownership"
- icon="im-with-her"
- (click)="changeOwnership($event, video)"
- ></my-button>
- </div>
+ <my-button i18n-label label="Change ownership"
+ className="action-button-change-ownership"
+ icon="im-with-her"
+ (click)="changeOwnership($event, video)"
+ ></my-button>
</div>
</div>
</div>
-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'
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,
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
const observables: Observable<any>[] = []
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)
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)
}
}
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'
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',
export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
marginContent = false // Disable margin
- currentRoute = '/video-channels/videos'
loadOnInit = false
private videoChannel: VideoChannel
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
) {
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.subscribe(videoChannel => {
this.videoChannel = videoChannel
- this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
this.reloadVideos()
this.generateSyndicationList()
data: {
meta: {
title: 'Video channel videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'video-channel-videos-list'
}
}
},
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 = [
{
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 ]
})
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',
constructor (
private i18n: I18n,
+ private viewportScroller: ViewportScroller,
private router: Router,
private authService: AuthService,
private serverService: ServerService,
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()) {
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(() => {
}
}
})
+ }
+ 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
this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
}
})
+ }
+ private initHotkeys () {
this.hotkeysService.add([
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
document.getElementById('search-video').focus()
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
}
}
--- /dev/null
+import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'
+
+export class CustomReuseStrategy implements RouteReuseStrategy {
+ storedRouteHandles = new Map<string, DetachedRouteHandle>()
+ 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']
+ }
+}
--- /dev/null
+export interface DisableForReuseHook {
+
+ disableForReuse (): void
+
+ enabledForReuse (): void
+
+}
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
<div
- myInfiniteScroller
- [pageHeight]="pageHeight" [firstLoadedPage]="firstLoadedPage"
- (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
- class="videos" #videosElement
+ myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
+ class="videos"
>
- <div *ngFor="let videos of videoPages; trackBy: pageByVideoId" class="videos-page">
- <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"></my-video-miniature>
- </div>
+ <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType">
+ </my-video-miniature>
</div>
</div>
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 () {
.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' })
}
}
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<void>()
- @Output() nearOfTop = new EventEmitter<void>()
- @Output() pageChanged = new EventEmitter<number>()
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) {
ngOnDestroy () {
if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
- if (this.scrollUpSub) this.scrollUpSub.unsubscribe()
- if (this.pageChangeSub) this.pageChangeSub.unsubscribe()
}
initialize () {
this.container = this.el.nativeElement
}
- this.middleScreen = window.innerHeight / 2
-
// Emit the last value
const throttleOptions = { leading: true, trailing: true }
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 () {
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'
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',
})
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()
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'
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',
})
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()
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'
})
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()
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'
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',
})
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()
data: {
meta: {
title: 'Trending videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'trending-videos-list'
}
}
},
data: {
meta: {
title: 'Recently added videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'recently-added-videos-list'
}
}
},
data: {
meta: {
title: 'Subscriptions'
+ },
+ reuse: {
+ enabled: true,
+ key: 'subscription-videos-list'
}
}
},
data: {
meta: {
title: 'Local videos'
+ },
+ reuse: {
+ enabled: true,
+ key: 'local-videos-list'
}
}
},
import * as Sequelize from 'sequelize'
-import { Op } from 'sequelize'
import {
AllowNull,
BeforeDestroy,
const query = {
where: {
updatedAt: {
- [Op.lt]: beforeUpdatedAt
+ [Sequelize.Op.lt]: beforeUpdatedAt
},
videoId
}
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'
const query = {
where: {
updatedAt: {
- [Op.lt]: beforeUpdatedAt
+ [Sequelize.Op.lt]: beforeUpdatedAt
},
videoId
}