diff options
author | Chocobozzz <me@florianbigard.com> | 2020-03-11 16:41:38 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2020-03-11 16:45:09 +0100 |
commit | 111fdc267b36201cf1be1fdf91017005102b4a5e (patch) | |
tree | 5ea98f766cc74de3434a855e998e763324e9da72 | |
parent | 764a965778ac89e027fd05dd35697c6763e0dc18 (diff) | |
download | PeerTube-111fdc267b36201cf1be1fdf91017005102b4a5e.tar.gz PeerTube-111fdc267b36201cf1be1fdf91017005102b4a5e.tar.zst PeerTube-111fdc267b36201cf1be1fdf91017005102b4a5e.zip |
Handle overview pagination in client
6 files changed, 135 insertions, 71 deletions
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index e015d0e14..da7832b32 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -263,17 +263,19 @@ export class ServerService { | |||
263 | .pipe(map(data => ({ data, translations }))) | 263 | .pipe(map(data => ({ data, translations }))) |
264 | }), | 264 | }), |
265 | map(({ data, translations }) => { | 265 | map(({ data, translations }) => { |
266 | const hashToPopulate: VideoConstant<T>[] = [] | 266 | const hashToPopulate: VideoConstant<T>[] = Object.keys(data) |
267 | 267 | .map(dataKey => { | |
268 | Object.keys(data) | 268 | const label = data[ dataKey ] |
269 | .forEach(dataKey => { | 269 | |
270 | const label = data[ dataKey ] | 270 | const id = attributeName === 'languages' |
271 | 271 | ? dataKey as T | |
272 | hashToPopulate.push({ | 272 | : parseInt(dataKey, 10) as T |
273 | id: (attributeName === 'languages' ? dataKey : parseInt(dataKey, 10)) as T, | 273 | |
274 | label: peertubeTranslate(label, translations) | 274 | return { |
275 | }) | 275 | id, |
276 | }) | 276 | label: peertubeTranslate(label, translations) |
277 | } | ||
278 | }) | ||
277 | 279 | ||
278 | if (sort === true) sortBy(hashToPopulate, 'label') | 280 | if (sort === true) sortBy(hashToPopulate, 'label') |
279 | 281 | ||
diff --git a/client/src/app/menu/language-chooser.component.ts b/client/src/app/menu/language-chooser.component.ts index fb74cdf19..9bc934ad4 100644 --- a/client/src/app/menu/language-chooser.component.ts +++ b/client/src/app/menu/language-chooser.component.ts | |||
@@ -36,6 +36,7 @@ export class LanguageChooserComponent { | |||
36 | getCurrentLanguage () { | 36 | getCurrentLanguage () { |
37 | const english = 'English' | 37 | const english = 'English' |
38 | const locale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId) | 38 | const locale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId) |
39 | |||
39 | if (locale) return I18N_LOCALES[locale] || english | 40 | if (locale) return I18N_LOCALES[locale] || english |
40 | return english | 41 | return english |
41 | } | 42 | } |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index ce209457c..37702e975 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -23,8 +23,10 @@ export class MenuComponent implements OnInit { | |||
23 | 23 | ||
24 | userHasAdminAccess = false | 24 | userHasAdminAccess = false |
25 | helpVisible = false | 25 | helpVisible = false |
26 | languages: VideoConstant<string>[] = [] | ||
27 | 26 | ||
27 | videoLanguages: string[] = [] | ||
28 | |||
29 | private languages: VideoConstant<string>[] = [] | ||
28 | private serverConfig: ServerConfig | 30 | private serverConfig: ServerConfig |
29 | private routesPerRight: { [ role in UserRight ]?: string } = { | 31 | private routesPerRight: { [ role in UserRight ]?: string } = { |
30 | [UserRight.MANAGE_USERS]: '/admin/users', | 32 | [UserRight.MANAGE_USERS]: '/admin/users', |
@@ -71,30 +73,32 @@ export class MenuComponent implements OnInit { | |||
71 | } | 73 | } |
72 | ) | 74 | ) |
73 | 75 | ||
74 | this.hotkeysService.cheatSheetToggle.subscribe(isOpen => this.helpVisible = isOpen) | 76 | this.hotkeysService.cheatSheetToggle |
77 | .subscribe(isOpen => this.helpVisible = isOpen) | ||
78 | |||
79 | this.serverService.getVideoLanguages() | ||
80 | .subscribe(languages => { | ||
81 | this.languages = languages | ||
75 | 82 | ||
76 | this.serverService.getVideoLanguages().subscribe(languages => this.languages = languages) | 83 | this.authService.userInformationLoaded |
84 | .subscribe(() => this.buildUserLanguages()) | ||
85 | }) | ||
77 | } | 86 | } |
78 | 87 | ||
79 | get language () { | 88 | get language () { |
80 | return this.languageChooserModal.getCurrentLanguage() | 89 | return this.languageChooserModal.getCurrentLanguage() |
81 | } | 90 | } |
82 | 91 | ||
83 | get videoLanguages (): string[] { | ||
84 | if (!this.user) return | ||
85 | if (!this.user.videoLanguages) return [this.i18n('any language')] | ||
86 | return this.user.videoLanguages | ||
87 | .map(locale => this.langForLocale(locale)) | ||
88 | .map(value => value === undefined ? '?' : value) | ||
89 | } | ||
90 | |||
91 | get nsfwPolicy () { | 92 | get nsfwPolicy () { |
92 | if (!this.user) return | 93 | if (!this.user) return |
94 | |||
93 | switch (this.user.nsfwPolicy) { | 95 | switch (this.user.nsfwPolicy) { |
94 | case 'do_not_list': | 96 | case 'do_not_list': |
95 | return this.i18n('hide') | 97 | return this.i18n('hide') |
98 | |||
96 | case 'blur': | 99 | case 'blur': |
97 | return this.i18n('blur') | 100 | return this.i18n('blur') |
101 | |||
98 | case 'display': | 102 | case 'display': |
99 | return this.i18n('display') | 103 | return this.i18n('display') |
100 | } | 104 | } |
@@ -156,13 +160,29 @@ export class MenuComponent implements OnInit { | |||
156 | toggleUseP2P () { | 160 | toggleUseP2P () { |
157 | if (!this.user) return | 161 | if (!this.user) return |
158 | this.user.webTorrentEnabled = !this.user.webTorrentEnabled | 162 | this.user.webTorrentEnabled = !this.user.webTorrentEnabled |
159 | this.userService.updateMyProfile({ | 163 | |
160 | webTorrentEnabled: this.user.webTorrentEnabled | 164 | this.userService.updateMyProfile({ webTorrentEnabled: this.user.webTorrentEnabled }) |
161 | }).subscribe(() => this.authService.refreshUserInformation()) | 165 | .subscribe(() => this.authService.refreshUserInformation()) |
162 | } | 166 | } |
163 | 167 | ||
164 | langForLocale (localeId: string) { | 168 | langForLocale (localeId: string) { |
165 | return this.languages.find(lang => lang.id = localeId).label | 169 | return this.languages.find(lang => lang.id === localeId).label |
170 | } | ||
171 | |||
172 | private buildUserLanguages () { | ||
173 | if (!this.user) { | ||
174 | this.videoLanguages = [] | ||
175 | return | ||
176 | } | ||
177 | |||
178 | if (!this.user.videoLanguages) { | ||
179 | this.videoLanguages = [ this.i18n('any language') ] | ||
180 | return | ||
181 | } | ||
182 | |||
183 | this.videoLanguages = this.user.videoLanguages | ||
184 | .map(locale => this.langForLocale(locale)) | ||
185 | .map(value => value === undefined ? '?' : value) | ||
166 | } | 186 | } |
167 | 187 | ||
168 | private computeIsUserHasAdminAccess () { | 188 | private computeIsUserHasAdminAccess () { |
diff --git a/client/src/app/shared/overview/overview.service.ts b/client/src/app/shared/overview/overview.service.ts index 79cb781f7..6d8af8052 100644 --- a/client/src/app/shared/overview/overview.service.ts +++ b/client/src/app/shared/overview/overview.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { catchError, map, switchMap, tap } from 'rxjs/operators' | 1 | import { catchError, map, switchMap, tap } from 'rxjs/operators' |
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { forkJoin, Observable, of } from 'rxjs' | 4 | import { forkJoin, Observable, of } from 'rxjs' |
5 | import { VideosOverview as VideosOverviewServer, peertubeTranslate } from '../../../../../shared/models' | 5 | import { VideosOverview as VideosOverviewServer, peertubeTranslate } from '../../../../../shared/models' |
@@ -21,9 +21,12 @@ export class OverviewService { | |||
21 | private serverService: ServerService | 21 | private serverService: ServerService |
22 | ) {} | 22 | ) {} |
23 | 23 | ||
24 | getVideosOverview (): Observable<VideosOverview> { | 24 | getVideosOverview (page: number): Observable<VideosOverview> { |
25 | let params = new HttpParams() | ||
26 | params = params.append('page', page + '') | ||
27 | |||
25 | return this.authHttp | 28 | return this.authHttp |
26 | .get<VideosOverviewServer>(OverviewService.BASE_OVERVIEW_URL + 'videos') | 29 | .get<VideosOverviewServer>(OverviewService.BASE_OVERVIEW_URL + 'videos', { params }) |
27 | .pipe( | 30 | .pipe( |
28 | switchMap(serverVideosOverview => this.updateVideosOverview(serverVideosOverview)), | 31 | switchMap(serverVideosOverview => this.updateVideosOverview(serverVideosOverview)), |
29 | catchError(err => this.restExtractor.handleError(err)) | 32 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html index 5fe1f5c80..84999cfb2 100644 --- a/client/src/app/videos/video-list/video-overview.component.html +++ b/client/src/app/videos/video-list/video-overview.component.html | |||
@@ -2,35 +2,44 @@ | |||
2 | 2 | ||
3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> | 3 | <div class="no-results" i18n *ngIf="notResults">No results.</div> |
4 | 4 | ||
5 | <div class="section" *ngFor="let object of overview.categories"> | 5 | <div |
6 | <div class="section-title"> | 6 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" |
7 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> | 7 | > |
8 | </div> | 8 | <ng-container *ngFor="let overview of overviews"> |
9 | 9 | ||
10 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> | 10 | <div class="section" *ngFor="let object of overview.categories"> |
11 | </my-video-miniature> | 11 | <div class="section-title"> |
12 | </div> | 12 | <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> |
13 | </div> | ||
13 | 14 | ||
14 | <div class="section" *ngFor="let object of overview.tags"> | 15 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> |
15 | <div class="section-title"> | 16 | </my-video-miniature> |
16 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> | 17 | </div> |
17 | </div> | ||
18 | 18 | ||
19 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> | 19 | <div class="section" *ngFor="let object of overview.tags"> |
20 | </my-video-miniature> | 20 | <div class="section-title"> |
21 | </div> | 21 | <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> |
22 | </div> | ||
23 | |||
24 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> | ||
25 | </my-video-miniature> | ||
26 | </div> | ||
27 | |||
28 | <div class="section channel" *ngFor="let object of overview.channels"> | ||
29 | <div class="section-title"> | ||
30 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> | ||
31 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> | ||
32 | |||
33 | <div>{{ object.channel.displayName }}</div> | ||
34 | </a> | ||
35 | </div> | ||
22 | 36 | ||
23 | <div class="section channel" *ngFor="let object of overview.channels"> | 37 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> |
24 | <div class="section-title"> | 38 | </my-video-miniature> |
25 | <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> | 39 | </div> |
26 | <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" /> | ||
27 | 40 | ||
28 | <div>{{ object.channel.displayName }}</div> | 41 | </ng-container> |
29 | </a> | ||
30 | </div> | ||
31 | 42 | ||
32 | <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false"> | ||
33 | </my-video-miniature> | ||
34 | </div> | 43 | </div> |
35 | 44 | ||
36 | </div> | 45 | </div> |
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts index 4fee92d54..101073949 100644 --- a/client/src/app/videos/video-list/video-overview.component.ts +++ b/client/src/app/videos/video-list/video-overview.component.ts | |||
@@ -5,6 +5,7 @@ import { VideosOverview } from '@app/shared/overview/videos-overview.model' | |||
5 | import { OverviewService } from '@app/shared/overview' | 5 | import { OverviewService } from '@app/shared/overview' |
6 | import { Video } from '@app/shared/video/video.model' | 6 | import { Video } from '@app/shared/video/video.model' |
7 | import { ScreenService } from '@app/shared/misc/screen.service' | 7 | import { ScreenService } from '@app/shared/misc/screen.service' |
8 | import { Subject } from 'rxjs' | ||
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-video-overview', | 11 | selector: 'my-video-overview', |
@@ -12,13 +13,17 @@ import { ScreenService } from '@app/shared/misc/screen.service' | |||
12 | styleUrls: [ './video-overview.component.scss' ] | 13 | styleUrls: [ './video-overview.component.scss' ] |
13 | }) | 14 | }) |
14 | export class VideoOverviewComponent implements OnInit { | 15 | export class VideoOverviewComponent implements OnInit { |
15 | overview: VideosOverview = { | 16 | onDataSubject = new Subject<any>() |
16 | categories: [], | 17 | |
17 | channels: [], | 18 | overviews: VideosOverview[] = [] |
18 | tags: [] | ||
19 | } | ||
20 | notResults = false | 19 | notResults = false |
21 | 20 | ||
21 | private loaded = false | ||
22 | private currentPage = 1 | ||
23 | private maxPage = 20 | ||
24 | private lastWasEmpty = false | ||
25 | private isLoading = false | ||
26 | |||
22 | constructor ( | 27 | constructor ( |
23 | private i18n: I18n, | 28 | private i18n: I18n, |
24 | private notifier: Notifier, | 29 | private notifier: Notifier, |
@@ -32,20 +37,7 @@ export class VideoOverviewComponent implements OnInit { | |||
32 | } | 37 | } |
33 | 38 | ||
34 | ngOnInit () { | 39 | ngOnInit () { |
35 | this.overviewService.getVideosOverview() | 40 | this.loadMoreResults() |
36 | .subscribe( | ||
37 | overview => { | ||
38 | this.overview = overview | ||
39 | |||
40 | if ( | ||
41 | this.overview.categories.length === 0 && | ||
42 | this.overview.channels.length === 0 && | ||
43 | this.overview.tags.length === 0 | ||
44 | ) this.notResults = true | ||
45 | }, | ||
46 | |||
47 | err => this.notifier.error(err.message) | ||
48 | ) | ||
49 | } | 41 | } |
50 | 42 | ||
51 | buildVideoChannelBy (object: { videos: Video[] }) { | 43 | buildVideoChannelBy (object: { videos: Video[] }) { |
@@ -61,4 +53,41 @@ export class VideoOverviewComponent implements OnInit { | |||
61 | 53 | ||
62 | return videos.slice(0, numberOfVideos * 2) | 54 | return videos.slice(0, numberOfVideos * 2) |
63 | } | 55 | } |
56 | |||
57 | onNearOfBottom () { | ||
58 | if (this.currentPage >= this.maxPage) return | ||
59 | if (this.lastWasEmpty) return | ||
60 | if (this.isLoading) return | ||
61 | |||
62 | this.currentPage++ | ||
63 | this.loadMoreResults() | ||
64 | } | ||
65 | |||
66 | private loadMoreResults () { | ||
67 | this.isLoading = true | ||
68 | |||
69 | this.overviewService.getVideosOverview(this.currentPage) | ||
70 | .subscribe( | ||
71 | overview => { | ||
72 | this.isLoading = false | ||
73 | |||
74 | if (overview.tags.length === 0 && overview.channels.length === 0 && overview.categories.length === 0) { | ||
75 | this.lastWasEmpty = true | ||
76 | if (this.loaded === false) this.notResults = true | ||
77 | |||
78 | return | ||
79 | } | ||
80 | |||
81 | this.loaded = true | ||
82 | this.onDataSubject.next(overview) | ||
83 | |||
84 | this.overviews.push(overview) | ||
85 | }, | ||
86 | |||
87 | err => { | ||
88 | this.notifier.error(err.message) | ||
89 | this.isLoading = false | ||
90 | } | ||
91 | ) | ||
92 | } | ||
64 | } | 93 | } |