diff options
Diffstat (limited to 'client/src/app/search')
-rw-r--r-- | client/src/app/search/advanced-search.model.ts | 160 | ||||
-rw-r--r-- | client/src/app/search/channel-lazy-load.resolver.ts | 43 | ||||
-rw-r--r-- | client/src/app/search/highlight.pipe.ts | 54 | ||||
-rw-r--r-- | client/src/app/search/index.ts | 3 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.html | 193 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.scss | 69 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.ts | 269 | ||||
-rw-r--r-- | client/src/app/search/search-routing.module.ts | 41 | ||||
-rw-r--r-- | client/src/app/search/search.component.html | 63 | ||||
-rw-r--r-- | client/src/app/search/search.component.scss | 191 | ||||
-rw-r--r-- | client/src/app/search/search.component.ts | 260 | ||||
-rw-r--r-- | client/src/app/search/search.module.ts | 43 | ||||
-rw-r--r-- | client/src/app/search/search.service.ts | 89 | ||||
-rw-r--r-- | client/src/app/search/video-lazy-load.resolver.ts | 43 |
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 @@ | |||
1 | import { NSFWQuery, SearchTargetType } from '@shared/models' | ||
2 | |||
3 | export 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 @@ | |||
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 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 @@ | |||
1 | import { PipeTransform, Pipe } from '@angular/core' | ||
2 | import { SafeHtml } from '@angular/platform-browser' | ||
3 | |||
4 | // Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369 | ||
5 | @Pipe({ name: 'highlight' }) | ||
6 | export 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 @@ | |||
1 | export * from './search-routing.module' | ||
2 | export * from './search.component' | ||
3 | export * 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 | |||
4 | form { | ||
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 | |||
30 | input[type=text] { | ||
31 | @include peertube-input-text(100%); | ||
32 | display: block; | ||
33 | } | ||
34 | |||
35 | input[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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { ValidatorFn } from '@angular/forms' | ||
3 | import { ServerService } from '@app/core' | ||
4 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
5 | import { VideoValidatorsService } from '@app/shared/shared-forms' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { 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 | }) | ||
14 | export 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 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
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' | ||
7 | |||
8 | const 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 | }) | ||
41 | export 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 @@ | |||
1 | import { forkJoin, of, Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' | ||
5 | import { immutableAssign } from '@app/helpers' | ||
6 | import { Video, VideoChannel } from '@app/shared/shared-main' | ||
7 | import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' | ||
8 | import { MetaService } from '@ngx-meta/core' | ||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | import { SearchTargetType, ServerConfig } from '@shared/models' | ||
11 | import { AdvancedSearch } from './advanced-search.model' | ||
12 | import { SearchService } from './search.service' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-search', | ||
16 | styleUrls: [ './search.component.scss' ], | ||
17 | templateUrl: './search.component.html' | ||
18 | }) | ||
19 | export 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 @@ | |||
1 | import { TagInputModule } from 'ngx-chips' | ||
2 | import { NgModule } from '@angular/core' | ||
3 | import { SharedFormModule } from '@app/shared/shared-forms' | ||
4 | import { SharedMainModule } from '@app/shared/shared-main' | ||
5 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | ||
6 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | ||
7 | import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' | ||
8 | import { HighlightPipe } from './highlight.pipe' | ||
9 | import { SearchFiltersComponent } from './search-filters.component' | ||
10 | import { SearchRoutingModule } from './search-routing.module' | ||
11 | import { SearchComponent } from './search.component' | ||
12 | import { SearchService } from './search.service' | ||
13 | import { 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 | }) | ||
43 | export 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 @@ | |||
1 | import { Observable } from 'rxjs' | ||
2 | import { catchError, map, switchMap } from 'rxjs/operators' | ||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core' | ||
6 | import { peertubeLocalStorage } from '@app/helpers' | ||
7 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
8 | import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | ||
9 | import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '@shared/models' | ||
10 | import { SearchTargetType } from '@shared/models/search/search-target-query.model' | ||
11 | import { environment } from '../../environments/environment' | ||
12 | |||
13 | @Injectable() | ||
14 | export 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 @@ | |||
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 | } | ||