X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2F%2Bsearch%2Fsearch.component.ts;h=fcf6ebbec77c40202159962b1580d4e2890a4f10;hb=db581cf7b99eb60d2e6d9bee5c020ef22b0a1a41;hp=1ed54937b510ecf7a1d3a6e03f0b1b77bb4acc4f;hpb=1942f11d5ee6926ad93dc1b79fae18325ba5de18;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index 1ed54937b..fcf6ebbec 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -1,14 +1,15 @@ -import { forkJoin, of, Subscription } from 'rxjs' +import { forkJoin, Subscription } from 'rxjs' +import { LinkType } from 'src/types/link.type' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' +import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' import { immutableAssign } from '@app/helpers' +import { validateHost } from '@app/shared/form-validators/host-validators' import { Video, VideoChannel } from '@app/shared/shared-main' import { AdvancedSearch, SearchService } from '@app/shared/shared-search' import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' -import { MetaService } from '@ngx-meta/core' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { SearchTargetType, ServerConfig } from '@shared/models' +import { VideoPlaylist } from '@app/shared/shared-video-playlist' +import { HTMLServerConfig, SearchTargetType } from '@shared/models' @Component({ selector: 'my-search', @@ -16,12 +17,13 @@ import { SearchTargetType, ServerConfig } from '@shared/models' templateUrl: './search.component.html' }) export class SearchComponent implements OnInit, OnDestroy { - results: (Video | VideoChannel)[] = [] + error: string - pagination: ComponentPagination = { + results: (Video | VideoChannel | VideoPlaylist)[] = [] + + pagination = { currentPage: 1, - itemsPerPage: 10, // Only for videos, use another variable for channels - totalItems: null + totalItems: null as number } advancedSearch: AdvancedSearch = new AdvancedSearch() isSearchFilterCollapsed = true @@ -39,20 +41,24 @@ export class SearchComponent implements OnInit, OnDestroy { } errorMessage: string - serverConfig: ServerConfig userMiniature: User private subActivatedRoute: Subscription private isInitialLoad = false // set to false to show the search filters on first arrival - private firstSearch = true private channelsPerPage = 2 + private playlistsPerPage = 2 + private videosPerPage = 10 + + private hasMoreResults = true + private isSearching = false private lastSearchTarget: SearchTargetType + private serverConfig: HTMLServerConfig + constructor ( - private i18n: I18n, private route: ActivatedRoute, private router: Router, private metaService: MetaService, @@ -65,37 +71,39 @@ export class SearchComponent implements OnInit, OnDestroy { ) { } ngOnInit () { - this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) + this.serverConfig = this.serverService.getHTMLConfig() - this.subActivatedRoute = this.route.queryParams.subscribe( - async queryParams => { - const querySearch = queryParams['search'] - const searchTarget = queryParams['searchTarget'] + this.subActivatedRoute = this.route.queryParams + .subscribe({ + next: queryParams => { + const querySearch = queryParams['search'] + const searchTarget = queryParams['searchTarget'] - // Search updated, reset filters - if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) { - this.resetPagination() - this.advancedSearch.reset() + // Search updated, reset filters + if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) { + this.resetPagination() + this.advancedSearch.reset() - this.currentSearch = querySearch || undefined - this.updateTitle() - } + this.currentSearch = querySearch || undefined + this.updateTitle() + } - this.advancedSearch = new AdvancedSearch(queryParams) - if (!this.advancedSearch.searchTarget) { - this.advancedSearch.searchTarget = await this.serverService.getDefaultSearchTarget() - } + this.advancedSearch = new AdvancedSearch(queryParams) + if (!this.advancedSearch.searchTarget) { + this.advancedSearch.searchTarget = this.getDefaultSearchTarget() + } - // Don't hide filters if we have some of them AND the user just came on the webpage - this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() - this.isInitialLoad = false + this.error = this.checkFieldsAndGetError() - this.search() - }, + // Don't hide filters if we have some of them AND the user just came on the webpage, or we have an error + this.isSearchFilterCollapsed = !this.error && (this.isInitialLoad === false || !this.advancedSearch.containsValues()) + this.isInitialLoad = false - err => this.notifier.error(err.text) - ) + this.search() + }, + + error: err => this.notifier.error(err.text) + }) this.userService.getAnonymousOrLoggedUser() .subscribe(user => this.userMiniature = user) @@ -107,62 +115,67 @@ export class SearchComponent implements OnInit, OnDestroy { if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() } - isVideoChannel (d: VideoChannel | Video): d is VideoChannel { + isVideoChannel (d: VideoChannel | Video | VideoPlaylist): d is VideoChannel { return d instanceof VideoChannel } - isVideo (v: VideoChannel | Video): v is Video { + isVideo (v: VideoChannel | Video | VideoPlaylist): v is Video { return v instanceof Video } + isPlaylist (v: VideoChannel | Video | VideoPlaylist): v is VideoPlaylist { + return v instanceof VideoPlaylist + } + isUserLoggedIn () { return this.authService.isLoggedIn() } search () { - forkJoin([ - this.getVideosObs(), - this.getVideoChannelObs() - ]).subscribe( - ([videosResult, videoChannelsResult]) => { - this.results = this.results - .concat(videoChannelsResult.data) - .concat(videosResult.data) - - this.pagination.totalItems = videosResult.total + videoChannelsResult.total - this.lastSearchTarget = this.advancedSearch.searchTarget + this.error = this.checkFieldsAndGetError() + if (this.error) return - // Focus on channels if there are no enough videos - if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { - this.resetPagination() - this.firstSearch = false + this.isSearching = true - this.channelsPerPage = 10 - this.search() + forkJoin([ + this.getVideoChannelObs(), + this.getVideoPlaylistObs(), + this.getVideosObs() + ]).subscribe({ + next: results => { + for (const result of results) { + this.results = this.results.concat(result.data) } - this.firstSearch = false + this.pagination.totalItems = results.reduce((p, r) => p += r.total, 0) + this.lastSearchTarget = this.advancedSearch.searchTarget + + this.hasMoreResults = this.results.length < this.pagination.totalItems }, - err => { + error: err => { if (this.advancedSearch.searchTarget !== 'search-index') { this.notifier.error(err.message) return } this.notifier.error( - this.i18n('Search index is unavailable. Retrying with instance results instead.'), - this.i18n('Search error') + $localize`Search index is unavailable. Retrying with instance results instead.`, + $localize`Search error` ) this.advancedSearch.searchTarget = 'local' this.search() + }, + + complete: () => { + this.isSearching = false } - ) + }) } onNearOfBottom () { // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + if (!this.hasMoreResults || this.isSearching) return this.pagination.currentPage += 1 this.search() @@ -178,23 +191,53 @@ export class SearchComponent implements OnInit, OnDestroy { return this.advancedSearch.size() } - // Add VideoChannel for typings, but the template already checks "video" argument is a video - removeVideoFromArray (video: Video | VideoChannel) { + // Add VideoChannel/VideoPlaylist for typings, but the template already checks "video" argument is a video + removeVideoFromArray (video: Video | VideoChannel | VideoPlaylist) { this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) } - getChannelUrl (channel: VideoChannel) { - if (this.advancedSearch.searchTarget === 'search-index' && channel.url) { + getLinkType (): LinkType { + if (this.advancedSearch.searchTarget === 'search-index') { const remoteUriConfig = this.serverConfig.search.remoteUri // Redirect on the external instance if not allowed to fetch remote data - const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users - const fromPath = window.location.pathname + window.location.search + if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) { + return 'external' + } - return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ] + return 'lazy-load' } - return [ '/video-channels', channel.nameWithHost ] + return 'internal' + } + + isExternalChannelUrl () { + return this.getLinkType() === 'external' + } + + getExternalChannelUrl (channel: VideoChannel) { + // Same algorithm than videos + if (this.getLinkType() === 'external') { + return channel.url + } + + // lazy-load or internal + return undefined + } + + getInternalChannelUrl (channel: VideoChannel) { + const linkType = this.getLinkType() + + if (linkType === 'internal') { + return [ '/c', channel.nameWithHost ] + } + + if (linkType === 'lazy-load') { + return [ '/search/lazy-load-channel', { url: channel.url } ] + } + + // external + return undefined } hideActions () { @@ -210,8 +253,11 @@ export class SearchComponent implements OnInit, OnDestroy { } private updateTitle () { - const suffix = this.currentSearch ? ' ' + this.currentSearch : '' - this.metaService.setTitle(this.i18n('Search') + suffix) + const suffix = this.currentSearch + ? ' ' + this.currentSearch + : '' + + this.metaService.setTitle($localize`Search` + suffix) } private updateUrlFromAdvancedSearch () { @@ -226,7 +272,7 @@ export class SearchComponent implements OnInit, OnDestroy { private getVideosObs () { const params = { search: this.currentSearch, - componentPagination: this.pagination, + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }), advancedSearch: this.advancedSearch } @@ -240,12 +286,10 @@ export class SearchComponent implements OnInit, OnDestroy { } private getVideoChannelObs () { - if (!this.currentSearch) return of({ data: [], total: 0 }) - const params = { search: this.currentSearch, componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), - searchTarget: this.advancedSearch.searchTarget + advancedSearch: this.advancedSearch } return this.hooks.wrapObsFun( @@ -256,4 +300,38 @@ export class SearchComponent implements OnInit, OnDestroy { 'filter:api.search.video-channels.list.result' ) } + + private getVideoPlaylistObs () { + const params = { + search: this.currentSearch, + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), + advancedSearch: this.advancedSearch + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideoPlaylists.bind(this.searchService), + params, + 'search', + 'filter:api.search.video-playlists.list.params', + 'filter:api.search.video-playlists.list.result' + ) + } + + private getDefaultSearchTarget (): SearchTargetType { + const searchIndexConfig = this.serverConfig.search.searchIndex + + if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) { + return 'search-index' + } + + return 'local' + } + + private checkFieldsAndGetError () { + if (this.advancedSearch.host && !validateHost(this.advancedSearch.host)) { + return $localize`PeerTube instance host filter is invalid` + } + + return undefined + } }