aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-29 15:19:22 +0200
committerChocobozzz <me@florianbigard.com>2021-07-29 15:19:22 +0200
commitaf7fd04a6706fb781e4622167b08dc6c9376f06a (patch)
tree4d6a84cd67143e07d762ba967f9d29e947e7436c /client/src
parent9c9a236b541a286e165d67341e4ddd6ea2fabdf1 (diff)
downloadPeerTube-af7fd04a6706fb781e4622167b08dc6c9376f06a.tar.gz
PeerTube-af7fd04a6706fb781e4622167b08dc6c9376f06a.tar.zst
PeerTube-af7fd04a6706fb781e4622167b08dc6c9376f06a.zip
Add ability to filter by host in search page
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+search/search-filters.component.html16
-rw-r--r--client/src/app/+search/search-filters.component.ts8
-rw-r--r--client/src/app/+search/search.component.html2
-rw-r--r--client/src/app/+search/search.component.scss4
-rw-r--r--client/src/app/+search/search.component.ts24
-rw-r--r--client/src/app/shared/form-validators/host-validators.ts4
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts34
-rw-r--r--client/src/app/shared/shared-search/search.service.ts29
8 files changed, 91 insertions, 30 deletions
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index 421bc7f6f..4b87a2102 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 64
65 <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> 65 <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
66 <input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> 66 <input type="radio" (change)="onDurationOrPublishedUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
67 <label [for]="date.id" class="radio">{{ date.label }}</label> 67 <label [for]="date.id" class="radio">{{ date.label }}</label>
68 </div> 68 </div>
69 </div> 69 </div>
@@ -79,7 +79,7 @@
79 <div class="row"> 79 <div class="row">
80 <div class="pl-0 col-sm-6"> 80 <div class="pl-0 col-sm-6">
81 <input 81 <input
82 (change)="onInputUpdated()" 82 (change)="onDurationOrPublishedUpdated()"
83 (keydown.enter)="$event.preventDefault()" 83 (keydown.enter)="$event.preventDefault()"
84 type="text" id="original-publication-after" name="original-publication-after" 84 type="text" id="original-publication-after" name="original-publication-after"
85 i18n-placeholder placeholder="After..." 85 i18n-placeholder placeholder="After..."
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 <div class="pr-0 col-sm-6"> 90 <div class="pr-0 col-sm-6">
91 <input 91 <input
92 (change)="onInputUpdated()" 92 (change)="onDurationOrPublishedUpdated()"
93 (keydown.enter)="$event.preventDefault()" 93 (keydown.enter)="$event.preventDefault()"
94 type="text" id="original-publication-before" name="original-publication-before" 94 type="text" id="original-publication-before" name="original-publication-before"
95 i18n-placeholder placeholder="Before..." 95 i18n-placeholder placeholder="Before..."
@@ -112,7 +112,7 @@
112 </div> 112 </div>
113 113
114 <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> 114 <div class="peertube-radio-container" *ngFor="let duration of durationRanges">
115 <input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> 115 <input type="radio" (change)="onDurationOrPublishedUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
116 <label [for]="duration.id" class="radio">{{ duration.label }}</label> 116 <label [for]="duration.id" class="radio">{{ duration.label }}</label>
117 </div> 117 </div>
118 </div> 118 </div>
@@ -174,6 +174,14 @@
174 <my-select-tags name="tagsOneOf" labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags> 174 <my-select-tags name="tagsOneOf" labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags>
175 </div> 175 </div>
176 176
177 <div class="form-group">
178 <label i18n for="host">PeerTube instance host</label>
179
180 <input (change)="onDurationOrPublishedUpdated()" (keydown.enter)="$event.preventDefault()" type="text" id="host" name="host"
181 placeholder="example.com" [(ngModel)]="advancedSearch.host" class="form-control"
182 >
183 </div>
184
177 <div class="form-group" *ngIf="isSearchTargetEnabled()"> 185 <div class="form-group" *ngIf="isSearchTargetEnabled()">
178 <div class="radio-label label-container"> 186 <div class="radio-label label-container">
179 <label i18n>Search target</label> 187 <label i18n>Search target</label>
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts
index afa523b91..5972ba553 100644
--- a/client/src/app/+search/search-filters.component.ts
+++ b/client/src/app/+search/search-filters.component.ts
@@ -108,14 +108,14 @@ export class SearchFiltersComponent implements OnInit {
108 this.loadOriginallyPublishedAtYears() 108 this.loadOriginallyPublishedAtYears()
109 } 109 }
110 110
111 onInputUpdated () { 111 onDurationOrPublishedUpdated () {
112 this.updateModelFromDurationRange() 112 this.updateModelFromDurationRange()
113 this.updateModelFromPublishedRange() 113 this.updateModelFromPublishedRange()
114 this.updateModelFromOriginallyPublishedAtYears() 114 this.updateModelFromOriginallyPublishedAtYears()
115 } 115 }
116 116
117 formUpdated () { 117 formUpdated () {
118 this.onInputUpdated() 118 this.onDurationOrPublishedUpdated()
119 this.filtered.emit(this.advancedSearch) 119 this.filtered.emit(this.advancedSearch)
120 } 120 }
121 121
@@ -127,7 +127,7 @@ export class SearchFiltersComponent implements OnInit {
127 this.durationRange = undefined 127 this.durationRange = undefined
128 this.publishedDateRange = undefined 128 this.publishedDateRange = undefined
129 129
130 this.onInputUpdated() 130 this.onDurationOrPublishedUpdated()
131 } 131 }
132 132
133 resetField (fieldName: string, value?: any) { 133 resetField (fieldName: string, value?: any) {
@@ -136,7 +136,7 @@ export class SearchFiltersComponent implements OnInit {
136 136
137 resetLocalField (fieldName: string, value?: any) { 137 resetLocalField (fieldName: string, value?: any) {
138 this[fieldName] = value 138 this[fieldName] = value
139 this.onInputUpdated() 139 this.onDurationOrPublishedUpdated()
140 } 140 }
141 141
142 resetOriginalPublicationYears () { 142 resetOriginalPublicationYears () {
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index b28abca6a..dc8b4d595 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -24,6 +24,8 @@
24 24
25 <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed"> 25 <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed">
26 <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters> 26 <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters>
27
28 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
27 </div> 29 </div>
28 </div> 30 </div>
29 31
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss
index fca704d27..b521825e5 100644
--- a/client/src/app/+search/search.component.scss
+++ b/client/src/app/+search/search.component.scss
@@ -15,6 +15,10 @@
15 padding: 40px; 15 padding: 40px;
16} 16}
17 17
18.alert-danger {
19 margin-top: 10px;
20}
21
18.results-header { 22.results-header {
19 font-size: 16px; 23 font-size: 16px;
20 padding-bottom: 20px; 24 padding-bottom: 20px;
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index 235bbfa4c..250062e0c 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -4,6 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
6import { immutableAssign } from '@app/helpers' 6import { immutableAssign } from '@app/helpers'
7import { validateHost } from '@app/shared/form-validators/host-validators'
7import { Video, VideoChannel } from '@app/shared/shared-main' 8import { Video, VideoChannel } from '@app/shared/shared-main'
8import { AdvancedSearch, SearchService } from '@app/shared/shared-search' 9import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
9import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' 10import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
@@ -16,6 +17,8 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models'
16 templateUrl: './search.component.html' 17 templateUrl: './search.component.html'
17}) 18})
18export class SearchComponent implements OnInit, OnDestroy { 19export class SearchComponent implements OnInit, OnDestroy {
20 error: string
21
19 results: (Video | VideoChannel)[] = [] 22 results: (Video | VideoChannel)[] = []
20 23
21 pagination = { 24 pagination = {
@@ -89,8 +92,10 @@ export class SearchComponent implements OnInit, OnDestroy {
89 this.advancedSearch.searchTarget = this.getDefaultSearchTarget() 92 this.advancedSearch.searchTarget = this.getDefaultSearchTarget()
90 } 93 }
91 94
92 // Don't hide filters if we have some of them AND the user just came on the webpage 95 this.error = this.checkFieldsAndGetError()
93 this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() 96
97 // Don't hide filters if we have some of them AND the user just came on the webpage, or we have an error
98 this.isSearchFilterCollapsed = !this.error && (this.isInitialLoad === false || !this.advancedSearch.containsValues())
94 this.isInitialLoad = false 99 this.isInitialLoad = false
95 100
96 this.search() 101 this.search()
@@ -126,6 +131,9 @@ export class SearchComponent implements OnInit, OnDestroy {
126 } 131 }
127 132
128 search () { 133 search () {
134 this.error = this.checkFieldsAndGetError()
135 if (this.error) return
136
129 this.isSearching = true 137 this.isSearching = true
130 138
131 forkJoin([ 139 forkJoin([
@@ -280,7 +288,7 @@ export class SearchComponent implements OnInit, OnDestroy {
280 const params = { 288 const params = {
281 search: this.currentSearch, 289 search: this.currentSearch,
282 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), 290 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
283 searchTarget: this.advancedSearch.searchTarget 291 advancedSearch: this.advancedSearch
284 } 292 }
285 293
286 return this.hooks.wrapObsFun( 294 return this.hooks.wrapObsFun(
@@ -298,7 +306,7 @@ export class SearchComponent implements OnInit, OnDestroy {
298 const params = { 306 const params = {
299 search: this.currentSearch, 307 search: this.currentSearch,
300 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), 308 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
301 searchTarget: this.advancedSearch.searchTarget 309 advancedSearch: this.advancedSearch
302 } 310 }
303 311
304 return this.hooks.wrapObsFun( 312 return this.hooks.wrapObsFun(
@@ -319,4 +327,12 @@ export class SearchComponent implements OnInit, OnDestroy {
319 327
320 return 'local' 328 return 'local'
321 } 329 }
330
331 private checkFieldsAndGetError () {
332 if (this.advancedSearch.host && !validateHost(this.advancedSearch.host)) {
333 return $localize`PeerTube instance host filter is invalid`
334 }
335
336 return undefined
337 }
322} 338}
diff --git a/client/src/app/shared/form-validators/host-validators.ts b/client/src/app/shared/form-validators/host-validators.ts
index d750113ef..6f410a50a 100644
--- a/client/src/app/shared/form-validators/host-validators.ts
+++ b/client/src/app/shared/form-validators/host-validators.ts
@@ -1,7 +1,7 @@
1import { AbstractControl, ValidatorFn, Validators } from '@angular/forms' 1import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model' 2import { BuildFormValidator } from './form-validator.model'
3 3
4function validateHost (value: string) { 4export function validateHost (value: string) {
5 // Thanks to http://stackoverflow.com/a/106223 5 // Thanks to http://stackoverflow.com/a/106223
6 const HOST_REGEXP = new RegExp( 6 const HOST_REGEXP = new RegExp(
7 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' 7 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
@@ -10,7 +10,7 @@ function validateHost (value: string) {
10 return HOST_REGEXP.test(value) 10 return HOST_REGEXP.test(value)
11} 11}
12 12
13function validateHandle (value: string) { 13export function validateHandle (value: string) {
14 if (!value) return false 14 if (!value) return false
15 15
16 return value.includes('@') 16 return value.includes('@')
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 2c83f53b6..9c55f6cd8 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -1,4 +1,11 @@
1import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' 1import {
2 BooleanBothQuery,
3 BooleanQuery,
4 SearchTargetType,
5 VideoChannelsSearchQuery,
6 VideoPlaylistsSearchQuery,
7 VideosSearchQuery
8} from '@shared/models'
2 9
3export class AdvancedSearch { 10export class AdvancedSearch {
4 startDate: string // ISO 8601 11 startDate: string // ISO 8601
@@ -23,6 +30,8 @@ export class AdvancedSearch {
23 30
24 isLive: BooleanQuery 31 isLive: BooleanQuery
25 32
33 host: string
34
26 sort: string 35 sort: string
27 36
28 searchTarget: SearchTargetType 37 searchTarget: SearchTargetType
@@ -45,6 +54,8 @@ export class AdvancedSearch {
45 54
46 isLive?: BooleanQuery 55 isLive?: BooleanQuery
47 56
57 host?: string
58
48 durationMin?: string 59 durationMin?: string
49 durationMax?: string 60 durationMax?: string
50 sort?: string 61 sort?: string
@@ -68,6 +79,8 @@ export class AdvancedSearch {
68 this.durationMin = parseInt(options.durationMin, 10) 79 this.durationMin = parseInt(options.durationMin, 10)
69 this.durationMax = parseInt(options.durationMax, 10) 80 this.durationMax = parseInt(options.durationMax, 10)
70 81
82 this.host = options.host || undefined
83
71 this.searchTarget = options.searchTarget || undefined 84 this.searchTarget = options.searchTarget || undefined
72 85
73 if (isNaN(this.durationMin)) this.durationMin = undefined 86 if (isNaN(this.durationMin)) this.durationMin = undefined
@@ -101,6 +114,7 @@ export class AdvancedSearch {
101 this.durationMin = undefined 114 this.durationMin = undefined
102 this.durationMax = undefined 115 this.durationMax = undefined
103 this.isLive = undefined 116 this.isLive = undefined
117 this.host = undefined
104 118
105 this.sort = '-match' 119 this.sort = '-match'
106 } 120 }
@@ -120,12 +134,13 @@ export class AdvancedSearch {
120 durationMin: this.durationMin, 134 durationMin: this.durationMin,
121 durationMax: this.durationMax, 135 durationMax: this.durationMax,
122 isLive: this.isLive, 136 isLive: this.isLive,
137 host: this.host,
123 sort: this.sort, 138 sort: this.sort,
124 searchTarget: this.searchTarget 139 searchTarget: this.searchTarget
125 } 140 }
126 } 141 }
127 142
128 toAPIObject (): VideosSearchQuery { 143 toVideosAPIObject (): VideosSearchQuery {
129 let isLive: boolean 144 let isLive: boolean
130 if (this.isLive) isLive = this.isLive === 'true' 145 if (this.isLive) isLive = this.isLive === 'true'
131 146
@@ -142,12 +157,27 @@ export class AdvancedSearch {
142 tagsAllOf: this.tagsAllOf, 157 tagsAllOf: this.tagsAllOf,
143 durationMin: this.durationMin, 158 durationMin: this.durationMin,
144 durationMax: this.durationMax, 159 durationMax: this.durationMax,
160 host: this.host,
145 isLive, 161 isLive,
146 sort: this.sort, 162 sort: this.sort,
147 searchTarget: this.searchTarget 163 searchTarget: this.searchTarget
148 } 164 }
149 } 165 }
150 166
167 toPlaylistAPIObject (): VideoPlaylistsSearchQuery {
168 return {
169 host: this.host,
170 searchTarget: this.searchTarget
171 }
172 }
173
174 toChannelAPIObject (): VideoChannelsSearchQuery {
175 return {
176 host: this.host,
177 searchTarget: this.searchTarget
178 }
179 }
180
151 size () { 181 size () {
152 let acc = 0 182 let acc = 0
153 183
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index ad258f5e5..2c26eb2e5 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -7,7 +7,6 @@ import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/sha
7import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 7import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
8import { 8import {
9 ResultList, 9 ResultList,
10 SearchTargetType,
11 Video as VideoServerModel, 10 Video as VideoServerModel,
12 VideoChannel as VideoChannelServerModel, 11 VideoChannel as VideoChannelServerModel,
13 VideoPlaylist as VideoPlaylistServerModel 12 VideoPlaylist as VideoPlaylistServerModel
@@ -33,8 +32,8 @@ export class SearchService {
33 } 32 }
34 33
35 searchVideos (parameters: { 34 searchVideos (parameters: {
36 search: string, 35 search: string
37 componentPagination?: ComponentPaginationLight, 36 componentPagination?: ComponentPaginationLight
38 advancedSearch?: AdvancedSearch 37 advancedSearch?: AdvancedSearch
39 }): Observable<ResultList<Video>> { 38 }): Observable<ResultList<Video>> {
40 const { search, componentPagination, advancedSearch } = parameters 39 const { search, componentPagination, advancedSearch } = parameters
@@ -52,7 +51,7 @@ export class SearchService {
52 if (search) params = params.append('search', search) 51 if (search) params = params.append('search', search)
53 52
54 if (advancedSearch) { 53 if (advancedSearch) {
55 const advancedSearchObject = advancedSearch.toAPIObject() 54 const advancedSearchObject = advancedSearch.toVideosAPIObject()
56 params = this.restService.addObjectParams(params, advancedSearchObject) 55 params = this.restService.addObjectParams(params, advancedSearchObject)
57 } 56 }
58 57
@@ -65,11 +64,11 @@ export class SearchService {
65 } 64 }
66 65
67 searchVideoChannels (parameters: { 66 searchVideoChannels (parameters: {
68 search: string, 67 search: string
69 searchTarget?: SearchTargetType, 68 advancedSearch?: AdvancedSearch
70 componentPagination?: ComponentPaginationLight 69 componentPagination?: ComponentPaginationLight
71 }): Observable<ResultList<VideoChannel>> { 70 }): Observable<ResultList<VideoChannel>> {
72 const { search, componentPagination, searchTarget } = parameters 71 const { search, advancedSearch, componentPagination } = parameters
73 72
74 const url = SearchService.BASE_SEARCH_URL + 'video-channels' 73 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
75 74
@@ -82,8 +81,9 @@ export class SearchService {
82 params = this.restService.addRestGetParams(params, pagination) 81 params = this.restService.addRestGetParams(params, pagination)
83 params = params.append('search', search) 82 params = params.append('search', search)
84 83
85 if (searchTarget) { 84 if (advancedSearch) {
86 params = params.append('searchTarget', searchTarget as string) 85 const advancedSearchObject = advancedSearch.toChannelAPIObject()
86 params = this.restService.addObjectParams(params, advancedSearchObject)
87 } 87 }
88 88
89 return this.authHttp 89 return this.authHttp
@@ -95,11 +95,11 @@ export class SearchService {
95 } 95 }
96 96
97 searchVideoPlaylists (parameters: { 97 searchVideoPlaylists (parameters: {
98 search: string, 98 search: string
99 searchTarget?: SearchTargetType, 99 advancedSearch?: AdvancedSearch
100 componentPagination?: ComponentPaginationLight 100 componentPagination?: ComponentPaginationLight
101 }): Observable<ResultList<VideoPlaylist>> { 101 }): Observable<ResultList<VideoPlaylist>> {
102 const { search, componentPagination, searchTarget } = parameters 102 const { search, advancedSearch, componentPagination } = parameters
103 103
104 const url = SearchService.BASE_SEARCH_URL + 'video-playlists' 104 const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
105 105
@@ -112,8 +112,9 @@ export class SearchService {
112 params = this.restService.addRestGetParams(params, pagination) 112 params = this.restService.addRestGetParams(params, pagination)
113 params = params.append('search', search) 113 params = params.append('search', search)
114 114
115 if (searchTarget) { 115 if (advancedSearch) {
116 params = params.append('searchTarget', searchTarget as string) 116 const advancedSearchObject = advancedSearch.toPlaylistAPIObject()
117 params = this.restService.addObjectParams(params, advancedSearchObject)
117 } 118 }
118 119
119 return this.authHttp 120 return this.authHttp