aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts2
-rw-r--r--client/src/app/+search/channel-lazy-load.resolver.ts35
-rw-r--r--client/src/app/+search/search-routing.module.ts10
-rw-r--r--client/src/app/+search/search.component.html9
-rw-r--r--client/src/app/+search/search.component.ts146
-rw-r--r--client/src/app/+search/search.module.ts10
-rw-r--r--client/src/app/+search/shared/abstract-lazy-load.resolver.ts (renamed from client/src/app/+search/video-lazy-load.resolver.ts)21
-rw-r--r--client/src/app/+search/shared/channel-lazy-load.resolver.ts24
-rw-r--r--client/src/app/+search/shared/index.ts4
-rw-r--r--client/src/app/+search/shared/playlist-lazy-load.resolver.ts24
-rw-r--r--client/src/app/+search/shared/video-lazy-load.resolver.ts24
-rw-r--r--client/src/app/header/search-typeahead.component.html2
-rw-r--r--client/src/app/shared/shared-main/angular/index.ts1
-rw-r--r--client/src/app/shared/shared-main/angular/link.component.html11
-rw-r--r--client/src/app/shared/shared-main/angular/link.component.scss7
-rw-r--r--client/src/app/shared/shared-main/angular/link.component.ts17
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts7
-rw-r--r--client/src/app/shared/shared-search/search.service.ts44
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html13
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts5
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html15
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts47
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.model.ts31
-rw-r--r--client/src/sass/include/_mixins.scss1
-rw-r--r--client/src/types/link.type.ts1
26 files changed, 349 insertions, 165 deletions
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts
index a8fdf6e29..86fe70710 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts
@@ -187,7 +187,7 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy {
187 // Reload playlist thumbnail if the first element changed 187 // Reload playlist thumbnail if the first element changed
188 const newFirst = this.findFirst() 188 const newFirst = this.findFirst()
189 if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { 189 if (oldFirst && newFirst && oldFirst.id !== newFirst.id) {
190 this.playlist.refreshThumbnail() 190 this.loadPlaylistInfo()
191 } 191 }
192 } 192 }
193 193
diff --git a/client/src/app/+search/channel-lazy-load.resolver.ts b/client/src/app/+search/channel-lazy-load.resolver.ts
deleted file mode 100644
index d9f7ec901..000000000
--- a/client/src/app/+search/channel-lazy-load.resolver.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import { map } from 'rxjs/operators'
2import { Injectable } from '@angular/core'
3import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'
4import { SearchService } from '@app/shared/shared-search'
5
6@Injectable()
7export class ChannelLazyLoadResolver implements Resolve<any> {
8 constructor (
9 private router: Router,
10 private searchService: SearchService
11 ) { }
12
13 resolve (route: ActivatedRouteSnapshot) {
14 const url = route.params.url
15
16 if (!url) {
17 console.error('Could not find url param.', { params: route.params })
18 return this.router.navigateByUrl('/404')
19 }
20
21 return this.searchService.searchVideoChannels({ search: url })
22 .pipe(
23 map(result => {
24 if (result.data.length !== 1) {
25 console.error('Cannot find result for this URL')
26 return this.router.navigateByUrl('/404')
27 }
28
29 const channel = result.data[0]
30
31 return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost)
32 })
33 )
34 }
35}
diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts
index 0d778af0d..5d00aae13 100644
--- a/client/src/app/+search/search-routing.module.ts
+++ b/client/src/app/+search/search-routing.module.ts
@@ -1,8 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
4import { SearchComponent } from './search.component' 3import { SearchComponent } from './search.component'
5import { VideoLazyLoadResolver } from './video-lazy-load.resolver' 4import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared'
6 5
7const searchRoutes: Routes = [ 6const searchRoutes: Routes = [
8 { 7 {
@@ -27,6 +26,13 @@ const searchRoutes: Routes = [
27 resolve: { 26 resolve: {
28 data: ChannelLazyLoadResolver 27 data: ChannelLazyLoadResolver
29 } 28 }
29 },
30 {
31 path: 'lazy-load-playlist',
32 component: SearchComponent,
33 resolve: {
34 data: PlaylistLazyLoadResolver
35 }
30 } 36 }
31] 37]
32 38
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 130be75fc..b28abca6a 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -59,10 +59,17 @@
59 <div *ngIf="isVideo(result)" class="entry video"> 59 <div *ngIf="isVideo(result)" class="entry video">
60 <my-video-miniature 60 <my-video-miniature
61 [video]="result" [user]="userMiniature" [displayAsRow]="true" [displayVideoActions]="!hideActions()" 61 [video]="result" [user]="userMiniature" [displayAsRow]="true" [displayVideoActions]="!hideActions()"
62 [displayOptions]="videoDisplayOptions" [videoLinkType]="getVideoLinkType()" 62 [displayOptions]="videoDisplayOptions" [videoLinkType]="getLinkType()"
63 (videoBlocked)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)" 63 (videoBlocked)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)"
64 ></my-video-miniature> 64 ></my-video-miniature>
65 </div> 65 </div>
66
67 <div *ngIf="isPlaylist(result)" class="entry video-playlist">
68 <my-video-playlist-miniature
69 [playlist]="result" [displayAsRow]="true" [displayChannel]="true"
70 [linkType]="getLinkType()"
71 ></my-video-playlist-miniature>
72 </div>
66 </ng-container> 73 </ng-container>
67 74
68</div> 75</div>
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index a4ab7a5b1..fdf9b7cc0 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -1,11 +1,13 @@
1import { forkJoin, of, Subscription } from 'rxjs' 1import { forkJoin, of, Subscription } from 'rxjs'
2import { LinkType } from 'src/types/link.type'
2import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
5import { immutableAssign } from '@app/helpers' 6import { immutableAssign } from '@app/helpers'
6import { Video, VideoChannel } from '@app/shared/shared-main' 7import { Video, VideoChannel } from '@app/shared/shared-main'
7import { AdvancedSearch, SearchService } from '@app/shared/shared-search' 8import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
8import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' 9import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
10import { VideoPlaylist } from '@app/shared/shared-video-playlist'
9import { HTMLServerConfig, SearchTargetType } from '@shared/models' 11import { HTMLServerConfig, SearchTargetType } from '@shared/models'
10 12
11@Component({ 13@Component({
@@ -16,10 +18,9 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models'
16export class SearchComponent implements OnInit, OnDestroy { 18export class SearchComponent implements OnInit, OnDestroy {
17 results: (Video | VideoChannel)[] = [] 19 results: (Video | VideoChannel)[] = []
18 20
19 pagination: ComponentPagination = { 21 pagination = {
20 currentPage: 1, 22 currentPage: 1,
21 itemsPerPage: 10, // Only for videos, use another variable for channels 23 totalItems: null as number
22 totalItems: null
23 } 24 }
24 advancedSearch: AdvancedSearch = new AdvancedSearch() 25 advancedSearch: AdvancedSearch = new AdvancedSearch()
25 isSearchFilterCollapsed = true 26 isSearchFilterCollapsed = true
@@ -45,6 +46,11 @@ export class SearchComponent implements OnInit, OnDestroy {
45 private firstSearch = true 46 private firstSearch = true
46 47
47 private channelsPerPage = 2 48 private channelsPerPage = 2
49 private playlistsPerPage = 2
50 private videosPerPage = 10
51
52 private hasMoreResults = true
53 private isSearching = false
48 54
49 private lastSearchTarget: SearchTargetType 55 private lastSearchTarget: SearchTargetType
50 56
@@ -104,77 +110,62 @@ export class SearchComponent implements OnInit, OnDestroy {
104 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() 110 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
105 } 111 }
106 112
107 isVideoChannel (d: VideoChannel | Video): d is VideoChannel { 113 isVideoChannel (d: VideoChannel | Video | VideoPlaylist): d is VideoChannel {
108 return d instanceof VideoChannel 114 return d instanceof VideoChannel
109 } 115 }
110 116
111 isVideo (v: VideoChannel | Video): v is Video { 117 isVideo (v: VideoChannel | Video | VideoPlaylist): v is Video {
112 return v instanceof Video 118 return v instanceof Video
113 } 119 }
114 120
121 isPlaylist (v: VideoChannel | Video | VideoPlaylist): v is VideoPlaylist {
122 return v instanceof VideoPlaylist
123 }
124
115 isUserLoggedIn () { 125 isUserLoggedIn () {
116 return this.authService.isLoggedIn() 126 return this.authService.isLoggedIn()
117 } 127 }
118 128
119 getVideoLinkType (): VideoLinkType { 129 search () {
120 if (this.advancedSearch.searchTarget === 'search-index') { 130 this.isSearching = true
121 const remoteUriConfig = this.serverConfig.search.remoteUri
122 131
123 // Redirect on the external instance if not allowed to fetch remote data 132 forkJoin([
124 if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) { 133 this.getVideoChannelObs(),
125 return 'external' 134 this.getVideoPlaylistObs(),
135 this.getVideosObs()
136 ]).subscribe(results => {
137 for (const result of results) {
138 this.results = this.results.concat(result.data)
126 } 139 }
127 140
128 return 'lazy-load' 141 this.pagination.totalItems = results.reduce((p, r) => p += r.total, 0)
129 } 142 this.lastSearchTarget = this.advancedSearch.searchTarget
130 143
131 return 'internal' 144 this.hasMoreResults = this.results.length < this.pagination.totalItems
132 } 145 },
133 146
134 search () { 147 err => {
135 forkJoin([ 148 if (this.advancedSearch.searchTarget !== 'search-index') {
136 this.getVideosObs(), 149 this.notifier.error(err.message)
137 this.getVideoChannelObs() 150 return
138 ]).subscribe( 151 }
139 ([videosResult, videoChannelsResult]) => {
140 this.results = this.results
141 .concat(videoChannelsResult.data)
142 .concat(videosResult.data)
143
144 this.pagination.totalItems = videosResult.total + videoChannelsResult.total
145 this.lastSearchTarget = this.advancedSearch.searchTarget
146
147 // Focus on channels if there are no enough videos
148 if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
149 this.resetPagination()
150 this.firstSearch = false
151
152 this.channelsPerPage = 10
153 this.search()
154 }
155
156 this.firstSearch = false
157 },
158 152
159 err => { 153 this.notifier.error(
160 if (this.advancedSearch.searchTarget !== 'search-index') { 154 $localize`Search index is unavailable. Retrying with instance results instead.`,
161 this.notifier.error(err.message) 155 $localize`Search error`
162 return 156 )
163 } 157 this.advancedSearch.searchTarget = 'local'
158 this.search()
159 },
164 160
165 this.notifier.error( 161 () => {
166 $localize`Search index is unavailable. Retrying with instance results instead.`, 162 this.isSearching = false
167 $localize`Search error` 163 })
168 )
169 this.advancedSearch.searchTarget = 'local'
170 this.search()
171 }
172 )
173 } 164 }
174 165
175 onNearOfBottom () { 166 onNearOfBottom () {
176 // Last page 167 // Last page
177 if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return 168 if (!this.hasMoreResults || this.isSearching) return
178 169
179 this.pagination.currentPage += 1 170 this.pagination.currentPage += 1
180 this.search() 171 this.search()
@@ -190,18 +181,33 @@ export class SearchComponent implements OnInit, OnDestroy {
190 return this.advancedSearch.size() 181 return this.advancedSearch.size()
191 } 182 }
192 183
193 // Add VideoChannel for typings, but the template already checks "video" argument is a video 184 // Add VideoChannel/VideoPlaylist for typings, but the template already checks "video" argument is a video
194 removeVideoFromArray (video: Video | VideoChannel) { 185 removeVideoFromArray (video: Video | VideoChannel | VideoPlaylist) {
195 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) 186 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
196 } 187 }
197 188
189 getLinkType (): LinkType {
190 if (this.advancedSearch.searchTarget === 'search-index') {
191 const remoteUriConfig = this.serverConfig.search.remoteUri
192
193 // Redirect on the external instance if not allowed to fetch remote data
194 if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
195 return 'external'
196 }
197
198 return 'lazy-load'
199 }
200
201 return 'internal'
202 }
203
198 isExternalChannelUrl () { 204 isExternalChannelUrl () {
199 return this.getVideoLinkType() === 'external' 205 return this.getLinkType() === 'external'
200 } 206 }
201 207
202 getExternalChannelUrl (channel: VideoChannel) { 208 getExternalChannelUrl (channel: VideoChannel) {
203 // Same algorithm than videos 209 // Same algorithm than videos
204 if (this.getVideoLinkType() === 'external') { 210 if (this.getLinkType() === 'external') {
205 return channel.url 211 return channel.url
206 } 212 }
207 213
@@ -210,7 +216,7 @@ export class SearchComponent implements OnInit, OnDestroy {
210 } 216 }
211 217
212 getInternalChannelUrl (channel: VideoChannel) { 218 getInternalChannelUrl (channel: VideoChannel) {
213 const linkType = this.getVideoLinkType() 219 const linkType = this.getLinkType()
214 220
215 if (linkType === 'internal') { 221 if (linkType === 'internal') {
216 return [ '/c', channel.nameWithHost ] 222 return [ '/c', channel.nameWithHost ]
@@ -256,7 +262,7 @@ export class SearchComponent implements OnInit, OnDestroy {
256 private getVideosObs () { 262 private getVideosObs () {
257 const params = { 263 const params = {
258 search: this.currentSearch, 264 search: this.currentSearch,
259 componentPagination: this.pagination, 265 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }),
260 advancedSearch: this.advancedSearch 266 advancedSearch: this.advancedSearch
261 } 267 }
262 268
@@ -287,6 +293,24 @@ export class SearchComponent implements OnInit, OnDestroy {
287 ) 293 )
288 } 294 }
289 295
296 private getVideoPlaylistObs () {
297 if (!this.currentSearch) return of({ data: [], total: 0 })
298
299 const params = {
300 search: this.currentSearch,
301 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
302 searchTarget: this.advancedSearch.searchTarget
303 }
304
305 return this.hooks.wrapObsFun(
306 this.searchService.searchVideoPlaylists.bind(this.searchService),
307 params,
308 'search',
309 'filter:api.search.video-playlists.list.params',
310 'filter:api.search.video-playlists.list.result'
311 )
312 }
313
290 private getDefaultSearchTarget (): SearchTargetType { 314 private getDefaultSearchTarget (): SearchTargetType {
291 const searchIndexConfig = this.serverConfig.search.searchIndex 315 const searchIndexConfig = this.serverConfig.search.searchIndex
292 316
diff --git a/client/src/app/+search/search.module.ts b/client/src/app/+search/search.module.ts
index 390833abc..26f1523fd 100644
--- a/client/src/app/+search/search.module.ts
+++ b/client/src/app/+search/search.module.ts
@@ -5,12 +5,12 @@ import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedSearchModule } from '@app/shared/shared-search' 5import { SharedSearchModule } from '@app/shared/shared-search'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
8import { SearchService } from '../shared/shared-search/search.service' 9import { SearchService } from '../shared/shared-search/search.service'
9import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
10import { SearchFiltersComponent } from './search-filters.component' 10import { SearchFiltersComponent } from './search-filters.component'
11import { SearchRoutingModule } from './search-routing.module' 11import { SearchRoutingModule } from './search-routing.module'
12import { SearchComponent } from './search.component' 12import { SearchComponent } from './search.component'
13import { VideoLazyLoadResolver } from './video-lazy-load.resolver' 13import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared'
14 14
15@NgModule({ 15@NgModule({
16 imports: [ 16 imports: [
@@ -21,7 +21,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
21 SharedFormModule, 21 SharedFormModule,
22 SharedActorImageModule, 22 SharedActorImageModule,
23 SharedUserSubscriptionModule, 23 SharedUserSubscriptionModule,
24 SharedVideoMiniatureModule 24 SharedVideoMiniatureModule,
25 SharedVideoPlaylistModule
25 ], 26 ],
26 27
27 declarations: [ 28 declarations: [
@@ -36,7 +37,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
36 providers: [ 37 providers: [
37 SearchService, 38 SearchService,
38 VideoLazyLoadResolver, 39 VideoLazyLoadResolver,
39 ChannelLazyLoadResolver 40 ChannelLazyLoadResolver,
41 PlaylistLazyLoadResolver
40 ] 42 ]
41}) 43})
42export class SearchModule { } 44export class SearchModule { }
diff --git a/client/src/app/+search/video-lazy-load.resolver.ts b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts
index e43e0089b..31240f451 100644
--- a/client/src/app/+search/video-lazy-load.resolver.ts
+++ b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts
@@ -1,14 +1,10 @@
1import { Observable } from 'rxjs'
1import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
2import { Injectable } from '@angular/core'
3import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' 3import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'
4import { SearchService } from '@app/shared/shared-search' 4import { ResultList } from '@shared/models/result-list.model'
5 5
6@Injectable() 6export abstract class AbstractLazyLoadResolver <T> implements Resolve<any> {
7export class VideoLazyLoadResolver implements Resolve<any> { 7 protected router: Router
8 constructor (
9 private router: Router,
10 private searchService: SearchService
11 ) { }
12 8
13 resolve (route: ActivatedRouteSnapshot) { 9 resolve (route: ActivatedRouteSnapshot) {
14 const url = route.params.url 10 const url = route.params.url
@@ -18,7 +14,7 @@ export class VideoLazyLoadResolver implements Resolve<any> {
18 return this.router.navigateByUrl('/404') 14 return this.router.navigateByUrl('/404')
19 } 15 }
20 16
21 return this.searchService.searchVideos({ search: url }) 17 return this.finder(url)
22 .pipe( 18 .pipe(
23 map(result => { 19 map(result => {
24 if (result.data.length !== 1) { 20 if (result.data.length !== 1) {
@@ -26,10 +22,13 @@ export class VideoLazyLoadResolver implements Resolve<any> {
26 return this.router.navigateByUrl('/404') 22 return this.router.navigateByUrl('/404')
27 } 23 }
28 24
29 const video = result.data[0] 25 const redirectUrl = this.buildUrl(result.data[0])
30 26
31 return this.router.navigateByUrl('/w/' + video.uuid) 27 return this.router.navigateByUrl(redirectUrl)
32 }) 28 })
33 ) 29 )
34 } 30 }
31
32 protected abstract finder (url: string): Observable<ResultList<T>>
33 protected abstract buildUrl (e: T): string
35} 34}
diff --git a/client/src/app/+search/shared/channel-lazy-load.resolver.ts b/client/src/app/+search/shared/channel-lazy-load.resolver.ts
new file mode 100644
index 000000000..5e010f795
--- /dev/null
+++ b/client/src/app/+search/shared/channel-lazy-load.resolver.ts
@@ -0,0 +1,24 @@
1import { Injectable } from '@angular/core'
2import { Router } from '@angular/router'
3import { VideoChannel } from '@app/shared/shared-main'
4import { SearchService } from '@app/shared/shared-search'
5import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
6
7@Injectable()
8export class ChannelLazyLoadResolver extends AbstractLazyLoadResolver<VideoChannel> {
9
10 constructor (
11 protected router: Router,
12 private searchService: SearchService
13 ) {
14 super()
15 }
16
17 protected finder (url: string) {
18 return this.searchService.searchVideoChannels({ search: url })
19 }
20
21 protected buildUrl (channel: VideoChannel) {
22 return '/video-channels/' + channel.nameWithHost
23 }
24}
diff --git a/client/src/app/+search/shared/index.ts b/client/src/app/+search/shared/index.ts
new file mode 100644
index 000000000..1e68989ae
--- /dev/null
+++ b/client/src/app/+search/shared/index.ts
@@ -0,0 +1,4 @@
1export * from './abstract-lazy-load.resolver'
2export * from './channel-lazy-load.resolver'
3export * from './playlist-lazy-load.resolver'
4export * from './video-lazy-load.resolver'
diff --git a/client/src/app/+search/shared/playlist-lazy-load.resolver.ts b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts
new file mode 100644
index 000000000..14ae798df
--- /dev/null
+++ b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts
@@ -0,0 +1,24 @@
1import { Injectable } from '@angular/core'
2import { Router } from '@angular/router'
3import { SearchService } from '@app/shared/shared-search'
4import { VideoPlaylist } from '@app/shared/shared-video-playlist'
5import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
6
7@Injectable()
8export class PlaylistLazyLoadResolver extends AbstractLazyLoadResolver<VideoPlaylist> {
9
10 constructor (
11 protected router: Router,
12 private searchService: SearchService
13 ) {
14 super()
15 }
16
17 protected finder (url: string) {
18 return this.searchService.searchVideoPlaylists({ search: url })
19 }
20
21 protected buildUrl (playlist: VideoPlaylist) {
22 return '/w/p/' + playlist.uuid
23 }
24}
diff --git a/client/src/app/+search/shared/video-lazy-load.resolver.ts b/client/src/app/+search/shared/video-lazy-load.resolver.ts
new file mode 100644
index 000000000..12b5b2e82
--- /dev/null
+++ b/client/src/app/+search/shared/video-lazy-load.resolver.ts
@@ -0,0 +1,24 @@
1import { Injectable } from '@angular/core'
2import { Router } from '@angular/router'
3import { Video } from '@app/shared/shared-main'
4import { SearchService } from '@app/shared/shared-search'
5import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver'
6
7@Injectable()
8export class VideoLazyLoadResolver extends AbstractLazyLoadResolver<Video> {
9
10 constructor (
11 protected router: Router,
12 private searchService: SearchService
13 ) {
14 super()
15 }
16
17 protected finder (url: string) {
18 return this.searchService.searchVideos({ search: url })
19 }
20
21 protected buildUrl (video: Video) {
22 return '/w/' + video.uuid
23 }
24}
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index f84086b4a..0ced249a7 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -1,6 +1,6 @@
1<div class="d-inline-flex position-relative" id="typeahead-container"> 1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <input 2 <input
3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…" 3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…"
4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" (keydown.enter)="doSearch()" 4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" (keydown.enter)="doSearch()"
5 aria-label="Search" autocomplete="off" 5 aria-label="Search" autocomplete="off"
6 > 6 >
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts
index 8ea47bb33..069b7f654 100644
--- a/client/src/app/shared/shared-main/angular/index.ts
+++ b/client/src/app/shared/shared-main/angular/index.ts
@@ -3,5 +3,6 @@ export * from './bytes.pipe'
3export * from './duration-formatter.pipe' 3export * from './duration-formatter.pipe'
4export * from './from-now.pipe' 4export * from './from-now.pipe'
5export * from './infinite-scroller.directive' 5export * from './infinite-scroller.directive'
6export * from './link.component'
6export * from './number-formatter.pipe' 7export * from './number-formatter.pipe'
7export * from './peertube-template.directive' 8export * from './peertube-template.directive'
diff --git a/client/src/app/shared/shared-main/angular/link.component.html b/client/src/app/shared/shared-main/angular/link.component.html
new file mode 100644
index 000000000..e61a1e085
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/link.component.html
@@ -0,0 +1,11 @@
1<ng-template #content>
2 <ng-content></ng-content>
3</ng-template>
4
5<a *ngIf="!href" [routerLink]="internalLink" [attr.title]="title" [tabindex]="tabindex">
6 <ng-template *ngTemplateOutlet="content"></ng-template>
7</a>
8
9<a *ngIf="href" [href]="href" [target]="target" [attr.title]="title" [tabindex]="tabindex">
10 <ng-template *ngTemplateOutlet="content"></ng-template>
11</a>
diff --git a/client/src/app/shared/shared-main/angular/link.component.scss b/client/src/app/shared/shared-main/angular/link.component.scss
new file mode 100644
index 000000000..bb86d5488
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/link.component.scss
@@ -0,0 +1,7 @@
1a {
2 color: inherit;
3 text-decoration: inherit;
4 position: inherit;
5 width: inherit;
6 height: inherit;
7}
diff --git a/client/src/app/shared/shared-main/angular/link.component.ts b/client/src/app/shared/shared-main/angular/link.component.ts
new file mode 100644
index 000000000..76d1201b9
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/link.component.ts
@@ -0,0 +1,17 @@
1import { Component, Input, ViewEncapsulation } from '@angular/core'
2
3@Component({
4 selector: 'my-link',
5 styleUrls: [ './link.component.scss' ],
6 templateUrl: './link.component.html'
7})
8export class LinkComponent {
9 @Input() internalLink?: any[]
10
11 @Input() href?: string
12 @Input() target?: string
13
14 @Input() title?: string
15
16 @Input() tabindex: string | number
17}
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index c8dd01429..e5dfc59b2 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -4,7 +4,7 @@ import { CommonModule, DatePipe } from '@angular/common'
4import { HttpClientModule } from '@angular/common/http' 4import { HttpClientModule } from '@angular/common/http'
5import { NgModule } from '@angular/core' 5import { NgModule } from '@angular/core'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { ActivatedRouteSnapshot, RouterModule } from '@angular/router' 7import { RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule, 9 NgbButtonsModule,
10 NgbCollapseModule, 10 NgbCollapseModule,
@@ -24,6 +24,7 @@ import {
24 DurationFormatterPipe, 24 DurationFormatterPipe,
25 FromNowPipe, 25 FromNowPipe,
26 InfiniteScrollerDirective, 26 InfiniteScrollerDirective,
27 LinkComponent,
27 NumberFormatterPipe, 28 NumberFormatterPipe,
28 PeerTubeTemplateDirective 29 PeerTubeTemplateDirective
29} from './angular' 30} from './angular'
@@ -35,11 +36,11 @@ import { FeedComponent } from './feeds'
35import { LoaderComponent, SmallLoaderComponent } from './loaders' 36import { LoaderComponent, SmallLoaderComponent } from './loaders'
36import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc' 37import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
37import { PluginPlaceholderComponent } from './plugins' 38import { PluginPlaceholderComponent } from './plugins'
39import { ActorRedirectGuard } from './router'
38import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 40import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
39import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 41import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
40import { VideoCaptionService } from './video-caption' 42import { VideoCaptionService } from './video-caption'
41import { VideoChannelService } from './video-channel' 43import { VideoChannelService } from './video-channel'
42import { ActorRedirectGuard } from './router'
43 44
44@NgModule({ 45@NgModule({
45 imports: [ 46 imports: [
@@ -76,6 +77,7 @@ import { ActorRedirectGuard } from './router'
76 77
77 InfiniteScrollerDirective, 78 InfiniteScrollerDirective,
78 PeerTubeTemplateDirective, 79 PeerTubeTemplateDirective,
80 LinkComponent,
79 81
80 ActionDropdownComponent, 82 ActionDropdownComponent,
81 ButtonComponent, 83 ButtonComponent,
@@ -130,6 +132,7 @@ import { ActorRedirectGuard } from './router'
130 132
131 InfiniteScrollerDirective, 133 InfiniteScrollerDirective,
132 PeerTubeTemplateDirective, 134 PeerTubeTemplateDirective,
135 LinkComponent,
133 136
134 ActionDropdownComponent, 137 ActionDropdownComponent,
135 ButtonComponent, 138 ButtonComponent,
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index 15c4a7012..ad258f5e5 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -3,10 +3,17 @@ import { catchError, map, switchMap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core'
6import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
7import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 6import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { ResultList, SearchTargetType, Video as VideoServerModel, VideoChannel as VideoChannelServerModel } from '@shared/models' 7import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
8import {
9 ResultList,
10 SearchTargetType,
11 Video as VideoServerModel,
12 VideoChannel as VideoChannelServerModel,
13 VideoPlaylist as VideoPlaylistServerModel
14} from '@shared/models'
9import { environment } from '../../../environments/environment' 15import { environment } from '../../../environments/environment'
16import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist'
10import { AdvancedSearch } from './advanced-search.model' 17import { AdvancedSearch } from './advanced-search.model'
11 18
12@Injectable() 19@Injectable()
@@ -17,7 +24,8 @@ export class SearchService {
17 private authHttp: HttpClient, 24 private authHttp: HttpClient,
18 private restExtractor: RestExtractor, 25 private restExtractor: RestExtractor,
19 private restService: RestService, 26 private restService: RestService,
20 private videoService: VideoService 27 private videoService: VideoService,
28 private playlistService: VideoPlaylistService
21 ) { 29 ) {
22 // Add ability to override search endpoint if the user updated this local storage key 30 // Add ability to override search endpoint if the user updated this local storage key
23 const searchUrl = peertubeLocalStorage.getItem('search-url') 31 const searchUrl = peertubeLocalStorage.getItem('search-url')
@@ -85,4 +93,34 @@ export class SearchService {
85 catchError(err => this.restExtractor.handleError(err)) 93 catchError(err => this.restExtractor.handleError(err))
86 ) 94 )
87 } 95 }
96
97 searchVideoPlaylists (parameters: {
98 search: string,
99 searchTarget?: SearchTargetType,
100 componentPagination?: ComponentPaginationLight
101 }): Observable<ResultList<VideoPlaylist>> {
102 const { search, componentPagination, searchTarget } = parameters
103
104 const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
105
106 let pagination: RestPagination
107 if (componentPagination) {
108 pagination = this.restService.componentPaginationToRestPagination(componentPagination)
109 }
110
111 let params = new HttpParams()
112 params = this.restService.addRestGetParams(params, pagination)
113 params = params.append('search', search)
114
115 if (searchTarget) {
116 params = params.append('searchTarget', searchTarget as string)
117 }
118
119 return this.authHttp
120 .get<ResultList<VideoPlaylistServerModel>>(url, { params })
121 .pipe(
122 switchMap(res => this.playlistService.extractPlaylists(res)),
123 catchError(err => this.restExtractor.handleError(err))
124 )
125 }
88} 126}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 6c34123ed..7765d5be7 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -21,13 +21,12 @@
21 ></my-actor-avatar> 21 ></my-actor-avatar>
22 22
23 <div class="w-100 d-flex flex-column"> 23 <div class="w-100 d-flex flex-column">
24 <a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name" 24 <my-link
25 [routerLink]="videoRouterLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 25 [internalLink]="videoRouterLink" [href]="videoHref" [target]="videoTarget"
26 >{{ video.name }}</a> 26 [title]="video.name"class="video-miniature-name" [ngClass]="{ 'blur-filter': isVideoBlur }" tabindex="-1"
27 27 >
28 <a *ngIf="videoHref" tabindex="-1" class="video-miniature-name" 28 {{ video.name }}
29 [href]="videoHref" [target]="videoTarget" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 29 </my-link>
30 >{{ video.name }}</a>
31 30
32 <span class="video-miniature-created-at-views"> 31 <span class="video-miniature-created-at-views">
33 <my-date-toggle *ngIf="displayOptions.date" [date]="video.publishedAt"></my-date-toggle> 32 <my-date-toggle *ngIf="displayOptions.date" [date]="video.publishedAt"></my-date-toggle>
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index fc066846a..fe161c977 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -12,6 +12,7 @@ import {
12} from '@angular/core' 12} from '@angular/core'
13import { AuthService, ScreenService, ServerService, User } from '@app/core' 13import { AuthService, ScreenService, ServerService, User } from '@app/core'
14import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' 14import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models'
15import { LinkType } from '../../../types/link.type'
15import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' 16import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
16import { Video } from '../shared-main' 17import { Video } from '../shared-main'
17import { VideoPlaylistService } from '../shared-video-playlist' 18import { VideoPlaylistService } from '../shared-video-playlist'
@@ -28,8 +29,6 @@ export type MiniatureDisplayOptions = {
28 blacklistInfo?: boolean 29 blacklistInfo?: boolean
29 nsfw?: boolean 30 nsfw?: boolean
30} 31}
31export type VideoLinkType = 'internal' | 'lazy-load' | 'external'
32
33@Component({ 32@Component({
34 selector: 'my-video-miniature', 33 selector: 'my-video-miniature',
35 styleUrls: [ './video-miniature.component.scss' ], 34 styleUrls: [ './video-miniature.component.scss' ],
@@ -56,7 +55,7 @@ export class VideoMiniatureComponent implements OnInit {
56 55
57 @Input() displayAsRow = false 56 @Input() displayAsRow = false
58 57
59 @Input() videoLinkType: VideoLinkType = 'internal' 58 @Input() videoLinkType: LinkType = 'internal'
60 59
61 @Output() videoBlocked = new EventEmitter() 60 @Output() videoBlocked = new EventEmitter()
62 @Output() videoUnblocked = new EventEmitter() 61 @Output() videoUnblocked = new EventEmitter()
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index 81c36e6fe..95f11f030 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -1,7 +1,7 @@
1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }"> 1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }">
2 <a 2 <my-link
3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" 3 [internalLink]="routerLink" [href]="playlistHref" [target]="playlistTarget"
4 class="miniature-thumbnail" 4 [title]="playlist.description" class="miniature-thumbnail"
5 > 5 >
6 <img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" /> 6 <img alt="" [attr.aria-labelledby]="playlist.displayName" [attr.src]="playlist.thumbnailUrl" />
7 7
@@ -12,12 +12,15 @@
12 <div class="play-overlay"> 12 <div class="play-overlay">
13 <div class="icon"></div> 13 <div class="icon"></div>
14 </div> 14 </div>
15 </a> 15 </my-link>
16 16
17 <div class="miniature-info"> 17 <div class="miniature-info">
18 <a tabindex="-1" class="miniature-name" [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"> 18 <my-link
19 [internalLink]="routerLink" [href]="playlistHref" [target]="playlistTarget"
20 [title]="playlist.description" class="miniature-name" tabindex="-1"
21 >
19 {{ playlist.displayName }} 22 {{ playlist.displayName }}
20 </a> 23 </my-link>
21 24
22 <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> 25 <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
23 {{ playlist.videoChannelBy }} 26 {{ playlist.videoChannelBy }}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
index 95bf469ac..cf7513984 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
@@ -75,7 +75,10 @@
75} 75}
76 76
77.miniature:not(.display-as-row) { 77.miniature:not(.display-as-row) {
78
78 .miniature-thumbnail { 79 .miniature-thumbnail {
80 @include block-ratio($selector: '::ng-deep a');
81
79 margin-top: 10px; 82 margin-top: 10px;
80 margin-bottom: 5px; 83 margin-bottom: 5px;
81 } 84 }
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index 9bbec6038..8de5092a9 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -1,4 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { LinkType } from 'src/types/link.type'
2import { Component, Input, OnInit } from '@angular/core'
2import { VideoPlaylist } from './video-playlist.model' 3import { VideoPlaylist } from './video-playlist.model'
3 4
4@Component({ 5@Component({
@@ -6,18 +7,52 @@ import { VideoPlaylist } from './video-playlist.model'
6 styleUrls: [ './video-playlist-miniature.component.scss' ], 7 styleUrls: [ './video-playlist-miniature.component.scss' ],
7 templateUrl: './video-playlist-miniature.component.html' 8 templateUrl: './video-playlist-miniature.component.html'
8}) 9})
9export class VideoPlaylistMiniatureComponent { 10export class VideoPlaylistMiniatureComponent implements OnInit {
10 @Input() playlist: VideoPlaylist 11 @Input() playlist: VideoPlaylist
12
11 @Input() toManage = false 13 @Input() toManage = false
14
12 @Input() displayChannel = false 15 @Input() displayChannel = false
13 @Input() displayDescription = false 16 @Input() displayDescription = false
14 @Input() displayPrivacy = false 17 @Input() displayPrivacy = false
15 @Input() displayAsRow = false 18 @Input() displayAsRow = false
16 19
17 getPlaylistUrl () { 20 @Input() linkType: LinkType = 'internal'
18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] 21
19 if (this.playlist.videosLength === 0) return null 22 routerLink: any
23 playlistHref: string
24 playlistTarget: string
25
26 ngOnInit () {
27 this.buildPlaylistUrl()
28 }
29
30 buildPlaylistUrl () {
31 if (this.toManage) {
32 this.routerLink = [ '/my-library/video-playlists', this.playlist.uuid ]
33 return
34 }
35
36 if (this.playlist.videosLength === 0) {
37 this.routerLink = null
38 return
39 }
40
41 if (this.linkType === 'internal' || !this.playlist.url) {
42 this.routerLink = [ '/w/p', this.playlist.uuid ]
43 return
44 }
45
46 if (this.linkType === 'external') {
47 this.routerLink = null
48 this.playlistHref = this.playlist.url
49 this.playlistTarget = '_blank'
50 return
51 }
52
53 // Lazy load
54 this.routerLink = [ '/search/lazy-load-playlist', { url: this.playlist.url } ]
20 55
21 return [ '/w/p', this.playlist.uuid ] 56 return
22 } 57 }
23} 58}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
index 5b6ba9dbf..d67f372f4 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
@@ -1,5 +1,5 @@
1import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' 1import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
2import { Account, Actor, VideoChannel } from '@app/shared/shared-main' 2import { Actor } from '@app/shared/shared-main'
3import { peertubeTranslate } from '@shared/core-utils/i18n' 3import { peertubeTranslate } from '@shared/core-utils/i18n'
4import { 4import {
5 AccountSummary, 5 AccountSummary,
@@ -15,12 +15,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
15 uuid: string 15 uuid: string
16 isLocal: boolean 16 isLocal: boolean
17 17
18 url: string
19
18 displayName: string 20 displayName: string
19 description: string 21 description: string
20 privacy: VideoConstant<VideoPlaylistPrivacy> 22 privacy: VideoConstant<VideoPlaylistPrivacy>
21 23
22 thumbnailPath: string
23
24 videosLength: number 24 videosLength: number
25 25
26 type: VideoConstant<VideoPlaylistType> 26 type: VideoConstant<VideoPlaylistType>
@@ -31,6 +31,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
31 ownerAccount: AccountSummary 31 ownerAccount: AccountSummary
32 videoChannel?: VideoChannelSummary 32 videoChannel?: VideoChannelSummary
33 33
34 thumbnailPath: string
34 thumbnailUrl: string 35 thumbnailUrl: string
35 36
36 embedPath: string 37 embedPath: string
@@ -40,14 +41,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
40 41
41 videoChannelBy?: string 42 videoChannelBy?: string
42 43
43 private thumbnailVersion: number
44 private originThumbnailUrl: string
45
46 constructor (hash: ServerVideoPlaylist, translations: {}) { 44 constructor (hash: ServerVideoPlaylist, translations: {}) {
47 const absoluteAPIUrl = getAbsoluteAPIUrl() 45 const absoluteAPIUrl = getAbsoluteAPIUrl()
48 46
49 this.id = hash.id 47 this.id = hash.id
50 this.uuid = hash.uuid 48 this.uuid = hash.uuid
49 this.url = hash.url
51 this.isLocal = hash.isLocal 50 this.isLocal = hash.isLocal
52 51
53 this.displayName = hash.displayName 52 this.displayName = hash.displayName
@@ -57,15 +56,12 @@ export class VideoPlaylist implements ServerVideoPlaylist {
57 56
58 this.thumbnailPath = hash.thumbnailPath 57 this.thumbnailPath = hash.thumbnailPath
59 58
60 if (this.thumbnailPath) { 59 this.thumbnailUrl = this.thumbnailPath
61 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath 60 ? hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath)
62 this.originThumbnailUrl = this.thumbnailUrl 61 : absoluteAPIUrl + '/client/assets/images/default-playlist.jpg'
63 } else {
64 this.thumbnailUrl = window.location.origin + '/client/assets/images/default-playlist.jpg'
65 }
66 62
67 this.embedPath = hash.embedPath 63 this.embedPath = hash.embedPath
68 this.embedUrl = getAbsoluteEmbedUrl() + hash.embedPath 64 this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath)
69 65
70 this.videosLength = hash.videosLength 66 this.videosLength = hash.videosLength
71 67
@@ -88,13 +84,4 @@ export class VideoPlaylist implements ServerVideoPlaylist {
88 this.displayName = peertubeTranslate(this.displayName, translations) 84 this.displayName = peertubeTranslate(this.displayName, translations)
89 } 85 }
90 } 86 }
91
92 refreshThumbnail () {
93 if (!this.originThumbnailUrl) return
94
95 if (!this.thumbnailVersion) this.thumbnailVersion = 0
96 this.thumbnailVersion++
97
98 this.thumbnailUrl = this.originThumbnailUrl + '?v' + this.thumbnailVersion
99 }
100} 87}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index a835381d6..62e1ca163 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -880,6 +880,7 @@
880 width: 100%; 880 width: 100%;
881 height: 100%; 881 height: 100%;
882 top: 0; 882 top: 0;
883
883 @content; 884 @content;
884 } 885 }
885} 886}
diff --git a/client/src/types/link.type.ts b/client/src/types/link.type.ts
new file mode 100644
index 000000000..b611ae3fa
--- /dev/null
+++ b/client/src/types/link.type.ts
@@ -0,0 +1 @@
export type LinkType = 'internal' | 'lazy-load' | 'external'