aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-08-25 11:42:30 +0200
committerChocobozzz <me@florianbigard.com>2021-08-25 11:42:30 +0200
commitfdec51e3846d50e3375612a6820ed3ab0b5fcd25 (patch)
tree17283e85a6794c9e8fe3f5d4478a406d8d188425 /client/src/app/+videos
parent59c8902a57991be29f0aacac1642389fb770c6ed (diff)
parentdd24f1bb0a4b252e5342b251ba36853364da7b8e (diff)
downloadPeerTube-fdec51e3846d50e3375612a6820ed3ab0b5fcd25.tar.gz
PeerTube-fdec51e3846d50e3375612a6820ed3ab0b5fcd25.tar.zst
PeerTube-fdec51e3846d50e3375612a6820ed3ab0b5fcd25.zip
Merge branch 'feature/video-filters' into develop
Diffstat (limited to 'client/src/app/+videos')
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html8
-rw-r--r--client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html2
-rw-r--r--client/src/app/+videos/video-list/index.ts4
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html2
-rw-r--r--client/src/app/+videos/video-list/trending/index.ts2
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.html8
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.scss20
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts109
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending.component.ts127
-rw-r--r--client/src/app/+videos/video-list/video-local.component.ts81
-rw-r--r--client/src/app/+videos/video-list/video-recently-added.component.ts73
-rw-r--r--client/src/app/+videos/video-list/video-user-subscriptions.component.html17
-rw-r--r--client/src/app/+videos/video-list/video-user-subscriptions.component.ts133
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.html22
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.ts219
-rw-r--r--client/src/app/+videos/videos-routing.module.ts54
-rw-r--r--client/src/app/+videos/videos.module.ts12
17 files changed, 342 insertions, 551 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
index 9e6fde2e0..0e00c9c0e 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html
@@ -27,13 +27,7 @@
27 27
28 <div *ngIf="totalNotDeletedComments === 0 && comments.length === 0" i18n>No comments.</div> 28 <div *ngIf="totalNotDeletedComments === 0 && comments.length === 0" i18n>No comments.</div>
29 29
30 <div 30 <div class="comment-threads" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
31 class="comment-threads"
32 myInfiniteScroller
33 [autoInit]="true"
34 (nearOfBottom)="onNearOfBottom()"
35 [dataObservable]="onDataSubject.asObservable()"
36 >
37 <div> 31 <div>
38 <div class="anchor" #commentHighlightBlock id="highlighted-comment"></div> 32 <div class="anchor" #commentHighlightBlock id="highlighted-comment"></div>
39 <my-video-comment 33 <my-video-comment
diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
index c270142a3..da81d76d1 100644
--- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
+++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.html
@@ -1,6 +1,6 @@
1<div 1<div
2 *ngIf="playlist && currentPlaylistPosition" class="playlist" 2 *ngIf="playlist && currentPlaylistPosition" class="playlist"
3 myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()" 3 myInfiniteScroller [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()"
4> 4>
5 <div class="playlist-info"> 5 <div class="playlist-info">
6 <div class="playlist-display-name"> 6 <div class="playlist-display-name">
diff --git a/client/src/app/+videos/video-list/index.ts b/client/src/app/+videos/video-list/index.ts
index dc27e29e2..3492f43f4 100644
--- a/client/src/app/+videos/video-list/index.ts
+++ b/client/src/app/+videos/video-list/index.ts
@@ -1,4 +1,2 @@
1export * from './overview' 1export * from './overview'
2export * from './trending' 2export * from './videos-list-common-page.component'
3export * from './video-local.component'
4export * from './video-recently-added.component'
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index d3c602aa5..1a715560c 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -4,7 +4,7 @@
4 <div class="no-results" i18n *ngIf="notResults">No results.</div> 4 <div class="no-results" i18n *ngIf="notResults">No results.</div>
5 5
6 <div 6 <div
7 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" 7 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"
8 > 8 >
9 <ng-container *ngFor="let overview of overviews"> 9 <ng-container *ngFor="let overview of overviews">
10 10
diff --git a/client/src/app/+videos/video-list/trending/index.ts b/client/src/app/+videos/video-list/trending/index.ts
deleted file mode 100644
index 70835885a..000000000
--- a/client/src/app/+videos/video-list/trending/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './video-trending-header.component'
2export * from './video-trending.component'
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.html b/client/src/app/+videos/video-list/trending/video-trending-header.component.html
deleted file mode 100644
index db81ce6a1..000000000
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
1<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
2 <ng-container *ngFor="let button of buttons">
3 <label *ngIf="!button.hidden" ngbButtonLabel class="btn-light" placement="bottom right-bottom left-bottom" [ngbTooltip]="button.tooltip" container="body">
4 <my-global-icon [iconName]="button.iconName"></my-global-icon>
5 <input ngbButton type="radio" [value]="button.value"> {{ button.label }}
6 </label>
7 </ng-container>
8</div>
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.scss b/client/src/app/+videos/video-list/trending/video-trending-header.component.scss
deleted file mode 100644
index 54b072314..000000000
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.scss
+++ /dev/null
@@ -1,20 +0,0 @@
1@use '_mixins' as *;
2
3.btn-group label {
4 border: 1px solid transparent;
5 border-radius: 9999px !important;
6 padding: 5px 16px;
7 opacity: .8;
8
9 &:not(:first-child) {
10 @include margin-left(.5rem);
11 }
12
13 my-global-icon {
14 @include margin-right(.1rem);
15
16 position: relative;
17 top: -2px;
18 height: 1rem;
19 }
20}
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
deleted file mode 100644
index c94655c74..000000000
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
+++ /dev/null
@@ -1,109 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, HostBinding, Inject, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, RedirectService } from '@app/core'
5import { ServerService } from '@app/core/server/server.service'
6import { GlobalIconName } from '@app/shared/shared-icons'
7import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
8
9interface VideoTrendingHeaderItem {
10 label: string
11 iconName: GlobalIconName
12 value: string
13 tooltip?: string
14 hidden?: boolean
15}
16
17@Component({
18 selector: 'my-video-trending-title-page',
19 styleUrls: [ './video-trending-header.component.scss' ],
20 templateUrl: './video-trending-header.component.html'
21})
22export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit, OnDestroy {
23 @HostBinding('class') class = 'title-page title-page-single'
24
25 buttons: VideoTrendingHeaderItem[]
26
27 private algorithmChangeSub: Subscription
28
29 constructor (
30 @Inject('data') public data: any,
31 private route: ActivatedRoute,
32 private router: Router,
33 private auth: AuthService,
34 private serverService: ServerService,
35 private redirectService: RedirectService
36 ) {
37 super(data)
38
39 this.buttons = [
40 {
41 label: $localize`:A variant of Trending videos based on the number of recent interactions, minus user history:Best`,
42 iconName: 'award',
43 value: 'best',
44 tooltip: $localize`Videos with the most interactions for recent videos, minus user history`,
45 hidden: true
46 },
47 {
48 label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
49 iconName: 'flame',
50 value: 'hot',
51 tooltip: $localize`Videos with the most interactions for recent videos`,
52 hidden: true
53 },
54 {
55 label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
56 iconName: 'trending',
57 value: 'most-viewed',
58 tooltip: $localize`Videos with the most views during the last 24 hours`
59 },
60 {
61 label: $localize`:A variant of Trending videos based on the number of likes:Likes`,
62 iconName: 'like',
63 value: 'most-liked',
64 tooltip: $localize`Videos that have the most likes`
65 }
66 ]
67 }
68
69 ngOnInit () {
70 const serverConfig = this.serverService.getHTMLConfig()
71 const algEnabled = serverConfig.trending.videos.algorithms.enabled
72
73 this.buttons = this.buttons.map(b => {
74 b.hidden = !algEnabled.includes(b.value)
75
76 // Best is adapted by the user history so
77 if (b.value === 'best' && !this.auth.isLoggedIn()) {
78 b.hidden = true
79 }
80
81 return b
82 })
83
84 this.algorithmChangeSub = this.route.queryParams.subscribe(
85 queryParams => {
86 this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
87 }
88 )
89 }
90
91 ngOnDestroy () {
92 if (this.algorithmChangeSub) this.algorithmChangeSub.unsubscribe()
93 }
94
95 setSort () {
96 const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
97 ? this.data.model
98 : undefined
99
100 this.router.navigate(
101 [],
102 {
103 relativeTo: this.route,
104 queryParams: { alg },
105 queryParamsHandling: 'merge'
106 }
107 )
108 }
109}
diff --git a/client/src/app/+videos/video-list/trending/video-trending.component.ts b/client/src/app/+videos/video-list/trending/video-trending.component.ts
deleted file mode 100644
index 085f29a8b..000000000
--- a/client/src/app/+videos/video-list/trending/video-trending.component.ts
+++ /dev/null
@@ -1,127 +0,0 @@
1import { Subscription } from 'rxjs'
2import { first, switchMap } from 'rxjs/operators'
3import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Params, Router } from '@angular/router'
5import { AuthService, LocalStorageService, Notifier, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
6import { HooksService } from '@app/core/plugins/hooks.service'
7import { immutableAssign } from '@app/helpers'
8import { VideoService } from '@app/shared/shared-main'
9import { AbstractVideoList } from '@app/shared/shared-video-miniature'
10import { VideoSortField } from '@shared/models'
11import { VideoTrendingHeaderComponent } from './video-trending-header.component'
12
13@Component({
14 selector: 'my-videos-hot',
15 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
16 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
17})
18export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 HeaderComponent = VideoTrendingHeaderComponent
20 titlePage: string
21 defaultSort: VideoSortField = '-trending'
22
23 loadUserVideoPreferences = true
24
25 private algorithmChangeSub: Subscription
26
27 constructor (
28 protected router: Router,
29 protected serverService: ServerService,
30 protected route: ActivatedRoute,
31 protected notifier: Notifier,
32 protected authService: AuthService,
33 protected userService: UserService,
34 protected screenService: ScreenService,
35 protected storageService: LocalStorageService,
36 protected cfr: ComponentFactoryResolver,
37 private videoService: VideoService,
38 private redirectService: RedirectService,
39 private hooks: HooksService
40 ) {
41 super()
42
43 this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
44
45 this.headerComponentInjector = this.getInjector()
46 }
47
48 ngOnInit () {
49 super.ngOnInit()
50
51 this.generateSyndicationList()
52
53 // Subscribe to alg change after we loaded the data
54 // The initial alg load is handled by the parent class
55 this.algorithmChangeSub = this.onDataSubject
56 .pipe(
57 first(),
58 switchMap(() => this.route.queryParams)
59 ).subscribe(queryParams => {
60 const oldSort = this.sort
61
62 this.loadPageRouteParams(queryParams)
63
64 if (oldSort !== this.sort) this.reloadVideos()
65 }
66 )
67 }
68
69 ngOnDestroy () {
70 super.ngOnDestroy()
71 if (this.algorithmChangeSub) this.algorithmChangeSub.unsubscribe()
72 }
73
74 getVideosObservable (page: number) {
75 const newPagination = immutableAssign(this.pagination, { currentPage: page })
76 const params = {
77 videoPagination: newPagination,
78 sort: this.sort,
79 categoryOneOf: this.categoryOneOf,
80 languageOneOf: this.languageOneOf,
81 nsfwPolicy: this.nsfwPolicy,
82 skipCount: true
83 }
84
85 return this.hooks.wrapObsFun(
86 this.videoService.getVideos.bind(this.videoService),
87 params,
88 'common',
89 'filter:api.trending-videos.videos.list.params',
90 'filter:api.trending-videos.videos.list.result'
91 )
92 }
93
94 generateSyndicationList () {
95 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
96 }
97
98 getInjector () {
99 return Injector.create({
100 providers: [ {
101 provide: 'data',
102 useValue: {
103 model: this.defaultSort
104 }
105 } ]
106 })
107 }
108
109 protected loadPageRouteParams (queryParams: Params) {
110 const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
111
112 this.sort = this.parseAlgorithm(algorithm)
113 }
114
115 private parseAlgorithm (algorithm: string): VideoSortField {
116 switch (algorithm) {
117 case 'most-viewed':
118 return '-trending'
119
120 case 'most-liked':
121 return '-likes'
122
123 default:
124 return '-' + algorithm as VideoSortField
125 }
126 }
127}
diff --git a/client/src/app/+videos/video-list/video-local.component.ts b/client/src/app/+videos/video-list/video-local.component.ts
deleted file mode 100644
index b576883d1..000000000
--- a/client/src/app/+videos/video-list/video-local.component.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service'
5import { immutableAssign } from '@app/helpers'
6import { VideoService } from '@app/shared/shared-main'
7import { AbstractVideoList } from '@app/shared/shared-video-miniature'
8import { VideoFilter, VideoSortField } from '@shared/models'
9
10@Component({
11 selector: 'my-videos-local',
12 styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
13 templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
14})
15export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
16 titlePage: string
17 sort = '-publishedAt' as VideoSortField
18 filter: VideoFilter = 'local'
19
20 loadUserVideoPreferences = true
21
22 constructor (
23 protected router: Router,
24 protected serverService: ServerService,
25 protected route: ActivatedRoute,
26 protected notifier: Notifier,
27 protected authService: AuthService,
28 protected userService: UserService,
29 protected screenService: ScreenService,
30 protected storageService: LocalStorageService,
31 protected cfr: ComponentFactoryResolver,
32 private videoService: VideoService,
33 private hooks: HooksService
34 ) {
35 super()
36
37 this.titlePage = $localize`Local videos`
38 }
39
40 ngOnInit () {
41 super.ngOnInit()
42
43 this.enableAllFilterIfPossible()
44 this.generateSyndicationList()
45 }
46
47 ngOnDestroy () {
48 super.ngOnDestroy()
49 }
50
51 getVideosObservable (page: number) {
52 const newPagination = immutableAssign(this.pagination, { currentPage: page })
53 const params = {
54 videoPagination: newPagination,
55 sort: this.sort,
56 filter: this.filter,
57 categoryOneOf: this.categoryOneOf,
58 languageOneOf: this.languageOneOf,
59 nsfwPolicy: this.nsfwPolicy,
60 skipCount: true
61 }
62
63 return this.hooks.wrapObsFun(
64 this.videoService.getVideos.bind(this.videoService),
65 params,
66 'common',
67 'filter:api.local-videos.videos.list.params',
68 'filter:api.local-videos.videos.list.result'
69 )
70 }
71
72 generateSyndicationList () {
73 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf)
74 }
75
76 toggleModerationDisplay () {
77 this.filter = this.buildLocalFilter(this.filter, 'local')
78
79 this.reloadVideos()
80 }
81}
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
deleted file mode 100644
index 506f92d25..000000000
--- a/client/src/app/+videos/video-list/video-recently-added.component.ts
+++ /dev/null
@@ -1,73 +0,0 @@
1import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service'
5import { immutableAssign } from '@app/helpers'
6import { VideoService } from '@app/shared/shared-main'
7import { AbstractVideoList } from '@app/shared/shared-video-miniature'
8import { VideoSortField } from '@shared/models'
9
10@Component({
11 selector: 'my-videos-recently-added',
12 styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
13 templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
14})
15export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
16 titlePage: string
17 sort: VideoSortField = '-publishedAt'
18 groupByDate = true
19
20 loadUserVideoPreferences = true
21
22 constructor (
23 protected route: ActivatedRoute,
24 protected serverService: ServerService,
25 protected router: Router,
26 protected notifier: Notifier,
27 protected authService: AuthService,
28 protected userService: UserService,
29 protected screenService: ScreenService,
30 protected storageService: LocalStorageService,
31 protected cfr: ComponentFactoryResolver,
32 private videoService: VideoService,
33 private hooks: HooksService
34 ) {
35 super()
36
37 this.titlePage = $localize`Recently added`
38 }
39
40 ngOnInit () {
41 super.ngOnInit()
42
43 this.generateSyndicationList()
44 }
45
46 ngOnDestroy () {
47 super.ngOnDestroy()
48 }
49
50 getVideosObservable (page: number) {
51 const newPagination = immutableAssign(this.pagination, { currentPage: page })
52 const params = {
53 videoPagination: newPagination,
54 sort: this.sort,
55 categoryOneOf: this.categoryOneOf,
56 languageOneOf: this.languageOneOf,
57 nsfwPolicy: this.nsfwPolicy,
58 skipCount: true
59 }
60
61 return this.hooks.wrapObsFun(
62 this.videoService.getVideos.bind(this.videoService),
63 params,
64 'common',
65 'filter:api.recently-added-videos.videos.list.params',
66 'filter:api.recently-added-videos.videos.list.result'
67 )
68 }
69
70 generateSyndicationList () {
71 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
72 }
73}
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.html b/client/src/app/+videos/video-list/video-user-subscriptions.component.html
new file mode 100644
index 000000000..2675b58bf
--- /dev/null
+++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.html
@@ -0,0 +1,17 @@
1<my-videos-list
2 [getVideosObservableFunction]="getVideosObservableFunction"
3 [getSyndicationItemsFunction]="getSyndicationItemsFunction"
4
5 [title]="titlePage"
6
7 [defaultSort]="defaultSort"
8
9 [displayFilters]="false"
10 [displayModerationBlock]="false"
11
12 [loadUserVideoPreferences]="false"
13 [groupByDate]="true"
14
15 [disabled]="disabled"
16>
17</my-videos-list>
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 a1498e797..43cbab9f6 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,94 +1,53 @@
1 1
2import { switchMap } from 'rxjs/operators' 2import { firstValueFrom } from 'rxjs'
3import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core' 3import { switchMap, tap } from 'rxjs/operators'
4import { ActivatedRoute, Router } from '@angular/router' 4import { Component } from '@angular/core'
5import { AuthService, LocalStorageService, Notifier, ScopedTokensService, ScreenService, ServerService, UserService } from '@app/core' 5import { AuthService, ComponentPaginationLight, DisableForReuseHook, ScopedTokensService } from '@app/core'
6import { HooksService } from '@app/core/plugins/hooks.service' 6import { HooksService } from '@app/core/plugins/hooks.service'
7import { immutableAssign } from '@app/helpers'
8import { VideoService } from '@app/shared/shared-main' 7import { VideoService } from '@app/shared/shared-main'
9import { UserSubscriptionService } from '@app/shared/shared-user-subscription' 8import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
10import { AbstractVideoList } from '@app/shared/shared-video-miniature' 9import { VideoFilters } from '@app/shared/shared-video-miniature'
11import { FeedFormat, VideoSortField } from '@shared/models' 10import { VideoSortField } from '@shared/models'
12import { environment } from '../../../environments/environment'
13import { copyToClipboard } from '../../../root-helpers/utils'
14 11
15@Component({ 12@Component({
16 selector: 'my-videos-user-subscriptions', 13 selector: 'my-videos-user-subscriptions',
17 styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ], 14 templateUrl: './video-user-subscriptions.component.html'
18 templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
19}) 15})
20export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { 16export class VideoUserSubscriptionsComponent implements DisableForReuseHook {
21 titlePage: string 17 getVideosObservableFunction = this.getVideosObservable.bind(this)
22 sort = '-publishedAt' as VideoSortField 18 getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
23 groupByDate = true 19
20 defaultSort = '-publishedAt' as VideoSortField
21
22 actions = [
23 {
24 routerLink: '/my-library/subscriptions',
25 label: $localize`Subscriptions`,
26 iconName: 'cog'
27 }
28 ]
29
30 titlePage = $localize`Videos from your subscriptions`
31
32 disabled = false
33
34 private feedToken: string
24 35
25 constructor ( 36 constructor (
26 protected router: Router, 37 private authService: AuthService,
27 protected serverService: ServerService,
28 protected route: ActivatedRoute,
29 protected notifier: Notifier,
30 protected authService: AuthService,
31 protected userService: UserService,
32 protected screenService: ScreenService,
33 protected storageService: LocalStorageService,
34 private userSubscription: UserSubscriptionService, 38 private userSubscription: UserSubscriptionService,
35 protected cfr: ComponentFactoryResolver,
36 private hooks: HooksService, 39 private hooks: HooksService,
37 private videoService: VideoService, 40 private videoService: VideoService,
38 private scopedTokensService: ScopedTokensService 41 private scopedTokensService: ScopedTokensService
39 ) { 42 ) {
40 super()
41 43
42 this.titlePage = $localize`Videos from your subscriptions`
43
44 this.actions.push({
45 routerLink: '/my-library/subscriptions',
46 label: $localize`Subscriptions`,
47 iconName: 'cog'
48 })
49 } 44 }
50 45
51 ngOnInit () { 46 getVideosObservable (pagination: ComponentPaginationLight, filters: VideoFilters) {
52 super.ngOnInit()
53
54 const user = this.authService.getUser()
55 let feedUrl = environment.originServerUrl
56
57 this.authService.userInformationLoaded
58 .pipe(switchMap(() => this.scopedTokensService.getScopedTokens()))
59 .subscribe({
60 next: tokens => {
61 const feeds = this.videoService.getVideoSubscriptionFeedUrls(user.account.id, tokens.feedToken)
62 feedUrl = feedUrl + feeds.find(f => f.format === FeedFormat.RSS).url
63
64 this.actions.unshift({
65 label: $localize`Copy feed URL`,
66 iconName: 'syndication',
67 justIcon: true,
68 href: feedUrl,
69 click: e => {
70 e.preventDefault()
71 copyToClipboard(feedUrl)
72 this.activateCopiedMessage()
73 }
74 })
75 },
76
77 error: err => {
78 this.notifier.error(err.message)
79 }
80 })
81 }
82
83 ngOnDestroy () {
84 super.ngOnDestroy()
85 }
86
87 getVideosObservable (page: number) {
88 const newPagination = immutableAssign(this.pagination, { currentPage: page })
89 const params = { 47 const params = {
90 videoPagination: newPagination, 48 ...filters.toVideosAPIObject(),
91 sort: this.sort, 49
50 videoPagination: pagination,
92 skipCount: true 51 skipCount: true
93 } 52 }
94 53
@@ -101,12 +60,32 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
101 ) 60 )
102 } 61 }
103 62
104 generateSyndicationList () { 63 getSyndicationItems () {
105 /* method disabled: the view provides its own */ 64 return this.loadFeedToken()
106 throw new Error('Method not implemented.') 65 .then(() => {
66 const user = this.authService.getUser()
67
68 return this.videoService.getVideoSubscriptionFeedUrls(user.account.id, this.feedToken)
69 })
107 } 70 }
108 71
109 activateCopiedMessage () { 72 disableForReuse () {
110 this.notifier.success($localize`Feed URL copied`) 73 this.disabled = true
74 }
75
76 enabledForReuse () {
77 this.disabled = false
78 }
79
80 private loadFeedToken () {
81 if (this.feedToken) return Promise.resolve(this.feedToken)
82
83 const obs = this.authService.userInformationLoaded
84 .pipe(
85 switchMap(() => this.scopedTokensService.getScopedTokens()),
86 tap(tokens => this.feedToken = tokens.feedToken)
87 )
88
89 return firstValueFrom(obs)
111 } 90 }
112} 91}
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.html b/client/src/app/+videos/video-list/videos-list-common-page.component.html
new file mode 100644
index 000000000..2831f996f
--- /dev/null
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.html
@@ -0,0 +1,22 @@
1<my-videos-list
2 [getVideosObservableFunction]="getVideosObservableFunction"
3 [getSyndicationItemsFunction]="getSyndicationItemsFunction"
4 [baseRouteBuilderFunction]="baseRouteBuilderFunction"
5
6 [title]="title"
7 [titleTooltip]="titleTooltip"
8
9 [defaultSort]="defaultSort"
10 [defaultScope]="defaultScope"
11
12 [displayFilters]="true"
13 [displayModerationBlock]="true"
14
15 [loadUserVideoPreferences]="true"
16 [groupByDate]="groupByDate"
17
18 [disabled]="disabled"
19
20 (filtersChanged)="onFiltersChanged($event)"
21>
22</my-videos-list>
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
new file mode 100644
index 000000000..ba64d4fec
--- /dev/null
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
@@ -0,0 +1,219 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'
3import { ComponentPaginationLight, DisableForReuseHook, MetaService, RedirectService, ServerService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service'
5import { VideoService } from '@app/shared/shared-main'
6import { VideoFilters, VideoFilterScope } from '@app/shared/shared-video-miniature/video-filters.model'
7import { ClientFilterHookName, VideoSortField } from '@shared/models'
8import { Subscription } from 'rxjs'
9
10export type VideosListCommonPageRouteData = {
11 sort: VideoSortField
12
13 scope: VideoFilterScope
14 hookParams: ClientFilterHookName
15 hookResult: ClientFilterHookName
16}
17
18@Component({
19 templateUrl: './videos-list-common-page.component.html'
20})
21export class VideosListCommonPageComponent implements OnInit, OnDestroy, DisableForReuseHook {
22 getVideosObservableFunction = this.getVideosObservable.bind(this)
23 getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
24 baseRouteBuilderFunction = this.baseRouteBuilder.bind(this)
25
26 title: string
27 titleTooltip: string
28
29 groupByDate: boolean
30
31 defaultSort: VideoSortField
32 defaultScope: VideoFilterScope
33
34 hookParams: ClientFilterHookName
35 hookResult: ClientFilterHookName
36
37 loadUserVideoPreferences = true
38
39 displayFilters = true
40
41 disabled = false
42
43 private trendingDays: number
44 private routeSub: Subscription
45
46 constructor (
47 private server: ServerService,
48 private route: ActivatedRoute,
49 private videoService: VideoService,
50 private hooks: HooksService,
51 private meta: MetaService,
52 private redirectService: RedirectService
53 ) {
54 }
55
56 ngOnInit () {
57 this.trendingDays = this.server.getHTMLConfig().trending.videos.intervalDays
58
59 this.routeSub = this.route.params.subscribe(params => {
60 this.update(params['page'])
61 })
62 }
63
64 ngOnDestroy () {
65 if (this.routeSub) this.routeSub.unsubscribe()
66 }
67
68 getVideosObservable (pagination: ComponentPaginationLight, filters: VideoFilters) {
69 const params = {
70 ...filters.toVideosAPIObject(),
71
72 videoPagination: pagination,
73 skipCount: true
74 }
75
76 return this.hooks.wrapObsFun(
77 this.videoService.getVideos.bind(this.videoService),
78 params,
79 'common',
80 this.hookParams,
81 this.hookResult
82 )
83 }
84
85 getSyndicationItems (filters: VideoFilters) {
86 const result = filters.toVideosAPIObject()
87
88 return this.videoService.getVideoFeedUrls(result.sort, result.filter)
89 }
90
91 onFiltersChanged (filters: VideoFilters) {
92 this.buildTitle(filters.scope, filters.sort)
93 this.updateGroupByDate(filters.sort)
94 }
95
96 baseRouteBuilder (filters: VideoFilters) {
97 const sanitizedSort = this.getSanitizedSort(filters.sort)
98
99 let suffix: string
100
101 if (filters.scope === 'local') suffix = 'local'
102 else if (sanitizedSort === 'publishedAt') suffix = 'recently-added'
103 else suffix = 'trending'
104
105 return [ '/videos', suffix ]
106 }
107
108 disableForReuse () {
109 this.disabled = true
110 }
111
112 enabledForReuse () {
113 this.disabled = false
114 }
115
116 update (page: string) {
117 const data = this.getData(page)
118
119 this.hookParams = data.hookParams
120 this.hookResult = data.hookResult
121
122 this.defaultSort = data.sort
123 this.defaultScope = data.scope
124
125 this.buildTitle()
126 this.updateGroupByDate(this.defaultSort)
127
128 this.meta.setTitle(this.title)
129 }
130
131 private getData (page: string) {
132 if (page === 'trending') return this.generateTrendingData(this.route.snapshot)
133
134 if (page === 'local') return this.generateLocalData()
135
136 return this.generateRecentlyAddedData()
137 }
138
139 private generateRecentlyAddedData (): VideosListCommonPageRouteData {
140 return {
141 sort: '-publishedAt',
142 scope: 'federated',
143 hookParams: 'filter:api.recently-added-videos.videos.list.params',
144 hookResult: 'filter:api.recently-added-videos.videos.list.result'
145 }
146 }
147
148 private generateLocalData (): VideosListCommonPageRouteData {
149 return {
150 sort: '-publishedAt' as VideoSortField,
151 scope: 'local',
152 hookParams: 'filter:api.local-videos.videos.list.params',
153 hookResult: 'filter:api.local-videos.videos.list.result'
154 }
155 }
156
157 private generateTrendingData (route: ActivatedRouteSnapshot): VideosListCommonPageRouteData {
158 const sort = route.queryParams['sort'] ?? this.parseTrendingAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
159
160 return {
161 sort,
162 scope: 'federated',
163 hookParams: 'filter:api.trending-videos.videos.list.params',
164 hookResult: 'filter:api.trending-videos.videos.list.result'
165 }
166 }
167
168 private parseTrendingAlgorithm (algorithm: string): VideoSortField {
169 switch (algorithm) {
170 case 'most-viewed':
171 return '-trending'
172
173 case 'most-liked':
174 return '-likes'
175
176 default:
177 return '-' + algorithm as VideoSortField
178 }
179 }
180
181 private updateGroupByDate (sort: VideoSortField) {
182 this.groupByDate = sort === '-publishedAt' || sort === 'publishedAt'
183 }
184
185 private buildTitle (scope: VideoFilterScope = this.defaultScope, sort: VideoSortField = this.defaultSort) {
186 const sanitizedSort = this.getSanitizedSort(sort)
187
188 if (scope === 'local') {
189 this.title = $localize`Local videos`
190 this.titleTooltip = $localize`Only videos uploaded on this instance are displayed`
191 return
192 }
193
194 if (sanitizedSort === 'publishedAt') {
195 this.title = $localize`Recently added`
196 this.titleTooltip = undefined
197 return
198 }
199
200 if ([ 'best', 'hot', 'trending', 'likes' ].includes(sanitizedSort)) {
201 this.title = $localize`Trending`
202
203 if (sanitizedSort === 'best') this.titleTooltip = $localize`Videos with the most interactions for recent videos, minus user history`
204 if (sanitizedSort === 'hot') this.titleTooltip = $localize`Videos with the most interactions for recent videos`
205 if (sanitizedSort === 'likes') this.titleTooltip = $localize`Videos that have the most likes`
206
207 if (sanitizedSort === 'trending') {
208 if (this.trendingDays === 1) this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
209 else this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days`
210 }
211
212 return
213 }
214 }
215
216 private getSanitizedSort (sort: VideoSortField) {
217 return sort.replace(/^-/, '') as VideoSortField
218 }
219}
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index 926dfaab0..7db519615 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -1,10 +1,8 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes, UrlSegment } from '@angular/router'
3import { LoginGuard } from '@app/core' 3import { LoginGuard } from '@app/core'
4import { VideoTrendingComponent } from './video-list' 4import { VideosListCommonPageComponent } from './video-list'
5import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 5import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
6import { VideoLocalComponent } from './video-list/video-local.component'
7import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
8import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component' 6import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
9import { VideosComponent } from './videos.component' 7import { VideosComponent } from './videos.component'
10 8
@@ -22,32 +20,35 @@ const videosRoutes: Routes = [
22 } 20 }
23 } 21 }
24 }, 22 },
23
25 { 24 {
26 path: 'trending', 25 // Old URL redirection
27 component: VideoTrendingComponent,
28 data: {
29 meta: {
30 title: $localize`Trending videos`
31 }
32 }
33 },
34 {
35 path: 'most-liked', 26 path: 'most-liked',
36 redirectTo: 'trending?alg=most-liked' 27 redirectTo: 'trending?sort=most-liked'
37 }, 28 },
38 { 29 {
39 path: 'recently-added', 30 matcher: (url: UrlSegment[]) => {
40 component: VideoRecentlyAddedComponent, 31 if (url.length === 1 && [ 'recently-added', 'trending', 'local' ].includes(url[0].path)) {
32 return {
33 consumed: url,
34 posParams: {
35 page: new UrlSegment(url[0].path, {})
36 }
37 }
38 }
39
40 return null
41 },
42
43 component: VideosListCommonPageComponent,
41 data: { 44 data: {
42 meta: {
43 title: $localize`Recently added videos`
44 },
45 reuse: { 45 reuse: {
46 enabled: true, 46 enabled: true,
47 key: 'recently-added-videos-list' 47 key: 'videos-list'
48 } 48 }
49 } 49 }
50 }, 50 },
51
51 { 52 {
52 path: 'subscriptions', 53 path: 'subscriptions',
53 canActivate: [ LoginGuard ], 54 canActivate: [ LoginGuard ],
@@ -61,19 +62,6 @@ const videosRoutes: Routes = [
61 key: 'subscription-videos-list' 62 key: 'subscription-videos-list'
62 } 63 }
63 } 64 }
64 },
65 {
66 path: 'local',
67 component: VideoLocalComponent,
68 data: {
69 meta: {
70 title: $localize`Local videos`
71 },
72 reuse: {
73 enabled: true,
74 key: 'local-videos-list'
75 }
76 }
77 } 65 }
78 ] 66 ]
79 } 67 }
diff --git a/client/src/app/+videos/videos.module.ts b/client/src/app/+videos/videos.module.ts
index 8a35015d6..523533c11 100644
--- a/client/src/app/+videos/videos.module.ts
+++ b/client/src/app/+videos/videos.module.ts
@@ -5,11 +5,8 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main' 5import { SharedMainModule } from '@app/shared/shared-main'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { OverviewService, VideoTrendingComponent } from './video-list' 8import { OverviewService, VideosListCommonPageComponent } from './video-list'
9import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 9import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
10import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
11import { VideoLocalComponent } from './video-list/video-local.component'
12import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
13import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component' 10import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
14import { VideosRoutingModule } from './videos-routing.module' 11import { VideosRoutingModule } from './videos-routing.module'
15import { VideosComponent } from './videos.component' 12import { VideosComponent } from './videos.component'
@@ -29,12 +26,9 @@ import { VideosComponent } from './videos.component'
29 declarations: [ 26 declarations: [
30 VideosComponent, 27 VideosComponent,
31 28
32 VideoTrendingHeaderComponent,
33 VideoTrendingComponent,
34 VideoRecentlyAddedComponent,
35 VideoLocalComponent,
36 VideoUserSubscriptionsComponent, 29 VideoUserSubscriptionsComponent,
37 VideoOverviewComponent 30 VideoOverviewComponent,
31 VideosListCommonPageComponent
38 ], 32 ],
39 33
40 exports: [ 34 exports: [