From 1942f11d5ee6926ad93dc1b79fae18325ba5de18 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:49:20 +0200 Subject: Lazy load all routes --- .../src/app/+search/channel-lazy-load.resolver.ts | 43 ++++ .../src/app/+search/search-filters.component.html | 193 +++++++++++++++ .../src/app/+search/search-filters.component.scss | 69 ++++++ client/src/app/+search/search-filters.component.ts | 269 +++++++++++++++++++++ client/src/app/+search/search-routing.module.ts | 41 ++++ client/src/app/+search/search.component.html | 63 +++++ client/src/app/+search/search.component.scss | 191 +++++++++++++++ client/src/app/+search/search.component.ts | 259 ++++++++++++++++++++ client/src/app/+search/search.module.ts | 44 ++++ client/src/app/+search/video-lazy-load.resolver.ts | 43 ++++ 10 files changed, 1215 insertions(+) create mode 100644 client/src/app/+search/channel-lazy-load.resolver.ts create mode 100644 client/src/app/+search/search-filters.component.html create mode 100644 client/src/app/+search/search-filters.component.scss create mode 100644 client/src/app/+search/search-filters.component.ts create mode 100644 client/src/app/+search/search-routing.module.ts create mode 100644 client/src/app/+search/search.component.html create mode 100644 client/src/app/+search/search.component.scss create mode 100644 client/src/app/+search/search.component.ts create mode 100644 client/src/app/+search/search.module.ts create mode 100644 client/src/app/+search/video-lazy-load.resolver.ts (limited to 'client/src/app/+search') diff --git a/client/src/app/+search/channel-lazy-load.resolver.ts b/client/src/app/+search/channel-lazy-load.resolver.ts new file mode 100644 index 000000000..17a212829 --- /dev/null +++ b/client/src/app/+search/channel-lazy-load.resolver.ts @@ -0,0 +1,43 @@ +import { map } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' +import { SearchService } from '@app/shared/shared-search' + +@Injectable() +export class ChannelLazyLoadResolver implements Resolve { + constructor ( + private router: Router, + private searchService: SearchService + ) { } + + resolve (route: ActivatedRouteSnapshot) { + const url = route.params.url + const externalRedirect = route.params.externalRedirect + const fromPath = route.params.fromPath + + if (!url) { + console.error('Could not find url param.', { params: route.params }) + return this.router.navigateByUrl('/404') + } + + if (externalRedirect === 'true') { + window.open(url) + this.router.navigateByUrl(fromPath) + return + } + + return this.searchService.searchVideoChannels({ search: url }) + .pipe( + map(result => { + if (result.data.length !== 1) { + console.error('Cannot find result for this URL') + return this.router.navigateByUrl('/404') + } + + const channel = result.data[0] + + return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost) + }) + ) + } +} 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..e20aef8fb --- /dev/null +++ b/client/src/app/+search/search-filters.component.html @@ -0,0 +1,193 @@ +
+ +
+
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+
+ +
+
+ + + +
+ +
+ + + +
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+ + + +
+
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..a88a1c0b0 --- /dev/null +++ b/client/src/app/+search/search-filters.component.scss @@ -0,0 +1,69 @@ +@import '_variables'; +@import '_mixins'; + +form { + margin-top: 40px; +} + +.radio-label { + font-size: 15px; + font-weight: $font-bold; +} + +.peertube-radio-container { + @include peertube-radio-container; + + display: inline-block; + margin-right: 30px; +} + +.peertube-select-container { + @include peertube-select-container(auto); + + margin-bottom: 1rem; +} + +.form-group { + margin-bottom: 25px; +} + +input[type=text] { + @include peertube-input-text(100%); + display: block; +} + +input[type=submit] { + @include peertube-button-link; + @include orange-button; +} + +.submit-button { + text-align: right; +} + +.reset-button { + @include peertube-button; + + font-weight: $font-semibold; + display: inline-block; + padding: 0 10px 0 10px; + white-space: nowrap; + background: transparent; + + margin-right: 1rem; +} + +.reset-button-small { + font-size: 80%; + height: unset; + line-height: unset; + margin: unset; + margin-bottom: 0.5rem; +} + +.label-container { + display: flex; + white-space: nowrap; +} + +@include ng2-tags; 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..fc1db3258 --- /dev/null +++ b/client/src/app/+search/search-filters.component.ts @@ -0,0 +1,269 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' +import { ValidatorFn } from '@angular/forms' +import { ServerService } from '@app/core' +import { VideoValidatorsService } from '@app/shared/shared-forms' +import { AdvancedSearch } from '@app/shared/shared-search' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ServerConfig, VideoConstant } from '@shared/models' + +@Component({ + selector: 'my-search-filters', + styleUrls: [ './search-filters.component.scss' ], + templateUrl: './search-filters.component.html' +}) +export class SearchFiltersComponent implements OnInit { + @Input() advancedSearch: AdvancedSearch = new AdvancedSearch() + + @Output() filtered = new EventEmitter() + + videoCategories: VideoConstant[] = [] + videoLicences: VideoConstant[] = [] + videoLanguages: VideoConstant[] = [] + + tagValidators: ValidatorFn[] + tagValidatorsMessages: { [ name: string ]: string } + + publishedDateRanges: { id: string, label: string }[] = [] + sorts: { id: string, label: string }[] = [] + durationRanges: { id: string, label: string }[] = [] + + publishedDateRange: string + durationRange: string + + originallyPublishedStartYear: string + originallyPublishedEndYear: string + + private serverConfig: ServerConfig + + constructor ( + private i18n: I18n, + private videoValidatorsService: VideoValidatorsService, + private serverService: ServerService + ) { + this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS + this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES + this.publishedDateRanges = [ + { + id: 'any_published_date', + label: this.i18n('Any') + }, + { + id: 'today', + label: this.i18n('Today') + }, + { + id: 'last_7days', + label: this.i18n('Last 7 days') + }, + { + id: 'last_30days', + label: this.i18n('Last 30 days') + }, + { + id: 'last_365days', + label: this.i18n('Last 365 days') + } + ] + + this.durationRanges = [ + { + id: 'any_duration', + label: this.i18n('Any') + }, + { + id: 'short', + label: this.i18n('Short (< 4 min)') + }, + { + id: 'medium', + label: this.i18n('Medium (4-10 min)') + }, + { + id: 'long', + label: this.i18n('Long (> 10 min)') + } + ] + + this.sorts = [ + { + id: '-match', + label: this.i18n('Relevance') + }, + { + id: '-publishedAt', + label: this.i18n('Publish date') + }, + { + id: '-views', + label: this.i18n('Views') + } + ] + } + + ngOnInit () { + this.serverConfig = this.serverService.getTmpConfig() + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.serverService.getVideoCategories().subscribe(categories => this.videoCategories = categories) + this.serverService.getVideoLicences().subscribe(licences => this.videoLicences = licences) + this.serverService.getVideoLanguages().subscribe(languages => this.videoLanguages = languages) + + this.loadFromDurationRange() + this.loadFromPublishedRange() + this.loadOriginallyPublishedAtYears() + } + + inputUpdated () { + this.updateModelFromDurationRange() + this.updateModelFromPublishedRange() + this.updateModelFromOriginallyPublishedAtYears() + } + + formUpdated () { + this.inputUpdated() + this.filtered.emit(this.advancedSearch) + } + + reset () { + this.advancedSearch.reset() + this.durationRange = undefined + this.publishedDateRange = undefined + this.originallyPublishedStartYear = undefined + this.originallyPublishedEndYear = undefined + this.inputUpdated() + } + + resetField (fieldName: string, value?: any) { + this.advancedSearch[fieldName] = value + } + + resetLocalField (fieldName: string, value?: any) { + this[fieldName] = value + this.inputUpdated() + } + + resetOriginalPublicationYears () { + this.originallyPublishedStartYear = this.originallyPublishedEndYear = undefined + } + + isSearchTargetEnabled () { + return this.serverConfig.search.searchIndex.enabled && this.serverConfig.search.searchIndex.disableLocalSearch !== true + } + + private loadOriginallyPublishedAtYears () { + this.originallyPublishedStartYear = this.advancedSearch.originallyPublishedStartDate + ? new Date(this.advancedSearch.originallyPublishedStartDate).getFullYear().toString() + : null + + this.originallyPublishedEndYear = this.advancedSearch.originallyPublishedEndDate + ? new Date(this.advancedSearch.originallyPublishedEndDate).getFullYear().toString() + : null + } + + private loadFromDurationRange () { + if (this.advancedSearch.durationMin || this.advancedSearch.durationMax) { + const fourMinutes = 60 * 4 + const tenMinutes = 60 * 10 + + if (this.advancedSearch.durationMin === fourMinutes && this.advancedSearch.durationMax === tenMinutes) { + this.durationRange = 'medium' + } else if (this.advancedSearch.durationMax === fourMinutes) { + this.durationRange = 'short' + } else if (this.advancedSearch.durationMin === tenMinutes) { + this.durationRange = 'long' + } + } + } + + private loadFromPublishedRange () { + if (this.advancedSearch.startDate) { + const date = new Date(this.advancedSearch.startDate) + const now = new Date() + + const diff = Math.abs(date.getTime() - now.getTime()) + + const dayMS = 1000 * 3600 * 24 + const numberOfDays = diff / dayMS + + if (numberOfDays >= 365) this.publishedDateRange = 'last_365days' + else if (numberOfDays >= 30) this.publishedDateRange = 'last_30days' + else if (numberOfDays >= 7) this.publishedDateRange = 'last_7days' + else if (numberOfDays >= 0) this.publishedDateRange = 'today' + } + } + + private updateModelFromOriginallyPublishedAtYears () { + const baseDate = new Date() + baseDate.setHours(0, 0, 0, 0) + baseDate.setMonth(0, 1) + + if (this.originallyPublishedStartYear) { + const year = parseInt(this.originallyPublishedStartYear, 10) + const start = new Date(baseDate) + start.setFullYear(year) + + this.advancedSearch.originallyPublishedStartDate = start.toISOString() + } else { + this.advancedSearch.originallyPublishedStartDate = null + } + + if (this.originallyPublishedEndYear) { + const year = parseInt(this.originallyPublishedEndYear, 10) + const end = new Date(baseDate) + end.setFullYear(year) + + this.advancedSearch.originallyPublishedEndDate = end.toISOString() + } else { + this.advancedSearch.originallyPublishedEndDate = null + } + } + + private updateModelFromDurationRange () { + if (!this.durationRange) return + + const fourMinutes = 60 * 4 + const tenMinutes = 60 * 10 + + switch (this.durationRange) { + case 'short': + this.advancedSearch.durationMin = undefined + this.advancedSearch.durationMax = fourMinutes + break + + case 'medium': + this.advancedSearch.durationMin = fourMinutes + this.advancedSearch.durationMax = tenMinutes + break + + case 'long': + this.advancedSearch.durationMin = tenMinutes + this.advancedSearch.durationMax = undefined + break + } + } + + private updateModelFromPublishedRange () { + if (!this.publishedDateRange) return + + // today + const date = new Date() + date.setHours(0, 0, 0, 0) + + switch (this.publishedDateRange) { + case 'last_7days': + date.setDate(date.getDate() - 7) + break + + case 'last_30days': + date.setDate(date.getDate() - 30) + break + + case 'last_365days': + date.setDate(date.getDate() - 365) + break + } + + this.advancedSearch.startDate = date.toISOString() + } +} diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts new file mode 100644 index 000000000..14a0d0a13 --- /dev/null +++ b/client/src/app/+search/search-routing.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MetaGuard } from '@ngx-meta/core' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' +import { SearchComponent } from './search.component' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' + +const searchRoutes: Routes = [ + { + path: '', + component: SearchComponent, + canActivate: [ MetaGuard ], + data: { + meta: { + title: 'Search' + } + } + }, + { + path: 'lazy-load-video', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: VideoLazyLoadResolver + } + }, + { + path: 'lazy-load-channel', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: ChannelLazyLoadResolver + } + } +] + +@NgModule({ + imports: [ RouterModule.forChild(searchRoutes) ], + exports: [ RouterModule ] +}) +export class SearchRoutingModule {} diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html new file mode 100644 index 000000000..9bff024ad --- /dev/null +++ b/client/src/app/+search/search.component.html @@ -0,0 +1,63 @@ +
+
+
+
+ {{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} + + on this instance + on the vidiverse + + + for {{ currentSearch }} + +
+ +
+ + + Filters + {{ numberOfFilters() }} + +
+
+ +
+ +
+
+ +
+ No results found +
+ + +
+ + Avatar + + +
+ +
{{ result.displayName }}
+
{{ result.nameWithHost }}
+
+ +
{{ result.followersCount }} subscribers
+
+ + +
+ +
+ +
+
+ +
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss new file mode 100644 index 000000000..6e59adb60 --- /dev/null +++ b/client/src/app/+search/search.component.scss @@ -0,0 +1,191 @@ +@import '_variables'; +@import '_mixins'; + +.search-result { + padding: 40px; + + .results-header { + font-size: 16px; + padding-bottom: 20px; + margin-bottom: 30px; + border-bottom: 1px solid #DADADA; + + .first-line { + display: flex; + flex-direction: row; + + .results-counter { + flex-grow: 1; + + .search-value { + font-weight: $font-semibold; + } + } + + .results-filter-button { + cursor: pointer; + + .icon.icon-filter { + @include icon(20px); + + position: relative; + top: -1px; + margin-right: 5px; + background-image: url('../../assets/images/search/filter.svg'); + } + } + } + } + + .entry { + display: flex; + min-height: 130px; + padding-bottom: 20px; + margin-bottom: 20px; + + &.video-channel { + img { + $image-size: 130px; + $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature + + @include avatar($image-size); + + margin: 0 ($margin-size + 10) 0 $margin-size; + } + + .video-channel-info { + flex-grow: 1; + width: fit-content; + + .video-channel-names { + @include disable-default-a-behaviour; + + display: flex; + align-items: baseline; + color: pvar(--mainForegroundColor); + width: fit-content; + + .video-channel-display-name { + font-weight: $font-semibold; + font-size: 18px; + } + + .video-channel-name { + font-size: 14px; + color: $grey-actor-name; + margin-left: 5px; + } + } + } + } + } +} + +@media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) { + .video-channel-info .video-channel-names { + flex-direction: column !important; + + .video-channel-name { + @include ellipsis; // Ellipsis and max-width on channel-name to not break screen + + max-width: 250px; + margin-left: 0 !important; + } + } + + :host-context(.main-col:not(.expanded)) { + // Override the min-width: 500px to not break screen + ::ng-deep .video-miniature-information { + min-width: 300px !important; + } + } +} + +@media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) { + :host-context(.main-col:not(.expanded)) { + .video-channel-info .video-channel-names { + .video-channel-name { + max-width: 160px; + } + } + + // Override the min-width: 500px to not break screen + ::ng-deep .video-miniature-information { + min-width: $video-thumbnail-width !important; + } + } + + :host-context(.expanded) { + // Override the min-width: 500px to not break screen + ::ng-deep .video-miniature-information { + min-width: 300px !important; + } + } +} + +@media screen and (max-width: $small-view) { + .search-result { + .entry.video-channel, + .entry.video { + flex-direction: column; + height: auto; + justify-content: center; + align-items: center; + text-align: center; + + img { + margin: 0; + } + + img { + margin: 0; + } + + .video-channel-info .video-channel-names { + align-items: center; + flex-direction: column !important; + + .video-channel-name { + margin-left: 0 !important; + } + } + + my-subscribe-button { + margin-top: 5px; + } + } + } +} + +@media screen and (max-width: $mobile-view) { + .search-result { + padding: 20px 10px; + + .results-header { + font-size: 15px !important; + } + + .entry { + &.video { + .video-info-name, + .video-info-account { + margin: auto; + } + + my-video-thumbnail { + margin-right: 0 !important; + + ::ng-deep .video-thumbnail { + width: 100%; + height: auto; + + img { + width: 100%; + height: auto; + } + } + } + } + } + } +} diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts new file mode 100644 index 000000000..1ed54937b --- /dev/null +++ b/client/src/app/+search/search.component.ts @@ -0,0 +1,259 @@ +import { forkJoin, of, Subscription } from 'rxjs' +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 { immutableAssign } from '@app/helpers' +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' + +@Component({ + selector: 'my-search', + styleUrls: [ './search.component.scss' ], + templateUrl: './search.component.html' +}) +export class SearchComponent implements OnInit, OnDestroy { + results: (Video | VideoChannel)[] = [] + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, // Only for videos, use another variable for channels + totalItems: null + } + advancedSearch: AdvancedSearch = new AdvancedSearch() + isSearchFilterCollapsed = true + currentSearch: string + + videoDisplayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + avatar: false, + privacyLabel: false, + privacyText: false, + state: false, + blacklistInfo: false + } + + 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 lastSearchTarget: SearchTargetType + + constructor ( + private i18n: I18n, + private route: ActivatedRoute, + private router: Router, + private metaService: MetaService, + private notifier: Notifier, + private searchService: SearchService, + private authService: AuthService, + private userService: UserService, + private hooks: HooksService, + private serverService: ServerService + ) { } + + ngOnInit () { + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + + this.subActivatedRoute = this.route.queryParams.subscribe( + async 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() + + this.currentSearch = querySearch || undefined + this.updateTitle() + } + + this.advancedSearch = new AdvancedSearch(queryParams) + if (!this.advancedSearch.searchTarget) { + this.advancedSearch.searchTarget = await this.serverService.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.search() + }, + + err => this.notifier.error(err.text) + ) + + this.userService.getAnonymousOrLoggedUser() + .subscribe(user => this.userMiniature = user) + + this.hooks.runAction('action:search.init', 'search') + } + + ngOnDestroy () { + if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() + } + + isVideoChannel (d: VideoChannel | Video): d is VideoChannel { + return d instanceof VideoChannel + } + + isVideo (v: VideoChannel | Video): v is Video { + return v instanceof Video + } + + 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 + + // 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.channelsPerPage = 10 + this.search() + } + + this.firstSearch = false + }, + + 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') + ) + this.advancedSearch.searchTarget = 'local' + this.search() + } + ) + } + + onNearOfBottom () { + // Last page + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + + this.pagination.currentPage += 1 + this.search() + } + + onFiltered () { + this.resetPagination() + + this.updateUrlFromAdvancedSearch() + } + + numberOfFilters () { + return this.advancedSearch.size() + } + + // Add VideoChannel for typings, but the template already checks "video" argument is a video + removeVideoFromArray (video: Video | VideoChannel) { + this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) + } + + getChannelUrl (channel: VideoChannel) { + if (this.advancedSearch.searchTarget === 'search-index' && channel.url) { + 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 + + return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ] + } + + return [ '/video-channels', channel.nameWithHost ] + } + + hideActions () { + return this.lastSearchTarget === 'search-index' + } + + private resetPagination () { + this.pagination.currentPage = 1 + this.pagination.totalItems = null + this.channelsPerPage = 2 + + this.results = [] + } + + private updateTitle () { + const suffix = this.currentSearch ? ' ' + this.currentSearch : '' + this.metaService.setTitle(this.i18n('Search') + suffix) + } + + private updateUrlFromAdvancedSearch () { + const search = this.currentSearch || undefined + + this.router.navigate([], { + relativeTo: this.route, + queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search }) + }) + } + + private getVideosObs () { + const params = { + search: this.currentSearch, + componentPagination: this.pagination, + advancedSearch: this.advancedSearch + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideos.bind(this.searchService), + params, + 'search', + 'filter:api.search.videos.list.params', + 'filter:api.search.videos.list.result' + ) + } + + 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 + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideoChannels.bind(this.searchService), + params, + 'search', + 'filter:api.search.video-channels.list.params', + 'filter:api.search.video-channels.list.result' + ) + } +} diff --git a/client/src/app/+search/search.module.ts b/client/src/app/+search/search.module.ts new file mode 100644 index 000000000..ee4f07ad1 --- /dev/null +++ b/client/src/app/+search/search.module.ts @@ -0,0 +1,44 @@ +import { TagInputModule } from 'ngx-chips' +import { NgModule } from '@angular/core' +import { SharedFormModule } from '@app/shared/shared-forms' +import { SharedMainModule } from '@app/shared/shared-main' +import { SharedSearchModule } from '@app/shared/shared-search' +import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' +import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' +import { SearchService } from '../shared/shared-search/search.service' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' +import { SearchFiltersComponent } from './search-filters.component' +import { SearchRoutingModule } from './search-routing.module' +import { SearchComponent } from './search.component' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' + +@NgModule({ + imports: [ + TagInputModule, + + SearchRoutingModule, + + SharedMainModule, + SharedSearchModule, + SharedFormModule, + SharedUserSubscriptionModule, + SharedVideoMiniatureModule + ], + + declarations: [ + SearchComponent, + SearchFiltersComponent + ], + + exports: [ + TagInputModule, + SearchComponent + ], + + providers: [ + SearchService, + VideoLazyLoadResolver, + ChannelLazyLoadResolver + ] +}) +export class SearchModule { } diff --git a/client/src/app/+search/video-lazy-load.resolver.ts b/client/src/app/+search/video-lazy-load.resolver.ts new file mode 100644 index 000000000..e8b2b8c74 --- /dev/null +++ b/client/src/app/+search/video-lazy-load.resolver.ts @@ -0,0 +1,43 @@ +import { map } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' +import { SearchService } from '@app/shared/shared-search' + +@Injectable() +export class VideoLazyLoadResolver implements Resolve { + constructor ( + private router: Router, + private searchService: SearchService + ) { } + + resolve (route: ActivatedRouteSnapshot) { + const url = route.params.url + const externalRedirect = route.params.externalRedirect + const fromPath = route.params.fromPath + + if (!url) { + console.error('Could not find url param.', { params: route.params }) + return this.router.navigateByUrl('/404') + } + + if (externalRedirect === 'true') { + window.open(url) + this.router.navigateByUrl(fromPath) + return + } + + return this.searchService.searchVideos({ search: url }) + .pipe( + map(result => { + if (result.data.length !== 1) { + console.error('Cannot find result for this URL') + return this.router.navigateByUrl('/404') + } + + const video = result.data[0] + + return this.router.navigateByUrl('/videos/watch/' + video.uuid) + }) + ) + } +} -- cgit v1.2.3