diff options
Diffstat (limited to 'client/src/app/search')
-rw-r--r-- | client/src/app/search/advanced-search.model.ts | 101 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.html | 87 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.scss | 40 | ||||
-rw-r--r-- | client/src/app/search/search-filters.component.ts | 170 | ||||
-rw-r--r-- | client/src/app/search/search.component.html | 30 | ||||
-rw-r--r-- | client/src/app/search/search.component.scss | 46 | ||||
-rw-r--r-- | client/src/app/search/search.component.ts | 29 | ||||
-rw-r--r-- | client/src/app/search/search.module.ts | 9 | ||||
-rw-r--r-- | client/src/app/search/search.service.ts | 17 |
9 files changed, 511 insertions, 18 deletions
diff --git a/client/src/app/search/advanced-search.model.ts b/client/src/app/search/advanced-search.model.ts new file mode 100644 index 000000000..a0f333175 --- /dev/null +++ b/client/src/app/search/advanced-search.model.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import { NSFWQuery } from '../../../../shared/models/search' | ||
2 | |||
3 | export class AdvancedSearch { | ||
4 | startDate: string // ISO 8601 | ||
5 | endDate: string // ISO 8601 | ||
6 | |||
7 | nsfw: NSFWQuery | ||
8 | |||
9 | categoryOneOf: string | ||
10 | |||
11 | licenceOneOf: string | ||
12 | |||
13 | languageOneOf: string | ||
14 | |||
15 | tagsOneOf: string | ||
16 | tagsAllOf: string | ||
17 | |||
18 | durationMin: number // seconds | ||
19 | durationMax: number // seconds | ||
20 | |||
21 | constructor (options?: { | ||
22 | startDate?: string | ||
23 | endDate?: string | ||
24 | nsfw?: NSFWQuery | ||
25 | categoryOneOf?: string | ||
26 | licenceOneOf?: string | ||
27 | languageOneOf?: string | ||
28 | tagsOneOf?: string | ||
29 | tagsAllOf?: string | ||
30 | durationMin?: string | ||
31 | durationMax?: string | ||
32 | }) { | ||
33 | if (!options) return | ||
34 | |||
35 | this.startDate = options.startDate | ||
36 | this.endDate = options.endDate | ||
37 | this.nsfw = options.nsfw | ||
38 | this.categoryOneOf = options.categoryOneOf | ||
39 | this.licenceOneOf = options.licenceOneOf | ||
40 | this.languageOneOf = options.languageOneOf | ||
41 | this.tagsOneOf = options.tagsOneOf | ||
42 | this.tagsAllOf = options.tagsAllOf | ||
43 | this.durationMin = parseInt(options.durationMin, 10) | ||
44 | this.durationMax = parseInt(options.durationMax, 10) | ||
45 | |||
46 | if (isNaN(this.durationMin)) this.durationMin = undefined | ||
47 | if (isNaN(this.durationMax)) this.durationMax = undefined | ||
48 | } | ||
49 | |||
50 | containsValues () { | ||
51 | const obj = this.toUrlObject() | ||
52 | for (const k of Object.keys(obj)) { | ||
53 | if (obj[k] !== undefined) return true | ||
54 | } | ||
55 | |||
56 | return false | ||
57 | } | ||
58 | |||
59 | reset () { | ||
60 | this.startDate = undefined | ||
61 | this.endDate = undefined | ||
62 | this.nsfw = undefined | ||
63 | this.categoryOneOf = undefined | ||
64 | this.licenceOneOf = undefined | ||
65 | this.languageOneOf = undefined | ||
66 | this.tagsOneOf = undefined | ||
67 | this.tagsAllOf = undefined | ||
68 | this.durationMin = undefined | ||
69 | this.durationMax = undefined | ||
70 | } | ||
71 | |||
72 | toUrlObject () { | ||
73 | return { | ||
74 | startDate: this.startDate, | ||
75 | endDate: this.endDate, | ||
76 | nsfw: this.nsfw, | ||
77 | categoryOneOf: this.categoryOneOf, | ||
78 | licenceOneOf: this.licenceOneOf, | ||
79 | languageOneOf: this.languageOneOf, | ||
80 | tagsOneOf: this.tagsOneOf, | ||
81 | tagsAllOf: this.tagsAllOf, | ||
82 | durationMin: this.durationMin, | ||
83 | durationMax: this.durationMax | ||
84 | } | ||
85 | } | ||
86 | |||
87 | toAPIObject () { | ||
88 | return { | ||
89 | startDate: this.startDate, | ||
90 | endDate: this.endDate, | ||
91 | nsfw: this.nsfw, | ||
92 | categoryOneOf: this.categoryOneOf ? this.categoryOneOf.split(',') : undefined, | ||
93 | licenceOneOf: this.licenceOneOf ? this.licenceOneOf.split(',') : undefined, | ||
94 | languageOneOf: this.languageOneOf ? this.languageOneOf.split(',') : undefined, | ||
95 | tagsOneOf: this.tagsOneOf ? this.tagsOneOf.split(',') : undefined, | ||
96 | tagsAllOf: this.tagsAllOf ? this.tagsAllOf.split(',') : undefined, | ||
97 | durationMin: this.durationMin, | ||
98 | durationMax: this.durationMax | ||
99 | } | ||
100 | } | ||
101 | } | ||
diff --git a/client/src/app/search/search-filters.component.html b/client/src/app/search/search-filters.component.html new file mode 100644 index 000000000..f8b3675e5 --- /dev/null +++ b/client/src/app/search/search-filters.component.html | |||
@@ -0,0 +1,87 @@ | |||
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 i18n class="radio-label">Published date</div> | ||
7 | |||
8 | <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> | ||
9 | <input type="radio" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> | ||
10 | <label [for]="date.id" class="radio">{{ date.label }}</label> | ||
11 | </div> | ||
12 | </div> | ||
13 | |||
14 | <div class="form-group"> | ||
15 | <div i18n class="radio-label">Duration</div> | ||
16 | |||
17 | <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> | ||
18 | <input type="radio" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> | ||
19 | <label [for]="duration.id" class="radio">{{ duration.label }}</label> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <div class="form-group"> | ||
24 | <div i18n class="radio-label">Display sensitive content</div> | ||
25 | |||
26 | <div class="peertube-radio-container"> | ||
27 | <input type="radio" name="sensitiveContent" id="sensitiveContentYes" value="both" [(ngModel)]="advancedSearch.nsfw"> | ||
28 | <label i18n for="sensitiveContentYes" class="radio">Yes</label> | ||
29 | </div> | ||
30 | |||
31 | <div class="peertube-radio-container"> | ||
32 | <input type="radio" name="sensitiveContent" id="sensitiveContentNo" value="false" [(ngModel)]="advancedSearch.nsfw"> | ||
33 | <label i18n for="sensitiveContentNo" class="radio">No</label> | ||
34 | </div> | ||
35 | </div> | ||
36 | |||
37 | </div> | ||
38 | |||
39 | <div class="col-lg-4 col-md-6 col-xs-12"> | ||
40 | <div class="form-group"> | ||
41 | <label i18n for="category">Category</label> | ||
42 | <div class="peertube-select-container"> | ||
43 | <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf"> | ||
44 | <option></option> | ||
45 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
46 | </select> | ||
47 | </div> | ||
48 | </div> | ||
49 | |||
50 | <div class="form-group"> | ||
51 | <label i18n for="licence">Licence</label> | ||
52 | <div class="peertube-select-container"> | ||
53 | <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf"> | ||
54 | <option></option> | ||
55 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
56 | </select> | ||
57 | </div> | ||
58 | </div> | ||
59 | |||
60 | <div class="form-group"> | ||
61 | <label i18n for="language">Language</label> | ||
62 | <div class="peertube-select-container"> | ||
63 | <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf"> | ||
64 | <option></option> | ||
65 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
66 | </select> | ||
67 | </div> | ||
68 | </div> | ||
69 | </div> | ||
70 | |||
71 | <div class="col-lg-4 col-md-6 col-xs-12"> | ||
72 | <div class="form-group"> | ||
73 | <label i18n for="tagsAllOf">All of these tags</label> | ||
74 | <input type="text" name="tagsAllOf" id="tagsAllOf" [(ngModel)]="advancedSearch.tagsAllOf" /> | ||
75 | </div> | ||
76 | |||
77 | <div class="form-group"> | ||
78 | <label i18n for="tagsOneOf">One of these tags</label> | ||
79 | <input type="text" name="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf" /> | ||
80 | </div> | ||
81 | </div> | ||
82 | </div> | ||
83 | |||
84 | <div class="submit-button"> | ||
85 | <input type="submit" i18n-value value="Filter"> | ||
86 | </div> | ||
87 | </form> \ No newline at end of file | ||
diff --git a/client/src/app/search/search-filters.component.scss b/client/src/app/search/search-filters.component.scss new file mode 100644 index 000000000..cfc48fbef --- /dev/null +++ b/client/src/app/search/search-filters.component.scss | |||
@@ -0,0 +1,40 @@ | |||
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 | |||
24 | .form-group { | ||
25 | margin-bottom: 25px; | ||
26 | } | ||
27 | |||
28 | input[type=text] { | ||
29 | @include peertube-input-text(100%); | ||
30 | display: block; | ||
31 | } | ||
32 | |||
33 | input[type=submit] { | ||
34 | @include peertube-button-link; | ||
35 | @include orange-button; | ||
36 | } | ||
37 | |||
38 | .submit-button { | ||
39 | text-align: right; | ||
40 | } \ No newline at end of file | ||
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts new file mode 100644 index 000000000..4219f99a9 --- /dev/null +++ b/client/src/app/search/search-filters.component.ts | |||
@@ -0,0 +1,170 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { ActivatedRoute } from '@angular/router' | ||
3 | import { RedirectService, ServerService } from '@app/core' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { SearchService } from '@app/search/search.service' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { MetaService } from '@ngx-meta/core' | ||
8 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
9 | import { VideoConstant } from '../../../../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-search-filters', | ||
13 | styleUrls: [ './search-filters.component.scss' ], | ||
14 | templateUrl: './search-filters.component.html' | ||
15 | }) | ||
16 | export class SearchFiltersComponent implements OnInit { | ||
17 | @Input() advancedSearch: AdvancedSearch = new AdvancedSearch() | ||
18 | |||
19 | @Output() filtered = new EventEmitter<AdvancedSearch>() | ||
20 | |||
21 | videoCategories: VideoConstant<string>[] = [] | ||
22 | videoLicences: VideoConstant<string>[] = [] | ||
23 | videoLanguages: VideoConstant<string>[] = [] | ||
24 | |||
25 | publishedDateRanges: { id: string, label: string }[] = [] | ||
26 | durationRanges: { id: string, label: string }[] = [] | ||
27 | |||
28 | publishedDateRange: string | ||
29 | durationRange: string | ||
30 | |||
31 | constructor ( | ||
32 | private i18n: I18n, | ||
33 | private route: ActivatedRoute, | ||
34 | private metaService: MetaService, | ||
35 | private redirectService: RedirectService, | ||
36 | private notificationsService: NotificationsService, | ||
37 | private searchService: SearchService, | ||
38 | private serverService: ServerService | ||
39 | ) { | ||
40 | this.publishedDateRanges = [ | ||
41 | { | ||
42 | id: 'today', | ||
43 | label: this.i18n('Today') | ||
44 | }, | ||
45 | { | ||
46 | id: 'last_7days', | ||
47 | label: this.i18n('Last 7 days') | ||
48 | }, | ||
49 | { | ||
50 | id: 'last_30days', | ||
51 | label: this.i18n('Last 30 days') | ||
52 | }, | ||
53 | { | ||
54 | id: 'last_365days', | ||
55 | label: this.i18n('Last 365 days') | ||
56 | } | ||
57 | ] | ||
58 | |||
59 | this.durationRanges = [ | ||
60 | { | ||
61 | id: 'short', | ||
62 | label: this.i18n('Short (< 4 minutes)') | ||
63 | }, | ||
64 | { | ||
65 | id: 'long', | ||
66 | label: this.i18n('Long (> 10 minutes)') | ||
67 | }, | ||
68 | { | ||
69 | id: 'medium', | ||
70 | label: this.i18n('Medium (4-10 minutes)') | ||
71 | } | ||
72 | ] | ||
73 | } | ||
74 | |||
75 | ngOnInit () { | ||
76 | this.videoCategories = this.serverService.getVideoCategories() | ||
77 | this.videoLicences = this.serverService.getVideoLicences() | ||
78 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
79 | |||
80 | this.loadFromDurationRange() | ||
81 | this.loadFromPublishedRange() | ||
82 | } | ||
83 | |||
84 | formUpdated () { | ||
85 | this.updateModelFromDurationRange() | ||
86 | this.updateModelFromPublishedRange() | ||
87 | |||
88 | this.filtered.emit(this.advancedSearch) | ||
89 | } | ||
90 | |||
91 | private loadFromDurationRange () { | ||
92 | if (this.advancedSearch.durationMin || this.advancedSearch.durationMax) { | ||
93 | const fourMinutes = 60 * 4 | ||
94 | const tenMinutes = 60 * 10 | ||
95 | |||
96 | if (this.advancedSearch.durationMin === fourMinutes && this.advancedSearch.durationMax === tenMinutes) { | ||
97 | this.durationRange = 'medium' | ||
98 | } else if (this.advancedSearch.durationMax === fourMinutes) { | ||
99 | this.durationRange = 'short' | ||
100 | } else if (this.advancedSearch.durationMin === tenMinutes) { | ||
101 | this.durationRange = 'long' | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private loadFromPublishedRange () { | ||
107 | if (this.advancedSearch.startDate) { | ||
108 | const date = new Date(this.advancedSearch.startDate) | ||
109 | const now = new Date() | ||
110 | |||
111 | const diff = Math.abs(date.getTime() - now.getTime()) | ||
112 | |||
113 | const dayMS = 1000 * 3600 * 24 | ||
114 | const numberOfDays = diff / dayMS | ||
115 | |||
116 | if (numberOfDays >= 365) this.publishedDateRange = 'last_365days' | ||
117 | else if (numberOfDays >= 30) this.publishedDateRange = 'last_30days' | ||
118 | else if (numberOfDays >= 7) this.publishedDateRange = 'last_7days' | ||
119 | else if (numberOfDays >= 0) this.publishedDateRange = 'today' | ||
120 | } | ||
121 | } | ||
122 | |||
123 | private updateModelFromDurationRange () { | ||
124 | if (!this.durationRange) return | ||
125 | |||
126 | const fourMinutes = 60 * 4 | ||
127 | const tenMinutes = 60 * 10 | ||
128 | |||
129 | switch (this.durationRange) { | ||
130 | case 'short': | ||
131 | this.advancedSearch.durationMin = undefined | ||
132 | this.advancedSearch.durationMax = fourMinutes | ||
133 | break | ||
134 | |||
135 | case 'medium': | ||
136 | this.advancedSearch.durationMin = fourMinutes | ||
137 | this.advancedSearch.durationMax = tenMinutes | ||
138 | break | ||
139 | |||
140 | case 'long': | ||
141 | this.advancedSearch.durationMin = tenMinutes | ||
142 | this.advancedSearch.durationMax = undefined | ||
143 | break | ||
144 | } | ||
145 | } | ||
146 | |||
147 | private updateModelFromPublishedRange () { | ||
148 | if (!this.publishedDateRange) return | ||
149 | |||
150 | // today | ||
151 | const date = new Date() | ||
152 | date.setHours(0, 0, 0, 0) | ||
153 | |||
154 | switch (this.publishedDateRange) { | ||
155 | case 'last_7days': | ||
156 | date.setDate(date.getDate() - 7) | ||
157 | break | ||
158 | |||
159 | case 'last_30days': | ||
160 | date.setDate(date.getDate() - 30) | ||
161 | break | ||
162 | |||
163 | case 'last_365days': | ||
164 | date.setDate(date.getDate() - 365) | ||
165 | break | ||
166 | } | ||
167 | |||
168 | this.advancedSearch.startDate = date.toISOString() | ||
169 | } | ||
170 | } | ||
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index b8c4d7dc5..3a63dbcec 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html | |||
@@ -1,10 +1,28 @@ | |||
1 | <div i18n *ngIf="pagination.totalItems === 0" class="no-result"> | ||
2 | No results found | ||
3 | </div> | ||
4 | |||
5 | <div myInfiniteScroller [autoLoading]="true" (nearOfBottom)="onNearOfBottom()" class="search-result"> | 1 | <div myInfiniteScroller [autoLoading]="true" (nearOfBottom)="onNearOfBottom()" class="search-result"> |
6 | <div i18n *ngIf="pagination.totalItems" class="results-counter"> | 2 | <div i18n class="results-header"> |
7 | {{ pagination.totalItems | myNumberFormatter }} results for <span class="search-value">{{ currentSearch }}</span> | 3 | <div class="first-line"> |
4 | <div class="results-counter"> | ||
5 | <ng-container *ngIf="pagination.totalItems"> | ||
6 | {{ pagination.totalItems | myNumberFormatter }} results for <span class="search-value">{{ currentSearch }}</span> | ||
7 | </ng-container> | ||
8 | </div> | ||
9 | |||
10 | <div | ||
11 | class="results-filter-button" (click)="isSearchFilterCollapsed = !isSearchFilterCollapsed" role="button" | ||
12 | [attr.aria-expanded]="isSearchFilterCollapsed" aria-controls="collapseBasic" | ||
13 | > | ||
14 | <span class="icon icon-filter"></span> | ||
15 | <ng-container i18n>Filters</ng-container> | ||
16 | </div> | ||
17 | </div> | ||
18 | |||
19 | <div class="results-filter" [collapse]="isSearchFilterCollapsed"> | ||
20 | <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered($event)"></my-search-filters> | ||
21 | </div> | ||
22 | </div> | ||
23 | |||
24 | <div i18n *ngIf="pagination.totalItems === 0" class="no-result"> | ||
25 | No results found | ||
8 | </div> | 26 | </div> |
9 | 27 | ||
10 | <div *ngFor="let video of videos" class="entry video"> | 28 | <div *ngFor="let video of videos" class="entry video"> |
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index 06e3c9542..f70d4bf87 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .no-result { | 4 | .no-result { |
5 | height: 70vh; | 5 | height: 40vh; |
6 | display: flex; | 6 | display: flex; |
7 | align-items: center; | 7 | align-items: center; |
8 | justify-content: center; | 8 | justify-content: center; |
@@ -11,17 +11,49 @@ | |||
11 | } | 11 | } |
12 | 12 | ||
13 | .search-result { | 13 | .search-result { |
14 | margin-left: 40px; | 14 | margin: 40px; |
15 | margin-top: 40px; | ||
16 | 15 | ||
17 | .results-counter { | 16 | .results-header { |
18 | font-size: 15px; | 17 | font-size: 16px; |
19 | padding-bottom: 20px; | 18 | padding-bottom: 20px; |
20 | margin-bottom: 30px; | 19 | margin-bottom: 30px; |
21 | border-bottom: 1px solid #DADADA; | 20 | border-bottom: 1px solid #DADADA; |
22 | 21 | ||
23 | .search-value { | 22 | .first-line { |
24 | font-weight: $font-semibold; | 23 | display: flex; |
24 | flex-direction: row; | ||
25 | |||
26 | .results-counter { | ||
27 | flex-grow: 1; | ||
28 | |||
29 | .search-value { | ||
30 | font-weight: $font-semibold; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .results-filter-button { | ||
35 | |||
36 | .icon.icon-filter { | ||
37 | @include icon(20px); | ||
38 | |||
39 | position: relative; | ||
40 | top: -1px; | ||
41 | margin-right: 5px; | ||
42 | background-image: url('../../assets/images/search/filter.svg'); | ||
43 | } | ||
44 | } | ||
45 | } | ||
46 | |||
47 | .results-filter { | ||
48 | // Animation when we show/hide the filters | ||
49 | transition: max-height 0.3s; | ||
50 | display: block !important; | ||
51 | overflow: hidden !important; | ||
52 | max-height: 0; | ||
53 | |||
54 | &.show { | ||
55 | max-height: 800px; | ||
56 | } | ||
25 | } | 57 | } |
26 | } | 58 | } |
27 | 59 | ||
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index be1cb3689..09028fec5 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { RedirectService } from '@app/core' | 3 | import { RedirectService } from '@app/core' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
5 | import { Subscription } from 'rxjs' | 5 | import { Subscription } from 'rxjs' |
@@ -8,6 +8,7 @@ import { ComponentPagination } from '@app/shared/rest/component-pagination.model | |||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { Video } from '../../../../shared' | 9 | import { Video } from '../../../../shared' |
10 | import { MetaService } from '@ngx-meta/core' | 10 | import { MetaService } from '@ngx-meta/core' |
11 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
11 | 12 | ||
12 | @Component({ | 13 | @Component({ |
13 | selector: 'my-search', | 14 | selector: 'my-search', |
@@ -21,6 +22,8 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
21 | itemsPerPage: 10, // It's per object type (so 10 videos, 10 video channels etc) | 22 | itemsPerPage: 10, // It's per object type (so 10 videos, 10 video channels etc) |
22 | totalItems: null | 23 | totalItems: null |
23 | } | 24 | } |
25 | advancedSearch: AdvancedSearch = new AdvancedSearch() | ||
26 | isSearchFilterCollapsed = true | ||
24 | 27 | ||
25 | private subActivatedRoute: Subscription | 28 | private subActivatedRoute: Subscription |
26 | private currentSearch: string | 29 | private currentSearch: string |
@@ -28,6 +31,7 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
28 | constructor ( | 31 | constructor ( |
29 | private i18n: I18n, | 32 | private i18n: I18n, |
30 | private route: ActivatedRoute, | 33 | private route: ActivatedRoute, |
34 | private router: Router, | ||
31 | private metaService: MetaService, | 35 | private metaService: MetaService, |
32 | private redirectService: RedirectService, | 36 | private redirectService: RedirectService, |
33 | private notificationsService: NotificationsService, | 37 | private notificationsService: NotificationsService, |
@@ -35,6 +39,9 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
35 | ) { } | 39 | ) { } |
36 | 40 | ||
37 | ngOnInit () { | 41 | ngOnInit () { |
42 | this.advancedSearch = new AdvancedSearch(this.route.snapshot.queryParams) | ||
43 | if (this.advancedSearch.containsValues()) this.isSearchFilterCollapsed = false | ||
44 | |||
38 | this.subActivatedRoute = this.route.queryParams.subscribe( | 45 | this.subActivatedRoute = this.route.queryParams.subscribe( |
39 | queryParams => { | 46 | queryParams => { |
40 | const querySearch = queryParams['search'] | 47 | const querySearch = queryParams['search'] |
@@ -42,6 +49,9 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
42 | if (!querySearch) return this.redirectService.redirectToHomepage() | 49 | if (!querySearch) return this.redirectService.redirectToHomepage() |
43 | if (querySearch === this.currentSearch) return | 50 | if (querySearch === this.currentSearch) return |
44 | 51 | ||
52 | // Search updated, reset filters | ||
53 | if (this.currentSearch) this.advancedSearch.reset() | ||
54 | |||
45 | this.currentSearch = querySearch | 55 | this.currentSearch = querySearch |
46 | this.updateTitle() | 56 | this.updateTitle() |
47 | 57 | ||
@@ -57,7 +67,7 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
57 | } | 67 | } |
58 | 68 | ||
59 | search () { | 69 | search () { |
60 | return this.searchService.searchVideos(this.currentSearch, this.pagination) | 70 | return this.searchService.searchVideos(this.currentSearch, this.pagination, this.advancedSearch) |
61 | .subscribe( | 71 | .subscribe( |
62 | ({ videos, totalVideos }) => { | 72 | ({ videos, totalVideos }) => { |
63 | this.videos = this.videos.concat(videos) | 73 | this.videos = this.videos.concat(videos) |
@@ -78,6 +88,14 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
78 | this.search() | 88 | this.search() |
79 | } | 89 | } |
80 | 90 | ||
91 | onFiltered () { | ||
92 | this.updateUrlFromAdvancedSearch() | ||
93 | // Hide the filters | ||
94 | this.isSearchFilterCollapsed = true | ||
95 | |||
96 | this.reload() | ||
97 | } | ||
98 | |||
81 | private reload () { | 99 | private reload () { |
82 | this.pagination.currentPage = 1 | 100 | this.pagination.currentPage = 1 |
83 | this.pagination.totalItems = null | 101 | this.pagination.totalItems = null |
@@ -90,4 +108,11 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
90 | private updateTitle () { | 108 | private updateTitle () { |
91 | this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) | 109 | this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) |
92 | } | 110 | } |
111 | |||
112 | private updateUrlFromAdvancedSearch () { | ||
113 | this.router.navigate([], { | ||
114 | relativeTo: this.route, | ||
115 | queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search: this.currentSearch }) | ||
116 | }) | ||
117 | } | ||
93 | } | 118 | } |
diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts index c6ec74d20..488046cf1 100644 --- a/client/src/app/search/search.module.ts +++ b/client/src/app/search/search.module.ts | |||
@@ -3,15 +3,20 @@ import { SharedModule } from '../shared' | |||
3 | import { SearchComponent } from '@app/search/search.component' | 3 | import { SearchComponent } from '@app/search/search.component' |
4 | import { SearchService } from '@app/search/search.service' | 4 | import { SearchService } from '@app/search/search.service' |
5 | import { SearchRoutingModule } from '@app/search/search-routing.module' | 5 | import { SearchRoutingModule } from '@app/search/search-routing.module' |
6 | import { SearchFiltersComponent } from '@app/search/search-filters.component' | ||
7 | import { CollapseModule } from 'ngx-bootstrap/collapse' | ||
6 | 8 | ||
7 | @NgModule({ | 9 | @NgModule({ |
8 | imports: [ | 10 | imports: [ |
9 | SearchRoutingModule, | 11 | SearchRoutingModule, |
10 | SharedModule | 12 | SharedModule, |
13 | |||
14 | CollapseModule.forRoot() | ||
11 | ], | 15 | ], |
12 | 16 | ||
13 | declarations: [ | 17 | declarations: [ |
14 | SearchComponent | 18 | SearchComponent, |
19 | SearchFiltersComponent | ||
15 | ], | 20 | ], |
16 | 21 | ||
17 | exports: [ | 22 | exports: [ |
diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts index 02d5f5915..c6106afd6 100644 --- a/client/src/app/search/search.service.ts +++ b/client/src/app/search/search.service.ts | |||
@@ -8,6 +8,7 @@ import { RestExtractor, RestService } from '@app/shared' | |||
8 | import { environment } from 'environments/environment' | 8 | import { environment } from 'environments/environment' |
9 | import { ResultList, Video } from '../../../../shared' | 9 | import { ResultList, Video } from '../../../../shared' |
10 | import { Video as VideoServerModel } from '@app/shared/video/video.model' | 10 | import { Video as VideoServerModel } from '@app/shared/video/video.model' |
11 | import { AdvancedSearch } from '@app/search/advanced-search.model' | ||
11 | 12 | ||
12 | export type SearchResult = { | 13 | export type SearchResult = { |
13 | videosResult: { totalVideos: number, videos: Video[] } | 14 | videosResult: { totalVideos: number, videos: Video[] } |
@@ -26,7 +27,8 @@ export class SearchService { | |||
26 | 27 | ||
27 | searchVideos ( | 28 | searchVideos ( |
28 | search: string, | 29 | search: string, |
29 | componentPagination: ComponentPagination | 30 | componentPagination: ComponentPagination, |
31 | advancedSearch: AdvancedSearch | ||
30 | ): Observable<{ videos: Video[], totalVideos: number }> { | 32 | ): Observable<{ videos: Video[], totalVideos: number }> { |
31 | const url = SearchService.BASE_SEARCH_URL + 'videos' | 33 | const url = SearchService.BASE_SEARCH_URL + 'videos' |
32 | 34 | ||
@@ -36,6 +38,19 @@ export class SearchService { | |||
36 | params = this.restService.addRestGetParams(params, pagination) | 38 | params = this.restService.addRestGetParams(params, pagination) |
37 | params = params.append('search', search) | 39 | params = params.append('search', search) |
38 | 40 | ||
41 | const advancedSearchObject = advancedSearch.toAPIObject() | ||
42 | |||
43 | for (const name of Object.keys(advancedSearchObject)) { | ||
44 | const value = advancedSearchObject[name] | ||
45 | if (!value) continue | ||
46 | |||
47 | if (Array.isArray(value)) { | ||
48 | for (const v of value) params = params.append(name, v) | ||
49 | } else { | ||
50 | params = params.append(name, value) | ||
51 | } | ||
52 | } | ||
53 | |||
39 | return this.authHttp | 54 | return this.authHttp |
40 | .get<ResultList<VideoServerModel>>(url, { params }) | 55 | .get<ResultList<VideoServerModel>>(url, { params }) |
41 | .pipe( | 56 | .pipe( |