diff options
Diffstat (limited to 'client')
23 files changed, 393 insertions, 455 deletions
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 13b634a01..7535eef08 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { ConfirmService } from '../../core/confirm' | 5 | import { ConfirmService } from '../../core/confirm' |
@@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators' | |||
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { Subscription } from 'rxjs' | 12 | import { Subscription } from 'rxjs' |
14 | import { ScreenService } from '@app/shared/misc/screen.service' | 13 | import { ScreenService } from '@app/shared/misc/screen.service' |
15 | import { Notifier } from '@app/core' | 14 | import { Notifier, ServerService } from '@app/core' |
16 | 15 | ||
17 | @Component({ | 16 | @Component({ |
18 | selector: 'my-account-videos', | 17 | selector: 'my-account-videos', |
@@ -25,7 +24,6 @@ import { Notifier } from '@app/core' | |||
25 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 24 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
26 | titlePage: string | 25 | titlePage: string |
27 | marginContent = false // Disable margin | 26 | marginContent = false // Disable margin |
28 | currentRoute = '/accounts/videos' | ||
29 | loadOnInit = false | 27 | loadOnInit = false |
30 | 28 | ||
31 | private account: Account | 29 | private account: Account |
@@ -33,13 +31,13 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
33 | 31 | ||
34 | constructor ( | 32 | constructor ( |
35 | protected router: Router, | 33 | protected router: Router, |
34 | protected serverService: ServerService, | ||
36 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
37 | protected authService: AuthService, | 36 | protected authService: AuthService, |
38 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
39 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
40 | protected location: Location, | ||
41 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
42 | protected i18n: I18n, | 40 | private i18n: I18n, |
43 | private accountService: AccountService, | 41 | private accountService: AccountService, |
44 | private videoService: VideoService | 42 | private videoService: VideoService |
45 | ) { | 43 | ) { |
@@ -55,7 +53,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
55 | this.accountSub = this.accountService.accountLoaded | 53 | this.accountSub = this.accountService.accountLoaded |
56 | .subscribe(account => { | 54 | .subscribe(account => { |
57 | this.account = account | 55 | this.account = account |
58 | this.currentRoute = '/accounts/' + this.account.nameWithHost + '/videos' | ||
59 | 56 | ||
60 | this.reloadVideos() | 57 | this.reloadVideos() |
61 | this.generateSyndicationList() | 58 | this.generateSyndicationList() |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index ffe606b43..531d763c4 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -23,6 +23,10 @@ const accountsRoutes: Routes = [ | |||
23 | data: { | 23 | data: { |
24 | meta: { | 24 | meta: { |
25 | title: 'Account videos' | 25 | title: 'Account videos' |
26 | }, | ||
27 | reuse: { | ||
28 | enabled: true, | ||
29 | key: 'account-videos-list' | ||
26 | } | 30 | } |
27 | } | 31 | } |
28 | }, | 32 | }, |
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html index fe579ffd7..961ac51d3 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html | |||
@@ -1,49 +1,42 @@ | |||
1 | <div i18n *ngIf="pagination.totalItems === 0">No results.</div> | 1 | <div i18n *ngIf="pagination.totalItems === 0">No results.</div> |
2 | <div | ||
3 | myInfiniteScroller | ||
4 | [pageHeight]="pageHeight" | ||
5 | (nearOfTop)="onNearOfTop()" | ||
6 | (nearOfBottom)="onNearOfBottom()" | ||
7 | (pageChanged)="onPageChanged($event)" | ||
8 | class="videos" #videosElement | ||
9 | > | ||
10 | <div *ngFor="let videos of videoPages; let i = index" class="videos-page"> | ||
11 | <div class="video" *ngFor="let video of videos; let j = index"> | ||
12 | <div class="checkbox-container"> | ||
13 | <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox> | ||
14 | </div> | ||
15 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | ||
16 | 2 | ||
17 | <div class="video-info"> | 3 | <div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="videos"> |
18 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | 4 | <div class="video" *ngFor="let video of videos; let i = index"> |
19 | <div>{{ video.account.displayName }}</div> | 5 | <div class="checkbox-container"> |
20 | <div>{{ video.publishedAt | myFromNow }}</div> | 6 | <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox> |
21 | <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div> | 7 | </div> |
22 | <div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div> | ||
23 | </div> | ||
24 | 8 | ||
25 | <!-- Display only once --> | 9 | <my-video-thumbnail [video]="video"></my-video-thumbnail> |
26 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0"> | ||
27 | <div class="action-selection-mode-child"> | ||
28 | <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> | ||
29 | Cancel | ||
30 | </span> | ||
31 | 10 | ||
32 | <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()"> | 11 | <div class="video-info"> |
33 | <my-global-icon iconName="tick"></my-global-icon> | 12 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> |
34 | <ng-container i18n>Unblacklist</ng-container> | 13 | <div>{{ video.account.displayName }}</div> |
35 | </span> | 14 | <div>{{ video.publishedAt | myFromNow }}</div> |
36 | </div> | 15 | <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div> |
37 | </div> | 16 | <div><span i18n>Sensitive: </span><span> {{ video.nsfw }}</span></div> |
17 | </div> | ||
18 | |||
19 | <!-- Display only once --> | ||
20 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0"> | ||
21 | <div class="action-selection-mode-child"> | ||
22 | <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> | ||
23 | Cancel | ||
24 | </span> | ||
38 | 25 | ||
39 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> | 26 | <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()"> |
40 | <my-button | 27 | <my-global-icon iconName="tick"></my-global-icon> |
41 | i18n-label | 28 | <ng-container i18n>Unblacklist</ng-container> |
42 | label="Unblacklist" | 29 | </span> |
43 | icon="tick" | ||
44 | (click)="removeVideoFromBlacklist(video)" | ||
45 | ></my-button> | ||
46 | </div> | 30 | </div> |
47 | </div> | 31 | </div> |
48 | 32 | ||
49 | </div> \ No newline at end of file | 33 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> |
34 | <my-button | ||
35 | i18n-label | ||
36 | label="Unblacklist" | ||
37 | icon="tick" | ||
38 | (click)="removeVideoFromBlacklist(video)" | ||
39 | ></my-button> | ||
40 | </div> | ||
41 | </div> | ||
42 | </div> | ||
diff --git a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts index b79f574c9..af68d7e2e 100644 --- a/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts | |||
@@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
4 | import { Router, ActivatedRoute } from '@angular/router' | 4 | import { Router, ActivatedRoute } from '@angular/router' |
5 | import { AbstractVideoList } from '@app/shared/video/abstract-video-list' | 5 | import { AbstractVideoList } from '@app/shared/video/abstract-video-list' |
6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
7 | import { Notifier, AuthService } from '@app/core' | 7 | import { Notifier, AuthService, ServerService } from '@app/core' |
8 | import { Video } from '@shared/models' | 8 | import { Video } from '@shared/models' |
9 | import { VideoBlacklistService } from '@app/shared' | 9 | import { VideoBlacklistService } from '@app/shared' |
10 | import { immutableAssign } from '@app/shared/misc/utils' | 10 | import { immutableAssign } from '@app/shared/misc/utils' |
@@ -17,7 +17,6 @@ import { ScreenService } from '@app/shared/misc/screen.service' | |||
17 | }) | 17 | }) |
18 | export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | titlePage: string | 19 | titlePage: string |
20 | currentRoute = '/admin/moderation/video-auto-blacklist/list' | ||
21 | checkedVideos: { [ id: number ]: boolean } = {} | 20 | checkedVideos: { [ id: number ]: boolean } = {} |
22 | pagination: ComponentPagination = { | 21 | pagination: ComponentPagination = { |
23 | currentPage: 1, | 22 | currentPage: 1, |
@@ -25,18 +24,15 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement | |||
25 | totalItems: null | 24 | totalItems: null |
26 | } | 25 | } |
27 | 26 | ||
28 | protected baseVideoWidth = -1 | ||
29 | protected baseVideoHeight = 155 | ||
30 | |||
31 | constructor ( | 27 | constructor ( |
32 | protected router: Router, | 28 | protected router: Router, |
33 | protected route: ActivatedRoute, | 29 | protected route: ActivatedRoute, |
34 | protected i18n: I18n, | ||
35 | protected notifier: Notifier, | 30 | protected notifier: Notifier, |
36 | protected location: Location, | ||
37 | protected authService: AuthService, | 31 | protected authService: AuthService, |
38 | protected screenService: ScreenService, | 32 | protected screenService: ScreenService, |
39 | private videoBlacklistService: VideoBlacklistService, | 33 | protected serverService: ServerService, |
34 | private i18n: I18n, | ||
35 | private videoBlacklistService: VideoBlacklistService | ||
40 | ) { | 36 | ) { |
41 | super() | 37 | super() |
42 | 38 | ||
@@ -96,5 +92,4 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement | |||
96 | error => this.notifier.error(error.message) | 92 | error => this.notifier.error(error.message) |
97 | ) | 93 | ) |
98 | } | 94 | } |
99 | |||
100 | } | 95 | } |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html index 2349f02f5..00ee5fbd1 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.html +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html | |||
@@ -13,16 +13,14 @@ | |||
13 | 13 | ||
14 | <div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div> | 14 | <div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div> |
15 | 15 | ||
16 | <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement> | 16 | <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos"> |
17 | <div *ngFor="let videos of videoPages;" class="videos-page"> | 17 | <div class="video" *ngFor="let video of videos"> |
18 | <div class="video" *ngFor="let video of videos"> | 18 | <my-video-thumbnail [video]="video"></my-video-thumbnail> |
19 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | ||
20 | 19 | ||
21 | <div class="video-info"> | 20 | <div class="video-info"> |
22 | <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | 21 | <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> |
23 | <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span> | 22 | <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span> |
24 | <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> | 23 | <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> |
25 | </div> | ||
26 | </div> | 24 | </div> |
27 | </div> | 25 | </div> |
28 | </div> | 26 | </div> |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts index 394091bad..73340d21a 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.ts +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 4 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
@@ -11,7 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
11 | import { ScreenService } from '@app/shared/misc/screen.service' | 10 | import { ScreenService } from '@app/shared/misc/screen.service' |
12 | import { UserHistoryService } from '@app/shared/users/user-history.service' | 11 | import { UserHistoryService } from '@app/shared/users/user-history.service' |
13 | import { UserService } from '@app/shared' | 12 | import { UserService } from '@app/shared' |
14 | import { Notifier } from '@app/core' | 13 | import { Notifier, ServerService } from '@app/core' |
15 | 14 | ||
16 | @Component({ | 15 | @Component({ |
17 | selector: 'my-account-history', | 16 | selector: 'my-account-history', |
@@ -20,7 +19,6 @@ import { Notifier } from '@app/core' | |||
20 | }) | 19 | }) |
21 | export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { | 20 | export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { |
22 | titlePage: string | 21 | titlePage: string |
23 | currentRoute = '/my-account/history/videos' | ||
24 | pagination: ComponentPagination = { | 22 | pagination: ComponentPagination = { |
25 | currentPage: 1, | 23 | currentPage: 1, |
26 | itemsPerPage: 5, | 24 | itemsPerPage: 5, |
@@ -28,16 +26,13 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn | |||
28 | } | 26 | } |
29 | videosHistoryEnabled: boolean | 27 | videosHistoryEnabled: boolean |
30 | 28 | ||
31 | protected baseVideoWidth = -1 | ||
32 | protected baseVideoHeight = 155 | ||
33 | |||
34 | constructor ( | 29 | constructor ( |
35 | protected router: Router, | 30 | protected router: Router, |
31 | protected serverService: ServerService, | ||
36 | protected route: ActivatedRoute, | 32 | protected route: ActivatedRoute, |
37 | protected authService: AuthService, | 33 | protected authService: AuthService, |
38 | protected userService: UserService, | 34 | protected userService: UserService, |
39 | protected notifier: Notifier, | 35 | protected notifier: Notifier, |
40 | protected location: Location, | ||
41 | protected screenService: ScreenService, | 36 | protected screenService: ScreenService, |
42 | protected i18n: I18n, | 37 | protected i18n: I18n, |
43 | private confirmService: ConfirmService, | 38 | private confirmService: ConfirmService, |
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 07557a029..018d6f996 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -118,6 +118,10 @@ const myAccountRoutes: Routes = [ | |||
118 | data: { | 118 | data: { |
119 | meta: { | 119 | meta: { |
120 | title: 'Account videos' | 120 | title: 'Account videos' |
121 | }, | ||
122 | reuse: { | ||
123 | enabled: true, | ||
124 | key: 'my-account-videos-list' | ||
121 | } | 125 | } |
122 | } | 126 | } |
123 | }, | 127 | }, |
@@ -172,6 +176,10 @@ const myAccountRoutes: Routes = [ | |||
172 | data: { | 176 | data: { |
173 | meta: { | 177 | meta: { |
174 | title: 'Videos history' | 178 | title: 'Videos history' |
179 | }, | ||
180 | reuse: { | ||
181 | enabled: true, | ||
182 | key: 'my-videos-history-list' | ||
175 | } | 183 | } |
176 | } | 184 | } |
177 | }, | 185 | }, |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index b09e845ac..1f3ac0005 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -1,54 +1,47 @@ | |||
1 | <div i18n *ngIf="pagination.totalItems === 0">No results.</div> | 1 | <div i18n *ngIf="pagination.totalItems === 0">No results.</div> |
2 | 2 | ||
3 | <div | 3 | <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos"> |
4 | myInfiniteScroller | 4 | <div class="video" *ngFor="let video of videos; let i = index"> |
5 | [pageHeight]="pageHeight" | 5 | <div class="checkbox-container"> |
6 | (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)" | 6 | <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox> |
7 | class="videos" #videosElement | 7 | </div> |
8 | > | ||
9 | <div *ngFor="let videos of videoPages; let i = index" class="videos-page"> | ||
10 | <div class="video" *ngFor="let video of videos; let j = index"> | ||
11 | <div class="checkbox-container"> | ||
12 | <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox> | ||
13 | </div> | ||
14 | 8 | ||
15 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | 9 | <my-video-thumbnail [video]="video"></my-video-thumbnail> |
16 | 10 | ||
17 | <div class="video-info"> | 11 | <div class="video-info"> |
18 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | 12 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> |
19 | <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | 13 | <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> |
20 | <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div> | 14 | <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div> |
21 | <div *ngIf="video.blacklisted" class="video-info-blacklisted"> | 15 | <div *ngIf="video.blacklisted" class="video-info-blacklisted"> |
22 | <span class="blacklisted-label" i18n>Blacklisted</span> | 16 | <span class="blacklisted-label" i18n>Blacklisted</span> |
23 | <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> | 17 | <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> |
24 | </div> | ||
25 | </div> | 18 | </div> |
19 | </div> | ||
26 | 20 | ||
27 | <!-- Display only once --> | 21 | <!-- Display only once --> |
28 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0"> | 22 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> |
29 | <div class="action-selection-mode-child"> | 23 | <div class="action-selection-mode-child"> |
30 | <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> | 24 | <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> |
31 | Cancel | 25 | Cancel |
32 | </span> | 26 | </span> |
33 | 27 | ||
34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | 28 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> |
35 | <my-global-icon iconName="delete"></my-global-icon> | 29 | <my-global-icon iconName="delete"></my-global-icon> |
36 | <ng-container i18n>Delete</ng-container> | 30 | <ng-container i18n>Delete</ng-container> |
37 | </span> | 31 | </span> |
38 | </div> | ||
39 | </div> | 32 | </div> |
33 | </div> | ||
40 | 34 | ||
41 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> | 35 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> |
42 | <my-delete-button (click)="deleteVideo(video)"></my-delete-button> | 36 | <my-delete-button (click)="deleteVideo(video)"></my-delete-button> |
43 | 37 | ||
44 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> | 38 | <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> |
45 | 39 | ||
46 | <my-button i18n-label label="Change ownership" | 40 | <my-button i18n-label label="Change ownership" |
47 | className="action-button-change-ownership" | 41 | className="action-button-change-ownership" |
48 | icon="im-with-her" | 42 | icon="im-with-her" |
49 | (click)="changeOwnership($event, video)" | 43 | (click)="changeOwnership($event, video)" |
50 | ></my-button> | 44 | ></my-button> |
51 | </div> | ||
52 | </div> | 45 | </div> |
53 | </div> | 46 | </div> |
54 | </div> | 47 | </div> |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 41608f796..eb5096a5e 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import { from as observableFrom, Observable } from 'rxjs' | 1 | import { concat, Observable } from 'rxjs' |
2 | import { concatAll, tap } from 'rxjs/operators' | 2 | import { tap, toArray } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 3 | import { Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { Location } from '@angular/common' | ||
6 | import { immutableAssign } from '@app/shared/misc/utils' | 5 | import { immutableAssign } from '@app/shared/misc/utils' |
7 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
8 | import { Notifier } from '@app/core' | 7 | import { Notifier, ServerService } from '@app/core' |
9 | import { AuthService } from '../../core/auth' | 8 | import { AuthService } from '../../core/auth' |
10 | import { ConfirmService } from '../../core/confirm' | 9 | import { ConfirmService } from '../../core/confirm' |
11 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 10 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -22,8 +21,9 @@ import { VideoChangeOwnershipComponent } from './video-change-ownership/video-ch | |||
22 | styleUrls: [ './my-account-videos.component.scss' ] | 21 | styleUrls: [ './my-account-videos.component.scss' ] |
23 | }) | 22 | }) |
24 | export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 23 | export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
24 | @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent | ||
25 | |||
25 | titlePage: string | 26 | titlePage: string |
26 | currentRoute = '/my-account/videos' | ||
27 | checkedVideos: { [ id: number ]: boolean } = {} | 27 | checkedVideos: { [ id: number ]: boolean } = {} |
28 | pagination: ComponentPagination = { | 28 | pagination: ComponentPagination = { |
29 | currentPage: 1, | 29 | currentPage: 1, |
@@ -31,19 +31,14 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
31 | totalItems: null | 31 | totalItems: null |
32 | } | 32 | } |
33 | 33 | ||
34 | protected baseVideoWidth = -1 | ||
35 | protected baseVideoHeight = 155 | ||
36 | |||
37 | @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent | ||
38 | |||
39 | constructor ( | 34 | constructor ( |
40 | protected router: Router, | 35 | protected router: Router, |
36 | protected serverService: ServerService, | ||
41 | protected route: ActivatedRoute, | 37 | protected route: ActivatedRoute, |
42 | protected authService: AuthService, | 38 | protected authService: AuthService, |
43 | protected notifier: Notifier, | 39 | protected notifier: Notifier, |
44 | protected location: Location, | ||
45 | protected screenService: ScreenService, | 40 | protected screenService: ScreenService, |
46 | protected i18n: I18n, | 41 | private i18n: I18n, |
47 | private confirmService: ConfirmService, | 42 | private confirmService: ConfirmService, |
48 | private videoService: VideoService, | 43 | private videoService: VideoService, |
49 | @Inject(LOCALE_ID) private localeId: string | 44 | @Inject(LOCALE_ID) private localeId: string |
@@ -93,19 +88,18 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
93 | const observables: Observable<any>[] = [] | 88 | const observables: Observable<any>[] = [] |
94 | for (const videoId of toDeleteVideosIds) { | 89 | for (const videoId of toDeleteVideosIds) { |
95 | const o = this.videoService.removeVideo(videoId) | 90 | const o = this.videoService.removeVideo(videoId) |
96 | .pipe(tap(() => this.spliceVideosById(videoId))) | 91 | .pipe(tap(() => this.removeVideoFromArray(videoId))) |
97 | 92 | ||
98 | observables.push(o) | 93 | observables.push(o) |
99 | } | 94 | } |
100 | 95 | ||
101 | observableFrom(observables) | 96 | concat(...observables) |
102 | .pipe(concatAll()) | 97 | .pipe(toArray()) |
103 | .subscribe( | 98 | .subscribe( |
104 | res => { | 99 | () => { |
105 | this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })) | 100 | this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })) |
106 | 101 | ||
107 | this.abortSelectionMode() | 102 | this.abortSelectionMode() |
108 | this.reloadVideos() | ||
109 | }, | 103 | }, |
110 | 104 | ||
111 | err => this.notifier.error(err.message) | 105 | err => this.notifier.error(err.message) |
@@ -156,20 +150,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
156 | return ' - ' + suffix | 150 | return ' - ' + suffix |
157 | } | 151 | } |
158 | 152 | ||
159 | protected buildVideoHeight () { | 153 | private removeVideoFromArray (id: number) { |
160 | // In account videos, the video height is fixed | 154 | this.videos = this.videos.filter(v => v.id !== id) |
161 | return this.baseVideoHeight | ||
162 | } | ||
163 | |||
164 | private spliceVideosById (id: number) { | ||
165 | for (const key of Object.keys(this.loadedPages)) { | ||
166 | const videos: Video[] = this.loadedPages[ key ] | ||
167 | const index = videos.findIndex(v => v.id === id) | ||
168 | |||
169 | if (index !== -1) { | ||
170 | videos.splice(index, 1) | ||
171 | return | ||
172 | } | ||
173 | } | ||
174 | } | 155 | } |
175 | } | 156 | } |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index dea378a6e..8af31000e 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { ConfirmService } from '../../core/confirm' | 5 | import { ConfirmService } from '../../core/confirm' |
@@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators' | |||
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { Subscription } from 'rxjs' | 12 | import { Subscription } from 'rxjs' |
14 | import { ScreenService } from '@app/shared/misc/screen.service' | 13 | import { ScreenService } from '@app/shared/misc/screen.service' |
15 | import { Notifier } from '@app/core' | 14 | import { Notifier, ServerService } from '@app/core' |
16 | 15 | ||
17 | @Component({ | 16 | @Component({ |
18 | selector: 'my-video-channel-videos', | 17 | selector: 'my-video-channel-videos', |
@@ -25,7 +24,6 @@ import { Notifier } from '@app/core' | |||
25 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 24 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
26 | titlePage: string | 25 | titlePage: string |
27 | marginContent = false // Disable margin | 26 | marginContent = false // Disable margin |
28 | currentRoute = '/video-channels/videos' | ||
29 | loadOnInit = false | 27 | loadOnInit = false |
30 | 28 | ||
31 | private videoChannel: VideoChannel | 29 | private videoChannel: VideoChannel |
@@ -33,13 +31,13 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
33 | 31 | ||
34 | constructor ( | 32 | constructor ( |
35 | protected router: Router, | 33 | protected router: Router, |
34 | protected serverService: ServerService, | ||
36 | protected route: ActivatedRoute, | 35 | protected route: ActivatedRoute, |
37 | protected authService: AuthService, | 36 | protected authService: AuthService, |
38 | protected notifier: Notifier, | 37 | protected notifier: Notifier, |
39 | protected confirmService: ConfirmService, | 38 | protected confirmService: ConfirmService, |
40 | protected location: Location, | ||
41 | protected screenService: ScreenService, | 39 | protected screenService: ScreenService, |
42 | protected i18n: I18n, | 40 | private i18n: I18n, |
43 | private videoChannelService: VideoChannelService, | 41 | private videoChannelService: VideoChannelService, |
44 | private videoService: VideoService | 42 | private videoService: VideoService |
45 | ) { | 43 | ) { |
@@ -55,7 +53,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
55 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded | 53 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded |
56 | .subscribe(videoChannel => { | 54 | .subscribe(videoChannel => { |
57 | this.videoChannel = videoChannel | 55 | this.videoChannel = videoChannel |
58 | this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos' | ||
59 | 56 | ||
60 | this.reloadVideos() | 57 | this.reloadVideos() |
61 | this.generateSyndicationList() | 58 | this.generateSyndicationList() |
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index cedd07d39..d4872a0a5 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts | |||
@@ -23,6 +23,10 @@ const videoChannelsRoutes: Routes = [ | |||
23 | data: { | 23 | data: { |
24 | meta: { | 24 | meta: { |
25 | title: 'Video channel videos' | 25 | title: 'Video channel videos' |
26 | }, | ||
27 | reuse: { | ||
28 | enabled: true, | ||
29 | key: 'video-channel-videos-list' | ||
26 | } | 30 | } |
27 | } | 31 | } |
28 | }, | 32 | }, |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index cff37a7d6..db8888dba 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' |
3 | 3 | ||
4 | import { PreloadSelectedModulesList } from './core' | 4 | import { PreloadSelectedModulesList } from './core' |
5 | import { AppComponent } from '@app/app.component' | 5 | import { AppComponent } from '@app/app.component' |
6 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' | ||
6 | 7 | ||
7 | const routes: Routes = [ | 8 | const routes: Routes = [ |
8 | { | 9 | { |
@@ -43,12 +44,14 @@ const routes: Routes = [ | |||
43 | imports: [ | 44 | imports: [ |
44 | RouterModule.forRoot(routes, { | 45 | RouterModule.forRoot(routes, { |
45 | useHash: Boolean(history.pushState) === false, | 46 | useHash: Boolean(history.pushState) === false, |
47 | scrollPositionRestoration: 'disabled', | ||
46 | preloadingStrategy: PreloadSelectedModulesList, | 48 | preloadingStrategy: PreloadSelectedModulesList, |
47 | anchorScrolling: 'enabled' | 49 | anchorScrolling: 'disabled' |
48 | }) | 50 | }) |
49 | ], | 51 | ], |
50 | providers: [ | 52 | providers: [ |
51 | PreloadSelectedModulesList | 53 | PreloadSelectedModulesList, |
54 | { provide: RouteReuseStrategy, useClass: CustomReuseStrategy } | ||
52 | ], | 55 | ], |
53 | exports: [ RouterModule ] | 56 | exports: [ RouterModule ] |
54 | }) | 57 | }) |
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index c5c5a8f66..ad0588b99 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
3 | import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' | 3 | import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' |
4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' |
5 | import { is18nPath } from '../../../shared/models/i18n' | 5 | import { is18nPath } from '../../../shared/models/i18n' |
6 | import { ScreenService } from '@app/shared/misc/screen.service' | 6 | import { ScreenService } from '@app/shared/misc/screen.service' |
7 | import { skip, debounceTime } from 'rxjs/operators' | 7 | import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators' |
8 | import { HotkeysService, Hotkey } from 'angular2-hotkeys' | 8 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { fromEvent } from 'rxjs' | 10 | import { fromEvent } from 'rxjs' |
11 | import { ViewportScroller } from '@angular/common' | ||
11 | 12 | ||
12 | @Component({ | 13 | @Component({ |
13 | selector: 'my-app', | 14 | selector: 'my-app', |
@@ -22,6 +23,7 @@ export class AppComponent implements OnInit { | |||
22 | 23 | ||
23 | constructor ( | 24 | constructor ( |
24 | private i18n: I18n, | 25 | private i18n: I18n, |
26 | private viewportScroller: ViewportScroller, | ||
25 | private router: Router, | 27 | private router: Router, |
26 | private authService: AuthService, | 28 | private authService: AuthService, |
27 | private serverService: ServerService, | 29 | private serverService: ServerService, |
@@ -52,15 +54,6 @@ export class AppComponent implements OnInit { | |||
52 | ngOnInit () { | 54 | ngOnInit () { |
53 | document.getElementById('incompatible-browser').className += ' browser-ok' | 55 | document.getElementById('incompatible-browser').className += ' browser-ok' |
54 | 56 | ||
55 | this.router.events.subscribe(e => { | ||
56 | if (e instanceof NavigationEnd) { | ||
57 | const pathname = window.location.pathname | ||
58 | if (!pathname || pathname === '/' || is18nPath(pathname)) { | ||
59 | this.redirectService.redirectToHomepage(true) | ||
60 | } | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | this.authService.loadClientCredentials() | 57 | this.authService.loadClientCredentials() |
65 | 58 | ||
66 | if (this.isUserLoggedIn()) { | 59 | if (this.isUserLoggedIn()) { |
@@ -81,15 +74,94 @@ export class AppComponent implements OnInit { | |||
81 | this.isMenuDisplayed = false | 74 | this.isMenuDisplayed = false |
82 | } | 75 | } |
83 | 76 | ||
84 | this.router.events.subscribe( | 77 | this.initRouteEvents() |
85 | e => { | 78 | this.injectJS() |
86 | // User clicked on a link in the menu, change the page | 79 | this.injectCSS() |
87 | if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) { | 80 | |
88 | this.isMenuDisplayed = false | 81 | this.initHotkeys() |
89 | } | 82 | |
83 | fromEvent(window, 'resize') | ||
84 | .pipe(debounceTime(200)) | ||
85 | .subscribe(() => this.onResize()) | ||
86 | } | ||
87 | |||
88 | isUserLoggedIn () { | ||
89 | return this.authService.isLoggedIn() | ||
90 | } | ||
91 | |||
92 | toggleMenu () { | ||
93 | this.isMenuDisplayed = !this.isMenuDisplayed | ||
94 | this.isMenuChangedByUser = true | ||
95 | } | ||
96 | |||
97 | onResize () { | ||
98 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser | ||
99 | } | ||
100 | |||
101 | private initRouteEvents () { | ||
102 | let resetScroll = true | ||
103 | const eventsObs = this.router.events | ||
104 | |||
105 | const scrollEvent = eventsObs.pipe(filter((e: Event): e is Scroll => e instanceof Scroll)) | ||
106 | const navigationEndEvent = eventsObs.pipe(filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd)) | ||
107 | |||
108 | scrollEvent.subscribe(e => { | ||
109 | if (e.position) { | ||
110 | return this.viewportScroller.scrollToPosition(e.position) | ||
90 | } | 111 | } |
91 | ) | ||
92 | 112 | ||
113 | if (e.anchor) { | ||
114 | return this.viewportScroller.scrollToAnchor(e.anchor) | ||
115 | } | ||
116 | |||
117 | if (resetScroll) { | ||
118 | return this.viewportScroller.scrollToPosition([ 0, 0 ]) | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | // When we add the a-state parameter, we don't want to alter the scroll | ||
123 | navigationEndEvent.pipe(pairwise()) | ||
124 | .subscribe(([ e1, e2 ]) => { | ||
125 | try { | ||
126 | resetScroll = false | ||
127 | |||
128 | const previousUrl = new URL(window.location.origin + e1.url) | ||
129 | const nextUrl = new URL(window.location.origin + e2.url) | ||
130 | |||
131 | if (previousUrl.pathname !== nextUrl.pathname) { | ||
132 | resetScroll = true | ||
133 | return | ||
134 | } | ||
135 | |||
136 | const nextSearchParams = nextUrl.searchParams | ||
137 | nextSearchParams.delete('a-state') | ||
138 | |||
139 | const previousSearchParams = previousUrl.searchParams | ||
140 | |||
141 | nextSearchParams.sort() | ||
142 | previousSearchParams.sort() | ||
143 | |||
144 | if (nextSearchParams.toString() !== previousSearchParams.toString()) { | ||
145 | resetScroll = true | ||
146 | } | ||
147 | } catch (e) { | ||
148 | console.error('Cannot parse URL to check next scroll.', e) | ||
149 | resetScroll = true | ||
150 | } | ||
151 | }) | ||
152 | |||
153 | navigationEndEvent.pipe( | ||
154 | map(() => window.location.pathname), | ||
155 | filter(pathname => !pathname || pathname === '/' || is18nPath(pathname)) | ||
156 | ).subscribe(() => this.redirectService.redirectToHomepage(true)) | ||
157 | |||
158 | eventsObs.pipe( | ||
159 | filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart), | ||
160 | filter(() => this.screenService.isInSmallView()) | ||
161 | ).subscribe(() => this.isMenuDisplayed = false) // User clicked on a link in the menu, change the page | ||
162 | } | ||
163 | |||
164 | private injectJS () { | ||
93 | // Inject JS | 165 | // Inject JS |
94 | this.serverService.configLoaded | 166 | this.serverService.configLoaded |
95 | .subscribe(() => { | 167 | .subscribe(() => { |
@@ -104,7 +176,9 @@ export class AppComponent implements OnInit { | |||
104 | } | 176 | } |
105 | } | 177 | } |
106 | }) | 178 | }) |
179 | } | ||
107 | 180 | ||
181 | private injectCSS () { | ||
108 | // Inject CSS if modified (admin config settings) | 182 | // Inject CSS if modified (admin config settings) |
109 | this.serverService.configLoaded | 183 | this.serverService.configLoaded |
110 | .pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server | 184 | .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 { | |||
120 | this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) | 194 | this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) |
121 | } | 195 | } |
122 | }) | 196 | }) |
197 | } | ||
123 | 198 | ||
199 | private initHotkeys () { | ||
124 | this.hotkeysService.add([ | 200 | this.hotkeysService.add([ |
125 | new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { | 201 | new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { |
126 | document.getElementById('search-video').focus() | 202 | document.getElementById('search-video').focus() |
@@ -155,22 +231,5 @@ export class AppComponent implements OnInit { | |||
155 | return false | 231 | return false |
156 | }, undefined, this.i18n('Toggle Dark theme')) | 232 | }, undefined, this.i18n('Toggle Dark theme')) |
157 | ]) | 233 | ]) |
158 | |||
159 | fromEvent(window, 'resize') | ||
160 | .pipe(debounceTime(200)) | ||
161 | .subscribe(() => this.onResize()) | ||
162 | } | ||
163 | |||
164 | isUserLoggedIn () { | ||
165 | return this.authService.isLoggedIn() | ||
166 | } | ||
167 | |||
168 | toggleMenu () { | ||
169 | this.isMenuDisplayed = !this.isMenuDisplayed | ||
170 | this.isMenuChangedByUser = true | ||
171 | } | ||
172 | |||
173 | onResize () { | ||
174 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser | ||
175 | } | 234 | } |
176 | } | 235 | } |
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts new file mode 100644 index 000000000..a9f61acec --- /dev/null +++ b/client/src/app/core/routing/custom-reuse-strategy.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router' | ||
2 | |||
3 | export class CustomReuseStrategy implements RouteReuseStrategy { | ||
4 | storedRouteHandles = new Map<string, DetachedRouteHandle>() | ||
5 | recentlyUsed: string | ||
6 | |||
7 | private readonly MAX_SIZE = 2 | ||
8 | |||
9 | // Decides if the route should be stored | ||
10 | shouldDetach (route: ActivatedRouteSnapshot): boolean { | ||
11 | return this.isReuseEnabled(route) | ||
12 | } | ||
13 | |||
14 | // Store the information for the route we're destructing | ||
15 | store (route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { | ||
16 | if (!handle) return | ||
17 | |||
18 | const key = this.generateKey(route) | ||
19 | this.recentlyUsed = key | ||
20 | |||
21 | console.log('Storing component %s to reuse later.', key); | ||
22 | |||
23 | (handle as any).componentRef.instance.disableForReuse() | ||
24 | |||
25 | this.storedRouteHandles.set(key, handle) | ||
26 | |||
27 | this.gb() | ||
28 | } | ||
29 | |||
30 | // Return true if we have a stored route object for the next route | ||
31 | shouldAttach (route: ActivatedRouteSnapshot): boolean { | ||
32 | const key = this.generateKey(route) | ||
33 | return this.isReuseEnabled(route) && this.storedRouteHandles.has(key) | ||
34 | } | ||
35 | |||
36 | // If we returned true in shouldAttach(), now return the actual route data for restoration | ||
37 | retrieve (route: ActivatedRouteSnapshot): DetachedRouteHandle { | ||
38 | if (!this.isReuseEnabled(route)) return undefined | ||
39 | |||
40 | const key = this.generateKey(route) | ||
41 | this.recentlyUsed = key | ||
42 | |||
43 | console.log('Reusing component %s.', key) | ||
44 | |||
45 | const handle = this.storedRouteHandles.get(key) | ||
46 | if (!handle) return handle; | ||
47 | |||
48 | (handle as any).componentRef.instance.enabledForReuse() | ||
49 | |||
50 | return handle | ||
51 | } | ||
52 | |||
53 | // Reuse the route if we're going to and from the same route | ||
54 | shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { | ||
55 | return future.routeConfig === curr.routeConfig | ||
56 | } | ||
57 | |||
58 | private gb () { | ||
59 | if (this.storedRouteHandles.size >= this.MAX_SIZE) { | ||
60 | this.storedRouteHandles.forEach((r, key) => { | ||
61 | if (key === this.recentlyUsed) return | ||
62 | |||
63 | console.log('Removing stored component %s.', key); | ||
64 | |||
65 | (r as any).componentRef.destroy() | ||
66 | this.storedRouteHandles.delete(key) | ||
67 | }) | ||
68 | } | ||
69 | } | ||
70 | |||
71 | private generateKey (route: ActivatedRouteSnapshot) { | ||
72 | const reuse = route.data.reuse | ||
73 | if (!reuse) return undefined | ||
74 | |||
75 | return reuse.key + JSON.stringify(route.queryParams) | ||
76 | } | ||
77 | |||
78 | private isReuseEnabled (route: ActivatedRouteSnapshot) { | ||
79 | return route.data.reuse && route.data.reuse.enabled && route.queryParams['a-state'] | ||
80 | } | ||
81 | } | ||
diff --git a/client/src/app/core/routing/disable-for-reuse-hook.ts b/client/src/app/core/routing/disable-for-reuse-hook.ts new file mode 100644 index 000000000..c5eb5c578 --- /dev/null +++ b/client/src/app/core/routing/disable-for-reuse-hook.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export interface DisableForReuseHook { | ||
2 | |||
3 | disableForReuse (): void | ||
4 | |||
5 | enabledForReuse (): void | ||
6 | |||
7 | } | ||
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 1f97bc389..e134654a3 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -19,13 +19,10 @@ | |||
19 | 19 | ||
20 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> | 20 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> |
21 | <div | 21 | <div |
22 | myInfiniteScroller | 22 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" |
23 | [pageHeight]="pageHeight" [firstLoadedPage]="firstLoadedPage" | 23 | class="videos" |
24 | (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)" | ||
25 | class="videos" #videosElement | ||
26 | > | 24 | > |
27 | <div *ngFor="let videos of videoPages; trackBy: pageByVideoId" class="videos-page"> | 25 | <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"> |
28 | <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"></my-video-miniature> | 26 | </my-video-miniature> |
29 | </div> | ||
30 | </div> | 27 | </div> |
31 | </div> | 28 | </div> |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 2cd5bc393..467f629ea 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -1,66 +1,52 @@ | |||
1 | import { debounceTime } from 'rxjs/operators' | 1 | import { debounceTime } from 'rxjs/operators' |
2 | import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' | 2 | import { OnDestroy, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { Location } from '@angular/common' | ||
5 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | ||
6 | import { fromEvent, Observable, Subscription } from 'rxjs' | 4 | import { fromEvent, Observable, Subscription } from 'rxjs' |
7 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
8 | import { ComponentPagination } from '../rest/component-pagination.model' | 6 | import { ComponentPagination } from '../rest/component-pagination.model' |
9 | import { VideoSortField } from './sort-field.type' | 7 | import { VideoSortField } from './sort-field.type' |
10 | import { Video } from './video.model' | 8 | import { Video } from './video.model' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
12 | import { ScreenService } from '@app/shared/misc/screen.service' | 9 | import { ScreenService } from '@app/shared/misc/screen.service' |
13 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' | 10 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' |
14 | import { Syndication } from '@app/shared/video/syndication.model' | 11 | import { Syndication } from '@app/shared/video/syndication.model' |
15 | import { Notifier } from '@app/core' | 12 | import { Notifier, ServerService } from '@app/core' |
16 | 13 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | |
17 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | ||
18 | private static LINES_PER_PAGE = 4 | ||
19 | |||
20 | @ViewChild('videosElement') videosElement: ElementRef | ||
21 | @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective | ||
22 | 14 | ||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook { | ||
23 | pagination: ComponentPagination = { | 16 | pagination: ComponentPagination = { |
24 | currentPage: 1, | 17 | currentPage: 1, |
25 | itemsPerPage: 10, | 18 | itemsPerPage: 25, |
26 | totalItems: null | 19 | totalItems: null |
27 | } | 20 | } |
28 | sort: VideoSortField = '-publishedAt' | 21 | sort: VideoSortField = '-publishedAt' |
22 | |||
29 | categoryOneOf?: number | 23 | categoryOneOf?: number |
30 | defaultSort: VideoSortField = '-publishedAt' | 24 | defaultSort: VideoSortField = '-publishedAt' |
25 | |||
31 | syndicationItems: Syndication[] = [] | 26 | syndicationItems: Syndication[] = [] |
32 | 27 | ||
33 | loadOnInit = true | 28 | loadOnInit = true |
34 | marginContent = true | 29 | marginContent = true |
35 | pageHeight: number | 30 | videos: Video[] = [] |
36 | videoWidth: number | ||
37 | videoHeight: number | ||
38 | videoPages: Video[][] = [] | ||
39 | ownerDisplayType: OwnerDisplayType = 'account' | 31 | ownerDisplayType: OwnerDisplayType = 'account' |
40 | firstLoadedPage: number | ||
41 | displayModerationBlock = false | 32 | displayModerationBlock = false |
42 | titleTooltip: string | 33 | titleTooltip: string |
43 | 34 | ||
44 | protected baseVideoWidth = 238 | 35 | disabled = false |
45 | protected baseVideoHeight = 225 | ||
46 | 36 | ||
47 | protected abstract notifier: Notifier | 37 | protected abstract notifier: Notifier |
48 | protected abstract authService: AuthService | 38 | protected abstract authService: AuthService |
49 | protected abstract router: Router | ||
50 | protected abstract route: ActivatedRoute | 39 | protected abstract route: ActivatedRoute |
40 | protected abstract serverService: ServerService | ||
51 | protected abstract screenService: ScreenService | 41 | protected abstract screenService: ScreenService |
52 | protected abstract i18n: I18n | 42 | protected abstract router: Router |
53 | protected abstract location: Location | ||
54 | protected abstract currentRoute: string | ||
55 | abstract titlePage: string | 43 | abstract titlePage: string |
56 | 44 | ||
57 | protected loadedPages: { [ id: number ]: Video[] } = {} | ||
58 | protected loadingPage: { [ id: number ]: boolean } = {} | ||
59 | protected otherRouteParams = {} | ||
60 | |||
61 | private resizeSubscription: Subscription | 45 | private resizeSubscription: Subscription |
46 | private angularState: number | ||
47 | |||
48 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }> | ||
62 | 49 | ||
63 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> | ||
64 | abstract generateSyndicationList (): void | 50 | abstract generateSyndicationList (): void |
65 | 51 | ||
66 | get user () { | 52 | get user () { |
@@ -77,207 +63,87 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
77 | .subscribe(() => this.calcPageSizes()) | 63 | .subscribe(() => this.calcPageSizes()) |
78 | 64 | ||
79 | this.calcPageSizes() | 65 | this.calcPageSizes() |
80 | if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage) | 66 | if (this.loadOnInit === true) this.loadMoreVideos() |
81 | } | 67 | } |
82 | 68 | ||
83 | ngOnDestroy () { | 69 | ngOnDestroy () { |
84 | if (this.resizeSubscription) this.resizeSubscription.unsubscribe() | 70 | if (this.resizeSubscription) this.resizeSubscription.unsubscribe() |
85 | } | 71 | } |
86 | 72 | ||
87 | pageByVideoId (index: number, page: Video[]) { | 73 | disableForReuse () { |
88 | // Video are unique in all pages | 74 | this.disabled = true |
89 | return page.length !== 0 ? page[0].id : 0 | ||
90 | } | 75 | } |
91 | 76 | ||
92 | videoById (index: number, video: Video) { | 77 | enabledForReuse () { |
93 | return video.id | 78 | this.disabled = false |
94 | } | 79 | } |
95 | 80 | ||
96 | onNearOfTop () { | 81 | videoById (index: number, video: Video) { |
97 | this.previousPage() | 82 | return video.id |
98 | } | 83 | } |
99 | 84 | ||
100 | onNearOfBottom () { | 85 | onNearOfBottom () { |
101 | if (this.hasMoreVideos()) { | 86 | if (this.disabled) return |
102 | this.nextPage() | ||
103 | } | ||
104 | } | ||
105 | 87 | ||
106 | onPageChanged (page: number) { | 88 | // Last page |
107 | this.pagination.currentPage = page | 89 | if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return |
108 | this.setNewRouteParams() | ||
109 | } | ||
110 | 90 | ||
111 | reloadVideos () { | 91 | this.pagination.currentPage += 1 |
112 | this.loadedPages = {} | ||
113 | this.loadMoreVideos(this.pagination.currentPage) | ||
114 | } | ||
115 | |||
116 | loadMoreVideos (page: number, loadOnTop = false) { | ||
117 | this.adjustVideoPageHeight() | ||
118 | 92 | ||
119 | const currentY = window.scrollY | 93 | this.setScrollRouteParams() |
120 | 94 | ||
121 | if (this.loadedPages[page] !== undefined) return | 95 | this.loadMoreVideos() |
122 | if (this.loadingPage[page] === true) return | 96 | } |
123 | 97 | ||
124 | this.loadingPage[page] = true | 98 | loadMoreVideos () { |
125 | const observable = this.getVideosObservable(page) | 99 | const observable = this.getVideosObservable(this.pagination.currentPage) |
126 | 100 | ||
127 | observable.subscribe( | 101 | observable.subscribe( |
128 | ({ videos, totalVideos }) => { | 102 | ({ videos, totalVideos }) => { |
129 | this.loadingPage[page] = false | ||
130 | |||
131 | if (this.firstLoadedPage === undefined || this.firstLoadedPage > page) this.firstLoadedPage = page | ||
132 | |||
133 | // Paging is too high, return to the first one | ||
134 | if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { | ||
135 | this.pagination.currentPage = 1 | ||
136 | this.setNewRouteParams() | ||
137 | return this.reloadVideos() | ||
138 | } | ||
139 | |||
140 | this.loadedPages[page] = videos | ||
141 | this.buildVideoPages() | ||
142 | this.pagination.totalItems = totalVideos | 103 | this.pagination.totalItems = totalVideos |
143 | 104 | this.videos = this.videos.concat(videos) | |
144 | // Initialize infinite scroller now we loaded the first page | ||
145 | if (Object.keys(this.loadedPages).length === 1) { | ||
146 | // Wait elements creation | ||
147 | setTimeout(() => { | ||
148 | this.infiniteScroller.initialize() | ||
149 | |||
150 | // At our first load, we did not load the first page | ||
151 | // Load the previous page so the user can move on the top (and browser previous pages) | ||
152 | if (this.pagination.currentPage > 1) this.loadMoreVideos(this.pagination.currentPage - 1, true) | ||
153 | }, 500) | ||
154 | } | ||
155 | |||
156 | // Insert elements on the top but keep the scroll in the previous position | ||
157 | if (loadOnTop) setTimeout(() => { window.scrollTo(0, currentY + this.pageHeight) }, 0) | ||
158 | }, | 105 | }, |
159 | error => { | ||
160 | this.loadingPage[page] = false | ||
161 | this.notifier.error(error.message) | ||
162 | } | ||
163 | ) | ||
164 | } | ||
165 | |||
166 | toggleModerationDisplay () { | ||
167 | throw new Error('toggleModerationDisplay is not implemented') | ||
168 | } | ||
169 | 106 | ||
170 | protected hasMoreVideos () { | 107 | error => this.notifier.error(error.message) |
171 | // No results | 108 | ) |
172 | if (this.pagination.totalItems === 0) return false | ||
173 | |||
174 | // Not loaded yet | ||
175 | if (!this.pagination.totalItems) return true | ||
176 | |||
177 | const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage | ||
178 | return maxPage > this.maxPageLoaded() | ||
179 | } | ||
180 | |||
181 | protected previousPage () { | ||
182 | const min = this.minPageLoaded() | ||
183 | |||
184 | if (min > 1) { | ||
185 | this.loadMoreVideos(min - 1, true) | ||
186 | } | ||
187 | } | 109 | } |
188 | 110 | ||
189 | protected nextPage () { | 111 | reloadVideos () { |
190 | this.loadMoreVideos(this.maxPageLoaded() + 1) | 112 | this.pagination.currentPage = 1 |
113 | this.videos = [] | ||
114 | this.loadMoreVideos() | ||
191 | } | 115 | } |
192 | 116 | ||
193 | protected buildRouteParams () { | 117 | toggleModerationDisplay () { |
194 | // There is always a sort and a current page | 118 | throw new Error('toggleModerationDisplay is not implemented') |
195 | const params = { | ||
196 | sort: this.sort, | ||
197 | page: this.pagination.currentPage | ||
198 | } | ||
199 | |||
200 | return Object.assign(params, this.otherRouteParams) | ||
201 | } | 119 | } |
202 | 120 | ||
203 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | 121 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { |
204 | this.sort = routeParams['sort'] as VideoSortField || this.defaultSort | 122 | this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort |
205 | this.categoryOneOf = routeParams['categoryOneOf'] | 123 | this.categoryOneOf = routeParams[ 'categoryOneOf' ] |
206 | if (routeParams['page'] !== undefined) { | 124 | this.angularState = routeParams[ 'a-state' ] |
207 | this.pagination.currentPage = parseInt(routeParams['page'], 10) | ||
208 | } else { | ||
209 | this.pagination.currentPage = 1 | ||
210 | } | ||
211 | } | ||
212 | |||
213 | protected setNewRouteParams () { | ||
214 | const paramsObject = this.buildRouteParams() | ||
215 | |||
216 | const queryParams = Object.keys(paramsObject) | ||
217 | .map(p => p + '=' + paramsObject[p]) | ||
218 | .join('&') | ||
219 | this.location.replaceState(this.currentRoute, queryParams) | ||
220 | } | ||
221 | |||
222 | protected buildVideoPages () { | ||
223 | this.videoPages = Object.values(this.loadedPages) | ||
224 | } | ||
225 | |||
226 | protected adjustVideoPageHeight () { | ||
227 | const numberOfPagesLoaded = Object.keys(this.loadedPages).length | ||
228 | if (!numberOfPagesLoaded) return | ||
229 | |||
230 | this.pageHeight = this.videosElement.nativeElement.offsetHeight / numberOfPagesLoaded | ||
231 | } | ||
232 | |||
233 | protected buildVideoHeight () { | ||
234 | // Same ratios than base width/height | ||
235 | return this.videosElement.nativeElement.offsetWidth * (this.baseVideoHeight / this.baseVideoWidth) | ||
236 | } | ||
237 | |||
238 | private minPageLoaded () { | ||
239 | return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10))) | ||
240 | } | ||
241 | |||
242 | private maxPageLoaded () { | ||
243 | return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10))) | ||
244 | } | 125 | } |
245 | 126 | ||
246 | private calcPageSizes () { | 127 | private calcPageSizes () { |
247 | if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) { | 128 | if (this.screenService.isInMobileView()) { |
248 | this.pagination.itemsPerPage = 5 | 129 | this.pagination.itemsPerPage = 5 |
249 | |||
250 | // Video takes all the width | ||
251 | this.videoWidth = -1 | ||
252 | this.videoHeight = this.buildVideoHeight() | ||
253 | this.pageHeight = this.pagination.itemsPerPage * this.videoHeight | ||
254 | } else { | ||
255 | this.videoWidth = this.baseVideoWidth | ||
256 | this.videoHeight = this.baseVideoHeight | ||
257 | |||
258 | const videosWidth = this.videosElement.nativeElement.offsetWidth | ||
259 | this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE | ||
260 | this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE | ||
261 | } | 130 | } |
131 | } | ||
262 | 132 | ||
263 | // Rebuild pages because maybe we modified the number of items per page | 133 | private setScrollRouteParams () { |
264 | const videos = [].concat(...this.videoPages) | 134 | // Already set |
265 | this.loadedPages = {} | 135 | if (this.angularState) return |
266 | 136 | ||
267 | let i = 1 | 137 | this.angularState = 42 |
268 | // Don't include the last page if it not complete | ||
269 | while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop | ||
270 | this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage) | ||
271 | i++ | ||
272 | } | ||
273 | 138 | ||
274 | // Re fetch the last page | 139 | const queryParams = { |
275 | if (videos.length !== 0) { | 140 | 'a-state': this.angularState, |
276 | this.loadMoreVideos(i) | 141 | categoryOneOf: this.categoryOneOf |
277 | } else { | ||
278 | this.buildVideoPages() | ||
279 | } | 142 | } |
280 | 143 | ||
281 | console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage) | 144 | let path = this.router.url |
145 | if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute | ||
146 | |||
147 | this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' }) | ||
282 | } | 148 | } |
283 | } | 149 | } |
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts index a9e75007c..5f8a1dd6e 100644 --- a/client/src/app/shared/video/infinite-scroller.directive.ts +++ b/client/src/app/shared/video/infinite-scroller.directive.ts | |||
@@ -6,24 +6,15 @@ import { fromEvent, Subscription } from 'rxjs' | |||
6 | selector: '[myInfiniteScroller]' | 6 | selector: '[myInfiniteScroller]' |
7 | }) | 7 | }) |
8 | export class InfiniteScrollerDirective implements OnInit, OnDestroy { | 8 | export class InfiniteScrollerDirective implements OnInit, OnDestroy { |
9 | @Input() containerHeight: number | ||
10 | @Input() pageHeight: number | ||
11 | @Input() firstLoadedPage = 1 | ||
12 | @Input() percentLimit = 70 | 9 | @Input() percentLimit = 70 |
13 | @Input() autoInit = false | 10 | @Input() autoInit = false |
14 | @Input() onItself = false | 11 | @Input() onItself = false |
15 | 12 | ||
16 | @Output() nearOfBottom = new EventEmitter<void>() | 13 | @Output() nearOfBottom = new EventEmitter<void>() |
17 | @Output() nearOfTop = new EventEmitter<void>() | ||
18 | @Output() pageChanged = new EventEmitter<number>() | ||
19 | 14 | ||
20 | private decimalLimit = 0 | 15 | private decimalLimit = 0 |
21 | private lastCurrentBottom = -1 | 16 | private lastCurrentBottom = -1 |
22 | private lastCurrentTop = 0 | ||
23 | private scrollDownSub: Subscription | 17 | private scrollDownSub: Subscription |
24 | private scrollUpSub: Subscription | ||
25 | private pageChangeSub: Subscription | ||
26 | private middleScreen: number | ||
27 | private container: HTMLElement | 18 | private container: HTMLElement |
28 | 19 | ||
29 | constructor (private el: ElementRef) { | 20 | constructor (private el: ElementRef) { |
@@ -36,8 +27,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy { | |||
36 | 27 | ||
37 | ngOnDestroy () { | 28 | ngOnDestroy () { |
38 | if (this.scrollDownSub) this.scrollDownSub.unsubscribe() | 29 | if (this.scrollDownSub) this.scrollDownSub.unsubscribe() |
39 | if (this.scrollUpSub) this.scrollUpSub.unsubscribe() | ||
40 | if (this.pageChangeSub) this.pageChangeSub.unsubscribe() | ||
41 | } | 30 | } |
42 | 31 | ||
43 | initialize () { | 32 | initialize () { |
@@ -45,8 +34,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy { | |||
45 | this.container = this.el.nativeElement | 34 | this.container = this.el.nativeElement |
46 | } | 35 | } |
47 | 36 | ||
48 | this.middleScreen = window.innerHeight / 2 | ||
49 | |||
50 | // Emit the last value | 37 | // Emit the last value |
51 | const throttleOptions = { leading: true, trailing: true } | 38 | const throttleOptions = { leading: true, trailing: true } |
52 | 39 | ||
@@ -72,40 +59,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy { | |||
72 | filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit) | 59 | filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit) |
73 | ) | 60 | ) |
74 | .subscribe(() => this.nearOfBottom.emit()) | 61 | .subscribe(() => this.nearOfBottom.emit()) |
75 | |||
76 | // Scroll up | ||
77 | this.scrollUpSub = scrollObservable | ||
78 | .pipe( | ||
79 | // Check we scroll up | ||
80 | filter(({ current }) => { | ||
81 | const res = this.lastCurrentTop > current | ||
82 | |||
83 | this.lastCurrentTop = current | ||
84 | return res | ||
85 | }), | ||
86 | filter(({ current, maximumScroll }) => { | ||
87 | return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit | ||
88 | }) | ||
89 | ) | ||
90 | .subscribe(() => this.nearOfTop.emit()) | ||
91 | |||
92 | // Page change | ||
93 | this.pageChangeSub = scrollObservable | ||
94 | .pipe( | ||
95 | distinct(), | ||
96 | map(({ current }) => this.calculateCurrentPage(current)), | ||
97 | distinctUntilChanged() | ||
98 | ) | ||
99 | .subscribe(res => this.pageChanged.emit(res)) | ||
100 | } | ||
101 | |||
102 | private calculateCurrentPage (current: number) { | ||
103 | const scrollY = current + this.middleScreen | ||
104 | |||
105 | const page = Math.max(1, Math.ceil(scrollY / this.pageHeight)) | ||
106 | |||
107 | // Offset page | ||
108 | return page + (this.firstLoadedPage - 1) | ||
109 | } | 62 | } |
110 | 63 | ||
111 | private getScrollInfo () { | 64 | private getScrollInfo () { |
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts index c0be4b885..13d4023c2 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
4 | import { Location } from '@angular/common' | ||
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
7 | import { VideoSortField } from '../../shared/video/sort-field.type' | 6 | import { VideoSortField } from '../../shared/video/sort-field.type' |
@@ -10,7 +9,7 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ | |||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { ScreenService } from '@app/shared/misc/screen.service' | 10 | import { ScreenService } from '@app/shared/misc/screen.service' |
12 | import { UserRight } from '../../../../../shared/models/users' | 11 | import { UserRight } from '../../../../../shared/models/users' |
13 | import { Notifier } from '@app/core' | 12 | import { Notifier, ServerService } from '@app/core' |
14 | 13 | ||
15 | @Component({ | 14 | @Component({ |
16 | selector: 'my-videos-local', | 15 | selector: 'my-videos-local', |
@@ -19,18 +18,17 @@ import { Notifier } from '@app/core' | |||
19 | }) | 18 | }) |
20 | export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy { | 19 | export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy { |
21 | titlePage: string | 20 | titlePage: string |
22 | currentRoute = '/videos/local' | ||
23 | sort = '-publishedAt' as VideoSortField | 21 | sort = '-publishedAt' as VideoSortField |
24 | filter: VideoFilter = 'local' | 22 | filter: VideoFilter = 'local' |
25 | 23 | ||
26 | constructor ( | 24 | constructor ( |
27 | protected router: Router, | 25 | protected router: Router, |
26 | protected serverService: ServerService, | ||
28 | protected route: ActivatedRoute, | 27 | protected route: ActivatedRoute, |
29 | protected notifier: Notifier, | 28 | protected notifier: Notifier, |
30 | protected authService: AuthService, | 29 | protected authService: AuthService, |
31 | protected location: Location, | ||
32 | protected i18n: I18n, | ||
33 | protected screenService: ScreenService, | 30 | protected screenService: ScreenService, |
31 | private i18n: I18n, | ||
34 | private videoService: VideoService | 32 | private videoService: VideoService |
35 | ) { | 33 | ) { |
36 | super() | 34 | super() |
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index f99c8abb6..80cef813e 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -8,7 +7,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type' | |||
8 | import { VideoService } from '../../shared/video/video.service' | 7 | import { VideoService } from '../../shared/video/video.service' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { ScreenService } from '@app/shared/misc/screen.service' | 9 | import { ScreenService } from '@app/shared/misc/screen.service' |
11 | import { Notifier } from '@app/core' | 10 | import { Notifier, ServerService } from '@app/core' |
12 | 11 | ||
13 | @Component({ | 12 | @Component({ |
14 | selector: 'my-videos-recently-added', | 13 | selector: 'my-videos-recently-added', |
@@ -17,17 +16,16 @@ import { Notifier } from '@app/core' | |||
17 | }) | 16 | }) |
18 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | titlePage: string | 18 | titlePage: string |
20 | currentRoute = '/videos/recently-added' | ||
21 | sort: VideoSortField = '-publishedAt' | 19 | sort: VideoSortField = '-publishedAt' |
22 | 20 | ||
23 | constructor ( | 21 | constructor ( |
24 | protected router: Router, | ||
25 | protected route: ActivatedRoute, | 22 | protected route: ActivatedRoute, |
26 | protected location: Location, | 23 | protected serverService: ServerService, |
24 | protected router: Router, | ||
27 | protected notifier: Notifier, | 25 | protected notifier: Notifier, |
28 | protected authService: AuthService, | 26 | protected authService: AuthService, |
29 | protected i18n: I18n, | ||
30 | protected screenService: ScreenService, | 27 | protected screenService: ScreenService, |
28 | private i18n: I18n, | ||
31 | private videoService: VideoService | 29 | private videoService: VideoService |
32 | ) { | 30 | ) { |
33 | super() | 31 | super() |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index a66a0f97c..e2ad95bc4 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -17,18 +16,16 @@ import { Notifier, ServerService } from '@app/core' | |||
17 | }) | 16 | }) |
18 | export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | titlePage: string | 18 | titlePage: string |
20 | currentRoute = '/videos/trending' | ||
21 | defaultSort: VideoSortField = '-trending' | 19 | defaultSort: VideoSortField = '-trending' |
22 | 20 | ||
23 | constructor ( | 21 | constructor ( |
24 | protected router: Router, | 22 | protected router: Router, |
23 | protected serverService: ServerService, | ||
25 | protected route: ActivatedRoute, | 24 | protected route: ActivatedRoute, |
26 | protected notifier: Notifier, | 25 | protected notifier: Notifier, |
27 | protected authService: AuthService, | 26 | protected authService: AuthService, |
28 | protected location: Location, | ||
29 | protected screenService: ScreenService, | 27 | protected screenService: ScreenService, |
30 | private serverService: ServerService, | 28 | private i18n: I18n, |
31 | protected i18n: I18n, | ||
32 | private videoService: VideoService | 29 | private videoService: VideoService |
33 | ) { | 30 | ) { |
34 | super() | 31 | super() |
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts index bee828e12..2f0685ccc 100644 --- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
4 | import { Location } from '@angular/common' | ||
5 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 5 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
7 | import { VideoSortField } from '../../shared/video/sort-field.type' | 6 | import { VideoSortField } from '../../shared/video/sort-field.type' |
@@ -9,7 +8,7 @@ import { VideoService } from '../../shared/video/video.service' | |||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { ScreenService } from '@app/shared/misc/screen.service' | 9 | import { ScreenService } from '@app/shared/misc/screen.service' |
11 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' | 10 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' |
12 | import { Notifier } from '@app/core' | 11 | import { Notifier, ServerService } from '@app/core' |
13 | 12 | ||
14 | @Component({ | 13 | @Component({ |
15 | selector: 'my-videos-user-subscriptions', | 14 | selector: 'my-videos-user-subscriptions', |
@@ -18,18 +17,17 @@ import { Notifier } from '@app/core' | |||
18 | }) | 17 | }) |
19 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { |
20 | titlePage: string | 19 | titlePage: string |
21 | currentRoute = '/videos/subscriptions' | ||
22 | sort = '-publishedAt' as VideoSortField | 20 | sort = '-publishedAt' as VideoSortField |
23 | ownerDisplayType: OwnerDisplayType = 'auto' | 21 | ownerDisplayType: OwnerDisplayType = 'auto' |
24 | 22 | ||
25 | constructor ( | 23 | constructor ( |
26 | protected router: Router, | 24 | protected router: Router, |
25 | protected serverService: ServerService, | ||
27 | protected route: ActivatedRoute, | 26 | protected route: ActivatedRoute, |
28 | protected notifier: Notifier, | 27 | protected notifier: Notifier, |
29 | protected authService: AuthService, | 28 | protected authService: AuthService, |
30 | protected location: Location, | ||
31 | protected i18n: I18n, | ||
32 | protected screenService: ScreenService, | 29 | protected screenService: ScreenService, |
30 | private i18n: I18n, | ||
33 | private videoService: VideoService | 31 | private videoService: VideoService |
34 | ) { | 32 | ) { |
35 | super() | 33 | super() |
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts index 69a9232ce..505173a5b 100644 --- a/client/src/app/videos/videos-routing.module.ts +++ b/client/src/app/videos/videos-routing.module.ts | |||
@@ -29,6 +29,10 @@ const videosRoutes: Routes = [ | |||
29 | data: { | 29 | data: { |
30 | meta: { | 30 | meta: { |
31 | title: 'Trending videos' | 31 | title: 'Trending videos' |
32 | }, | ||
33 | reuse: { | ||
34 | enabled: true, | ||
35 | key: 'trending-videos-list' | ||
32 | } | 36 | } |
33 | } | 37 | } |
34 | }, | 38 | }, |
@@ -38,6 +42,10 @@ const videosRoutes: Routes = [ | |||
38 | data: { | 42 | data: { |
39 | meta: { | 43 | meta: { |
40 | title: 'Recently added videos' | 44 | title: 'Recently added videos' |
45 | }, | ||
46 | reuse: { | ||
47 | enabled: true, | ||
48 | key: 'recently-added-videos-list' | ||
41 | } | 49 | } |
42 | } | 50 | } |
43 | }, | 51 | }, |
@@ -47,6 +55,10 @@ const videosRoutes: Routes = [ | |||
47 | data: { | 55 | data: { |
48 | meta: { | 56 | meta: { |
49 | title: 'Subscriptions' | 57 | title: 'Subscriptions' |
58 | }, | ||
59 | reuse: { | ||
60 | enabled: true, | ||
61 | key: 'subscription-videos-list' | ||
50 | } | 62 | } |
51 | } | 63 | } |
52 | }, | 64 | }, |
@@ -56,6 +68,10 @@ const videosRoutes: Routes = [ | |||
56 | data: { | 68 | data: { |
57 | meta: { | 69 | meta: { |
58 | title: 'Local videos' | 70 | title: 'Local videos' |
71 | }, | ||
72 | reuse: { | ||
73 | enabled: true, | ||
74 | key: 'local-videos-list' | ||
59 | } | 75 | } |
60 | } | 76 | } |
61 | }, | 77 | }, |