Restore videos list components
This commit is contained in:
parent
7ccddd7b52
commit
489290b8b1
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
|
@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-videos',
|
selector: 'my-account-videos',
|
||||||
|
@ -25,7 +24,6 @@ import { Notifier } from '@app/core'
|
||||||
export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
marginContent = false // Disable margin
|
marginContent = false // Disable margin
|
||||||
currentRoute = '/accounts/videos'
|
|
||||||
loadOnInit = false
|
loadOnInit = false
|
||||||
|
|
||||||
private account: Account
|
private account: Account
|
||||||
|
@ -33,13 +31,13 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected confirmService: ConfirmService,
|
protected confirmService: ConfirmService,
|
||||||
protected location: Location,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected i18n: I18n,
|
private i18n: I18n,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
|
@ -55,7 +53,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
||||||
this.accountSub = this.accountService.accountLoaded
|
this.accountSub = this.accountService.accountLoaded
|
||||||
.subscribe(account => {
|
.subscribe(account => {
|
||||||
this.account = account
|
this.account = account
|
||||||
this.currentRoute = '/accounts/' + this.account.nameWithHost + '/videos'
|
|
||||||
|
|
||||||
this.reloadVideos()
|
this.reloadVideos()
|
||||||
this.generateSyndicationList()
|
this.generateSyndicationList()
|
||||||
|
|
|
@ -23,6 +23,10 @@ const accountsRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Account videos'
|
title: 'Account videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'account-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,49 +1,42 @@
|
||||||
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
<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">
|
<div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="videos">
|
||||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
<div class="video" *ngFor="let video of videos; let i = index">
|
||||||
<div>{{ video.account.displayName }}</div>
|
<div class="checkbox-container">
|
||||||
<div>{{ video.publishedAt | myFromNow }}</div>
|
<my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
|
||||||
<div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
|
</div>
|
||||||
<div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Display only once -->
|
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||||
<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-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
|
<div class="video-info">
|
||||||
<my-global-icon iconName="tick"></my-global-icon>
|
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||||
<ng-container i18n>Unblacklist</ng-container>
|
<div>{{ video.account.displayName }}</div>
|
||||||
</span>
|
<div>{{ video.publishedAt | myFromNow }}</div>
|
||||||
</div>
|
<div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
|
||||||
</div>
|
<div><span i18n>Sensitive: </span><span> {{ video.nsfw }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
<!-- Display only once -->
|
||||||
<my-button
|
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
|
||||||
i18n-label
|
<div class="action-selection-mode-child">
|
||||||
label="Unblacklist"
|
<span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
|
||||||
icon="tick"
|
Cancel
|
||||||
(click)="removeVideoFromBlacklist(video)"
|
</span>
|
||||||
></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>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
||||||
|
<my-button
|
||||||
|
i18n-label
|
||||||
|
label="Unblacklist"
|
||||||
|
icon="tick"
|
||||||
|
(click)="removeVideoFromBlacklist(video)"
|
||||||
|
></my-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Router, ActivatedRoute } from '@angular/router'
|
import { Router, ActivatedRoute } from '@angular/router'
|
||||||
import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
|
import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
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 { Video } from '@shared/models'
|
||||||
import { VideoBlacklistService } from '@app/shared'
|
import { VideoBlacklistService } from '@app/shared'
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
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 {
|
export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/admin/moderation/video-auto-blacklist/list'
|
|
||||||
checkedVideos: { [ id: number ]: boolean } = {}
|
checkedVideos: { [ id: number ]: boolean } = {}
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
@ -25,18 +24,15 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement
|
||||||
totalItems: null
|
totalItems: null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected baseVideoWidth = -1
|
|
||||||
protected baseVideoHeight = 155
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected i18n: I18n,
|
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected location: Location,
|
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
private videoBlacklistService: VideoBlacklistService,
|
protected serverService: ServerService,
|
||||||
|
private i18n: I18n,
|
||||||
|
private videoBlacklistService: VideoBlacklistService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
@ -96,5 +92,4 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement
|
||||||
error => this.notifier.error(error.message)
|
error => this.notifier.error(error.message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,14 @@
|
||||||
|
|
||||||
<div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
|
<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 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
|
||||||
<div *ngFor="let videos of videoPages;" class="videos-page">
|
<div class="video" *ngFor="let video of videos">
|
||||||
<div class="video" *ngFor="let video of videos">
|
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||||
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
|
||||||
|
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
<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>
|
<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>
|
<a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
import { AuthService } from '../../core/auth'
|
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 { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { UserHistoryService } from '@app/shared/users/user-history.service'
|
import { UserHistoryService } from '@app/shared/users/user-history.service'
|
||||||
import { UserService } from '@app/shared'
|
import { UserService } from '@app/shared'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-history',
|
selector: 'my-account-history',
|
||||||
|
@ -20,7 +19,6 @@ import { Notifier } from '@app/core'
|
||||||
})
|
})
|
||||||
export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/my-account/history/videos'
|
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 5,
|
itemsPerPage: 5,
|
||||||
|
@ -28,16 +26,13 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn
|
||||||
}
|
}
|
||||||
videosHistoryEnabled: boolean
|
videosHistoryEnabled: boolean
|
||||||
|
|
||||||
protected baseVideoWidth = -1
|
|
||||||
protected baseVideoHeight = 155
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected location: Location,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected i18n: I18n,
|
protected i18n: I18n,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
|
|
|
@ -118,6 +118,10 @@ const myAccountRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Account videos'
|
title: 'Account videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'my-account-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -172,6 +176,10 @@ const myAccountRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Videos history'
|
title: 'Videos history'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'my-videos-history-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,54 +1,47 @@
|
||||||
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
<div i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
||||||
|
|
||||||
<div
|
<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
|
||||||
myInfiniteScroller
|
<div class="video" *ngFor="let video of videos; let i = index">
|
||||||
[pageHeight]="pageHeight"
|
<div class="checkbox-container">
|
||||||
(nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
|
<my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
|
||||||
class="videos" #videosElement
|
</div>
|
||||||
>
|
|
||||||
<div *ngFor="let videos of videoPages; let i = index" class="videos-page">
|
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||||
<div class="video" *ngFor="let video of videos; let j = index">
|
|
||||||
<div class="checkbox-container">
|
<div class="video-info">
|
||||||
<my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
<!-- 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>
|
||||||
|
|
||||||
<div class="video-info">
|
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
||||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
<my-global-icon iconName="delete"></my-global-icon>
|
||||||
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
|
<ng-container i18n>Delete</ng-container>
|
||||||
<div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
|
</span>
|
||||||
<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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Display only once -->
|
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
|
||||||
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
|
<my-delete-button (click)="deleteVideo(video)"></my-delete-button>
|
||||||
<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-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
|
||||||
<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-button i18n-label label="Change ownership"
|
||||||
<my-delete-button (click)="deleteVideo(video)"></my-delete-button>
|
className="action-button-change-ownership"
|
||||||
|
icon="im-with-her"
|
||||||
<my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
|
(click)="changeOwnership($event, video)"
|
||||||
|
></my-button>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { from as observableFrom, Observable } from 'rxjs'
|
import { concat, Observable } from 'rxjs'
|
||||||
import { concatAll, tap } from 'rxjs/operators'
|
import { tap, toArray } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit, Inject, LOCALE_ID, ViewChild } from '@angular/core'
|
import { Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
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 { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
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' ]
|
styleUrls: [ './my-account-videos.component.scss' ]
|
||||||
})
|
})
|
||||||
export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
|
@ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
||||||
|
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/my-account/videos'
|
|
||||||
checkedVideos: { [ id: number ]: boolean } = {}
|
checkedVideos: { [ id: number ]: boolean } = {}
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
@ -31,19 +31,14 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
||||||
totalItems: null
|
totalItems: null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected baseVideoWidth = -1
|
|
||||||
protected baseVideoHeight = 155
|
|
||||||
|
|
||||||
@ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected location: Location,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected i18n: I18n,
|
private i18n: I18n,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
@Inject(LOCALE_ID) private localeId: string
|
@Inject(LOCALE_ID) private localeId: string
|
||||||
|
@ -93,19 +88,18 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
||||||
const observables: Observable<any>[] = []
|
const observables: Observable<any>[] = []
|
||||||
for (const videoId of toDeleteVideosIds) {
|
for (const videoId of toDeleteVideosIds) {
|
||||||
const o = this.videoService.removeVideo(videoId)
|
const o = this.videoService.removeVideo(videoId)
|
||||||
.pipe(tap(() => this.spliceVideosById(videoId)))
|
.pipe(tap(() => this.removeVideoFromArray(videoId)))
|
||||||
|
|
||||||
observables.push(o)
|
observables.push(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
observableFrom(observables)
|
concat(...observables)
|
||||||
.pipe(concatAll())
|
.pipe(toArray())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
() => {
|
||||||
this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }))
|
this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }))
|
||||||
|
|
||||||
this.abortSelectionMode()
|
this.abortSelectionMode()
|
||||||
this.reloadVideos()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
@ -156,20 +150,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
||||||
return ' - ' + suffix
|
return ' - ' + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildVideoHeight () {
|
private removeVideoFromArray (id: number) {
|
||||||
// In account videos, the video height is fixed
|
this.videos = this.videos.filter(v => v.id !== id)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
|
@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-channel-videos',
|
selector: 'my-video-channel-videos',
|
||||||
|
@ -25,7 +24,6 @@ import { Notifier } from '@app/core'
|
||||||
export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
marginContent = false // Disable margin
|
marginContent = false // Disable margin
|
||||||
currentRoute = '/video-channels/videos'
|
|
||||||
loadOnInit = false
|
loadOnInit = false
|
||||||
|
|
||||||
private videoChannel: VideoChannel
|
private videoChannel: VideoChannel
|
||||||
|
@ -33,13 +31,13 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected confirmService: ConfirmService,
|
protected confirmService: ConfirmService,
|
||||||
protected location: Location,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected i18n: I18n,
|
private i18n: I18n,
|
||||||
private videoChannelService: VideoChannelService,
|
private videoChannelService: VideoChannelService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
|
@ -55,7 +53,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
||||||
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
|
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
|
||||||
.subscribe(videoChannel => {
|
.subscribe(videoChannel => {
|
||||||
this.videoChannel = videoChannel
|
this.videoChannel = videoChannel
|
||||||
this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
|
|
||||||
|
|
||||||
this.reloadVideos()
|
this.reloadVideos()
|
||||||
this.generateSyndicationList()
|
this.generateSyndicationList()
|
||||||
|
|
|
@ -23,6 +23,10 @@ const videoChannelsRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Video channel videos'
|
title: 'Video channel videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'video-channel-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
|
||||||
|
|
||||||
import { PreloadSelectedModulesList } from './core'
|
import { PreloadSelectedModulesList } from './core'
|
||||||
import { AppComponent } from '@app/app.component'
|
import { AppComponent } from '@app/app.component'
|
||||||
|
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -43,12 +44,14 @@ const routes: Routes = [
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot(routes, {
|
RouterModule.forRoot(routes, {
|
||||||
useHash: Boolean(history.pushState) === false,
|
useHash: Boolean(history.pushState) === false,
|
||||||
|
scrollPositionRestoration: 'disabled',
|
||||||
preloadingStrategy: PreloadSelectedModulesList,
|
preloadingStrategy: PreloadSelectedModulesList,
|
||||||
anchorScrolling: 'enabled'
|
anchorScrolling: 'disabled'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PreloadSelectedModulesList
|
PreloadSelectedModulesList,
|
||||||
|
{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }
|
||||||
],
|
],
|
||||||
exports: [ RouterModule ]
|
exports: [ RouterModule ]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
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 { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
|
||||||
import { is18nPath } from '../../../shared/models/i18n'
|
import { is18nPath } from '../../../shared/models/i18n'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { skip, debounceTime } from 'rxjs/operators'
|
import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
|
||||||
import { HotkeysService, Hotkey } from 'angular2-hotkeys'
|
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { fromEvent } from 'rxjs'
|
import { fromEvent } from 'rxjs'
|
||||||
|
import { ViewportScroller } from '@angular/common'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
|
@ -22,6 +23,7 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private i18n: I18n,
|
private i18n: I18n,
|
||||||
|
private viewportScroller: ViewportScroller,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
|
@ -52,15 +54,6 @@ export class AppComponent implements OnInit {
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
document.getElementById('incompatible-browser').className += ' browser-ok'
|
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()
|
this.authService.loadClientCredentials()
|
||||||
|
|
||||||
if (this.isUserLoggedIn()) {
|
if (this.isUserLoggedIn()) {
|
||||||
|
@ -81,15 +74,94 @@ export class AppComponent implements OnInit {
|
||||||
this.isMenuDisplayed = false
|
this.isMenuDisplayed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.events.subscribe(
|
this.initRouteEvents()
|
||||||
e => {
|
this.injectJS()
|
||||||
// User clicked on a link in the menu, change the page
|
this.injectCSS()
|
||||||
if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
|
|
||||||
this.isMenuDisplayed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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
|
// Inject JS
|
||||||
this.serverService.configLoaded
|
this.serverService.configLoaded
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
@ -104,7 +176,9 @@ export class AppComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private injectCSS () {
|
||||||
// Inject CSS if modified (admin config settings)
|
// Inject CSS if modified (admin config settings)
|
||||||
this.serverService.configLoaded
|
this.serverService.configLoaded
|
||||||
.pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server
|
.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)
|
this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private initHotkeys () {
|
||||||
this.hotkeysService.add([
|
this.hotkeysService.add([
|
||||||
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
|
new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
|
||||||
document.getElementById('search-video').focus()
|
document.getElementById('search-video').focus()
|
||||||
|
@ -155,22 +231,5 @@ export class AppComponent implements OnInit {
|
||||||
return false
|
return false
|
||||||
}, undefined, this.i18n('Toggle Dark theme'))
|
}, 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
81
client/src/app/core/routing/custom-reuse-strategy.ts
Normal file
81
client/src/app/core/routing/custom-reuse-strategy.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
}
|
7
client/src/app/core/routing/disable-for-reuse-hook.ts
Normal file
7
client/src/app/core/routing/disable-for-reuse-hook.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export interface DisableForReuseHook {
|
||||||
|
|
||||||
|
disableForReuse (): void
|
||||||
|
|
||||||
|
enabledForReuse (): void
|
||||||
|
|
||||||
|
}
|
|
@ -19,13 +19,10 @@
|
||||||
|
|
||||||
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
<div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
|
||||||
<div
|
<div
|
||||||
myInfiniteScroller
|
myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
|
||||||
[pageHeight]="pageHeight" [firstLoadedPage]="firstLoadedPage"
|
class="videos"
|
||||||
(nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
|
|
||||||
class="videos" #videosElement
|
|
||||||
>
|
>
|
||||||
<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 *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"></my-video-miniature>
|
</my-video-miniature>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,66 +1,52 @@
|
||||||
import { debounceTime } from 'rxjs/operators'
|
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 { 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 { fromEvent, Observable, Subscription } from 'rxjs'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { ComponentPagination } from '../rest/component-pagination.model'
|
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||||
import { VideoSortField } from './sort-field.type'
|
import { VideoSortField } from './sort-field.type'
|
||||||
import { Video } from './video.model'
|
import { Video } from './video.model'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
|
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
|
||||||
import { Syndication } from '@app/shared/video/syndication.model'
|
import { Syndication } from '@app/shared/video/syndication.model'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
|
||||||
export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
|
||||||
private static LINES_PER_PAGE = 4
|
|
||||||
|
|
||||||
@ViewChild('videosElement') videosElement: ElementRef
|
|
||||||
@ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
|
|
||||||
|
|
||||||
|
export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 10,
|
itemsPerPage: 25,
|
||||||
totalItems: null
|
totalItems: null
|
||||||
}
|
}
|
||||||
sort: VideoSortField = '-publishedAt'
|
sort: VideoSortField = '-publishedAt'
|
||||||
|
|
||||||
categoryOneOf?: number
|
categoryOneOf?: number
|
||||||
defaultSort: VideoSortField = '-publishedAt'
|
defaultSort: VideoSortField = '-publishedAt'
|
||||||
|
|
||||||
syndicationItems: Syndication[] = []
|
syndicationItems: Syndication[] = []
|
||||||
|
|
||||||
loadOnInit = true
|
loadOnInit = true
|
||||||
marginContent = true
|
marginContent = true
|
||||||
pageHeight: number
|
videos: Video[] = []
|
||||||
videoWidth: number
|
|
||||||
videoHeight: number
|
|
||||||
videoPages: Video[][] = []
|
|
||||||
ownerDisplayType: OwnerDisplayType = 'account'
|
ownerDisplayType: OwnerDisplayType = 'account'
|
||||||
firstLoadedPage: number
|
|
||||||
displayModerationBlock = false
|
displayModerationBlock = false
|
||||||
titleTooltip: string
|
titleTooltip: string
|
||||||
|
|
||||||
protected baseVideoWidth = 238
|
disabled = false
|
||||||
protected baseVideoHeight = 225
|
|
||||||
|
|
||||||
protected abstract notifier: Notifier
|
protected abstract notifier: Notifier
|
||||||
protected abstract authService: AuthService
|
protected abstract authService: AuthService
|
||||||
protected abstract router: Router
|
|
||||||
protected abstract route: ActivatedRoute
|
protected abstract route: ActivatedRoute
|
||||||
|
protected abstract serverService: ServerService
|
||||||
protected abstract screenService: ScreenService
|
protected abstract screenService: ScreenService
|
||||||
protected abstract i18n: I18n
|
protected abstract router: Router
|
||||||
protected abstract location: Location
|
|
||||||
protected abstract currentRoute: string
|
|
||||||
abstract titlePage: string
|
abstract titlePage: string
|
||||||
|
|
||||||
protected loadedPages: { [ id: number ]: Video[] } = {}
|
|
||||||
protected loadingPage: { [ id: number ]: boolean } = {}
|
|
||||||
protected otherRouteParams = {}
|
|
||||||
|
|
||||||
private resizeSubscription: Subscription
|
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
|
abstract generateSyndicationList (): void
|
||||||
|
|
||||||
get user () {
|
get user () {
|
||||||
|
@ -77,207 +63,87 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
||||||
.subscribe(() => this.calcPageSizes())
|
.subscribe(() => this.calcPageSizes())
|
||||||
|
|
||||||
this.calcPageSizes()
|
this.calcPageSizes()
|
||||||
if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
|
if (this.loadOnInit === true) this.loadMoreVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
|
if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
pageByVideoId (index: number, page: Video[]) {
|
disableForReuse () {
|
||||||
// Video are unique in all pages
|
this.disabled = true
|
||||||
return page.length !== 0 ? page[0].id : 0
|
}
|
||||||
|
|
||||||
|
enabledForReuse () {
|
||||||
|
this.disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
videoById (index: number, video: Video) {
|
videoById (index: number, video: Video) {
|
||||||
return video.id
|
return video.id
|
||||||
}
|
}
|
||||||
|
|
||||||
onNearOfTop () {
|
|
||||||
this.previousPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
onNearOfBottom () {
|
onNearOfBottom () {
|
||||||
if (this.hasMoreVideos()) {
|
if (this.disabled) return
|
||||||
this.nextPage()
|
|
||||||
}
|
// Last page
|
||||||
|
if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
|
||||||
|
|
||||||
|
this.pagination.currentPage += 1
|
||||||
|
|
||||||
|
this.setScrollRouteParams()
|
||||||
|
|
||||||
|
this.loadMoreVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChanged (page: number) {
|
loadMoreVideos () {
|
||||||
this.pagination.currentPage = page
|
const observable = this.getVideosObservable(this.pagination.currentPage)
|
||||||
this.setNewRouteParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadVideos () {
|
|
||||||
this.loadedPages = {}
|
|
||||||
this.loadMoreVideos(this.pagination.currentPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreVideos (page: number, loadOnTop = false) {
|
|
||||||
this.adjustVideoPageHeight()
|
|
||||||
|
|
||||||
const currentY = window.scrollY
|
|
||||||
|
|
||||||
if (this.loadedPages[page] !== undefined) return
|
|
||||||
if (this.loadingPage[page] === true) return
|
|
||||||
|
|
||||||
this.loadingPage[page] = true
|
|
||||||
const observable = this.getVideosObservable(page)
|
|
||||||
|
|
||||||
observable.subscribe(
|
observable.subscribe(
|
||||||
({ videos, totalVideos }) => {
|
({ 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
|
this.pagination.totalItems = totalVideos
|
||||||
|
this.videos = this.videos.concat(videos)
|
||||||
// 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)
|
|
||||||
},
|
},
|
||||||
error => {
|
|
||||||
this.loadingPage[page] = false
|
error => this.notifier.error(error.message)
|
||||||
this.notifier.error(error.message)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadVideos () {
|
||||||
|
this.pagination.currentPage = 1
|
||||||
|
this.videos = []
|
||||||
|
this.loadMoreVideos()
|
||||||
|
}
|
||||||
|
|
||||||
toggleModerationDisplay () {
|
toggleModerationDisplay () {
|
||||||
throw new Error('toggleModerationDisplay is not implemented')
|
throw new Error('toggleModerationDisplay is not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
protected hasMoreVideos () {
|
|
||||||
// No results
|
|
||||||
if (this.pagination.totalItems === 0) return false
|
|
||||||
|
|
||||||
// Not loaded yet
|
|
||||||
if (!this.pagination.totalItems) return true
|
|
||||||
|
|
||||||
const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
|
|
||||||
return maxPage > this.maxPageLoaded()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected previousPage () {
|
|
||||||
const min = this.minPageLoaded()
|
|
||||||
|
|
||||||
if (min > 1) {
|
|
||||||
this.loadMoreVideos(min - 1, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected nextPage () {
|
|
||||||
this.loadMoreVideos(this.maxPageLoaded() + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected buildRouteParams () {
|
|
||||||
// There is always a sort and a current page
|
|
||||||
const params = {
|
|
||||||
sort: this.sort,
|
|
||||||
page: this.pagination.currentPage
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.assign(params, this.otherRouteParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
|
protected loadRouteParams (routeParams: { [ key: string ]: any }) {
|
||||||
this.sort = routeParams['sort'] as VideoSortField || this.defaultSort
|
this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort
|
||||||
this.categoryOneOf = routeParams['categoryOneOf']
|
this.categoryOneOf = routeParams[ 'categoryOneOf' ]
|
||||||
if (routeParams['page'] !== undefined) {
|
this.angularState = routeParams[ 'a-state' ]
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private calcPageSizes () {
|
private calcPageSizes () {
|
||||||
if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
|
if (this.screenService.isInMobileView()) {
|
||||||
this.pagination.itemsPerPage = 5
|
this.pagination.itemsPerPage = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Video takes all the width
|
private setScrollRouteParams () {
|
||||||
this.videoWidth = -1
|
// Already set
|
||||||
this.videoHeight = this.buildVideoHeight()
|
if (this.angularState) return
|
||||||
this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
|
|
||||||
} else {
|
|
||||||
this.videoWidth = this.baseVideoWidth
|
|
||||||
this.videoHeight = this.baseVideoHeight
|
|
||||||
|
|
||||||
const videosWidth = this.videosElement.nativeElement.offsetWidth
|
this.angularState = 42
|
||||||
this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
|
|
||||||
this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
|
const queryParams = {
|
||||||
|
'a-state': this.angularState,
|
||||||
|
categoryOneOf: this.categoryOneOf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild pages because maybe we modified the number of items per page
|
let path = this.router.url
|
||||||
const videos = [].concat(...this.videoPages)
|
if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute
|
||||||
this.loadedPages = {}
|
|
||||||
|
|
||||||
let i = 1
|
this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' })
|
||||||
// Don't include the last page if it not complete
|
|
||||||
while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop
|
|
||||||
this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re fetch the last page
|
|
||||||
if (videos.length !== 0) {
|
|
||||||
this.loadMoreVideos(i)
|
|
||||||
} else {
|
|
||||||
this.buildVideoPages()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,15 @@ import { fromEvent, Subscription } from 'rxjs'
|
||||||
selector: '[myInfiniteScroller]'
|
selector: '[myInfiniteScroller]'
|
||||||
})
|
})
|
||||||
export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
||||||
@Input() containerHeight: number
|
|
||||||
@Input() pageHeight: number
|
|
||||||
@Input() firstLoadedPage = 1
|
|
||||||
@Input() percentLimit = 70
|
@Input() percentLimit = 70
|
||||||
@Input() autoInit = false
|
@Input() autoInit = false
|
||||||
@Input() onItself = false
|
@Input() onItself = false
|
||||||
|
|
||||||
@Output() nearOfBottom = new EventEmitter<void>()
|
@Output() nearOfBottom = new EventEmitter<void>()
|
||||||
@Output() nearOfTop = new EventEmitter<void>()
|
|
||||||
@Output() pageChanged = new EventEmitter<number>()
|
|
||||||
|
|
||||||
private decimalLimit = 0
|
private decimalLimit = 0
|
||||||
private lastCurrentBottom = -1
|
private lastCurrentBottom = -1
|
||||||
private lastCurrentTop = 0
|
|
||||||
private scrollDownSub: Subscription
|
private scrollDownSub: Subscription
|
||||||
private scrollUpSub: Subscription
|
|
||||||
private pageChangeSub: Subscription
|
|
||||||
private middleScreen: number
|
|
||||||
private container: HTMLElement
|
private container: HTMLElement
|
||||||
|
|
||||||
constructor (private el: ElementRef) {
|
constructor (private el: ElementRef) {
|
||||||
|
@ -36,8 +27,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
|
if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
|
||||||
if (this.scrollUpSub) this.scrollUpSub.unsubscribe()
|
|
||||||
if (this.pageChangeSub) this.pageChangeSub.unsubscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize () {
|
initialize () {
|
||||||
|
@ -45,8 +34,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
|
||||||
this.container = this.el.nativeElement
|
this.container = this.el.nativeElement
|
||||||
}
|
}
|
||||||
|
|
||||||
this.middleScreen = window.innerHeight / 2
|
|
||||||
|
|
||||||
// Emit the last value
|
// Emit the last value
|
||||||
const throttleOptions = { leading: true, trailing: true }
|
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)
|
filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
|
||||||
)
|
)
|
||||||
.subscribe(() => this.nearOfBottom.emit())
|
.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 () {
|
private getScrollInfo () {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||||
import { VideoSortField } from '../../shared/video/sort-field.type'
|
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 { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { UserRight } from '../../../../../shared/models/users'
|
import { UserRight } from '../../../../../shared/models/users'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-local',
|
selector: 'my-videos-local',
|
||||||
|
@ -19,18 +18,17 @@ import { Notifier } from '@app/core'
|
||||||
})
|
})
|
||||||
export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/videos/local'
|
|
||||||
sort = '-publishedAt' as VideoSortField
|
sort = '-publishedAt' as VideoSortField
|
||||||
filter: VideoFilter = 'local'
|
filter: VideoFilter = 'local'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected location: Location,
|
|
||||||
protected i18n: I18n,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
|
private i18n: I18n,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
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 { VideoService } from '../../shared/video/video.service'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-recently-added',
|
selector: 'my-videos-recently-added',
|
||||||
|
@ -17,17 +16,16 @@ import { Notifier } from '@app/core'
|
||||||
})
|
})
|
||||||
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/videos/recently-added'
|
|
||||||
sort: VideoSortField = '-publishedAt'
|
sort: VideoSortField = '-publishedAt'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected location: Location,
|
protected serverService: ServerService,
|
||||||
|
protected router: Router,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected i18n: I18n,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
|
private i18n: I18n,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
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 {
|
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/videos/trending'
|
|
||||||
defaultSort: VideoSortField = '-trending'
|
defaultSort: VideoSortField = '-trending'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected location: Location,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
private serverService: ServerService,
|
private i18n: I18n,
|
||||||
protected i18n: I18n,
|
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { immutableAssign } from '@app/shared/misc/utils'
|
import { immutableAssign } from '@app/shared/misc/utils'
|
||||||
import { Location } from '@angular/common'
|
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||||
import { VideoSortField } from '../../shared/video/sort-field.type'
|
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 { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
|
import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-user-subscriptions',
|
selector: 'my-videos-user-subscriptions',
|
||||||
|
@ -18,18 +17,17 @@ import { Notifier } from '@app/core'
|
||||||
})
|
})
|
||||||
export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
currentRoute = '/videos/subscriptions'
|
|
||||||
sort = '-publishedAt' as VideoSortField
|
sort = '-publishedAt' as VideoSortField
|
||||||
ownerDisplayType: OwnerDisplayType = 'auto'
|
ownerDisplayType: OwnerDisplayType = 'auto'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notifier: Notifier,
|
protected notifier: Notifier,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected location: Location,
|
|
||||||
protected i18n: I18n,
|
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
|
private i18n: I18n,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
|
@ -29,6 +29,10 @@ const videosRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Trending videos'
|
title: 'Trending videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'trending-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -38,6 +42,10 @@ const videosRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Recently added videos'
|
title: 'Recently added videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'recently-added-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -47,6 +55,10 @@ const videosRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Subscriptions'
|
title: 'Subscriptions'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'subscription-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -56,6 +68,10 @@ const videosRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Local videos'
|
title: 'Local videos'
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'local-videos-list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { Op } from 'sequelize'
|
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BeforeDestroy,
|
BeforeDestroy,
|
||||||
|
@ -458,7 +457,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
[Op.lt]: beforeUpdatedAt
|
[Sequelize.Op.lt]: beforeUpdatedAt
|
||||||
},
|
},
|
||||||
videoId
|
videoId
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { Op } from 'sequelize'
|
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||||
|
@ -206,7 +205,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
[Op.lt]: beforeUpdatedAt
|
[Sequelize.Op.lt]: beforeUpdatedAt
|
||||||
},
|
},
|
||||||
videoId
|
videoId
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user