mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
Add ability to search playlists
This commit is contained in:
@@ -187,7 +187,7 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy {
|
||||
// Reload playlist thumbnail if the first element changed
|
||||
const newFirst = this.findFirst()
|
||||
if (oldFirst && newFirst && oldFirst.id !== newFirst.id) {
|
||||
this.playlist.refreshThumbnail()
|
||||
this.loadPlaylistInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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<any> {
|
||||
constructor (
|
||||
private router: Router,
|
||||
private searchService: SearchService
|
||||
) { }
|
||||
|
||||
resolve (route: ActivatedRouteSnapshot) {
|
||||
const url = route.params.url
|
||||
|
||||
if (!url) {
|
||||
console.error('Could not find url param.', { params: route.params })
|
||||
return this.router.navigateByUrl('/404')
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
|
||||
import { SearchComponent } from './search.component'
|
||||
import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
||||
import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared'
|
||||
|
||||
const searchRoutes: Routes = [
|
||||
{
|
||||
@@ -27,6 +26,13 @@ const searchRoutes: Routes = [
|
||||
resolve: {
|
||||
data: ChannelLazyLoadResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'lazy-load-playlist',
|
||||
component: SearchComponent,
|
||||
resolve: {
|
||||
data: PlaylistLazyLoadResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -59,10 +59,17 @@
|
||||
<div *ngIf="isVideo(result)" class="entry video">
|
||||
<my-video-miniature
|
||||
[video]="result" [user]="userMiniature" [displayAsRow]="true" [displayVideoActions]="!hideActions()"
|
||||
[displayOptions]="videoDisplayOptions" [videoLinkType]="getVideoLinkType()"
|
||||
[displayOptions]="videoDisplayOptions" [videoLinkType]="getLinkType()"
|
||||
(videoBlocked)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)"
|
||||
></my-video-miniature>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isPlaylist(result)" class="entry video-playlist">
|
||||
<my-video-playlist-miniature
|
||||
[playlist]="result" [displayAsRow]="true" [displayChannel]="true"
|
||||
[linkType]="getLinkType()"
|
||||
></my-video-playlist-miniature>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { forkJoin, of, 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, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
|
||||
import { AuthService, HooksService, MetaService, 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, VideoLinkType } from '@app/shared/shared-video-miniature'
|
||||
import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist'
|
||||
import { HTMLServerConfig, SearchTargetType } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
@@ -16,10 +18,9 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models'
|
||||
export class SearchComponent implements OnInit, OnDestroy {
|
||||
results: (Video | VideoChannel)[] = []
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
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
|
||||
@@ -45,6 +46,11 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||
private firstSearch = true
|
||||
|
||||
private channelsPerPage = 2
|
||||
private playlistsPerPage = 2
|
||||
private videosPerPage = 10
|
||||
|
||||
private hasMoreResults = true
|
||||
private isSearching = false
|
||||
|
||||
private lastSearchTarget: SearchTargetType
|
||||
|
||||
@@ -104,77 +110,62 @@ 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()
|
||||
}
|
||||
|
||||
getVideoLinkType (): VideoLinkType {
|
||||
if (this.advancedSearch.searchTarget === 'search-index') {
|
||||
const remoteUriConfig = this.serverConfig.search.remoteUri
|
||||
|
||||
// Redirect on the external instance if not allowed to fetch remote data
|
||||
if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
|
||||
return 'external'
|
||||
}
|
||||
|
||||
return 'lazy-load'
|
||||
}
|
||||
|
||||
return 'internal'
|
||||
}
|
||||
|
||||
search () {
|
||||
this.isSearching = true
|
||||
|
||||
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(
|
||||
$localize`Search index is unavailable. Retrying with instance results instead.`,
|
||||
$localize`Search error`
|
||||
)
|
||||
this.advancedSearch.searchTarget = 'local'
|
||||
this.search()
|
||||
this.getVideoChannelObs(),
|
||||
this.getVideoPlaylistObs(),
|
||||
this.getVideosObs()
|
||||
]).subscribe(results => {
|
||||
for (const result of results) {
|
||||
this.results = this.results.concat(result.data)
|
||||
}
|
||||
)
|
||||
|
||||
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 => {
|
||||
if (this.advancedSearch.searchTarget !== 'search-index') {
|
||||
this.notifier.error(err.message)
|
||||
return
|
||||
}
|
||||
|
||||
this.notifier.error(
|
||||
$localize`Search index is unavailable. Retrying with instance results instead.`,
|
||||
$localize`Search error`
|
||||
)
|
||||
this.advancedSearch.searchTarget = 'local'
|
||||
this.search()
|
||||
},
|
||||
|
||||
() => {
|
||||
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()
|
||||
@@ -190,18 +181,33 @@ 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)
|
||||
}
|
||||
|
||||
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
|
||||
if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
|
||||
return 'external'
|
||||
}
|
||||
|
||||
return 'lazy-load'
|
||||
}
|
||||
|
||||
return 'internal'
|
||||
}
|
||||
|
||||
isExternalChannelUrl () {
|
||||
return this.getVideoLinkType() === 'external'
|
||||
return this.getLinkType() === 'external'
|
||||
}
|
||||
|
||||
getExternalChannelUrl (channel: VideoChannel) {
|
||||
// Same algorithm than videos
|
||||
if (this.getVideoLinkType() === 'external') {
|
||||
if (this.getLinkType() === 'external') {
|
||||
return channel.url
|
||||
}
|
||||
|
||||
@@ -210,7 +216,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getInternalChannelUrl (channel: VideoChannel) {
|
||||
const linkType = this.getVideoLinkType()
|
||||
const linkType = this.getLinkType()
|
||||
|
||||
if (linkType === 'internal') {
|
||||
return [ '/c', channel.nameWithHost ]
|
||||
@@ -256,7 +262,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
|
||||
}
|
||||
|
||||
@@ -287,6 +293,24 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
}
|
||||
|
||||
private getVideoPlaylistObs () {
|
||||
if (!this.currentSearch) return of({ data: [], total: 0 })
|
||||
|
||||
const params = {
|
||||
search: this.currentSearch,
|
||||
componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
|
||||
searchTarget: this.advancedSearch.searchTarget
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ 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 { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
|
||||
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'
|
||||
import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -21,7 +21,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
||||
SharedFormModule,
|
||||
SharedActorImageModule,
|
||||
SharedUserSubscriptionModule,
|
||||
SharedVideoMiniatureModule
|
||||
SharedVideoMiniatureModule,
|
||||
SharedVideoPlaylistModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
@@ -36,7 +37,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
||||
providers: [
|
||||
SearchService,
|
||||
VideoLazyLoadResolver,
|
||||
ChannelLazyLoadResolver
|
||||
ChannelLazyLoadResolver,
|
||||
PlaylistLazyLoadResolver
|
||||
]
|
||||
})
|
||||
export class SearchModule { }
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'
|
||||
import { SearchService } from '@app/shared/shared-search'
|
||||
import { ResultList } from '@shared/models/result-list.model'
|
||||
|
||||
@Injectable()
|
||||
export class VideoLazyLoadResolver implements Resolve<any> {
|
||||
constructor (
|
||||
private router: Router,
|
||||
private searchService: SearchService
|
||||
) { }
|
||||
export abstract class AbstractLazyLoadResolver <T> implements Resolve<any> {
|
||||
protected router: Router
|
||||
|
||||
resolve (route: ActivatedRouteSnapshot) {
|
||||
const url = route.params.url
|
||||
@@ -18,7 +14,7 @@ export class VideoLazyLoadResolver implements Resolve<any> {
|
||||
return this.router.navigateByUrl('/404')
|
||||
}
|
||||
|
||||
return this.searchService.searchVideos({ search: url })
|
||||
return this.finder(url)
|
||||
.pipe(
|
||||
map(result => {
|
||||
if (result.data.length !== 1) {
|
||||
@@ -26,10 +22,13 @@ export class VideoLazyLoadResolver implements Resolve<any> {
|
||||
return this.router.navigateByUrl('/404')
|
||||
}
|
||||
|
||||
const video = result.data[0]
|
||||
const redirectUrl = this.buildUrl(result.data[0])
|
||||
|
||||
return this.router.navigateByUrl('/w/' + video.uuid)
|
||||
return this.router.navigateByUrl(redirectUrl)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract finder (url: string): Observable<ResultList<T>>
|
||||
protected abstract buildUrl (e: T): string
|
||||
}
|
||||
24
client/src/app/+search/shared/channel-lazy-load.resolver.ts
Normal file
24
client/src/app/+search/shared/channel-lazy-load.resolver.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { VideoChannel } from '@app/shared/shared-main'
|
||||
import { SearchService } from '@app/shared/shared-search'
|
||||
import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
|
||||
|
||||
@Injectable()
|
||||
export class ChannelLazyLoadResolver extends AbstractLazyLoadResolver<VideoChannel> {
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected finder (url: string) {
|
||||
return this.searchService.searchVideoChannels({ search: url })
|
||||
}
|
||||
|
||||
protected buildUrl (channel: VideoChannel) {
|
||||
return '/video-channels/' + channel.nameWithHost
|
||||
}
|
||||
}
|
||||
4
client/src/app/+search/shared/index.ts
Normal file
4
client/src/app/+search/shared/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './abstract-lazy-load.resolver'
|
||||
export * from './channel-lazy-load.resolver'
|
||||
export * from './playlist-lazy-load.resolver'
|
||||
export * from './video-lazy-load.resolver'
|
||||
24
client/src/app/+search/shared/playlist-lazy-load.resolver.ts
Normal file
24
client/src/app/+search/shared/playlist-lazy-load.resolver.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { SearchService } from '@app/shared/shared-search'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist'
|
||||
import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
|
||||
|
||||
@Injectable()
|
||||
export class PlaylistLazyLoadResolver extends AbstractLazyLoadResolver<VideoPlaylist> {
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected finder (url: string) {
|
||||
return this.searchService.searchVideoPlaylists({ search: url })
|
||||
}
|
||||
|
||||
protected buildUrl (playlist: VideoPlaylist) {
|
||||
return '/w/p/' + playlist.uuid
|
||||
}
|
||||
}
|
||||
24
client/src/app/+search/shared/video-lazy-load.resolver.ts
Normal file
24
client/src/app/+search/shared/video-lazy-load.resolver.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { Video } from '@app/shared/shared-main'
|
||||
import { SearchService } from '@app/shared/shared-search'
|
||||
import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
|
||||
|
||||
@Injectable()
|
||||
export class VideoLazyLoadResolver extends AbstractLazyLoadResolver<Video> {
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected finder (url: string) {
|
||||
return this.searchService.searchVideos({ search: url })
|
||||
}
|
||||
|
||||
protected buildUrl (video: Video) {
|
||||
return '/w/' + video.uuid
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="d-inline-flex position-relative" id="typeahead-container">
|
||||
<input
|
||||
type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
|
||||
type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…"
|
||||
[(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" (keydown.enter)="doSearch()"
|
||||
aria-label="Search" autocomplete="off"
|
||||
>
|
||||
|
||||
@@ -3,5 +3,6 @@ export * from './bytes.pipe'
|
||||
export * from './duration-formatter.pipe'
|
||||
export * from './from-now.pipe'
|
||||
export * from './infinite-scroller.directive'
|
||||
export * from './link.component'
|
||||
export * from './number-formatter.pipe'
|
||||
export * from './peertube-template.directive'
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<ng-template #content>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
||||
<a *ngIf="!href" [routerLink]="internalLink" [attr.title]="title" [tabindex]="tabindex">
|
||||
<ng-template *ngTemplateOutlet="content"></ng-template>
|
||||
</a>
|
||||
|
||||
<a *ngIf="href" [href]="href" [target]="target" [attr.title]="title" [tabindex]="tabindex">
|
||||
<ng-template *ngTemplateOutlet="content"></ng-template>
|
||||
</a>
|
||||
@@ -0,0 +1,7 @@
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
position: inherit;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
17
client/src/app/shared/shared-main/angular/link.component.ts
Normal file
17
client/src/app/shared/shared-main/angular/link.component.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Component, Input, ViewEncapsulation } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-link',
|
||||
styleUrls: [ './link.component.scss' ],
|
||||
templateUrl: './link.component.html'
|
||||
})
|
||||
export class LinkComponent {
|
||||
@Input() internalLink?: any[]
|
||||
|
||||
@Input() href?: string
|
||||
@Input() target?: string
|
||||
|
||||
@Input() title?: string
|
||||
|
||||
@Input() tabindex: string | number
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { CommonModule, DatePipe } from '@angular/common'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRouteSnapshot, RouterModule } from '@angular/router'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbButtonsModule,
|
||||
NgbCollapseModule,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DurationFormatterPipe,
|
||||
FromNowPipe,
|
||||
InfiniteScrollerDirective,
|
||||
LinkComponent,
|
||||
NumberFormatterPipe,
|
||||
PeerTubeTemplateDirective
|
||||
} from './angular'
|
||||
@@ -35,11 +36,11 @@ import { FeedComponent } from './feeds'
|
||||
import { LoaderComponent, SmallLoaderComponent } from './loaders'
|
||||
import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
|
||||
import { PluginPlaceholderComponent } from './plugins'
|
||||
import { ActorRedirectGuard } from './router'
|
||||
import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
|
||||
import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
|
||||
import { VideoCaptionService } from './video-caption'
|
||||
import { VideoChannelService } from './video-channel'
|
||||
import { ActorRedirectGuard } from './router'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -76,6 +77,7 @@ import { ActorRedirectGuard } from './router'
|
||||
|
||||
InfiniteScrollerDirective,
|
||||
PeerTubeTemplateDirective,
|
||||
LinkComponent,
|
||||
|
||||
ActionDropdownComponent,
|
||||
ButtonComponent,
|
||||
@@ -130,6 +132,7 @@ import { ActorRedirectGuard } from './router'
|
||||
|
||||
InfiniteScrollerDirective,
|
||||
PeerTubeTemplateDirective,
|
||||
LinkComponent,
|
||||
|
||||
ActionDropdownComponent,
|
||||
ButtonComponent,
|
||||
|
||||
@@ -3,10 +3,17 @@ import { catchError, map, switchMap } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
||||
import { ResultList, SearchTargetType, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '@shared/models'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import {
|
||||
ResultList,
|
||||
SearchTargetType,
|
||||
Video as VideoServerModel,
|
||||
VideoChannel as VideoChannelServerModel,
|
||||
VideoPlaylist as VideoPlaylistServerModel
|
||||
} from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist'
|
||||
import { AdvancedSearch } from './advanced-search.model'
|
||||
|
||||
@Injectable()
|
||||
@@ -17,7 +24,8 @@ export class SearchService {
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
private restService: RestService,
|
||||
private videoService: VideoService
|
||||
private videoService: VideoService,
|
||||
private playlistService: VideoPlaylistService
|
||||
) {
|
||||
// Add ability to override search endpoint if the user updated this local storage key
|
||||
const searchUrl = peertubeLocalStorage.getItem('search-url')
|
||||
@@ -85,4 +93,34 @@ export class SearchService {
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
searchVideoPlaylists (parameters: {
|
||||
search: string,
|
||||
searchTarget?: SearchTargetType,
|
||||
componentPagination?: ComponentPaginationLight
|
||||
}): Observable<ResultList<VideoPlaylist>> {
|
||||
const { search, componentPagination, searchTarget } = parameters
|
||||
|
||||
const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
|
||||
|
||||
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<ResultList<VideoPlaylistServerModel>>(url, { params })
|
||||
.pipe(
|
||||
switchMap(res => this.playlistService.extractPlaylists(res)),
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
></my-actor-avatar>
|
||||
|
||||
<div class="w-100 d-flex flex-column">
|
||||
<a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name"
|
||||
[routerLink]="videoRouterLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
|
||||
>{{ video.name }}</a>
|
||||
|
||||
<a *ngIf="videoHref" tabindex="-1" class="video-miniature-name"
|
||||
[href]="videoHref" [target]="videoTarget" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
|
||||
>{{ video.name }}</a>
|
||||
<my-link
|
||||
[internalLink]="videoRouterLink" [href]="videoHref" [target]="videoTarget"
|
||||
[title]="video.name"class="video-miniature-name" [ngClass]="{ 'blur-filter': isVideoBlur }" tabindex="-1"
|
||||
>
|
||||
{{ video.name }}
|
||||
</my-link>
|
||||
|
||||
<span class="video-miniature-created-at-views">
|
||||
<my-date-toggle *ngIf="displayOptions.date" [date]="video.publishedAt"></my-date-toggle>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||
import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models'
|
||||
import { LinkType } from '../../../types/link.type'
|
||||
import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
|
||||
import { Video } from '../shared-main'
|
||||
import { VideoPlaylistService } from '../shared-video-playlist'
|
||||
@@ -28,8 +29,6 @@ export type MiniatureDisplayOptions = {
|
||||
blacklistInfo?: boolean
|
||||
nsfw?: boolean
|
||||
}
|
||||
export type VideoLinkType = 'internal' | 'lazy-load' | 'external'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-miniature',
|
||||
styleUrls: [ './video-miniature.component.scss' ],
|
||||
@@ -56,7 +55,7 @@ export class VideoMiniatureComponent implements OnInit {
|
||||
|
||||
@Input() displayAsRow = false
|
||||
|
||||
@Input() videoLinkType: VideoLinkType = 'internal'
|
||||
@Input() videoLinkType: LinkType = 'internal'
|
||||
|
||||
@Output() videoBlocked = new EventEmitter()
|
||||
@Output() videoUnblocked = new EventEmitter()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }">
|
||||
<a
|
||||
[routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"
|
||||
class="miniature-thumbnail"
|
||||
<my-link
|
||||
[internalLink]="routerLink" [href]="playlistHref" [target]="playlistTarget"
|
||||
[title]="playlist.description" class="miniature-thumbnail"
|
||||
>
|
||||
<img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
<div class="play-overlay">
|
||||
<div class="icon"></div>
|
||||
</div>
|
||||
</a>
|
||||
</my-link>
|
||||
|
||||
<div class="miniature-info">
|
||||
<a tabindex="-1" class="miniature-name" [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description">
|
||||
<my-link
|
||||
[internalLink]="routerLink" [href]="playlistHref" [target]="playlistTarget"
|
||||
[title]="playlist.description" class="miniature-name" tabindex="-1"
|
||||
>
|
||||
{{ playlist.displayName }}
|
||||
</a>
|
||||
</my-link>
|
||||
|
||||
<a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
|
||||
{{ playlist.videoChannelBy }}
|
||||
|
||||
@@ -75,7 +75,10 @@
|
||||
}
|
||||
|
||||
.miniature:not(.display-as-row) {
|
||||
|
||||
.miniature-thumbnail {
|
||||
@include block-ratio($selector: '::ng-deep a');
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { LinkType } from 'src/types/link.type'
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { VideoPlaylist } from './video-playlist.model'
|
||||
|
||||
@Component({
|
||||
@@ -6,18 +7,52 @@ import { VideoPlaylist } from './video-playlist.model'
|
||||
styleUrls: [ './video-playlist-miniature.component.scss' ],
|
||||
templateUrl: './video-playlist-miniature.component.html'
|
||||
})
|
||||
export class VideoPlaylistMiniatureComponent {
|
||||
export class VideoPlaylistMiniatureComponent implements OnInit {
|
||||
@Input() playlist: VideoPlaylist
|
||||
|
||||
@Input() toManage = false
|
||||
|
||||
@Input() displayChannel = false
|
||||
@Input() displayDescription = false
|
||||
@Input() displayPrivacy = false
|
||||
@Input() displayAsRow = false
|
||||
|
||||
getPlaylistUrl () {
|
||||
if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ]
|
||||
if (this.playlist.videosLength === 0) return null
|
||||
@Input() linkType: LinkType = 'internal'
|
||||
|
||||
return [ '/w/p', this.playlist.uuid ]
|
||||
routerLink: any
|
||||
playlistHref: string
|
||||
playlistTarget: string
|
||||
|
||||
ngOnInit () {
|
||||
this.buildPlaylistUrl()
|
||||
}
|
||||
|
||||
buildPlaylistUrl () {
|
||||
if (this.toManage) {
|
||||
this.routerLink = [ '/my-library/video-playlists', this.playlist.uuid ]
|
||||
return
|
||||
}
|
||||
|
||||
if (this.playlist.videosLength === 0) {
|
||||
this.routerLink = null
|
||||
return
|
||||
}
|
||||
|
||||
if (this.linkType === 'internal' || !this.playlist.url) {
|
||||
this.routerLink = [ '/w/p', this.playlist.uuid ]
|
||||
return
|
||||
}
|
||||
|
||||
if (this.linkType === 'external') {
|
||||
this.routerLink = null
|
||||
this.playlistHref = this.playlist.url
|
||||
this.playlistTarget = '_blank'
|
||||
return
|
||||
}
|
||||
|
||||
// Lazy load
|
||||
this.routerLink = [ '/search/lazy-load-playlist', { url: this.playlist.url } ]
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
|
||||
import { Account, Actor, VideoChannel } from '@app/shared/shared-main'
|
||||
import { Actor } from '@app/shared/shared-main'
|
||||
import { peertubeTranslate } from '@shared/core-utils/i18n'
|
||||
import {
|
||||
AccountSummary,
|
||||
@@ -15,12 +15,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
uuid: string
|
||||
isLocal: boolean
|
||||
|
||||
url: string
|
||||
|
||||
displayName: string
|
||||
description: string
|
||||
privacy: VideoConstant<VideoPlaylistPrivacy>
|
||||
|
||||
thumbnailPath: string
|
||||
|
||||
videosLength: number
|
||||
|
||||
type: VideoConstant<VideoPlaylistType>
|
||||
@@ -31,6 +31,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
ownerAccount: AccountSummary
|
||||
videoChannel?: VideoChannelSummary
|
||||
|
||||
thumbnailPath: string
|
||||
thumbnailUrl: string
|
||||
|
||||
embedPath: string
|
||||
@@ -40,14 +41,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
|
||||
videoChannelBy?: string
|
||||
|
||||
private thumbnailVersion: number
|
||||
private originThumbnailUrl: string
|
||||
|
||||
constructor (hash: ServerVideoPlaylist, translations: {}) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
this.id = hash.id
|
||||
this.uuid = hash.uuid
|
||||
this.url = hash.url
|
||||
this.isLocal = hash.isLocal
|
||||
|
||||
this.displayName = hash.displayName
|
||||
@@ -57,15 +56,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
|
||||
this.thumbnailPath = hash.thumbnailPath
|
||||
|
||||
if (this.thumbnailPath) {
|
||||
this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
|
||||
this.originThumbnailUrl = this.thumbnailUrl
|
||||
} else {
|
||||
this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg'
|
||||
}
|
||||
this.thumbnailUrl = this.thumbnailPath
|
||||
? hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath)
|
||||
: absoluteAPIUrl + '/client/assets/images/default-playlist.jpg'
|
||||
|
||||
this.embedPath = hash.embedPath
|
||||
this.embedUrl = getAbsoluteEmbedUrl() + hash.embedPath
|
||||
this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath)
|
||||
|
||||
this.videosLength = hash.videosLength
|
||||
|
||||
@@ -88,13 +84,4 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
this.displayName = peertubeTranslate(this.displayName, translations)
|
||||
}
|
||||
}
|
||||
|
||||
refreshThumbnail () {
|
||||
if (!this.originThumbnailUrl) return
|
||||
|
||||
if (!this.thumbnailVersion) this.thumbnailVersion = 0
|
||||
this.thumbnailVersion++
|
||||
|
||||
this.thumbnailUrl = this.originThumbnailUrl + '?v' + this.thumbnailVersion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +880,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
1
client/src/types/link.type.ts
Normal file
1
client/src/types/link.type.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type LinkType = 'internal' | 'lazy-load' | 'external'
|
||||
Reference in New Issue
Block a user