]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/+search/search.component.ts
Refactor options in models
[github/Chocobozzz/PeerTube.git] / client / src / app / +search / search.component.ts
1 import { forkJoin, of, Subscription } from 'rxjs'
2 import { LinkType } from 'src/types/link.type'
3 import { Component, OnDestroy, OnInit } from '@angular/core'
4 import { ActivatedRoute, Router } from '@angular/router'
5 import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
6 import { immutableAssign } from '@app/helpers'
7 import { Video, VideoChannel } from '@app/shared/shared-main'
8 import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
9 import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
10 import { VideoPlaylist } from '@app/shared/shared-video-playlist'
11 import { HTMLServerConfig, SearchTargetType } from '@shared/models'
12
13 @Component({
14 selector: 'my-search',
15 styleUrls: [ './search.component.scss' ],
16 templateUrl: './search.component.html'
17 })
18 export class SearchComponent implements OnInit, OnDestroy {
19 results: (Video | VideoChannel)[] = []
20
21 pagination = {
22 currentPage: 1,
23 totalItems: null as number
24 }
25 advancedSearch: AdvancedSearch = new AdvancedSearch()
26 isSearchFilterCollapsed = true
27 currentSearch: string
28
29 videoDisplayOptions: MiniatureDisplayOptions = {
30 date: true,
31 views: true,
32 by: true,
33 avatar: false,
34 privacyLabel: false,
35 privacyText: false,
36 state: false,
37 blacklistInfo: false
38 }
39
40 errorMessage: string
41
42 userMiniature: User
43
44 private subActivatedRoute: Subscription
45 private isInitialLoad = false // set to false to show the search filters on first arrival
46
47 private channelsPerPage = 2
48 private playlistsPerPage = 2
49 private videosPerPage = 10
50
51 private hasMoreResults = true
52 private isSearching = false
53
54 private lastSearchTarget: SearchTargetType
55
56 private serverConfig: HTMLServerConfig
57
58 constructor (
59 private route: ActivatedRoute,
60 private router: Router,
61 private metaService: MetaService,
62 private notifier: Notifier,
63 private searchService: SearchService,
64 private authService: AuthService,
65 private userService: UserService,
66 private hooks: HooksService,
67 private serverService: ServerService
68 ) { }
69
70 ngOnInit () {
71 this.serverConfig = this.serverService.getHTMLConfig()
72
73 this.subActivatedRoute = this.route.queryParams.subscribe(
74 async queryParams => {
75 const querySearch = queryParams['search']
76 const searchTarget = queryParams['searchTarget']
77
78 // Search updated, reset filters
79 if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) {
80 this.resetPagination()
81 this.advancedSearch.reset()
82
83 this.currentSearch = querySearch || undefined
84 this.updateTitle()
85 }
86
87 this.advancedSearch = new AdvancedSearch(queryParams)
88 if (!this.advancedSearch.searchTarget) {
89 this.advancedSearch.searchTarget = this.getDefaultSearchTarget()
90 }
91
92 // Don't hide filters if we have some of them AND the user just came on the webpage
93 this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues()
94 this.isInitialLoad = false
95
96 this.search()
97 },
98
99 err => this.notifier.error(err.text)
100 )
101
102 this.userService.getAnonymousOrLoggedUser()
103 .subscribe(user => this.userMiniature = user)
104
105 this.hooks.runAction('action:search.init', 'search')
106 }
107
108 ngOnDestroy () {
109 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
110 }
111
112 isVideoChannel (d: VideoChannel | Video | VideoPlaylist): d is VideoChannel {
113 return d instanceof VideoChannel
114 }
115
116 isVideo (v: VideoChannel | Video | VideoPlaylist): v is Video {
117 return v instanceof Video
118 }
119
120 isPlaylist (v: VideoChannel | Video | VideoPlaylist): v is VideoPlaylist {
121 return v instanceof VideoPlaylist
122 }
123
124 isUserLoggedIn () {
125 return this.authService.isLoggedIn()
126 }
127
128 search () {
129 this.isSearching = true
130
131 forkJoin([
132 this.getVideoChannelObs(),
133 this.getVideoPlaylistObs(),
134 this.getVideosObs()
135 ]).subscribe(results => {
136 for (const result of results) {
137 this.results = this.results.concat(result.data)
138 }
139
140 this.pagination.totalItems = results.reduce((p, r) => p += r.total, 0)
141 this.lastSearchTarget = this.advancedSearch.searchTarget
142
143 this.hasMoreResults = this.results.length < this.pagination.totalItems
144 },
145
146 err => {
147 if (this.advancedSearch.searchTarget !== 'search-index') {
148 this.notifier.error(err.message)
149 return
150 }
151
152 this.notifier.error(
153 $localize`Search index is unavailable. Retrying with instance results instead.`,
154 $localize`Search error`
155 )
156 this.advancedSearch.searchTarget = 'local'
157 this.search()
158 },
159
160 () => {
161 this.isSearching = false
162 })
163 }
164
165 onNearOfBottom () {
166 // Last page
167 if (!this.hasMoreResults || this.isSearching) return
168
169 this.pagination.currentPage += 1
170 this.search()
171 }
172
173 onFiltered () {
174 this.resetPagination()
175
176 this.updateUrlFromAdvancedSearch()
177 }
178
179 numberOfFilters () {
180 return this.advancedSearch.size()
181 }
182
183 // Add VideoChannel/VideoPlaylist for typings, but the template already checks "video" argument is a video
184 removeVideoFromArray (video: Video | VideoChannel | VideoPlaylist) {
185 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
186 }
187
188 getLinkType (): LinkType {
189 if (this.advancedSearch.searchTarget === 'search-index') {
190 const remoteUriConfig = this.serverConfig.search.remoteUri
191
192 // Redirect on the external instance if not allowed to fetch remote data
193 if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
194 return 'external'
195 }
196
197 return 'lazy-load'
198 }
199
200 return 'internal'
201 }
202
203 isExternalChannelUrl () {
204 return this.getLinkType() === 'external'
205 }
206
207 getExternalChannelUrl (channel: VideoChannel) {
208 // Same algorithm than videos
209 if (this.getLinkType() === 'external') {
210 return channel.url
211 }
212
213 // lazy-load or internal
214 return undefined
215 }
216
217 getInternalChannelUrl (channel: VideoChannel) {
218 const linkType = this.getLinkType()
219
220 if (linkType === 'internal') {
221 return [ '/c', channel.nameWithHost ]
222 }
223
224 if (linkType === 'lazy-load') {
225 return [ '/search/lazy-load-channel', { url: channel.url } ]
226 }
227
228 // external
229 return undefined
230 }
231
232 hideActions () {
233 return this.lastSearchTarget === 'search-index'
234 }
235
236 private resetPagination () {
237 this.pagination.currentPage = 1
238 this.pagination.totalItems = null
239 this.channelsPerPage = 2
240
241 this.results = []
242 }
243
244 private updateTitle () {
245 const suffix = this.currentSearch
246 ? ' ' + this.currentSearch
247 : ''
248
249 this.metaService.setTitle($localize`Search` + suffix)
250 }
251
252 private updateUrlFromAdvancedSearch () {
253 const search = this.currentSearch || undefined
254
255 this.router.navigate([], {
256 relativeTo: this.route,
257 queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
258 })
259 }
260
261 private getVideosObs () {
262 const params = {
263 search: this.currentSearch,
264 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }),
265 advancedSearch: this.advancedSearch
266 }
267
268 return this.hooks.wrapObsFun(
269 this.searchService.searchVideos.bind(this.searchService),
270 params,
271 'search',
272 'filter:api.search.videos.list.params',
273 'filter:api.search.videos.list.result'
274 )
275 }
276
277 private getVideoChannelObs () {
278 if (!this.currentSearch) return of({ data: [], total: 0 })
279
280 const params = {
281 search: this.currentSearch,
282 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
283 searchTarget: this.advancedSearch.searchTarget
284 }
285
286 return this.hooks.wrapObsFun(
287 this.searchService.searchVideoChannels.bind(this.searchService),
288 params,
289 'search',
290 'filter:api.search.video-channels.list.params',
291 'filter:api.search.video-channels.list.result'
292 )
293 }
294
295 private getVideoPlaylistObs () {
296 if (!this.currentSearch) return of({ data: [], total: 0 })
297
298 const params = {
299 search: this.currentSearch,
300 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
301 searchTarget: this.advancedSearch.searchTarget
302 }
303
304 return this.hooks.wrapObsFun(
305 this.searchService.searchVideoPlaylists.bind(this.searchService),
306 params,
307 'search',
308 'filter:api.search.video-playlists.list.params',
309 'filter:api.search.video-playlists.list.result'
310 )
311 }
312
313 private getDefaultSearchTarget (): SearchTargetType {
314 const searchIndexConfig = this.serverConfig.search.searchIndex
315
316 if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) {
317 return 'search-index'
318 }
319
320 return 'local'
321 }
322 }