aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/shared-main
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-08-19 09:24:29 +0200
committerChocobozzz <me@florianbigard.com>2021-08-25 11:24:11 +0200
commitdd24f1bb0a4b252e5342b251ba36853364da7b8e (patch)
tree41a9506d07413f056fb90425705e258f96fdc77d /client/src/app/shared/shared-main
parent2e80d256cc75b4b02c8efc3d3e4cdf57ddf401a8 (diff)
downloadPeerTube-dd24f1bb0a4b252e5342b251ba36853364da7b8e.tar.gz
PeerTube-dd24f1bb0a4b252e5342b251ba36853364da7b8e.tar.zst
PeerTube-dd24f1bb0a4b252e5342b251ba36853364da7b8e.zip
Add video filters to common video pages
Diffstat (limited to 'client/src/app/shared/shared-main')
-rw-r--r--client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts22
-rw-r--r--client/src/app/shared/shared-main/feeds/feed.component.scss3
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html23
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss13
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.ts32
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html2
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts110
7 files changed, 89 insertions, 116 deletions
diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
index dc212788a..bebc6efa7 100644
--- a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
+++ b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts
@@ -1,16 +1,19 @@
1import { fromEvent, Observable, Subscription } from 'rxjs' 1import { fromEvent, Observable, Subscription } from 'rxjs'
2import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' 2import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
3import { AfterViewChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' 3import { AfterViewChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
4import { PeerTubeRouterService, RouterSetting } from '@app/core'
4 5
5@Directive({ 6@Directive({
6 selector: '[myInfiniteScroller]' 7 selector: '[myInfiniteScroller]'
7}) 8})
8export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewChecked { 9export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewChecked {
9 @Input() percentLimit = 70 10 @Input() percentLimit = 70
10 @Input() autoInit = false
11 @Input() onItself = false 11 @Input() onItself = false
12 @Input() dataObservable: Observable<any[]> 12 @Input() dataObservable: Observable<any[]>
13 13
14 // Add angular state in query params to reuse the routed component
15 @Input() setAngularState: boolean
16
14 @Output() nearOfBottom = new EventEmitter<void>() 17 @Output() nearOfBottom = new EventEmitter<void>()
15 18
16 private decimalLimit = 0 19 private decimalLimit = 0
@@ -20,7 +23,10 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
20 23
21 private checkScroll = false 24 private checkScroll = false
22 25
23 constructor (private el: ElementRef) { 26 constructor (
27 private peertubeRouter: PeerTubeRouterService,
28 private el: ElementRef
29 ) {
24 this.decimalLimit = this.percentLimit / 100 30 this.decimalLimit = this.percentLimit / 100
25 } 31 }
26 32
@@ -36,7 +42,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
36 } 42 }
37 43
38 ngOnInit () { 44 ngOnInit () {
39 if (this.autoInit === true) return this.initialize() 45 this.initialize()
40 } 46 }
41 47
42 ngOnDestroy () { 48 ngOnDestroy () {
@@ -67,7 +73,11 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
67 filter(({ current }) => this.isScrollingDown(current)), 73 filter(({ current }) => this.isScrollingDown(current)),
68 filter(({ current, maximumScroll }) => (current / maximumScroll) > this.decimalLimit) 74 filter(({ current, maximumScroll }) => (current / maximumScroll) > this.decimalLimit)
69 ) 75 )
70 .subscribe(() => this.nearOfBottom.emit()) 76 .subscribe(() => {
77 if (this.setAngularState) this.setScrollRouteParams()
78
79 this.nearOfBottom.emit()
80 })
71 81
72 if (this.dataObservable) { 82 if (this.dataObservable) {
73 this.dataObservable 83 this.dataObservable
@@ -96,4 +106,8 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
96 this.lastCurrentBottom = current 106 this.lastCurrentBottom = current
97 return result 107 return result
98 } 108 }
109
110 private setScrollRouteParams () {
111 this.peertubeRouter.addRouteSetting(RouterSetting.REUSE_COMPONENT)
112 }
99} 113}
diff --git a/client/src/app/shared/shared-main/feeds/feed.component.scss b/client/src/app/shared/shared-main/feeds/feed.component.scss
index a1838c485..bf1f4eeeb 100644
--- a/client/src/app/shared/shared-main/feeds/feed.component.scss
+++ b/client/src/app/shared/shared-main/feeds/feed.component.scss
@@ -7,12 +7,11 @@
7 a { 7 a {
8 color: #000; 8 color: #000;
9 display: block; 9 display: block;
10 min-width: 100px;
10 } 11 }
11} 12}
12 13
13my-global-icon { 14my-global-icon {
14 @include apply-svg-color(pvar(--mainForegroundColor));
15
16 cursor: pointer; 15 cursor: pointer;
17 width: 100%; 16 width: 100%;
18} 17}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index c20c02e23..1e2f6c6a9 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
@@ -1,13 +1,18 @@
1<div class="root"> 1<div class="root">
2 <input 2 <div class="input-group has-feedback has-clear">
3 #ref 3 <input
4 type="text" 4 #ref
5 [(ngModel)]="value" 5 type="text"
6 (keyup.enter)="searchChange()" 6 [(ngModel)]="value"
7 [hidden]="!inputShown" 7 (keyup.enter)="sendSearch()"
8 [name]="name" 8 [hidden]="!inputShown"
9 [placeholder]="placeholder" 9 [name]="name"
10 > 10 [placeholder]="placeholder"
11 >
12
13 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="onResetFilter()"></a>
14 <span class="sr-only" i18n>Clear filters</span>
15 </div>
11 16
12 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon> 17 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
13 18
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index 173204291..d5fcff760 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
@@ -11,20 +11,17 @@ my-global-icon {
11 height: 28px; 11 height: 28px;
12 width: 28px; 12 width: 28px;
13 cursor: pointer; 13 cursor: pointer;
14 color: pvar(--mainColor);
14 15
15 &:hover { 16 &:hover {
16 color: pvar(--mainHoverColor); 17 color: pvar(--mainHoverColor);
17 } 18 }
18
19 &[iconName=search] {
20 color: pvar(--mainForegroundColor);
21 }
22
23 &[iconName=cross] {
24 color: pvar(--mainForegroundColor);
25 }
26} 19}
27 20
28input { 21input {
29 @include peertube-input-text(200px); 22 @include peertube-input-text(200px);
23
24 &:focus {
25 box-shadow: 0 0 5px 0 #a5a5a5;
26 }
30} 27}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
index 292ec4c82..99abb94e7 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
@@ -1,7 +1,4 @@
1import { Subject } from 'rxjs'
2import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5 2
6@Component({ 3@Component({
7 selector: 'my-simple-search-input', 4 selector: 'my-simple-search-input',
@@ -22,23 +19,9 @@ export class SimpleSearchInputComponent implements OnInit {
22 value = '' 19 value = ''
23 inputShown: boolean 20 inputShown: boolean
24 21
25 private searchSubject = new Subject<string>() 22 private hasAlreadySentSearch = false
26
27 constructor (
28 private router: Router,
29 private route: ActivatedRoute
30 ) {}
31 23
32 ngOnInit () { 24 ngOnInit () {
33 this.searchSubject
34 .pipe(
35 debounceTime(400),
36 distinctUntilChanged()
37 )
38 .subscribe(value => this.searchChanged.emit(value))
39
40 this.searchSubject.next(this.value)
41
42 if (this.isInputShown()) this.showInput(false) 25 if (this.isInputShown()) this.showInput(false)
43 } 26 }
44 27
@@ -54,7 +37,7 @@ export class SimpleSearchInputComponent implements OnInit {
54 return 37 return
55 } 38 }
56 39
57 this.searchChange() 40 this.sendSearch()
58 } 41 }
59 42
60 showInput (focus = true) { 43 showInput (focus = true) {
@@ -80,9 +63,14 @@ export class SimpleSearchInputComponent implements OnInit {
80 this.hideInput() 63 this.hideInput()
81 } 64 }
82 65
83 searchChange () { 66 sendSearch () {
84 this.router.navigate([ './search' ], { relativeTo: this.route }) 67 this.hasAlreadySentSearch = true
68 this.searchChanged.emit(this.value)
69 }
70
71 onResetFilter () {
72 this.value = ''
85 73
86 this.searchSubject.next(this.value) 74 if (this.hasAlreadySentSearch) this.sendSearch()
87 } 75 }
88} 76}
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 325f0eaae..ee8df864a 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> 1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
2 2
3<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 3<div class="notifications" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <ng-container [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 60cc9d160..3481b116f 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -5,6 +5,7 @@ import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
6import { objectToFormData } from '@app/helpers' 6import { objectToFormData } from '@app/helpers'
7import { 7import {
8 BooleanBothQuery,
8 FeedFormat, 9 FeedFormat,
9 NSFWPolicyType, 10 NSFWPolicyType,
10 ResultList, 11 ResultList,
@@ -28,19 +29,21 @@ import { VideoDetails } from './video-details.model'
28import { VideoEdit } from './video-edit.model' 29import { VideoEdit } from './video-edit.model'
29import { Video } from './video.model' 30import { Video } from './video.model'
30 31
31export interface VideosProvider { 32export type CommonVideoParams = {
32 getVideos (parameters: { 33 videoPagination: ComponentPaginationLight
33 videoPagination: ComponentPaginationLight 34 sort: VideoSortField
34 sort: VideoSortField 35 filter?: VideoFilter
35 filter?: VideoFilter 36 categoryOneOf?: number[]
36 categoryOneOf?: number[] 37 languageOneOf?: string[]
37 languageOneOf?: string[] 38 isLive?: boolean
38 nsfwPolicy: NSFWPolicyType 39 skipCount?: boolean
39 }): Observable<ResultList<Video>> 40 // FIXME: remove?
41 nsfwPolicy?: NSFWPolicyType
42 nsfw?: BooleanBothQuery
40} 43}
41 44
42@Injectable() 45@Injectable()
43export class VideoService implements VideosProvider { 46export class VideoService {
44 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 47 static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
45 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' 48 static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
46 static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.' 49 static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.'
@@ -144,32 +147,16 @@ export class VideoService implements VideosProvider {
144 ) 147 )
145 } 148 }
146 149
147 getAccountVideos (parameters: { 150 getAccountVideos (parameters: CommonVideoParams & {
148 account: Pick<Account, 'nameWithHost'> 151 account: Pick<Account, 'nameWithHost'>
149 videoPagination: ComponentPaginationLight
150 sort: VideoSortField
151 nsfwPolicy?: NSFWPolicyType
152 videoFilter?: VideoFilter
153 search?: string 152 search?: string
154 }): Observable<ResultList<Video>> { 153 }): Observable<ResultList<Video>> {
155 const { account, videoPagination, sort, videoFilter, nsfwPolicy, search } = parameters 154 const { account, search } = parameters
156
157 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
158 155
159 let params = new HttpParams() 156 let params = new HttpParams()
160 params = this.restService.addRestGetParams(params, pagination, sort) 157 params = this.buildCommonVideosParams({ params, ...parameters })
161
162 if (nsfwPolicy) {
163 params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
164 }
165
166 if (videoFilter) {
167 params = params.set('filter', videoFilter)
168 }
169 158
170 if (search) { 159 if (search) params = params.set('search', search)
171 params = params.set('search', search)
172 }
173 160
174 return this.authHttp 161 return this.authHttp
175 .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) 162 .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
@@ -179,27 +166,13 @@ export class VideoService implements VideosProvider {
179 ) 166 )
180 } 167 }
181 168
182 getVideoChannelVideos (parameters: { 169 getVideoChannelVideos (parameters: CommonVideoParams & {
183 videoChannel: Pick<VideoChannel, 'nameWithHost'> 170 videoChannel: Pick<VideoChannel, 'nameWithHost'>
184 videoPagination: ComponentPaginationLight
185 sort: VideoSortField
186 nsfwPolicy?: NSFWPolicyType
187 videoFilter?: VideoFilter
188 }): Observable<ResultList<Video>> { 171 }): Observable<ResultList<Video>> {
189 const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters 172 const { videoChannel } = parameters
190
191 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
192 173
193 let params = new HttpParams() 174 let params = new HttpParams()
194 params = this.restService.addRestGetParams(params, pagination, sort) 175 params = this.buildCommonVideosParams({ params, ...parameters })
195
196 if (nsfwPolicy) {
197 params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
198 }
199
200 if (videoFilter) {
201 params = params.set('filter', videoFilter)
202 }
203 176
204 return this.authHttp 177 return this.authHttp
205 .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) 178 .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
@@ -209,30 +182,9 @@ export class VideoService implements VideosProvider {
209 ) 182 )
210 } 183 }
211 184
212 getVideos (parameters: { 185 getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> {
213 videoPagination: ComponentPaginationLight
214 sort: VideoSortField
215 filter?: VideoFilter
216 categoryOneOf?: number[]
217 languageOneOf?: string[]
218 isLive?: boolean
219 skipCount?: boolean
220 nsfwPolicy?: NSFWPolicyType
221 }): Observable<ResultList<Video>> {
222 const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive } = parameters
223
224 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
225
226 let params = new HttpParams() 186 let params = new HttpParams()
227 params = this.restService.addRestGetParams(params, pagination, sort) 187 params = this.buildCommonVideosParams({ params, ...parameters })
228
229 if (filter) params = params.set('filter', filter)
230 if (skipCount) params = params.set('skipCount', skipCount + '')
231
232 if (isLive) params = params.set('isLive', isLive)
233 if (nsfwPolicy) params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
234 if (languageOneOf) this.restService.addArrayParams(params, 'languageOneOf', languageOneOf)
235 if (categoryOneOf) this.restService.addArrayParams(params, 'categoryOneOf', categoryOneOf)
236 188
237 return this.authHttp 189 return this.authHttp
238 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params }) 190 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
@@ -421,4 +373,22 @@ export class VideoService implements VideosProvider {
421 catchError(err => this.restExtractor.handleError(err)) 373 catchError(err => this.restExtractor.handleError(err))
422 ) 374 )
423 } 375 }
376
377 private buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
378 const { params, videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive, nsfw } = options
379
380 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
381 let newParams = this.restService.addRestGetParams(params, pagination, sort)
382
383 if (filter) newParams = newParams.set('filter', filter)
384 if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
385
386 if (isLive) newParams = newParams.set('isLive', isLive)
387 if (nsfw) newParams = newParams.set('nsfw', nsfw)
388 if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
389 if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
390 if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
391
392 return newParams
393 }
424} 394}