Add ability to search playlists

This commit is contained in:
Chocobozzz
2021-06-17 16:02:38 +02:00
committed by Chocobozzz
parent 33eb19e519
commit 37a44fc915
79 changed files with 1652 additions and 549 deletions

View File

@@ -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()
}
}

View File

@@ -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)
})
)
}
}

View File

@@ -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
}
}
]

View File

@@ -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>

View File

@@ -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

View File

@@ -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 { }

View File

@@ -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
}

View 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
}
}

View 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'

View 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
}
}

View 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
}
}

View File

@@ -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"
>

View File

@@ -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'

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
a {
color: inherit;
text-decoration: inherit;
position: inherit;
width: inherit;
height: inherit;
}

View 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
}

View File

@@ -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,

View File

@@ -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))
)
}
}

View File

@@ -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>

View File

@@ -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()

View File

@@ -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 }}

View File

@@ -75,7 +75,10 @@
}
.miniature:not(.display-as-row) {
.miniature-thumbnail {
@include block-ratio($selector: '::ng-deep a');
margin-top: 10px;
margin-bottom: 5px;
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -880,6 +880,7 @@
width: 100%;
height: 100%;
top: 0;
@content;
}
}

View File

@@ -0,0 +1 @@
export type LinkType = 'internal' | 'lazy-load' | 'external'