From 5fb2e2888ce032c638e4b75d07458642f0833e52 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 29 May 2020 16:16:24 +0200 Subject: First implem global search --- .../edit-custom-config.component.html | 84 ++++++++- .../edit-custom-config.component.scss | 6 +- .../edit-custom-config.component.ts | 16 ++ client/src/app/app.module.ts | 3 +- client/src/app/core/server/server.service.ts | 45 +++-- client/src/app/header/index.ts | 1 - .../src/app/header/search-typeahead.component.html | 41 +++-- .../src/app/header/search-typeahead.component.scss | 8 +- .../src/app/header/search-typeahead.component.ts | 196 +++++++++++++-------- client/src/app/header/suggestion.component.html | 21 +-- client/src/app/header/suggestion.component.ts | 22 +-- client/src/app/header/suggestions.component.html | 6 - client/src/app/header/suggestions.component.ts | 24 --- client/src/app/search/advanced-search.model.ts | 21 ++- .../src/app/search/channel-lazy-load.resolver.ts | 45 +++++ .../src/app/search/search-filters.component.html | 64 ++++--- client/src/app/search/search-filters.component.ts | 8 +- client/src/app/search/search-routing.module.ts | 20 ++- client/src/app/search/search.component.html | 15 +- client/src/app/search/search.component.ts | 98 ++++++++--- client/src/app/search/search.module.ts | 14 +- client/src/app/search/search.service.ts | 48 +++-- client/src/app/search/video-lazy-load.resolver.ts | 43 +++++ client/src/app/shared/actor/actor.model.ts | 10 +- client/src/app/shared/angular/highlight.pipe.ts | 20 +-- .../custom-config-validators.service.ts | 8 + .../app/shared/users/user-notification.model.ts | 4 +- .../shared/video/video-miniature.component.html | 4 +- .../app/shared/video/video-miniature.component.ts | 38 +++- client/src/app/shared/video/video.model.ts | 13 +- 30 files changed, 660 insertions(+), 286 deletions(-) delete mode 100644 client/src/app/header/suggestions.component.html delete mode 100644 client/src/app/header/suggestions.component.ts create mode 100644 client/src/app/search/channel-lazy-load.resolver.ts create mode 100644 client/src/app/search/video-lazy-load.resolver.ts (limited to 'client/src/app') diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 4ee573696..b8682ffe0 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -396,9 +396,9 @@ -
+
-
NEW VIDEOS
+
VIDEOS
@@ -445,6 +445,86 @@
+
+
+
SEARCH
+
+ +
+ + + + +
+ + + Add ability for your users to fetch remote videos/actors by their URI, that may not be federated with your instance + + +
+ +
+ + + Add ability for anonymous to fetch remote videos/actors by their URI, that may not be federated with your instance + + +
+ +
+ + +
+ + + +
+ + +
{{ formErrors.search.searchIndex.url }}
+
+ +
+ +
+ +
+ + + The local search is used by default + + +
+ +
+
+
+ +
+ +
+ +
+
+
FEDERATION
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss index 2bfa92da4..9618100b5 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss @@ -64,8 +64,10 @@ textarea { } .disabled-checkbox-extra { - opacity: .5; - pointer-events: none; + &, ::ng-deep label { + opacity: .5; + pointer-events: none; + } } .form-group-right { diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 6d59494c8..3a47ba25e 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -221,6 +221,18 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A level: null, dismissable: null, message: null + }, + search: { + remoteUri: { + users: null, + anonymous: null + }, + searchIndex: { + enabled: null, + url: this.customConfigValidatorsService.SEARCH_INDEX_URL, + disableLocalSearch: null, + isDefaultSearch: null + } } } @@ -254,6 +266,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A return this.form.value['signup']['enabled'] === true } + isSearchIndexEnabled () { + return this.form.value['search']['searchIndex']['enabled'] === true + } + isAutoFollowIndexEnabled () { return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true } diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index e61346dac..89332ec5f 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -8,7 +8,7 @@ import 'focus-visible' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { CoreModule } from './core' -import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header' +import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' import { LoginModule } from './login' import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' import { SharedModule } from './shared' @@ -35,7 +35,6 @@ registerLocaleData(localeOc, 'oc') AvatarNotificationComponent, HeaderComponent, SearchTypeaheadComponent, - SuggestionsComponent, SuggestionComponent, CustomModalComponent, diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index fdfbe4c02..a804efd28 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -1,15 +1,16 @@ +import { Observable, of, Subject } from 'rxjs' import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators' import { HttpClient } from '@angular/common/http' import { Inject, Injectable, LOCALE_ID } from '@angular/core' -import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' -import { Observable, of, Subject } from 'rxjs' -import { getCompleteLocale, ServerConfig } from '../../../../../shared' -import { environment } from '../../../environments/environment' -import { VideoConstant } from '../../../../../shared/models/videos' -import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' +import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' import { sortBy } from '@app/shared/misc/utils' +import { SearchTargetType } from '@shared/models/search/search-target-query.model' import { ServerStats } from '@shared/models/server' +import { getCompleteLocale, ServerConfig } from '../../../../../shared' +import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' +import { VideoConstant } from '../../../../../shared/models/videos' +import { environment } from '../../../environments/environment' @Injectable() export class ServerService { @@ -47,12 +48,6 @@ export class ServerService { css: '' } }, - search: { - remoteUri: { - users: true, - anonymous: false - } - }, plugin: { registered: [], registeredExternalAuths: [], @@ -145,6 +140,18 @@ export class ServerService { message: '', level: 'info', dismissable: false + }, + search: { + remoteUri: { + users: true, + anonymous: false + }, + searchIndex: { + enabled: false, + url: '', + disableLocalSearch: false, + isDefaultSearch: false + } } } @@ -264,6 +271,20 @@ export class ServerService { return this.http.get(ServerService.BASE_STATS_URL) } + getDefaultSearchTarget (): Promise { + return this.getConfig().pipe( + map(config => { + const searchIndexConfig = config.search.searchIndex + + if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) { + return 'search-index' + } + + return 'local' + }) + ).toPromise() + } + private loadAttributeEnum ( baseUrl: string, attributeName: 'categories' | 'licences' | 'languages' | 'privacies', diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts index a882d4d1f..005e0c97d 100644 --- a/client/src/app/header/index.ts +++ b/client/src/app/header/index.ts @@ -1,4 +1,3 @@ export * from './header.component' export * from './search-typeahead.component' -export * from './suggestions.component' export * from './suggestion.component' diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index bbf3834c5..4355b67af 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html @@ -1,38 +1,43 @@
- - + +
    +
  • + +
  • +
-
- -
- -
- using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }} - -
+
+
+ +
+ using {{ serverConfig.search.searchIndex.url }} +
-
Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.
- +
+
Results will be augmented with those of a third-party index. Only data necessary to make the query will be sent.
-
+
- any instance - only followed instances - + any instance + only followed instances +
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index 0a30ebd55..4b56fd93a 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss @@ -36,7 +36,7 @@ #typeahead-help, #typeahead-instructions, -my-suggestions ::ng-deep ul { +li.suggestion { border: 1px solid pvar(--mainBackgroundColor); border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; @@ -90,7 +90,7 @@ my-suggestions ::ng-deep ul { } & > div:last-child { - // we have to switch the display and not the opacity, + // we have to switch the display and not the opacity, // to avoid clashing with the rest of the interface. display: none; } @@ -101,10 +101,10 @@ my-suggestions ::ng-deep ul { @media screen and (min-width: $mobile-view) { display: initial !important; } - + #typeahead-help, #typeahead-instructions, - my-suggestions ::ng-deep ul { + li.suggestion { box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px; } } diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index 2bf1072f4..6c8b8efee 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts @@ -1,23 +1,24 @@ -import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core' +import { of } from 'rxjs' +import { first, tap, delay } from 'rxjs/operators' +import { ListKeyManager } from '@angular/cdk/a11y' +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, AfterViewChecked } from '@angular/core' import { ActivatedRoute, Params, Router } from '@angular/router' import { AuthService, ServerService } from '@app/core' -import { first, tap } from 'rxjs/operators' -import { ListKeyManager } from '@angular/cdk/a11y' -import { Result, SuggestionComponent } from './suggestion.component' -import { of } from 'rxjs' import { ServerConfig } from '@shared/models' +import { SearchTargetType } from '@shared/models/search/search-target-query.model' +import { SuggestionComponent, SuggestionPayload, SuggestionPayloadType } from './suggestion.component' @Component({ selector: 'my-search-typeahead', templateUrl: './search-typeahead.component.html', styleUrls: [ './search-typeahead.component.scss' ] }) -export class SearchTypeaheadComponent implements OnInit, OnDestroy { - @ViewChild('searchVideo', { static: true }) searchInput: ElementRef +export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy { + @ViewChildren(SuggestionComponent) suggestionItems: QueryList hasChannel = false inChannel = false - newSearch = true + areSuggestionsOpened = true search = '' serverConfig: ServerConfig @@ -25,7 +26,11 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy { inThisChannelText: string keyboardEventsManager: ListKeyManager - results: Result[] = [] + results: SuggestionPayload[] = [] + + activeSearch: SuggestionPayloadType + + private scheduleKeyboardEventsInit = false constructor ( private authService: AuthService, @@ -38,109 +43,138 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy { this.route.queryParams .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null)) .subscribe(params => this.search = params.search) + } + + ngAfterViewInit () { this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) + .subscribe(config => { + this.serverConfig = config + + this.computeTypeahead() + + this.serverService.configReloaded + .subscribe(config => { + this.serverConfig = config + this.computeTypeahead() + }) + }) } - ngOnDestroy () { - if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() + ngAfterViewChecked () { + if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) { + // Avoid ExpressionChangedAfterItHasBeenCheckedError errors + setTimeout(() => this.initKeyboardEventsManager(), 0) + } } - get activeResult () { - return this.keyboardEventsManager?.activeItem?.result + ngOnDestroy () { + if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() } - get areInstructionsDisplayed () { + areInstructionsDisplayed () { return !this.search } - get showHelp () { - return this.search && this.newSearch && this.activeResult?.type === 'search-global' + showSearchGlobalHelp () { + return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index' } - get canSearchAnyURI () { + canSearchAnyURI () { if (!this.serverConfig) return false + return this.authService.isLoggedIn() ? this.serverConfig.search.remoteUri.users : this.serverConfig.search.remoteUri.anonymous } onSearchChange () { - this.computeResults() - } - - computeResults () { - this.newSearch = true - let results: Result[] = [] - - if (this.search) { - results = [ - /* Channel search is still unimplemented. Uncomment when it is. - { - text: this.search, - type: 'search-channel' - }, - */ - { - text: this.search, - type: 'search-instance', - default: true - }, - /* Global search is still unimplemented. Uncomment when it is. - { - text: this.search, - type: 'search-global' - }, - */ - ...results - ] + this.computeTypeahead() + } + + initKeyboardEventsManager () { + if (this.keyboardEventsManager) return + + this.keyboardEventsManager = new ListKeyManager(this.suggestionItems) + + const activeIndex = this.suggestionItems.toArray().findIndex(i => i.result.default === true) + if (activeIndex === -1) { + console.error('Cannot find active index.', { suggestionItems: this.suggestionItems }) } - this.results = results.filter( - (result: Result) => { - // if we're not in a channel or one of its videos/playlits, show all channel-related results - if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') - // if we're in a channel, show all channel-related results except for the channel redirection itself - if (this.inChannel) return result.type !== 'channel' - // all other result types are kept - return true - } + this.updateItemsState(activeIndex) + + this.keyboardEventsManager.change.subscribe( + _ => this.updateItemsState() ) } - setEventItems (event: { items: QueryList, index?: number }) { - event.items.forEach(e => { - if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) { - this.keyboardEventsManager.activeItem.active = true + computeTypeahead () { + const searchIndexConfig = this.serverConfig.search.searchIndex + + if (!this.activeSearch) { + if (searchIndexConfig.enabled && searchIndexConfig.isDefaultSearch) { + this.activeSearch = 'search-instance' } else { - e.active = false + this.activeSearch = 'search-index' } - }) + } + + this.areSuggestionsOpened = true + this.results = [] + + if (!this.search) return + + if (searchIndexConfig.enabled === false || searchIndexConfig.disableLocalSearch !== true) { + this.results.push({ + text: this.search, + type: 'search-instance', + default: this.activeSearch === 'search-instance' + }) + } + + if (searchIndexConfig.enabled) { + this.results.push({ + text: this.search, + type: 'search-index', + default: this.activeSearch === 'search-index' + }) + } + + this.scheduleKeyboardEventsInit = true } - initKeyboardEventsManager (event: { items: QueryList, index?: number }) { - if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() + updateItemsState (index?: number) { + if (index !== undefined) { + this.keyboardEventsManager.setActiveItem(index) + } - this.keyboardEventsManager = new ListKeyManager(event.items) + for (const item of this.suggestionItems) { + if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) { + item.active = true + this.activeSearch = item.result.type + continue + } - if (event.index !== undefined) { - this.keyboardEventsManager.setActiveItem(event.index) - } else { - this.keyboardEventsManager.setFirstItemActive() + item.active = false } + } - this.keyboardEventsManager.change.subscribe( - _ => this.setEventItems(event) - ) + onSuggestionlicked (payload: SuggestionPayload) { + this.doSearch(this.buildSearchTarget(payload)) + } + + onSuggestionHover (index: number) { + this.updateItemsState(index) } handleKey (event: KeyboardEvent) { - event.stopImmediatePropagation() if (!this.keyboardEventsManager) return switch (event.key) { case 'ArrowDown': case 'ArrowUp': + event.stopPropagation() + this.keyboardEventsManager.onKeydown(event) break } @@ -150,15 +184,19 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy { return window.location.pathname === '/search' } - doSearch () { - this.newSearch = false + doSearch (searchTarget?: SearchTargetType) { + this.areSuggestionsOpened = false const queryParams: Params = {} if (this.isOnSearch() && this.route.snapshot.queryParams) { Object.assign(queryParams, this.route.snapshot.queryParams) } - Object.assign(queryParams, { search: this.search }) + if (!searchTarget) { + searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result) + } + + Object.assign(queryParams, { search: this.search, searchTarget }) const o = this.authService.isLoggedIn() ? this.loadUserLanguagesIfNeeded(queryParams) @@ -176,4 +214,12 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy { tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) ) } + + private buildSearchTarget (result: SuggestionPayload): SearchTargetType { + if (result.type === 'search-index') { + return 'search-index' + } + + return 'local' + } } diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html index d7ae3450a..ab4b4b678 100644 --- a/client/src/app/header/suggestion.component.html +++ b/client/src/app/header/suggestion.component.html @@ -1,22 +1,17 @@
- - +
-
+
-
- In this channel +
In this instance - In the vidiverse - + In the vidiverse
- - -
\ No newline at end of file + diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts index 69641b511..250a5411e 100644 --- a/client/src/app/header/suggestion.component.ts +++ b/client/src/app/header/suggestion.component.ts @@ -1,24 +1,24 @@ -import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' +import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy, OnChanges } from '@angular/core' import { RouterLink } from '@angular/router' import { ListKeyManagerOption } from '@angular/cdk/a11y' -export type Result = { +export type SuggestionPayload = { text: string - type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' - routerLink?: RouterLink, - default?: boolean + type: SuggestionPayloadType + routerLink?: RouterLink + default: boolean } +export type SuggestionPayloadType = 'search-instance' | 'search-index' + @Component({ selector: 'my-suggestion', templateUrl: './suggestion.component.html', - styleUrls: [ './suggestion.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush + styleUrls: [ './suggestion.component.scss' ] }) export class SuggestionComponent implements OnInit, ListKeyManagerOption { - @Input() result: Result + @Input() result: SuggestionPayload @Input() highlight: string - @Output() selected = new EventEmitter() disabled = false active = false @@ -30,8 +30,4 @@ export class SuggestionComponent implements OnInit, ListKeyManagerOption { ngOnInit () { if (this.result.default) this.active = true } - - selectItem () { - this.selected.emit(this.result) - } } diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html deleted file mode 100644 index 8d017d78d..000000000 --- a/client/src/app/header/suggestions.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
    -
  • - -
  • -
\ No newline at end of file diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts deleted file mode 100644 index ee3ef73c2..000000000 --- a/client/src/app/header/suggestions.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' -import { SuggestionComponent } from './suggestion.component' - -@Component({ - selector: 'my-suggestions', - templateUrl: './suggestions.component.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SuggestionsComponent implements AfterViewInit { - @Input() results: any[] - @Input() highlight: string - @ViewChildren(SuggestionComponent) listItems: QueryList - @Output() init = new EventEmitter() - - ngAfterViewInit () { - this.listItems.changes.subscribe( - _ => this.init.emit({ items: this.listItems }) - ) - } - - hoverItem (index: number) { - this.init.emit({ items: this.listItems, index: index }) - } -} diff --git a/client/src/app/search/advanced-search.model.ts b/client/src/app/search/advanced-search.model.ts index 50f00bc27..643cc9a29 100644 --- a/client/src/app/search/advanced-search.model.ts +++ b/client/src/app/search/advanced-search.model.ts @@ -1,3 +1,4 @@ +import { SearchTargetType } from '@shared/models/search/search-target-query.model' import { NSFWQuery } from '../../../../shared/models/search' export class AdvancedSearch { @@ -23,6 +24,11 @@ export class AdvancedSearch { sort: string + searchTarget: SearchTargetType + + // Filters we don't want to count, because they are mandatory + private silentFilters = new Set([ 'sort', 'searchTarget' ]) + constructor (options?: { startDate?: string endDate?: string @@ -37,6 +43,7 @@ export class AdvancedSearch { durationMin?: string durationMax?: string sort?: string + searchTarget?: SearchTargetType }) { if (!options) return @@ -54,6 +61,8 @@ export class AdvancedSearch { this.durationMin = parseInt(options.durationMin, 10) this.durationMax = parseInt(options.durationMax, 10) + this.searchTarget = options.searchTarget || undefined + if (isNaN(this.durationMin)) this.durationMin = undefined if (isNaN(this.durationMax)) this.durationMax = undefined @@ -61,9 +70,11 @@ export class AdvancedSearch { } containsValues () { + const exceptions = new Set([ 'sort', 'searchTarget' ]) + const obj = this.toUrlObject() for (const k of Object.keys(obj)) { - if (k === 'sort') continue // Exception + if (this.silentFilters.has(k)) continue if (obj[k] !== undefined && obj[k] !== '') return true } @@ -102,7 +113,8 @@ export class AdvancedSearch { tagsAllOf: this.tagsAllOf, durationMin: this.durationMin, durationMax: this.durationMax, - sort: this.sort + sort: this.sort, + searchTarget: this.searchTarget } } @@ -120,7 +132,8 @@ export class AdvancedSearch { tagsAllOf: this.intoArray(this.tagsAllOf), durationMin: this.durationMin, durationMax: this.durationMax, - sort: this.sort + sort: this.sort, + searchTarget: this.searchTarget } } @@ -129,7 +142,7 @@ export class AdvancedSearch { const obj = this.toUrlObject() for (const k of Object.keys(obj)) { - if (k === 'sort') continue // Exception + if (this.silentFilters.has(k)) continue if (obj[k] !== undefined && obj[k] !== '') acc++ } 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..8be089cdd --- /dev/null +++ b/client/src/app/search/channel-lazy-load.resolver.ts @@ -0,0 +1,45 @@ +import { map } from 'rxjs/operators' +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' +import { SearchService } from './search.service' +import { RedirectService } from '@app/core' + +@Injectable() +export class ChannelLazyLoadResolver implements Resolve { + constructor ( + private router: Router, + private searchService: SearchService, + private redirectService: RedirectService + ) { } + + 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 index 54fc7338f..e20aef8fb 100644 --- a/client/src/app/search/search-filters.component.html +++ b/client/src/app/search/search-filters.component.html @@ -16,6 +16,25 @@
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
@@ -39,7 +58,7 @@
-
+
-
+
+
+ +
@@ -76,28 +98,6 @@
-
-
- - -
- -
- - -
- -
- - -
-
- -
- -
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts index 344a260df..af76260a7 100644 --- a/client/src/app/search/search-filters.component.ts +++ b/client/src/app/search/search-filters.component.ts @@ -44,7 +44,7 @@ export class SearchFiltersComponent implements OnInit { this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES this.publishedDateRanges = [ { - id: undefined, + id: 'any_published_date', label: this.i18n('Any') }, { @@ -67,7 +67,7 @@ export class SearchFiltersComponent implements OnInit { this.durationRanges = [ { - id: undefined, + id: 'any_duration', label: this.i18n('Any') }, { @@ -147,6 +147,10 @@ export class SearchFiltersComponent implements OnInit { 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() diff --git a/client/src/app/search/search-routing.module.ts b/client/src/app/search/search-routing.module.ts index 0ac9e6b57..9da900e9a 100644 --- a/client/src/app/search/search-routing.module.ts +++ b/client/src/app/search/search-routing.module.ts @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { MetaGuard } from '@ngx-meta/core' import { SearchComponent } from '@app/search/search.component' +import { MetaGuard } from '@ngx-meta/core' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' const searchRoutes: Routes = [ { @@ -13,6 +15,22 @@ const searchRoutes: Routes = [ title: 'Search' } } + }, + { + path: 'search/lazy-load-video', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: VideoLazyLoadResolver + } + }, + { + path: 'search/lazy-load-channel', + component: SearchComponent, + canActivate: [ MetaGuard ], + resolve: { + data: ChannelLazyLoadResolver + } } ] diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index a4a1d41b3..3cafc676d 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html @@ -2,7 +2,11 @@
- {{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} + {{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} + + on this instance + on the vidiverse + for {{ currentSearch }} @@ -31,12 +35,12 @@
- + Avatar
- +
{{ result.displayName }}
{{ result.nameWithHost }}
@@ -44,12 +48,13 @@
{{ result.followersCount }} subscribers
- +
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index 075994dd3..d3c0761d7 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts @@ -1,16 +1,18 @@ +import { forkJoin, of, Subscription } from 'rxjs' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, Notifier } from '@app/core' -import { forkJoin, of, Subscription } from 'rxjs' +import { AuthService, Notifier, ServerService } from '@app/core' +import { HooksService } from '@app/core/plugins/hooks.service' +import { AdvancedSearch } from '@app/search/advanced-search.model' import { SearchService } from '@app/search/search.service' +import { immutableAssign } from '@app/shared/misc/utils' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { MetaService } from '@ngx-meta/core' -import { AdvancedSearch } from '@app/search/advanced-search.model' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' -import { immutableAssign } from '@app/shared/misc/utils' import { Video } from '@app/shared/video/video.model' -import { HooksService } from '@app/core/plugins/hooks.service' +import { MetaService } from '@ngx-meta/core' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ServerConfig } from '@shared/models' +import { UserService } from '@app/shared' @Component({ selector: 'my-search', @@ -29,6 +31,9 @@ export class SearchComponent implements OnInit, OnDestroy { isSearchFilterCollapsed = true currentSearch: string + errorMessage: string + serverConfig: ServerConfig + private subActivatedRoute: Subscription private isInitialLoad = false // set to false to show the search filters on first arrival private firstSearch = true @@ -43,7 +48,8 @@ export class SearchComponent implements OnInit, OnDestroy { private notifier: Notifier, private searchService: SearchService, private authService: AuthService, - private hooks: HooksService + private hooks: HooksService, + private serverService: ServerService ) { } get user () { @@ -51,8 +57,11 @@ export class SearchComponent implements OnInit, OnDestroy { } ngOnInit () { + this.serverService.getConfig() + .subscribe(config => this.serverConfig = config) + this.subActivatedRoute = this.route.queryParams.subscribe( - queryParams => { + async queryParams => { const querySearch = queryParams['search'] // Search updated, reset filters @@ -65,6 +74,9 @@ export class SearchComponent implements OnInit, OnDestroy { } 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() @@ -99,28 +111,37 @@ export class SearchComponent implements OnInit, OnDestroy { 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 - - // 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() - } + ]).subscribe( + ([videosResult, videoChannelsResult]) => { + this.results = this.results + .concat(videoChannelsResult.data) + .concat(videosResult.data) + + this.pagination.totalItems = videosResult.total + videoChannelsResult.total + // Focus on channels if there are no enough videos + if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { + this.resetPagination() this.firstSearch = false - }, - err => this.notifier.error(err.message) - ) + this.channelsPerPage = 10 + this.search() + } + + this.firstSearch = false + }, + + err => { + if (this.advancedSearch.searchTarget !== 'search-index') this.notifier.error(err.message) + + 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 () { @@ -146,6 +167,24 @@ export class SearchComponent implements OnInit, OnDestroy { 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.advancedSearch.searchTarget === 'search-index' + } + private resetPagination () { this.pagination.currentPage = 1 this.pagination.totalItems = null @@ -189,7 +228,8 @@ export class SearchComponent implements OnInit, OnDestroy { const params = { search: this.currentSearch, - componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }) + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), + searchTarget: this.advancedSearch.searchTarget } return this.hooks.wrapObsFun( diff --git a/client/src/app/search/search.module.ts b/client/src/app/search/search.module.ts index 3b0fd6ee2..df5459802 100644 --- a/client/src/app/search/search.module.ts +++ b/client/src/app/search/search.module.ts @@ -1,10 +1,12 @@ -import { NgModule } from '@angular/core' import { TagInputModule } from 'ngx-chips' -import { SharedModule } from '../shared' +import { NgModule } from '@angular/core' +import { SearchFiltersComponent } from '@app/search/search-filters.component' +import { SearchRoutingModule } from '@app/search/search-routing.module' import { SearchComponent } from '@app/search/search.component' import { SearchService } from '@app/search/search.service' -import { SearchRoutingModule } from '@app/search/search-routing.module' -import { SearchFiltersComponent } from '@app/search/search-filters.component' +import { SharedModule } from '../shared' +import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' +import { VideoLazyLoadResolver } from './video-lazy-load.resolver' @NgModule({ imports: [ @@ -25,7 +27,9 @@ import { SearchFiltersComponent } from '@app/search/search-filters.component' ], providers: [ - SearchService + SearchService, + VideoLazyLoadResolver, + ChannelLazyLoadResolver ] }) export class SearchModule { } diff --git a/client/src/app/search/search.service.ts b/client/src/app/search/search.service.ts index 3cad5aaa7..fdb12ea2c 100644 --- a/client/src/app/search/search.service.ts +++ b/client/src/app/search/search.service.ts @@ -1,17 +1,18 @@ +import { Observable } from 'rxjs' import { catchError, map, switchMap } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' -import { Observable } from 'rxjs' -import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' -import { VideoService } from '@app/shared/video/video.service' -import { RestExtractor, RestService } from '@app/shared' -import { environment } from '../../environments/environment' -import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' -import { Video } from '@app/shared/video/video.model' import { AdvancedSearch } from '@app/search/advanced-search.model' +import { RestExtractor, RestPagination, RestService } from '@app/shared' +import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' +import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' -import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' +import { Video } from '@app/shared/video/video.model' +import { VideoService } from '@app/shared/video/video.service' +import { ResultList, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '../../../../shared' +import { environment } from '../../environments/environment' +import { SearchTargetType } from '@shared/models/search/search-target-query.model' @Injectable() export class SearchService { @@ -30,21 +31,27 @@ export class SearchService { searchVideos (parameters: { search: string, - componentPagination: ComponentPaginationLight, - advancedSearch: AdvancedSearch + componentPagination?: ComponentPaginationLight, + advancedSearch?: AdvancedSearch }): Observable> { const { search, componentPagination, advancedSearch } = parameters const url = SearchService.BASE_SEARCH_URL + 'videos' - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + let pagination: RestPagination + + if (componentPagination) { + pagination = this.restService.componentPaginationToRestPagination(componentPagination) + } let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination) if (search) params = params.append('search', search) - const advancedSearchObject = advancedSearch.toAPIObject() - params = this.restService.addObjectParams(params, advancedSearchObject) + if (advancedSearch) { + const advancedSearchObject = advancedSearch.toAPIObject() + params = this.restService.addObjectParams(params, advancedSearchObject) + } return this.authHttp .get>(url, { params }) @@ -56,17 +63,26 @@ export class SearchService { searchVideoChannels (parameters: { search: string, - componentPagination: ComponentPaginationLight + searchTarget?: SearchTargetType, + componentPagination?: ComponentPaginationLight }): Observable> { - const { search, componentPagination } = parameters + const { search, componentPagination, searchTarget } = parameters const url = SearchService.BASE_SEARCH_URL + 'video-channels' - const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let pagination: RestPagination + if (componentPagination) { + pagination = this.restService.componentPaginationToRestPagination(componentPagination) + } let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination) params = params.append('search', search) + if (searchTarget) { + params = params.append('searchTarget', searchTarget as string) + } + return this.authHttp .get>(url, { params }) .pipe( 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..8d846d367 --- /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 './search.service' + +@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) + }) + ) + } +} diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index 0e5060f67..a78303a2f 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts @@ -15,10 +15,14 @@ export abstract class Actor implements ActorServer { avatarUrl: string - static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) { - const absoluteAPIUrl = getAbsoluteAPIUrl() + static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) { + if (actor?.avatar?.url) return actor.avatar.url + + if (actor && actor.avatar) { + const absoluteAPIUrl = getAbsoluteAPIUrl() - if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path + return absoluteAPIUrl + actor.avatar.path + } return this.GET_DEFAULT_AVATAR_URL() } diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts index fb6042280..50ee5c1bd 100644 --- a/client/src/app/shared/angular/highlight.pipe.ts +++ b/client/src/app/shared/angular/highlight.pipe.ts @@ -11,19 +11,17 @@ export class HighlightPipe implements PipeTransform { /* use this for global search */ static MULTI_MATCH = 'Multi-Match' - // tslint:disable-next-line:no-empty - constructor () {} - transform ( - contentString: string = null, - stringToHighlight: string = null, - option = 'Single-And-StartsWith-Match', - caseSensitive = false, - highlightStyleName = 'search-highlight' + contentString: string = null, + stringToHighlight: string = null, + option = 'Single-And-StartsWith-Match', + caseSensitive = false, + highlightStyleName = 'search-highlight' ): SafeHtml { if (stringToHighlight && contentString && option) { let regex: any = '' const caseFlag: string = !caseSensitive ? 'i' : '' + switch (option) { case 'Single-Match': { regex = new RegExp(stringToHighlight, caseFlag) @@ -42,10 +40,12 @@ export class HighlightPipe implements PipeTransform { regex = new RegExp(stringToHighlight, 'gi') } } + const replaced = contentString.replace( - regex, - (match) => `${match}` + regex, + (match) => `${match}` ) + return replaced } else { return contentString diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts index abcbca817..fdb19e06a 100644 --- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts @@ -14,6 +14,7 @@ export class CustomConfigValidatorsService { readonly ADMIN_EMAIL: BuildFormValidator readonly TRANSCODING_THREADS: BuildFormValidator readonly INDEX_URL: BuildFormValidator + readonly SEARCH_INDEX_URL: BuildFormValidator constructor (private i18n: I18n) { this.INSTANCE_NAME = { @@ -86,5 +87,12 @@ export class CustomConfigValidatorsService { 'pattern': this.i18n('Index URL should be a URL') } } + + this.SEARCH_INDEX_URL = { + VALIDATORS: [ Validators.pattern(/^https?:\/\//) ], + MESSAGES: { + 'pattern': this.i18n('Search index URL should be a URL') + } + } } } diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts index ba29cb462..7b8368d87 100644 --- a/client/src/app/shared/users/user-notification.model.ts +++ b/client/src/app/shared/users/user-notification.model.ts @@ -1,4 +1,4 @@ -import { ActorInfo, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' +import { ActorInfo, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, Avatar } from '../../../../../shared' import { Actor } from '@app/shared/actor/actor.model' export class UserNotification implements UserNotificationServer { @@ -178,7 +178,7 @@ export class UserNotification implements UserNotificationServer { return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName } - private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) { + private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) { actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) } } diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index d354a2930..3e23cf18c 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -1,6 +1,6 @@
Unlisted @@ -12,7 +12,7 @@ {{ video.name }}
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index a1d4f0e81..aa1726ca7 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -1,3 +1,4 @@ +import { switchMap } from 'rxjs/operators' import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -9,15 +10,14 @@ import { OnInit, Output } from '@angular/core' -import { User } from '../users' -import { Video } from './video.model' import { AuthService, ServerService } from '@app/core' -import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '../../../../../shared' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' import { ScreenService } from '@app/shared/misc/screen.service' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' -import { switchMap } from 'rxjs/operators' +import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '../../../../../shared' +import { User } from '../users' +import { Video } from './video.model' export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' export type MiniatureDisplayOptions = { @@ -57,6 +57,8 @@ export class VideoMiniatureComponent implements OnInit { @Input() displayVideoActions = true @Input() fitWidth = false + @Input() useLazyLoadUrl = false + @Output() videoBlacklisted = new EventEmitter() @Output() videoUnblacklisted = new EventEmitter() @Output() videoRemoved = new EventEmitter() @@ -82,6 +84,8 @@ export class VideoMiniatureComponent implements OnInit { playlistElementId?: number } + videoLink: any[] = [] + private ownerDisplayTypeChosen: 'account' | 'videoChannel' constructor ( @@ -103,7 +107,10 @@ export class VideoMiniatureComponent implements OnInit { ngOnInit () { this.serverConfig = this.serverService.getTmpConfig() this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) + .subscribe(config => { + this.serverConfig = config + this.buildVideoLink() + }) this.setUpBy() @@ -113,6 +120,21 @@ export class VideoMiniatureComponent implements OnInit { } } + buildVideoLink () { + if (this.useLazyLoadUrl && this.video.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 + + this.videoLink = [ '/search/lazy-load-video', { url: this.video.url, externalRedirect, fromPath } ] + return + } + + this.videoLink = [ '/videos/watch', this.video.uuid ] + } + displayOwnerAccount () { return this.ownerDisplayTypeChosen === 'account' } @@ -203,7 +225,7 @@ export class VideoMiniatureComponent implements OnInit { } isWatchLaterPlaylistDisplayed () { - return this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined + return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined } private setUpBy () { diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 546518cca..97759f9c1 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -33,10 +33,15 @@ export class Video implements VideoServerModel { serverHost: string thumbnailPath: string thumbnailUrl: string + previewPath: string previewUrl: string + embedPath: string embedUrl: string + + url?: string + views: number likes: number dislikes: number @@ -100,13 +105,15 @@ export class Video implements VideoServerModel { this.name = hash.name this.thumbnailPath = hash.thumbnailPath - this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath + this.thumbnailUrl = hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath) this.previewPath = hash.previewPath - this.previewUrl = absoluteAPIUrl + hash.previewPath + this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) this.embedPath = hash.embedPath - this.embedUrl = absoluteAPIUrl + hash.embedPath + this.embedUrl = hash.embedUrl || (absoluteAPIUrl + hash.embedPath) + + this.url = hash.url this.views = hash.views this.likes = hash.likes -- cgit v1.2.3