aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/search
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-06-23 14:49:20 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-06-23 16:00:49 +0200
commit1942f11d5ee6926ad93dc1b79fae18325ba5de18 (patch)
tree3f2a3cd9466a56c419d197ac832a3e9cbc86bec4 /client/src/app/search
parent67ed6552b831df66713bac9e672738796128d33f (diff)
downloadPeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.gz
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.tar.zst
PeerTube-1942f11d5ee6926ad93dc1b79fae18325ba5de18.zip
Lazy load all routes
Diffstat (limited to 'client/src/app/search')
-rw-r--r--client/src/app/search/advanced-search.model.ts160
-rw-r--r--client/src/app/search/channel-lazy-load.resolver.ts43
-rw-r--r--client/src/app/search/highlight.pipe.ts54
-rw-r--r--client/src/app/search/index.ts3
-rw-r--r--client/src/app/search/search-filters.component.html193
-rw-r--r--client/src/app/search/search-filters.component.scss69
-rw-r--r--client/src/app/search/search-filters.component.ts269
-rw-r--r--client/src/app/search/search-routing.module.ts41
-rw-r--r--client/src/app/search/search.component.html63
-rw-r--r--client/src/app/search/search.component.scss191
-rw-r--r--client/src/app/search/search.component.ts260
-rw-r--r--client/src/app/search/search.module.ts43
-rw-r--r--client/src/app/search/search.service.ts89
-rw-r--r--client/src/app/search/video-lazy-load.resolver.ts43
14 files changed, 0 insertions, 1521 deletions
diff --git a/client/src/app/search/advanced-search.model.ts b/client/src/app/search/advanced-search.model.ts
deleted file mode 100644
index 516854a8c..000000000
--- a/client/src/app/search/advanced-search.model.ts
+++ /dev/null
@@ -1,160 +0,0 @@
1import { NSFWQuery, SearchTargetType } from '@shared/models'
2
3export class AdvancedSearch {
4 startDate: string // ISO 8601
5 endDate: string // ISO 8601
6
7 originallyPublishedStartDate: string // ISO 8601
8 originallyPublishedEndDate: string // ISO 8601
9
10 nsfw: NSFWQuery
11
12 categoryOneOf: string
13
14 licenceOneOf: string
15
16 languageOneOf: string
17
18 tagsOneOf: string
19 tagsAllOf: string
20
21 durationMin: number // seconds
22 durationMax: number // seconds
23
24 sort: string
25
26 searchTarget: SearchTargetType
27
28 // Filters we don't want to count, because they are mandatory
29 private silentFilters = new Set([ 'sort', 'searchTarget' ])
30
31 constructor (options?: {
32 startDate?: string
33 endDate?: string
34 originallyPublishedStartDate?: string
35 originallyPublishedEndDate?: string
36 nsfw?: NSFWQuery
37 categoryOneOf?: string
38 licenceOneOf?: string
39 languageOneOf?: string
40 tagsOneOf?: string
41 tagsAllOf?: string
42 durationMin?: string
43 durationMax?: string
44 sort?: string
45 searchTarget?: SearchTargetType
46 }) {
47 if (!options) return
48
49 this.startDate = options.startDate || undefined
50 this.endDate = options.endDate || undefined
51 this.originallyPublishedStartDate = options.originallyPublishedStartDate || undefined
52 this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined
53
54 this.nsfw = options.nsfw || undefined
55 this.categoryOneOf = options.categoryOneOf || undefined
56 this.licenceOneOf = options.licenceOneOf || undefined
57 this.languageOneOf = options.languageOneOf || undefined
58 this.tagsOneOf = options.tagsOneOf || undefined
59 this.tagsAllOf = options.tagsAllOf || undefined
60 this.durationMin = parseInt(options.durationMin, 10)
61 this.durationMax = parseInt(options.durationMax, 10)
62
63 this.searchTarget = options.searchTarget || undefined
64
65 if (isNaN(this.durationMin)) this.durationMin = undefined
66 if (isNaN(this.durationMax)) this.durationMax = undefined
67
68 this.sort = options.sort || '-match'
69 }
70
71 containsValues () {
72 const exceptions = new Set([ 'sort', 'searchTarget' ])
73
74 const obj = this.toUrlObject()
75 for (const k of Object.keys(obj)) {
76 if (this.silentFilters.has(k)) continue
77
78 if (obj[k] !== undefined && obj[k] !== '') return true
79 }
80
81 return false
82 }
83
84 reset () {
85 this.startDate = undefined
86 this.endDate = undefined
87 this.originallyPublishedStartDate = undefined
88 this.originallyPublishedEndDate = undefined
89 this.nsfw = undefined
90 this.categoryOneOf = undefined
91 this.licenceOneOf = undefined
92 this.languageOneOf = undefined
93 this.tagsOneOf = undefined
94 this.tagsAllOf = undefined
95 this.durationMin = undefined
96 this.durationMax = undefined
97
98 this.sort = '-match'
99 }
100
101 toUrlObject () {
102 return {
103 startDate: this.startDate,
104 endDate: this.endDate,
105 originallyPublishedStartDate: this.originallyPublishedStartDate,
106 originallyPublishedEndDate: this.originallyPublishedEndDate,
107 nsfw: this.nsfw,
108 categoryOneOf: this.categoryOneOf,
109 licenceOneOf: this.licenceOneOf,
110 languageOneOf: this.languageOneOf,
111 tagsOneOf: this.tagsOneOf,
112 tagsAllOf: this.tagsAllOf,
113 durationMin: this.durationMin,
114 durationMax: this.durationMax,
115 sort: this.sort,
116 searchTarget: this.searchTarget
117 }
118 }
119
120 toAPIObject () {
121 return {
122 startDate: this.startDate,
123 endDate: this.endDate,
124 originallyPublishedStartDate: this.originallyPublishedStartDate,
125 originallyPublishedEndDate: this.originallyPublishedEndDate,
126 nsfw: this.nsfw,
127 categoryOneOf: this.intoArray(this.categoryOneOf),
128 licenceOneOf: this.intoArray(this.licenceOneOf),
129 languageOneOf: this.intoArray(this.languageOneOf),
130 tagsOneOf: this.intoArray(this.tagsOneOf),
131 tagsAllOf: this.intoArray(this.tagsAllOf),
132 durationMin: this.durationMin,
133 durationMax: this.durationMax,
134 sort: this.sort,
135 searchTarget: this.searchTarget
136 }
137 }
138
139 size () {
140 let acc = 0
141
142 const obj = this.toUrlObject()
143 for (const k of Object.keys(obj)) {
144 if (this.silentFilters.has(k)) continue
145
146 if (obj[k] !== undefined && obj[k] !== '') acc++
147 }
148
149 return acc
150 }
151
152 private intoArray (value: any) {
153 if (!value) return undefined
154 if (Array.isArray(value)) return value
155
156 if (typeof value === 'string') return value.split(',')
157
158 return [ value ]
159 }
160}
diff --git a/client/src/app/search/channel-lazy-load.resolver.ts b/client/src/app/search/channel-lazy-load.resolver.ts
deleted file mode 100644
index 5b6961e98..000000000
--- a/client/src/app/search/channel-lazy-load.resolver.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { map } from 'rxjs/operators'
2import { Injectable } from '@angular/core'
3import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'
4import { SearchService } from './search.service'
5
6@Injectable()
7export class ChannelLazyLoadResolver 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.searchVideoChannels({ 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 channel = result.data[0]
38
39 return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost)
40 })
41 )
42 }
43}
diff --git a/client/src/app/search/highlight.pipe.ts b/client/src/app/search/highlight.pipe.ts
deleted file mode 100644
index 50ee5c1bd..000000000
--- a/client/src/app/search/highlight.pipe.ts
+++ /dev/null
@@ -1,54 +0,0 @@
1import { PipeTransform, Pipe } from '@angular/core'
2import { SafeHtml } from '@angular/platform-browser'
3
4// Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369
5@Pipe({ name: 'highlight' })
6export class HighlightPipe implements PipeTransform {
7 /* use this for single match search */
8 static SINGLE_MATCH = 'Single-Match'
9 /* use this for single match search with a restriction that target should start with search string */
10 static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
11 /* use this for global search */
12 static MULTI_MATCH = 'Multi-Match'
13
14 transform (
15 contentString: string = null,
16 stringToHighlight: string = null,
17 option = 'Single-And-StartsWith-Match',
18 caseSensitive = false,
19 highlightStyleName = 'search-highlight'
20 ): SafeHtml {
21 if (stringToHighlight && contentString && option) {
22 let regex: any = ''
23 const caseFlag: string = !caseSensitive ? 'i' : ''
24
25 switch (option) {
26 case 'Single-Match': {
27 regex = new RegExp(stringToHighlight, caseFlag)
28 break
29 }
30 case 'Single-And-StartsWith-Match': {
31 regex = new RegExp('^' + stringToHighlight, caseFlag)
32 break
33 }
34 case 'Multi-Match': {
35 regex = new RegExp(stringToHighlight, 'g' + caseFlag)
36 break
37 }
38 default: {
39 // default will be a global case-insensitive match
40 regex = new RegExp(stringToHighlight, 'gi')
41 }
42 }
43
44 const replaced = contentString.replace(
45 regex,
46 (match) => `<span class="${highlightStyleName}">${match}</span>`
47 )
48
49 return replaced
50 } else {
51 return contentString
52 }
53 }
54}
diff --git a/client/src/app/search/index.ts b/client/src/app/search/index.ts
deleted file mode 100644
index 40f4e021f..000000000
--- a/client/src/app/search/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './search-routing.module'
2export * from './search.component'
3export * from './search.module'
diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html
deleted file mode 100644
index e20aef8fb..000000000
--- a/client/src/app/search/search-filters.component.html
+++ /dev/null
@@ -1,193 +0,0 @@
1<form role="form" (ngSubmit)="formUpdated()">
2
3 <div class="row">
4 <div class="col-lg-4 col-md-6 col-xs-12">
5 <div class="form-group">
6 <div class="radio-label label-container">
7 <label i18n>Sort</label>
8 <button i18n class="reset-button reset-button-small" (click)="resetField('sort', '-match')" *ngIf="advancedSearch.sort !== '-match'">
9 Reset
10 </button>
11 </div>
12
13 <div class="peertube-radio-container" *ngFor="let sort of sorts">
14 <input type="radio" name="sort" [id]="sort.id" [value]="sort.id" [(ngModel)]="advancedSearch.sort">
15 <label [for]="sort.id" class="radio">{{ sort.label }}</label>
16 </div>
17 </div>
18
19 <div class="form-group">
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">
40 <label i18n>Published date</label>
41 <button i18n class="reset-button reset-button-small" (click)="resetLocalField('publishedDateRange')" *ngIf="publishedDateRange !== undefined">
42 Reset
43 </button>
44 </div>
45
46 <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
47 <input type="radio" (change)="inputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
48 <label [for]="date.id" class="radio">{{ date.label }}</label>
49 </div>
50 </div>
51
52 <div class="form-group">
53 <div class="label-container">
54 <label i18n for="original-publication-after">Original publication year</label>
55 <button i18n class="reset-button reset-button-small" (click)="resetOriginalPublicationYears()" *ngIf="originallyPublishedStartYear || originallyPublishedEndYear">
56 Reset
57 </button>
58 </div>
59
60 <div class="row">
61 <div class="pl-0 col-sm-6">
62 <input
63 (change)="inputUpdated()"
64 (keydown.enter)="$event.preventDefault()"
65 type="text" id="original-publication-after" name="original-publication-after"
66 i18n-placeholder placeholder="After..."
67 [(ngModel)]="originallyPublishedStartYear"
68 class="form-control"
69 >
70 </div>
71 <div class="pr-0 col-sm-6">
72 <input
73 (change)="inputUpdated()"
74 (keydown.enter)="$event.preventDefault()"
75 type="text" id="original-publication-before" name="original-publication-before"
76 i18n-placeholder placeholder="Before..."
77 [(ngModel)]="originallyPublishedEndYear"
78 class="form-control"
79 >
80 </div>
81 </div>
82 </div>
83
84 </div>
85
86 <div class="col-lg-4 col-md-6 col-xs-12">
87 <div class="form-group">
88 <div class="radio-label label-container">
89 <label i18n>Duration</label>
90 <button i18n class="reset-button reset-button-small" (click)="resetLocalField('durationRange')" *ngIf="durationRange !== undefined">
91 Reset
92 </button>
93 </div>
94
95 <div class="peertube-radio-container" *ngFor="let duration of durationRanges">
96 <input type="radio" (change)="inputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
97 <label [for]="duration.id" class="radio">{{ duration.label }}</label>
98 </div>
99 </div>
100
101 <div class="form-group">
102 <label i18n for="category">Category</label>
103 <button i18n class="reset-button reset-button-small" (click)="resetField('categoryOneOf')" *ngIf="advancedSearch.categoryOneOf !== undefined">
104 Reset
105 </button>
106 <div class="peertube-select-container">
107 <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf" class="form-control">
108 <option [value]="undefined" i18n>Display all categories</option>
109 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
110 </select>
111 </div>
112 </div>
113
114 <div class="form-group">
115 <label i18n for="licence">Licence</label>
116 <button i18n class="reset-button reset-button-small" (click)="resetField('licenceOneOf')" *ngIf="advancedSearch.licenceOneOf !== undefined">
117 Reset
118 </button>
119 <div class="peertube-select-container">
120 <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf" class="form-control">
121 <option [value]="undefined" i18n>Display all licenses</option>
122 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
123 </select>
124 </div>
125 </div>
126
127 <div class="form-group">
128 <label i18n for="language">Language</label>
129 <button i18n class="reset-button reset-button-small" (click)="resetField('languageOneOf')" *ngIf="advancedSearch.languageOneOf !== undefined">
130 Reset
131 </button>
132 <div class="peertube-select-container">
133 <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf" class="form-control">
134 <option [value]="undefined" i18n>Display all languages</option>
135 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
136 </select>
137 </div>
138 </div>
139 </div>
140
141 <div class="col-lg-4 col-md-6 col-xs-12">
142 <div class="form-group">
143 <label i18n for="tagsAllOf">All of these tags</label>
144 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf">
145 Reset
146 </button>
147 <tag-input
148 [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf"
149 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
150 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
151 [maxItems]="5" [modelAsStrings]="true"
152 ></tag-input>
153 </div>
154
155 <div class="form-group">
156 <label i18n for="tagsOneOf">One of these tags</label>
157 <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf">
158 Reset
159 </button>
160 <tag-input
161 [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf"
162 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
163 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
164 [maxItems]="5" [modelAsStrings]="true"
165 ></tag-input>
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>
183 </div>
184 </div>
185
186 <div class="submit-button">
187 <button i18n class="reset-button" (click)="reset()" *ngIf="advancedSearch.size()">
188 Reset
189 </button>
190
191 <input type="submit" i18n-value value="Filter">
192 </div>
193</form>
diff --git a/client/src/app/search/search-filters.component.scss b/client/src/app/search/search-filters.component.scss
deleted file mode 100644
index a88a1c0b0..000000000
--- a/client/src/app/search/search-filters.component.scss
+++ /dev/null
@@ -1,69 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4form {
5 margin-top: 40px;
6}
7
8.radio-label {
9 font-size: 15px;
10 font-weight: $font-bold;
11}
12
13.peertube-radio-container {
14 @include peertube-radio-container;
15
16 display: inline-block;
17 margin-right: 30px;
18}
19
20.peertube-select-container {
21 @include peertube-select-container(auto);
22
23 margin-bottom: 1rem;
24}
25
26.form-group {
27 margin-bottom: 25px;
28}
29
30input[type=text] {
31 @include peertube-input-text(100%);
32 display: block;
33}
34
35input[type=submit] {
36 @include peertube-button-link;
37 @include orange-button;
38}
39
40.submit-button {
41 text-align: right;
42}
43
44.reset-button {
45 @include peertube-button;
46
47 font-weight: $font-semibold;
48 display: inline-block;
49 padding: 0 10px 0 10px;
50 white-space: nowrap;
51 background: transparent;
52
53 margin-right: 1rem;
54}
55
56.reset-button-small {
57 font-size: 80%;
58 height: unset;
59 line-height: unset;
60 margin: unset;
61 margin-bottom: 0.5rem;
62}
63
64.label-container {
65 display: flex;
66 white-space: nowrap;
67}
68
69@include ng2-tags;
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts
deleted file mode 100644
index 14a5d0484..000000000
--- a/client/src/app/search/search-filters.component.ts
+++ /dev/null
@@ -1,269 +0,0 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { ValidatorFn } from '@angular/forms'
3import { ServerService } from '@app/core'
4import { AdvancedSearch } from '@app/search/advanced-search.model'
5import { VideoValidatorsService } from '@app/shared/shared-forms'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { ServerConfig, VideoConstant } from '@shared/models'
8
9@Component({
10 selector: 'my-search-filters',
11 styleUrls: [ './search-filters.component.scss' ],
12 templateUrl: './search-filters.component.html'
13})
14export class SearchFiltersComponent implements OnInit {
15 @Input() advancedSearch: AdvancedSearch = new AdvancedSearch()
16
17 @Output() filtered = new EventEmitter<AdvancedSearch>()
18
19 videoCategories: VideoConstant<number>[] = []
20 videoLicences: VideoConstant<number>[] = []
21 videoLanguages: VideoConstant<string>[] = []
22
23 tagValidators: ValidatorFn[]
24 tagValidatorsMessages: { [ name: string ]: string }
25
26 publishedDateRanges: { id: string, label: string }[] = []
27 sorts: { id: string, label: string }[] = []
28 durationRanges: { id: string, label: string }[] = []
29
30 publishedDateRange: string
31 durationRange: string
32
33 originallyPublishedStartYear: string
34 originallyPublishedEndYear: string
35
36 private serverConfig: ServerConfig
37
38 constructor (
39 private i18n: I18n,
40 private videoValidatorsService: VideoValidatorsService,
41 private serverService: ServerService
42 ) {
43 this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
44 this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
45 this.publishedDateRanges = [
46 {
47 id: 'any_published_date',
48 label: this.i18n('Any')
49 },
50 {
51 id: 'today',
52 label: this.i18n('Today')
53 },
54 {
55 id: 'last_7days',
56 label: this.i18n('Last 7 days')
57 },
58 {
59 id: 'last_30days',
60 label: this.i18n('Last 30 days')
61 },
62 {
63 id: 'last_365days',
64 label: this.i18n('Last 365 days')
65 }
66 ]
67
68 this.durationRanges = [
69 {
70 id: 'any_duration',
71 label: this.i18n('Any')
72 },
73 {
74 id: 'short',
75 label: this.i18n('Short (< 4 min)')
76 },
77 {
78 id: 'medium',
79 label: this.i18n('Medium (4-10 min)')
80 },
81 {
82 id: 'long',
83 label: this.i18n('Long (> 10 min)')
84 }
85 ]
86
87 this.sorts = [
88 {
89 id: '-match',
90 label: this.i18n('Relevance')
91 },
92 {
93 id: '-publishedAt',
94 label: this.i18n('Publish date')
95 },
96 {
97 id: '-views',
98 label: this.i18n('Views')
99 }
100 ]
101 }
102
103 ngOnInit () {
104 this.serverConfig = this.serverService.getTmpConfig()
105 this.serverService.getConfig()
106 .subscribe(config => this.serverConfig = config)
107
108 this.serverService.getVideoCategories().subscribe(categories => this.videoCategories = categories)
109 this.serverService.getVideoLicences().subscribe(licences => this.videoLicences = licences)
110 this.serverService.getVideoLanguages().subscribe(languages => this.videoLanguages = languages)
111
112 this.loadFromDurationRange()
113 this.loadFromPublishedRange()
114 this.loadOriginallyPublishedAtYears()
115 }
116
117 inputUpdated () {
118 this.updateModelFromDurationRange()
119 this.updateModelFromPublishedRange()
120 this.updateModelFromOriginallyPublishedAtYears()
121 }
122
123 formUpdated () {
124 this.inputUpdated()
125 this.filtered.emit(this.advancedSearch)
126 }
127
128 reset () {
129 this.advancedSearch.reset()
130 this.durationRange = undefined
131 this.publishedDateRange = undefined
132 this.originallyPublishedStartYear = undefined
133 this.originallyPublishedEndYear = undefined
134 this.inputUpdated()
135 }
136
137 resetField (fieldName: string, value?: any) {
138 this.advancedSearch[fieldName] = value
139 }
140
141 resetLocalField (fieldName: string, value?: any) {
142 this[fieldName] = value
143 this.inputUpdated()
144 }
145
146 resetOriginalPublicationYears () {
147 this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined
148 }
149
150 isSearchTargetEnabled () {
151 return this.serverConfig.search.searchIndex.enabled && this.serverConfig.search.searchIndex.disableLocalSearch !== true
152 }
153
154 private loadOriginallyPublishedAtYears () {
155 this.originallyPublishedStartYear = this.advancedSearch.originallyPublishedStartDate
156 ? new Date(this.advancedSearch.originallyPublishedStartDate).getFullYear().toString()
157 : null
158
159 this.originallyPublishedEndYear = this.advancedSearch.originallyPublishedEndDate
160 ? new Date(this.advancedSearch.originallyPublishedEndDate).getFullYear().toString()
161 : null
162 }
163
164 private loadFromDurationRange () {
165 if (this.advancedSearch.durationMin || this.advancedSearch.durationMax) {
166 const fourMinutes = 60 * 4
167 const tenMinutes = 60 * 10
168
169 if (this.advancedSearch.durationMin === fourMinutes && this.advancedSearch.durationMax === tenMinutes) {
170 this.durationRange = 'medium'
171 } else if (this.advancedSearch.durationMax === fourMinutes) {
172 this.durationRange = 'short'
173 } else if (this.advancedSearch.durationMin === tenMinutes) {
174 this.durationRange = 'long'
175 }
176 }
177 }
178
179 private loadFromPublishedRange () {
180 if (this.advancedSearch.startDate) {
181 const date = new Date(this.advancedSearch.startDate)
182 const now = new Date()
183
184 const diff = Math.abs(date.getTime() - now.getTime())
185
186 const dayMS = 1000 * 3600 * 24
187 const numberOfDays = diff / dayMS
188
189 if (numberOfDays >= 365) this.publishedDateRange = 'last_365days'
190 else if (numberOfDays >= 30) this.publishedDateRange = 'last_30days'
191 else if (numberOfDays >= 7) this.publishedDateRange = 'last_7days'
192 else if (numberOfDays >= 0) this.publishedDateRange = 'today'
193 }
194 }
195
196 private updateModelFromOriginallyPublishedAtYears () {
197 const baseDate = new Date()
198 baseDate.setHours(0, 0, 0, 0)
199 baseDate.setMonth(0, 1)
200
201 if (this.originallyPublishedStartYear) {
202 const year = parseInt(this.originallyPublishedStartYear, 10)
203 const start = new Date(baseDate)
204 start.setFullYear(year)
205
206 this.advancedSearch.originallyPublishedStartDate = start.toISOString()
207 } else {
208 this.advancedSearch.originallyPublishedStartDate = null
209 }
210
211 if (this.originallyPublishedEndYear) {
212 const year = parseInt(this.originallyPublishedEndYear, 10)
213 const end = new Date(baseDate)
214 end.setFullYear(year)
215
216 this.advancedSearch.originallyPublishedEndDate = end.toISOString()
217 } else {
218 this.advancedSearch.originallyPublishedEndDate = null
219 }
220 }
221
222 private updateModelFromDurationRange () {
223 if (!this.durationRange) return
224
225 const fourMinutes = 60 * 4
226 const tenMinutes = 60 * 10
227
228 switch (this.durationRange) {
229 case 'short':
230 this.advancedSearch.durationMin = undefined
231 this.advancedSearch.durationMax = fourMinutes
232 break
233
234 case 'medium':
235 this.advancedSearch.durationMin = fourMinutes
236 this.advancedSearch.durationMax = tenMinutes
237 break
238
239 case 'long':
240 this.advancedSearch.durationMin = tenMinutes
241 this.advancedSearch.durationMax = undefined
242 break
243 }
244 }
245
246 private updateModelFromPublishedRange () {
247 if (!this.publishedDateRange) return
248
249 // today
250 const date = new Date()
251 date.setHours(0, 0, 0, 0)
252
253 switch (this.publishedDateRange) {
254 case 'last_7days':
255 date.setDate(date.getDate() - 7)
256 break
257
258 case 'last_30days':
259 date.setDate(date.getDate() - 30)
260 break
261
262 case 'last_365days':
263 date.setDate(date.getDate() - 365)
264 break
265 }
266
267 this.advancedSearch.startDate = date.toISOString()
268 }
269}
diff --git a/client/src/app/search/search-routing.module.ts b/client/src/app/search/search-routing.module.ts
deleted file mode 100644
index 9da900e9a..000000000
--- a/client/src/app/search/search-routing.module.ts
+++ /dev/null
@@ -1,41 +0,0 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { SearchComponent } from '@app/search/search.component'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
6import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
7
8const searchRoutes: Routes = [
9 {
10 path: 'search',
11 component: SearchComponent,
12 canActivate: [ MetaGuard ],
13 data: {
14 meta: {
15 title: 'Search'
16 }
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 }
34 }
35]
36
37@NgModule({
38 imports: [ RouterModule.forChild(searchRoutes) ],
39 exports: [ RouterModule ]
40})
41export class SearchRoutingModule {}
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html
deleted file mode 100644
index 9bff024ad..000000000
--- a/client/src/app/search/search.component.html
+++ /dev/null
@@ -1,63 +0,0 @@
1<div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="search-result">
2 <div class="results-header">
3 <div class="first-line">
4 <div class="results-counter" *ngIf="pagination.totalItems">
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
10 <span *ngIf="currentSearch" i18n>
11 for <span class="search-value">{{ currentSearch }}</span>
12 </span>
13 </div>
14
15 <div
16 class="results-filter-button ml-auto" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button"
17 [attr.aria-expanded]="!isSearchFilterCollapsed" aria-controls="collapseBasic"
18 >
19 <span class="icon icon-filter"></span>
20 <ng-container i18n>
21 Filters
22 <span *ngIf="numberOfFilters() > 0" class="badge badge-secondary">{{ numberOfFilters() }}</span>
23 </ng-container>
24 </div>
25 </div>
26
27 <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed">
28 <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters>
29 </div>
30 </div>
31
32 <div i18n *ngIf="pagination.totalItems === 0 && results.length === 0" class="no-results">
33 No results found
34 </div>
35
36 <ng-container *ngFor="let result of results">
37 <div *ngIf="isVideoChannel(result)" class="entry video-channel">
38 <a [routerLink]="getChannelUrl(result)">
39 <img [src]="result.avatarUrl" alt="Avatar" />
40 </a>
41
42 <div class="video-channel-info">
43 <a [routerLink]="getChannelUrl(result)" class="video-channel-names">
44 <div class="video-channel-display-name">{{ result.displayName }}</div>
45 <div class="video-channel-name">{{ result.nameWithHost }}</div>
46 </a>
47
48 <div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div>
49 </div>
50
51 <my-subscribe-button *ngIf="!hideActions()" [videoChannels]="[result]"></my-subscribe-button>
52 </div>
53
54 <div *ngIf="isVideo(result)" class="entry video">
55 <my-video-miniature
56 [video]="result" [user]="userMiniature" [displayAsRow]="true" [displayVideoActions]="!hideActions()"
57 [displayOptions]="videoDisplayOptions" [useLazyLoadUrl]="advancedSearch.searchTarget === 'search-index'"
58 (videoBlocked)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)"
59 ></my-video-miniature>
60 </div>
61 </ng-container>
62
63</div>
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss
deleted file mode 100644
index 6e59adb60..000000000
--- a/client/src/app/search/search.component.scss
+++ /dev/null
@@ -1,191 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.search-result {
5 padding: 40px;
6
7 .results-header {
8 font-size: 16px;
9 padding-bottom: 20px;
10 margin-bottom: 30px;
11 border-bottom: 1px solid #DADADA;
12
13 .first-line {
14 display: flex;
15 flex-direction: row;
16
17 .results-counter {
18 flex-grow: 1;
19
20 .search-value {
21 font-weight: $font-semibold;
22 }
23 }
24
25 .results-filter-button {
26 cursor: pointer;
27
28 .icon.icon-filter {
29 @include icon(20px);
30
31 position: relative;
32 top: -1px;
33 margin-right: 5px;
34 background-image: url('../../assets/images/search/filter.svg');
35 }
36 }
37 }
38 }
39
40 .entry {
41 display: flex;
42 min-height: 130px;
43 padding-bottom: 20px;
44 margin-bottom: 20px;
45
46 &.video-channel {
47 img {
48 $image-size: 130px;
49 $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature
50
51 @include avatar($image-size);
52
53 margin: 0 ($margin-size + 10) 0 $margin-size;
54 }
55
56 .video-channel-info {
57 flex-grow: 1;
58 width: fit-content;
59
60 .video-channel-names {
61 @include disable-default-a-behaviour;
62
63 display: flex;
64 align-items: baseline;
65 color: pvar(--mainForegroundColor);
66 width: fit-content;
67
68 .video-channel-display-name {
69 font-weight: $font-semibold;
70 font-size: 18px;
71 }
72
73 .video-channel-name {
74 font-size: 14px;
75 color: $grey-actor-name;
76 margin-left: 5px;
77 }
78 }
79 }
80 }
81 }
82}
83
84@media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) {
85 .video-channel-info .video-channel-names {
86 flex-direction: column !important;
87
88 .video-channel-name {
89 @include ellipsis; // Ellipsis and max-width on channel-name to not break screen
90
91 max-width: 250px;
92 margin-left: 0 !important;
93 }
94 }
95
96 :host-context(.main-col:not(.expanded)) {
97 // Override the min-width: 500px to not break screen
98 ::ng-deep .video-miniature-information {
99 min-width: 300px !important;
100 }
101 }
102}
103
104@media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) {
105 :host-context(.main-col:not(.expanded)) {
106 .video-channel-info .video-channel-names {
107 .video-channel-name {
108 max-width: 160px;
109 }
110 }
111
112 // Override the min-width: 500px to not break screen
113 ::ng-deep .video-miniature-information {
114 min-width: $video-thumbnail-width !important;
115 }
116 }
117
118 :host-context(.expanded) {
119 // Override the min-width: 500px to not break screen
120 ::ng-deep .video-miniature-information {
121 min-width: 300px !important;
122 }
123 }
124}
125
126@media screen and (max-width: $small-view) {
127 .search-result {
128 .entry.video-channel,
129 .entry.video {
130 flex-direction: column;
131 height: auto;
132 justify-content: center;
133 align-items: center;
134 text-align: center;
135
136 img {
137 margin: 0;
138 }
139
140 img {
141 margin: 0;
142 }
143
144 .video-channel-info .video-channel-names {
145 align-items: center;
146 flex-direction: column !important;
147
148 .video-channel-name {
149 margin-left: 0 !important;
150 }
151 }
152
153 my-subscribe-button {
154 margin-top: 5px;
155 }
156 }
157 }
158}
159
160@media screen and (max-width: $mobile-view) {
161 .search-result {
162 padding: 20px 10px;
163
164 .results-header {
165 font-size: 15px !important;
166 }
167
168 .entry {
169 &.video {
170 .video-info-name,
171 .video-info-account {
172 margin: auto;
173 }
174
175 my-video-thumbnail {
176 margin-right: 0 !important;
177
178 ::ng-deep .video-thumbnail {
179 width: 100%;
180 height: auto;
181
182 img {
183 width: 100%;
184 height: auto;
185 }
186 }
187 }
188 }
189 }
190 }
191}
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
deleted file mode 100644
index 83b06e0ce..000000000
--- a/client/src/app/search/search.component.ts
+++ /dev/null
@@ -1,260 +0,0 @@
1import { forkJoin, of, Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core'
5import { immutableAssign } from '@app/helpers'
6import { Video, VideoChannel } from '@app/shared/shared-main'
7import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
8import { MetaService } from '@ngx-meta/core'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { SearchTargetType, ServerConfig } from '@shared/models'
11import { AdvancedSearch } from './advanced-search.model'
12import { SearchService } from './search.service'
13
14@Component({
15 selector: 'my-search',
16 styleUrls: [ './search.component.scss' ],
17 templateUrl: './search.component.html'
18})
19export class SearchComponent implements OnInit, OnDestroy {
20 results: (Video | VideoChannel)[] = []
21
22 pagination: ComponentPagination = {
23 currentPage: 1,
24 itemsPerPage: 10, // Only for videos, use another variable for channels
25 totalItems: null
26 }
27 advancedSearch: AdvancedSearch = new AdvancedSearch()
28 isSearchFilterCollapsed = true
29 currentSearch: string
30
31 videoDisplayOptions: MiniatureDisplayOptions = {
32 date: true,
33 views: true,
34 by: true,
35 avatar: false,
36 privacyLabel: false,
37 privacyText: false,
38 state: false,
39 blacklistInfo: false
40 }
41
42 errorMessage: string
43 serverConfig: ServerConfig
44
45 userMiniature: User
46
47 private subActivatedRoute: Subscription
48 private isInitialLoad = false // set to false to show the search filters on first arrival
49 private firstSearch = true
50
51 private channelsPerPage = 2
52
53 private lastSearchTarget: SearchTargetType
54
55 constructor (
56 private i18n: I18n,
57 private route: ActivatedRoute,
58 private router: Router,
59 private metaService: MetaService,
60 private notifier: Notifier,
61 private searchService: SearchService,
62 private authService: AuthService,
63 private userService: UserService,
64 private hooks: HooksService,
65 private serverService: ServerService
66 ) { }
67
68 ngOnInit () {
69 this.serverService.getConfig()
70 .subscribe(config => this.serverConfig = config)
71
72 this.subActivatedRoute = this.route.queryParams.subscribe(
73 async queryParams => {
74 const querySearch = queryParams['search']
75 const searchTarget = queryParams['searchTarget']
76
77 // Search updated, reset filters
78 if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) {
79 this.resetPagination()
80 this.advancedSearch.reset()
81
82 this.currentSearch = querySearch || undefined
83 this.updateTitle()
84 }
85
86 this.advancedSearch = new AdvancedSearch(queryParams)
87 if (!this.advancedSearch.searchTarget) {
88 this.advancedSearch.searchTarget = await this.serverService.getDefaultSearchTarget()
89 }
90
91 // Don't hide filters if we have some of them AND the user just came on the webpage
92 this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues()
93 this.isInitialLoad = false
94
95 this.search()
96 },
97
98 err => this.notifier.error(err.text)
99 )
100
101 this.userService.getAnonymousOrLoggedUser()
102 .subscribe(user => this.userMiniature = user)
103
104 this.hooks.runAction('action:search.init', 'search')
105 }
106
107 ngOnDestroy () {
108 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
109 }
110
111 isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
112 return d instanceof VideoChannel
113 }
114
115 isVideo (v: VideoChannel | Video): v is Video {
116 return v instanceof Video
117 }
118
119 isUserLoggedIn () {
120 return this.authService.isLoggedIn()
121 }
122
123 search () {
124 forkJoin([
125 this.getVideosObs(),
126 this.getVideoChannelObs()
127 ]).subscribe(
128 ([videosResult, videoChannelsResult]) => {
129 this.results = this.results
130 .concat(videoChannelsResult.data)
131 .concat(videosResult.data)
132
133 this.pagination.totalItems = videosResult.total + videoChannelsResult.total
134 this.lastSearchTarget = this.advancedSearch.searchTarget
135
136 // Focus on channels if there are no enough videos
137 if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
138 this.resetPagination()
139 this.firstSearch = false
140
141 this.channelsPerPage = 10
142 this.search()
143 }
144
145 this.firstSearch = false
146 },
147
148 err => {
149 if (this.advancedSearch.searchTarget !== 'search-index') {
150 this.notifier.error(err.message)
151 return
152 }
153
154 this.notifier.error(
155 this.i18n('Search index is unavailable. Retrying with instance results instead.'),
156 this.i18n('Search error')
157 )
158 this.advancedSearch.searchTarget = 'local'
159 this.search()
160 }
161 )
162 }
163
164 onNearOfBottom () {
165 // Last page
166 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
167
168 this.pagination.currentPage += 1
169 this.search()
170 }
171
172 onFiltered () {
173 this.resetPagination()
174
175 this.updateUrlFromAdvancedSearch()
176 }
177
178 numberOfFilters () {
179 return this.advancedSearch.size()
180 }
181
182 // Add VideoChannel for typings, but the template already checks "video" argument is a video
183 removeVideoFromArray (video: Video | VideoChannel) {
184 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
185 }
186
187 getChannelUrl (channel: VideoChannel) {
188 if (this.advancedSearch.searchTarget === 'search-index' && channel.url) {
189 const remoteUriConfig = this.serverConfig.search.remoteUri
190
191 // Redirect on the external instance if not allowed to fetch remote data
192 const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users
193 const fromPath = window.location.pathname + window.location.search
194
195 return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ]
196 }
197
198 return [ '/video-channels', channel.nameWithHost ]
199 }
200
201 hideActions () {
202 return this.lastSearchTarget === 'search-index'
203 }
204
205 private resetPagination () {
206 this.pagination.currentPage = 1
207 this.pagination.totalItems = null
208 this.channelsPerPage = 2
209
210 this.results = []
211 }
212
213 private updateTitle () {
214 const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
215 this.metaService.setTitle(this.i18n('Search') + suffix)
216 }
217
218 private updateUrlFromAdvancedSearch () {
219 const search = this.currentSearch || undefined
220
221 this.router.navigate([], {
222 relativeTo: this.route,
223 queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
224 })
225 }
226
227 private getVideosObs () {
228 const params = {
229 search: this.currentSearch,
230 componentPagination: this.pagination,
231 advancedSearch: this.advancedSearch
232 }
233
234 return this.hooks.wrapObsFun(
235 this.searchService.searchVideos.bind(this.searchService),
236 params,
237 'search',
238 'filter:api.search.videos.list.params',
239 'filter:api.search.videos.list.result'
240 )
241 }
242
243 private getVideoChannelObs () {
244 if (!this.currentSearch) return of({ data: [], total: 0 })
245
246 const params = {
247 search: this.currentSearch,
248 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
249 searchTarget: this.advancedSearch.searchTarget
250 }
251
252 return this.hooks.wrapObsFun(
253 this.searchService.searchVideoChannels.bind(this.searchService),
254 params,
255 'search',
256 'filter:api.search.video-channels.list.params',
257 'filter:api.search.video-channels.list.result'
258 )
259 }
260}
diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts
deleted file mode 100644
index 65c954de8..000000000
--- a/client/src/app/search/search.module.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { TagInputModule } from 'ngx-chips'
2import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
6import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
7import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
8import { HighlightPipe } from './highlight.pipe'
9import { SearchFiltersComponent } from './search-filters.component'
10import { SearchRoutingModule } from './search-routing.module'
11import { SearchComponent } from './search.component'
12import { SearchService } from './search.service'
13import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
14
15@NgModule({
16 imports: [
17 TagInputModule,
18
19 SearchRoutingModule,
20 SharedMainModule,
21 SharedFormModule,
22 SharedUserSubscriptionModule,
23 SharedVideoMiniatureModule
24 ],
25
26 declarations: [
27 SearchComponent,
28 SearchFiltersComponent
29 ],
30
31 exports: [
32 TagInputModule,
33 SearchComponent
34 ],
35
36 providers: [
37 SearchService,
38 VideoLazyLoadResolver,
39 ChannelLazyLoadResolver,
40 HighlightPipe
41 ]
42})
43export class SearchModule { }
diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts
deleted file mode 100644
index 36342034f..000000000
--- a/client/src/app/search/search.service.ts
+++ /dev/null
@@ -1,89 +0,0 @@
1import { Observable } from 'rxjs'
2import { catchError, map, switchMap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core'
6import { peertubeLocalStorage } from '@app/helpers'
7import { AdvancedSearch } from '@app/search/advanced-search.model'
8import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
9import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '@shared/models'
10import { SearchTargetType } from '@shared/models/search/search-target-query.model'
11import { environment } from '../../environments/environment'
12
13@Injectable()
14export class SearchService {
15 static BASE_SEARCH_URL = environment.apiUrl + '/api/v1/search/'
16
17 constructor (
18 private authHttp: HttpClient,
19 private restExtractor: RestExtractor,
20 private restService: RestService,
21 private videoService: VideoService
22 ) {
23 // Add ability to override search endpoint if the user updated this local storage key
24 const searchUrl = peertubeLocalStorage.getItem('search-url')
25 if (searchUrl) SearchService.BASE_SEARCH_URL = searchUrl
26 }
27
28 searchVideos (parameters: {
29 search: string,
30 componentPagination?: ComponentPaginationLight,
31 advancedSearch?: AdvancedSearch
32 }): Observable<ResultList<Video>> {
33 const { search, componentPagination, advancedSearch } = parameters
34
35 const url = SearchService.BASE_SEARCH_URL + 'videos'
36 let pagination: RestPagination
37
38 if (componentPagination) {
39 pagination = this.restService.componentPaginationToRestPagination(componentPagination)
40 }
41
42 let params = new HttpParams()
43 params = this.restService.addRestGetParams(params, pagination)
44
45 if (search) params = params.append('search', search)
46
47 if (advancedSearch) {
48 const advancedSearchObject = advancedSearch.toAPIObject()
49 params = this.restService.addObjectParams(params, advancedSearchObject)
50 }
51
52 return this.authHttp
53 .get<ResultList<VideoServerModel>>(url, { params })
54 .pipe(
55 switchMap(res => this.videoService.extractVideos(res)),
56 catchError(err => this.restExtractor.handleError(err))
57 )
58 }
59
60 searchVideoChannels (parameters: {
61 search: string,
62 searchTarget?: SearchTargetType,
63 componentPagination?: ComponentPaginationLight
64 }): Observable<ResultList<VideoChannel>> {
65 const { search, componentPagination, searchTarget } = parameters
66
67 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
68
69 let pagination: RestPagination
70 if (componentPagination) {
71 pagination = this.restService.componentPaginationToRestPagination(componentPagination)
72 }
73
74 let params = new HttpParams()
75 params = this.restService.addRestGetParams(params, pagination)
76 params = params.append('search', search)
77
78 if (searchTarget) {
79 params = params.append('searchTarget', searchTarget as string)
80 }
81
82 return this.authHttp
83 .get<ResultList<VideoChannelServerModel>>(url, { params })
84 .pipe(
85 map(res => VideoChannelService.extractVideoChannels(res)),
86 catchError(err => this.restExtractor.handleError(err))
87 )
88 }
89}
diff --git a/client/src/app/search/video-lazy-load.resolver.ts b/client/src/app/search/video-lazy-load.resolver.ts
deleted file mode 100644
index 8d846d367..000000000
--- a/client/src/app/search/video-lazy-load.resolver.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { map } from 'rxjs/operators'
2import { Injectable } from '@angular/core'
3import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'
4import { SearchService } from './search.service'
5
6@Injectable()
7export 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}