diff options
author | Chocobozzz <me@florianbigard.com> | 2020-05-29 16:16:24 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-06-10 14:02:41 +0200 |
commit | 5fb2e2888ce032c638e4b75d07458642f0833e52 (patch) | |
tree | 8830d873569316889b8134027e9a43b198cca38f /client/src/app/search | |
parent | 62e7be634bc189f942ae51cb4b080079ab503ff0 (diff) | |
download | PeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.tar.gz PeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.tar.zst PeerTube-5fb2e2888ce032c638e4b75d07458642f0833e52.zip |
First implem global search
Diffstat (limited to 'client/src/app/search')
-rw-r--r-- | client/src/app/search/advanced-search.model.ts | 21 | ||||
-rw-r--r-- | client/src/app/search/channel-lazy-load.resolver.ts | 45 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.html | 64 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.ts | 8 | ||||
-rw-r--r-- | client/src/app/search/search-routing.module.ts | 20 | ||||
-rw-r--r-- | client/src/app/search/search.component.html | 15 | ||||
-rw-r--r-- | client/src/app/search/search.component.ts | 98 | ||||
-rw-r--r-- | client/src/app/search/search.module.ts | 14 | ||||
-rw-r--r-- | client/src/app/search/search.service.ts | 48 | ||||
-rw-r--r-- | client/src/app/search/video-lazy-load.resolver.ts | 43 |
10 files changed, 290 insertions, 86 deletions
diff --git a/client/src/app/search/advanced-search.model.ts b/client/src/app/search/advanced-search.model.ts index 50f00bc27..643cc9a29 100644 --- a/client/src/app/search/advanced-search.model.ts +++ b/client/src/app/search/advanced-search.model.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { SearchTargetType } from '@shared/models/search/search-target-query.model' | ||
1 | import { NSFWQuery } from '../../../../shared/models/search' | 2 | import { NSFWQuery } from '../../../../shared/models/search' |
2 | 3 | ||
3 | export class AdvancedSearch { | 4 | export class AdvancedSearch { |
@@ -23,6 +24,11 @@ export class AdvancedSearch { | |||
23 | 24 | ||
24 | sort: string | 25 | sort: string |
25 | 26 | ||
27 | searchTarget: SearchTargetType | ||
28 | |||
29 | // Filters we don't want to count, because they are mandatory | ||
30 | private silentFilters = new Set([ 'sort', 'searchTarget' ]) | ||
31 | |||
26 | constructor (options?: { | 32 | constructor (options?: { |
27 | startDate?: string | 33 | startDate?: string |
28 | endDate?: string | 34 | endDate?: string |
@@ -37,6 +43,7 @@ export class AdvancedSearch { | |||
37 | durationMin?: string | 43 | durationMin?: string |
38 | durationMax?: string | 44 | durationMax?: string |
39 | sort?: string | 45 | sort?: string |
46 | searchTarget?: SearchTargetType | ||
40 | }) { | 47 | }) { |
41 | if (!options) return | 48 | if (!options) return |
42 | 49 | ||
@@ -54,6 +61,8 @@ export class AdvancedSearch { | |||
54 | this.durationMin = parseInt(options.durationMin, 10) | 61 | this.durationMin = parseInt(options.durationMin, 10) |
55 | this.durationMax = parseInt(options.durationMax, 10) | 62 | this.durationMax = parseInt(options.durationMax, 10) |
56 | 63 | ||
64 | this.searchTarget = options.searchTarget || undefined | ||
65 | |||
57 | if (isNaN(this.durationMin)) this.durationMin = undefined | 66 | if (isNaN(this.durationMin)) this.durationMin = undefined |
58 | if (isNaN(this.durationMax)) this.durationMax = undefined | 67 | if (isNaN(this.durationMax)) this.durationMax = undefined |
59 | 68 | ||
@@ -61,9 +70,11 @@ export class AdvancedSearch { | |||
61 | } | 70 | } |
62 | 71 | ||
63 | containsValues () { | 72 | containsValues () { |
73 | const exceptions = new Set([ 'sort', 'searchTarget' ]) | ||
74 | |||
64 | const obj = this.toUrlObject() | 75 | const obj = this.toUrlObject() |
65 | for (const k of Object.keys(obj)) { | 76 | for (const k of Object.keys(obj)) { |
66 | if (k === 'sort') continue // Exception | 77 | if (this.silentFilters.has(k)) continue |
67 | 78 | ||
68 | if (obj[k] !== undefined && obj[k] !== '') return true | 79 | if (obj[k] !== undefined && obj[k] !== '') return true |
69 | } | 80 | } |
@@ -102,7 +113,8 @@ export class AdvancedSearch { | |||
102 | tagsAllOf: this.tagsAllOf, | 113 | tagsAllOf: this.tagsAllOf, |
103 | durationMin: this.durationMin, | 114 | durationMin: this.durationMin, |
104 | durationMax: this.durationMax, | 115 | durationMax: this.durationMax, |
105 | sort: this.sort | 116 | sort: this.sort, |
117 | searchTarget: this.searchTarget | ||
106 | } | 118 | } |
107 | } | 119 | } |
108 | 120 | ||
@@ -120,7 +132,8 @@ export class AdvancedSearch { | |||
120 | tagsAllOf: this.intoArray(this.tagsAllOf), | 132 | tagsAllOf: this.intoArray(this.tagsAllOf), |
121 | durationMin: this.durationMin, | 133 | durationMin: this.durationMin, |
122 | durationMax: this.durationMax, | 134 | durationMax: this.durationMax, |
123 | sort: this.sort | 135 | sort: this.sort, |
136 | searchTarget: this.searchTarget | ||
124 | } | 137 | } |
125 | } | 138 | } |
126 | 139 | ||
@@ -129,7 +142,7 @@ export class AdvancedSearch { | |||
129 | 142 | ||
130 | const obj = this.toUrlObject() | 143 | const obj = this.toUrlObject() |
131 | for (const k of Object.keys(obj)) { | 144 | for (const k of Object.keys(obj)) { |
132 | if (k === 'sort') continue // Exception | 145 | if (this.silentFilters.has(k)) continue |
133 | 146 | ||
134 | if (obj[k] !== undefined && obj[k] !== '') acc++ | 147 | if (obj[k] !== undefined && obj[k] !== '') acc++ |
135 | } | 148 | } |
diff --git a/client/src/app/search/channel-lazy-load.resolver.ts b/client/src/app/search/channel-lazy-load.resolver.ts new file mode 100644 index 000000000..8be089cdd --- /dev/null +++ b/client/src/app/search/channel-lazy-load.resolver.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | import { map } from 'rxjs/operators' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' | ||
4 | import { SearchService } from './search.service' | ||
5 | import { RedirectService } from '@app/core' | ||
6 | |||
7 | @Injectable() | ||
8 | export class ChannelLazyLoadResolver implements Resolve<any> { | ||
9 | constructor ( | ||
10 | private router: Router, | ||
11 | private searchService: SearchService, | ||
12 | private redirectService: RedirectService | ||
13 | ) { } | ||
14 | |||
15 | resolve (route: ActivatedRouteSnapshot) { | ||
16 | const url = route.params.url | ||
17 | const externalRedirect = route.params.externalRedirect | ||
18 | const fromPath = route.params.fromPath | ||
19 | |||
20 | if (!url) { | ||
21 | console.error('Could not find url param.', { params: route.params }) | ||
22 | return this.router.navigateByUrl('/404') | ||
23 | } | ||
24 | |||
25 | if (externalRedirect === 'true') { | ||
26 | window.open(url) | ||
27 | this.router.navigateByUrl(fromPath) | ||
28 | return | ||
29 | } | ||
30 | |||
31 | return this.searchService.searchVideoChannels({ search: url }) | ||
32 | .pipe( | ||
33 | map(result => { | ||
34 | if (result.data.length !== 1) { | ||
35 | console.error('Cannot find result for this URL') | ||
36 | return this.router.navigateByUrl('/404') | ||
37 | } | ||
38 | |||
39 | const channel = result.data[0] | ||
40 | |||
41 | return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost) | ||
42 | }) | ||
43 | ) | ||
44 | } | ||
45 | } | ||
diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html index 54fc7338f..e20aef8fb 100644 --- a/client/src/app/search/search-filters.component.html +++ b/client/src/app/search/search-filters.component.html | |||
@@ -18,6 +18,25 @@ | |||
18 | 18 | ||
19 | <div class="form-group"> | 19 | <div class="form-group"> |
20 | <div class="radio-label label-container"> | 20 | <div class="radio-label label-container"> |
21 | <label i18n>Display sensitive content</label> | ||
22 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> | ||
23 | Reset | ||
24 | </button> | ||
25 | </div> | ||
26 | |||
27 | <div class="peertube-radio-container"> | ||
28 | <input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw"> | ||
29 | <label i18n for="sensitiveContentYes" class="radio">Yes</label> | ||
30 | </div> | ||
31 | |||
32 | <div class="peertube-radio-container"> | ||
33 | <input type="radio" name="sensitiveContent" id="sensitiveContentNo" value="false" [(ngModel)]="advancedSearch.nsfw"> | ||
34 | <label i18n for="sensitiveContentNo" class="radio">No</label> | ||
35 | </div> | ||
36 | </div> | ||
37 | |||
38 | <div class="form-group"> | ||
39 | <div class="radio-label label-container"> | ||
21 | <label i18n>Published date</label> | 40 | <label i18n>Published date</label> |
22 | <button i18n class="reset-button reset-button-small" (click)="resetLocalField('publishedDateRange')" *ngIf="publishedDateRange !== undefined"> | 41 | <button i18n class="reset-button reset-button-small" (click)="resetLocalField('publishedDateRange')" *ngIf="publishedDateRange !== undefined"> |
23 | Reset | 42 | Reset |
@@ -39,7 +58,7 @@ | |||
39 | </div> | 58 | </div> |
40 | 59 | ||
41 | <div class="row"> | 60 | <div class="row"> |
42 | <div class="col-sm-6"> | 61 | <div class="pl-0 col-sm-6"> |
43 | <input | 62 | <input |
44 | (change)="inputUpdated()" | 63 | (change)="inputUpdated()" |
45 | (keydown.enter)="$event.preventDefault()" | 64 | (keydown.enter)="$event.preventDefault()" |
@@ -49,7 +68,7 @@ | |||
49 | class="form-control" | 68 | class="form-control" |
50 | > | 69 | > |
51 | </div> | 70 | </div> |
52 | <div class="col-sm-6"> | 71 | <div class="pr-0 col-sm-6"> |
53 | <input | 72 | <input |
54 | (change)="inputUpdated()" | 73 | (change)="inputUpdated()" |
55 | (keydown.enter)="$event.preventDefault()" | 74 | (keydown.enter)="$event.preventDefault()" |
@@ -62,6 +81,9 @@ | |||
62 | </div> | 81 | </div> |
63 | </div> | 82 | </div> |
64 | 83 | ||
84 | </div> | ||
85 | |||
86 | <div class="col-lg-4 col-md-6 col-xs-12"> | ||
65 | <div class="form-group"> | 87 | <div class="form-group"> |
66 | <div class="radio-label label-container"> | 88 | <div class="radio-label label-container"> |
67 | <label i18n>Duration</label> | 89 | <label i18n>Duration</label> |
@@ -77,28 +99,6 @@ | |||
77 | </div> | 99 | </div> |
78 | 100 | ||
79 | <div class="form-group"> | 101 | <div class="form-group"> |
80 | <div class="radio-label label-container"> | ||
81 | <label i18n>Display sensitive content</label> | ||
82 | <button i18n class="reset-button reset-button-small" (click)="resetField('nsfw')" *ngIf="advancedSearch.nsfw !== undefined"> | ||
83 | Reset | ||
84 | </button> | ||
85 | </div> | ||
86 | |||
87 | <div class="peertube-radio-container"> | ||
88 | <input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw"> | ||
89 | <label i18n for="sensitiveContentYes" class="radio">Yes</label> | ||
90 | </div> | ||
91 | |||
92 | <div class="peertube-radio-container"> | ||
93 | <input type="radio" name="sensitiveContent" id="sensitiveContentNo" value="false" [(ngModel)]="advancedSearch.nsfw"> | ||
94 | <label i18n for="sensitiveContentNo" class="radio">No</label> | ||
95 | </div> | ||
96 | </div> | ||
97 | |||
98 | </div> | ||
99 | |||
100 | <div class="col-lg-4 col-md-6 col-xs-12"> | ||
101 | <div class="form-group"> | ||
102 | <label i18n for="category">Category</label> | 102 | <label i18n for="category">Category</label> |
103 | <button i18n class="reset-button reset-button-small" (click)="resetField('categoryOneOf')" *ngIf="advancedSearch.categoryOneOf !== undefined"> | 103 | <button i18n class="reset-button reset-button-small" (click)="resetField('categoryOneOf')" *ngIf="advancedSearch.categoryOneOf !== undefined"> |
104 | Reset | 104 | Reset |
@@ -164,6 +164,22 @@ | |||
164 | [maxItems]="5" [modelAsStrings]="true" | 164 | [maxItems]="5" [modelAsStrings]="true" |
165 | ></tag-input> | 165 | ></tag-input> |
166 | </div> | 166 | </div> |
167 | |||
168 | <div class="form-group" *ngIf="isSearchTargetEnabled()"> | ||
169 | <div class="radio-label label-container"> | ||
170 | <label i18n>Search target</label> | ||
171 | </div> | ||
172 | |||
173 | <div class="peertube-radio-container"> | ||
174 | <input type="radio" name="searchTarget" id="searchTargetLocal" value="local" [(ngModel)]="advancedSearch.searchTarget"> | ||
175 | <label i18n for="searchTargetLocal" class="radio">Instance</label> | ||
176 | </div> | ||
177 | |||
178 | <div class="peertube-radio-container"> | ||
179 | <input type="radio" name="searchTarget" id="searchTargetSearchIndex" value="search-index" [(ngModel)]="advancedSearch.searchTarget"> | ||
180 | <label i18n for="searchTargetSearchIndex" class="radio">Vidiverse</label> | ||
181 | </div> | ||
182 | </div> | ||
167 | </div> | 183 | </div> |
168 | </div> | 184 | </div> |
169 | 185 | ||
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts index 344a260df..af76260a7 100644 --- a/client/src/app/search/search-filters.component.ts +++ b/client/src/app/search/search-filters.component.ts | |||
@@ -44,7 +44,7 @@ export class SearchFiltersComponent implements OnInit { | |||
44 | this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES | 44 | this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES |
45 | this.publishedDateRanges = [ | 45 | this.publishedDateRanges = [ |
46 | { | 46 | { |
47 | id: undefined, | 47 | id: 'any_published_date', |
48 | label: this.i18n('Any') | 48 | label: this.i18n('Any') |
49 | }, | 49 | }, |
50 | { | 50 | { |
@@ -67,7 +67,7 @@ export class SearchFiltersComponent implements OnInit { | |||
67 | 67 | ||
68 | this.durationRanges = [ | 68 | this.durationRanges = [ |
69 | { | 69 | { |
70 | id: undefined, | 70 | id: 'any_duration', |
71 | label: this.i18n('Any') | 71 | label: this.i18n('Any') |
72 | }, | 72 | }, |
73 | { | 73 | { |
@@ -147,6 +147,10 @@ export class SearchFiltersComponent implements OnInit { | |||
147 | this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined | 147 | this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined |
148 | } | 148 | } |
149 | 149 | ||
150 | isSearchTargetEnabled () { | ||
151 | return this.serverConfig.search.searchIndex.enabled && this.serverConfig.search.searchIndex.disableLocalSearch !== true | ||
152 | } | ||
153 | |||
150 | private loadOriginallyPublishedAtYears () { | 154 | private loadOriginallyPublishedAtYears () { |
151 | this.originallyPublishedStartYear = this.advancedSearch.originallyPublishedStartDate | 155 | this.originallyPublishedStartYear = this.advancedSearch.originallyPublishedStartDate |
152 | ? new Date(this.advancedSearch.originallyPublishedStartDate).getFullYear().toString() | 156 | ? new Date(this.advancedSearch.originallyPublishedStartDate).getFullYear().toString() |
diff --git a/client/src/app/search/search-routing.module.ts b/client/src/app/search/search-routing.module.ts index 0ac9e6b57..9da900e9a 100644 --- a/client/src/app/search/search-routing.module.ts +++ b/client/src/app/search/search-routing.module.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | ||
4 | import { SearchComponent } from '@app/search/search.component' | 3 | import { SearchComponent } from '@app/search/search.component' |
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | import { VideoLazyLoadResolver } from './video-lazy-load.resolver' | ||
6 | import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' | ||
5 | 7 | ||
6 | const searchRoutes: Routes = [ | 8 | const searchRoutes: Routes = [ |
7 | { | 9 | { |
@@ -13,6 +15,22 @@ const searchRoutes: Routes = [ | |||
13 | title: 'Search' | 15 | title: 'Search' |
14 | } | 16 | } |
15 | } | 17 | } |
18 | }, | ||
19 | { | ||
20 | path: 'search/lazy-load-video', | ||
21 | component: SearchComponent, | ||
22 | canActivate: [ MetaGuard ], | ||
23 | resolve: { | ||
24 | data: VideoLazyLoadResolver | ||
25 | } | ||
26 | }, | ||
27 | { | ||
28 | path: 'search/lazy-load-channel', | ||
29 | component: SearchComponent, | ||
30 | canActivate: [ MetaGuard ], | ||
31 | resolve: { | ||
32 | data: ChannelLazyLoadResolver | ||
33 | } | ||
16 | } | 34 | } |
17 | ] | 35 | ] |
18 | 36 | ||
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index a4a1d41b3..3cafc676d 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html | |||
@@ -2,7 +2,11 @@ | |||
2 | <div class="results-header"> | 2 | <div class="results-header"> |
3 | <div class="first-line"> | 3 | <div class="first-line"> |
4 | <div class="results-counter" *ngIf="pagination.totalItems"> | 4 | <div class="results-counter" *ngIf="pagination.totalItems"> |
5 | <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span> | 5 | <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span> |
6 | |||
7 | <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> | ||
8 | <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> | ||
9 | |||
6 | <span *ngIf="currentSearch" i18n> | 10 | <span *ngIf="currentSearch" i18n> |
7 | for <span class="search-value">{{ currentSearch }}</span> | 11 | for <span class="search-value">{{ currentSearch }}</span> |
8 | </span> | 12 | </span> |
@@ -31,12 +35,12 @@ | |||
31 | 35 | ||
32 | <ng-container *ngFor="let result of results"> | 36 | <ng-container *ngFor="let result of results"> |
33 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> | 37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> |
34 | <a [routerLink]="[ '/video-channels', result.nameWithHost ]"> | 38 | <a [routerLink]="getChannelUrl(result)"> |
35 | <img [src]="result.avatarUrl" alt="Avatar" /> | 39 | <img [src]="result.avatarUrl" alt="Avatar" /> |
36 | </a> | 40 | </a> |
37 | 41 | ||
38 | <div class="video-channel-info"> | 42 | <div class="video-channel-info"> |
39 | <a [routerLink]="[ '/video-channels', result.nameWithHost ]" class="video-channel-names"> | 43 | <a [routerLink]="getChannelUrl(result)" class="video-channel-names"> |
40 | <div class="video-channel-display-name">{{ result.displayName }}</div> | 44 | <div class="video-channel-display-name">{{ result.displayName }}</div> |
41 | <div class="video-channel-name">{{ result.nameWithHost }}</div> | 45 | <div class="video-channel-name">{{ result.nameWithHost }}</div> |
42 | </a> | 46 | </a> |
@@ -44,12 +48,13 @@ | |||
44 | <div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div> | 48 | <div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div> |
45 | </div> | 49 | </div> |
46 | 50 | ||
47 | <my-subscribe-button [videoChannels]="[result]"></my-subscribe-button> | 51 | <my-subscribe-button *ngIf="!hideActions()" [videoChannels]="[result]"></my-subscribe-button> |
48 | </div> | 52 | </div> |
49 | 53 | ||
50 | <div *ngIf="isVideo(result)" class="entry video"> | 54 | <div *ngIf="isVideo(result)" class="entry video"> |
51 | <my-video-miniature | 55 | <my-video-miniature |
52 | [video]="result" [user]="user" [displayAsRow]="true" | 56 | [video]="result" [user]="user" [displayAsRow]="true" [displayVideoActions]="!hideActions()" |
57 | [useLazyLoadUrl]="advancedSearch.searchTarget === 'search-index'" | ||
53 | (videoBlacklisted)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)" | 58 | (videoBlacklisted)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)" |
54 | ></my-video-miniature> | 59 | ></my-video-miniature> |
55 | </div> | 60 | </div> |
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index 075994dd3..d3c0761d7 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts | |||
@@ -1,16 +1,18 @@ | |||
1 | import { forkJoin, of, Subscription } from 'rxjs' | ||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 2 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier } from '@app/core' | 4 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { forkJoin, of, Subscription } from 'rxjs' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
5 | import { SearchService } from '@app/search/search.service' | 7 | import { SearchService } from '@app/search/search.service' |
8 | import { immutableAssign } from '@app/shared/misc/utils' | ||
6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 9 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
8 | import { MetaService } from '@ngx-meta/core' | ||
9 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
10 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 10 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
11 | import { immutableAssign } from '@app/shared/misc/utils' | ||
12 | import { Video } from '@app/shared/video/video.model' | 11 | import { Video } from '@app/shared/video/video.model' |
13 | import { HooksService } from '@app/core/plugins/hooks.service' | 12 | import { MetaService } from '@ngx-meta/core' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
14 | import { ServerConfig } from '@shared/models' | ||
15 | import { UserService } from '@app/shared' | ||
14 | 16 | ||
15 | @Component({ | 17 | @Component({ |
16 | selector: 'my-search', | 18 | selector: 'my-search', |
@@ -29,6 +31,9 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
29 | isSearchFilterCollapsed = true | 31 | isSearchFilterCollapsed = true |
30 | currentSearch: string | 32 | currentSearch: string |
31 | 33 | ||
34 | errorMessage: string | ||
35 | serverConfig: ServerConfig | ||
36 | |||
32 | private subActivatedRoute: Subscription | 37 | private subActivatedRoute: Subscription |
33 | private isInitialLoad = false // set to false to show the search filters on first arrival | 38 | private isInitialLoad = false // set to false to show the search filters on first arrival |
34 | private firstSearch = true | 39 | private firstSearch = true |
@@ -43,7 +48,8 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
43 | private notifier: Notifier, | 48 | private notifier: Notifier, |
44 | private searchService: SearchService, | 49 | private searchService: SearchService, |
45 | private authService: AuthService, | 50 | private authService: AuthService, |
46 | private hooks: HooksService | 51 | private hooks: HooksService, |
52 | private serverService: ServerService | ||
47 | ) { } | 53 | ) { } |
48 | 54 | ||
49 | get user () { | 55 | get user () { |
@@ -51,8 +57,11 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
51 | } | 57 | } |
52 | 58 | ||
53 | ngOnInit () { | 59 | ngOnInit () { |
60 | this.serverService.getConfig() | ||
61 | .subscribe(config => this.serverConfig = config) | ||
62 | |||
54 | this.subActivatedRoute = this.route.queryParams.subscribe( | 63 | this.subActivatedRoute = this.route.queryParams.subscribe( |
55 | queryParams => { | 64 | async queryParams => { |
56 | const querySearch = queryParams['search'] | 65 | const querySearch = queryParams['search'] |
57 | 66 | ||
58 | // Search updated, reset filters | 67 | // Search updated, reset filters |
@@ -65,6 +74,9 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
65 | } | 74 | } |
66 | 75 | ||
67 | this.advancedSearch = new AdvancedSearch(queryParams) | 76 | this.advancedSearch = new AdvancedSearch(queryParams) |
77 | if (!this.advancedSearch.searchTarget) { | ||
78 | this.advancedSearch.searchTarget = await this.serverService.getDefaultSearchTarget() | ||
79 | } | ||
68 | 80 | ||
69 | // Don't hide filters if we have some of them AND the user just came on the webpage | 81 | // Don't hide filters if we have some of them AND the user just came on the webpage |
70 | this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() | 82 | this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() |
@@ -99,28 +111,37 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
99 | forkJoin([ | 111 | forkJoin([ |
100 | this.getVideosObs(), | 112 | this.getVideosObs(), |
101 | this.getVideoChannelObs() | 113 | this.getVideoChannelObs() |
102 | ]) | 114 | ]).subscribe( |
103 | .subscribe( | 115 | ([videosResult, videoChannelsResult]) => { |
104 | ([ videosResult, videoChannelsResult ]) => { | 116 | this.results = this.results |
105 | this.results = this.results | 117 | .concat(videoChannelsResult.data) |
106 | .concat(videoChannelsResult.data) | 118 | .concat(videosResult.data) |
107 | .concat(videosResult.data) | 119 | |
108 | this.pagination.totalItems = videosResult.total + videoChannelsResult.total | 120 | this.pagination.totalItems = videosResult.total + videoChannelsResult.total |
109 | |||
110 | // Focus on channels if there are no enough videos | ||
111 | if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { | ||
112 | this.resetPagination() | ||
113 | this.firstSearch = false | ||
114 | |||
115 | this.channelsPerPage = 10 | ||
116 | this.search() | ||
117 | } | ||
118 | 121 | ||
122 | // Focus on channels if there are no enough videos | ||
123 | if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { | ||
124 | this.resetPagination() | ||
119 | this.firstSearch = false | 125 | this.firstSearch = false |
120 | }, | ||
121 | 126 | ||
122 | err => this.notifier.error(err.message) | 127 | this.channelsPerPage = 10 |
123 | ) | 128 | this.search() |
129 | } | ||
130 | |||
131 | this.firstSearch = false | ||
132 | }, | ||
133 | |||
134 | err => { | ||
135 | if (this.advancedSearch.searchTarget !== 'search-index') this.notifier.error(err.message) | ||
136 | |||
137 | this.notifier.error( | ||
138 | this.i18n('Search index is unavailable. Retrying with instance results instead.'), | ||
139 | this.i18n('Search error') | ||
140 | ) | ||
141 | this.advancedSearch.searchTarget = 'local' | ||
142 | this.search() | ||
143 | } | ||
144 | ) | ||
124 | } | 145 | } |
125 | 146 | ||
126 | onNearOfBottom () { | 147 | onNearOfBottom () { |
@@ -146,6 +167,24 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
146 | this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) | 167 | this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) |
147 | } | 168 | } |
148 | 169 | ||
170 | getChannelUrl (channel: VideoChannel) { | ||
171 | if (this.advancedSearch.searchTarget === 'search-index' && channel.url) { | ||
172 | const remoteUriConfig = this.serverConfig.search.remoteUri | ||
173 | |||
174 | // Redirect on the external instance if not allowed to fetch remote data | ||
175 | const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users | ||
176 | const fromPath = window.location.pathname + window.location.search | ||
177 | |||
178 | return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ] | ||
179 | } | ||
180 | |||
181 | return [ '/video-channels', channel.nameWithHost ] | ||
182 | } | ||
183 | |||
184 | hideActions () { | ||
185 | return this.advancedSearch.searchTarget === 'search-index' | ||
186 | } | ||
187 | |||
149 | private resetPagination () { | 188 | private resetPagination () { |
150 | this.pagination.currentPage = 1 | 189 | this.pagination.currentPage = 1 |
151 | this.pagination.totalItems = null | 190 | this.pagination.totalItems = null |
@@ -189,7 +228,8 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
189 | 228 | ||
190 | const params = { | 229 | const params = { |
191 | search: this.currentSearch, | 230 | search: this.currentSearch, |
192 | componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }) | 231 | componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), |
232 | searchTarget: this.advancedSearch.searchTarget | ||
193 | } | 233 | } |
194 | 234 | ||
195 | return this.hooks.wrapObsFun( | 235 | return this.hooks.wrapObsFun( |
diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts index 3b0fd6ee2..df5459802 100644 --- a/client/src/app/search/search.module.ts +++ b/client/src/app/search/search.module.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { TagInputModule } from 'ngx-chips' | 1 | import { TagInputModule } from 'ngx-chips' |
3 | import { SharedModule } from '../shared' | 2 | import { NgModule } from '@angular/core' |
3 | import { SearchFiltersComponent } from '@app/search/search-filters.component' | ||
4 | import { SearchRoutingModule } from '@app/search/search-routing.module' | ||
4 | import { SearchComponent } from '@app/search/search.component' | 5 | import { SearchComponent } from '@app/search/search.component' |
5 | import { SearchService } from '@app/search/search.service' | 6 | import { SearchService } from '@app/search/search.service' |
6 | import { SearchRoutingModule } from '@app/search/search-routing.module' | 7 | import { SharedModule } from '../shared' |
7 | import { SearchFiltersComponent } from '@app/search/search-filters.component' | 8 | import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' |
9 | import { VideoLazyLoadResolver } from './video-lazy-load.resolver' | ||
8 | 10 | ||
9 | @NgModule({ | 11 | @NgModule({ |
10 | imports: [ | 12 | imports: [ |
@@ -25,7 +27,9 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component' | |||
25 | ], | 27 | ], |
26 | 28 | ||
27 | providers: [ | 29 | providers: [ |
28 | SearchService | 30 | SearchService, |
31 | VideoLazyLoadResolver, | ||
32 | ChannelLazyLoadResolver | ||
29 | ] | 33 | ] |
30 | }) | 34 | }) |
31 | export class SearchModule { } | 35 | export class SearchModule { } |
diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts index 3cad5aaa7..fdb12ea2c 100644 --- a/client/src/app/search/search.service.ts +++ b/client/src/app/search/search.service.ts | |||
@@ -1,17 +1,18 @@ | |||
1 | import { Observable } from 'rxjs' | ||
1 | import { catchError, map, switchMap } from 'rxjs/operators' | 2 | import { catchError, map, switchMap } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
4 | import { Observable } from 'rxjs' | ||
5 | import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' | ||
6 | import { VideoService } from '@app/shared/video/video.service' | ||
7 | import { RestExtractor, RestService } from '@app/shared' | ||
8 | import { environment } from '../../environments/environment' | ||
9 | import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' | ||
10 | import { Video } from '@app/shared/video/video.model' | ||
11 | import { AdvancedSearch } from '@app/search/advanced-search.model' | 5 | import { AdvancedSearch } from '@app/search/advanced-search.model' |
6 | import { RestExtractor, RestPagination, RestService } from '@app/shared' | ||
7 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | ||
8 | import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' | ||
12 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 9 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
13 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 10 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
14 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | 11 | import { Video } from '@app/shared/video/video.model' |
12 | import { VideoService } from '@app/shared/video/video.service' | ||
13 | import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' | ||
14 | import { environment } from '../../environments/environment' | ||
15 | import { SearchTargetType } from '@shared/models/search/search-target-query.model' | ||
15 | 16 | ||
16 | @Injectable() | 17 | @Injectable() |
17 | export class SearchService { | 18 | export class SearchService { |
@@ -30,21 +31,27 @@ export class SearchService { | |||
30 | 31 | ||
31 | searchVideos (parameters: { | 32 | searchVideos (parameters: { |
32 | search: string, | 33 | search: string, |
33 | componentPagination: ComponentPaginationLight, | 34 | componentPagination?: ComponentPaginationLight, |
34 | advancedSearch: AdvancedSearch | 35 | advancedSearch?: AdvancedSearch |
35 | }): Observable<ResultList<Video>> { | 36 | }): Observable<ResultList<Video>> { |
36 | const { search, componentPagination, advancedSearch } = parameters | 37 | const { search, componentPagination, advancedSearch } = parameters |
37 | 38 | ||
38 | const url = SearchService.BASE_SEARCH_URL + 'videos' | 39 | const url = SearchService.BASE_SEARCH_URL + 'videos' |
39 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 40 | let pagination: RestPagination |
41 | |||
42 | if (componentPagination) { | ||
43 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
44 | } | ||
40 | 45 | ||
41 | let params = new HttpParams() | 46 | let params = new HttpParams() |
42 | params = this.restService.addRestGetParams(params, pagination) | 47 | params = this.restService.addRestGetParams(params, pagination) |
43 | 48 | ||
44 | if (search) params = params.append('search', search) | 49 | if (search) params = params.append('search', search) |
45 | 50 | ||
46 | const advancedSearchObject = advancedSearch.toAPIObject() | 51 | if (advancedSearch) { |
47 | params = this.restService.addObjectParams(params, advancedSearchObject) | 52 | const advancedSearchObject = advancedSearch.toAPIObject() |
53 | params = this.restService.addObjectParams(params, advancedSearchObject) | ||
54 | } | ||
48 | 55 | ||
49 | return this.authHttp | 56 | return this.authHttp |
50 | .get<ResultList<VideoServerModel>>(url, { params }) | 57 | .get<ResultList<VideoServerModel>>(url, { params }) |
@@ -56,17 +63,26 @@ export class SearchService { | |||
56 | 63 | ||
57 | searchVideoChannels (parameters: { | 64 | searchVideoChannels (parameters: { |
58 | search: string, | 65 | search: string, |
59 | componentPagination: ComponentPaginationLight | 66 | searchTarget?: SearchTargetType, |
67 | componentPagination?: ComponentPaginationLight | ||
60 | }): Observable<ResultList<VideoChannel>> { | 68 | }): Observable<ResultList<VideoChannel>> { |
61 | const { search, componentPagination } = parameters | 69 | const { search, componentPagination, searchTarget } = parameters |
62 | 70 | ||
63 | const url = SearchService.BASE_SEARCH_URL + 'video-channels' | 71 | const url = SearchService.BASE_SEARCH_URL + 'video-channels' |
64 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 72 | |
73 | let pagination: RestPagination | ||
74 | if (componentPagination) { | ||
75 | pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
76 | } | ||
65 | 77 | ||
66 | let params = new HttpParams() | 78 | let params = new HttpParams() |
67 | params = this.restService.addRestGetParams(params, pagination) | 79 | params = this.restService.addRestGetParams(params, pagination) |
68 | params = params.append('search', search) | 80 | params = params.append('search', search) |
69 | 81 | ||
82 | if (searchTarget) { | ||
83 | params = params.append('searchTarget', searchTarget as string) | ||
84 | } | ||
85 | |||
70 | return this.authHttp | 86 | return this.authHttp |
71 | .get<ResultList<VideoChannelServerModel>>(url, { params }) | 87 | .get<ResultList<VideoChannelServerModel>>(url, { params }) |
72 | .pipe( | 88 | .pipe( |
diff --git a/client/src/app/search/video-lazy-load.resolver.ts b/client/src/app/search/video-lazy-load.resolver.ts new file mode 100644 index 000000000..8d846d367 --- /dev/null +++ b/client/src/app/search/video-lazy-load.resolver.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { map } from 'rxjs/operators' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' | ||
4 | import { SearchService } from './search.service' | ||
5 | |||
6 | @Injectable() | ||
7 | export class VideoLazyLoadResolver implements Resolve<any> { | ||
8 | constructor ( | ||
9 | private router: Router, | ||
10 | private searchService: SearchService | ||
11 | ) { } | ||
12 | |||
13 | resolve (route: ActivatedRouteSnapshot) { | ||
14 | const url = route.params.url | ||
15 | const externalRedirect = route.params.externalRedirect | ||
16 | const fromPath = route.params.fromPath | ||
17 | |||
18 | if (!url) { | ||
19 | console.error('Could not find url param.', { params: route.params }) | ||
20 | return this.router.navigateByUrl('/404') | ||
21 | } | ||
22 | |||
23 | if (externalRedirect === 'true') { | ||
24 | window.open(url) | ||
25 | this.router.navigateByUrl(fromPath) | ||
26 | return | ||
27 | } | ||
28 | |||
29 | return this.searchService.searchVideos({ search: url }) | ||
30 | .pipe( | ||
31 | map(result => { | ||
32 | if (result.data.length !== 1) { | ||
33 | console.error('Cannot find result for this URL') | ||
34 | return this.router.navigateByUrl('/404') | ||
35 | } | ||
36 | |||
37 | const video = result.data[0] | ||
38 | |||
39 | return this.router.navigateByUrl('/videos/watch/' + video.uuid) | ||
40 | }) | ||
41 | ) | ||
42 | } | ||
43 | } | ||