modularize abstract video list header and implement video hotness recommendation variant
This commit is contained in:
parent
7a4994873c
commit
5bcbcbe338
|
@ -459,6 +459,7 @@
|
||||||
* `language` by Aaron Jin (CC-BY)
|
* `language` by Aaron Jin (CC-BY)
|
||||||
* `video-language` by Rigel Kent (CC-BY)
|
* `video-language` by Rigel Kent (CC-BY)
|
||||||
* `peertube-x` by Solen DP (CC-BY)
|
* `peertube-x` by Solen DP (CC-BY)
|
||||||
|
* `flame` by Freepik (Flaticon License)
|
||||||
|
|
||||||
|
|
||||||
# Contributors to our 2020 crowdfunding :heart:
|
# Contributors to our 2020 crowdfunding :heart:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { first, tap } from 'rxjs/operators'
|
import { first, tap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { immutableAssign } from '@app/helpers'
|
import { immutableAssign } from '@app/helpers'
|
||||||
|
@ -11,9 +11,7 @@ import { VideoFilter } from '@shared/models'
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-search',
|
selector: 'my-account-search',
|
||||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html',
|
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html',
|
||||||
styleUrls: [
|
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ]
|
||||||
'../../shared/shared-video-miniature/abstract-video-list.scss'
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AccountSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class AccountSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
titlePage: string
|
titlePage: string
|
||||||
|
@ -35,6 +33,7 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
|
||||||
protected confirmService: ConfirmService,
|
protected confirmService: ConfirmService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
|
@ -99,6 +98,7 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
generateSyndicationList () {
|
generateSyndicationList () {
|
||||||
/* disable syndication */
|
/* method disabled */
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { first, tap } from 'rxjs/operators'
|
import { first, tap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { immutableAssign } from '@app/helpers'
|
import { immutableAssign } from '@app/helpers'
|
||||||
|
@ -35,7 +35,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService,
|
||||||
|
protected cfr: ComponentFactoryResolver
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
AuthService,
|
AuthService,
|
||||||
|
@ -42,7 +42,8 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private userHistoryService: UserHistoryService
|
private userHistoryService: UserHistoryService,
|
||||||
|
protected cfr: ComponentFactoryResolver
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
|
||||||
}
|
}
|
||||||
|
|
||||||
generateSyndicationList () {
|
generateSyndicationList () {
|
||||||
|
/* method disabled */
|
||||||
throw new Error('Method not implemented.')
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { first, tap } from 'rxjs/operators'
|
import { first, tap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { immutableAssign } from '@app/helpers'
|
import { immutableAssign } from '@app/helpers'
|
||||||
|
@ -34,6 +34,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
||||||
protected confirmService: ConfirmService,
|
protected confirmService: ConfirmService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoChannelService: VideoChannelService,
|
private videoChannelService: VideoChannelService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export * from './overview'
|
export * from './overview'
|
||||||
|
export * from './trending'
|
||||||
export * from './video-local.component'
|
export * from './video-local.component'
|
||||||
export * from './video-recently-added.component'
|
export * from './video-recently-added.component'
|
||||||
export * from './video-trending.component'
|
|
||||||
export * from './video-most-liked.component'
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './video-trending-header.component'
|
||||||
|
export * from './video-trending.component'
|
||||||
|
export * from './video-hot.component'
|
||||||
|
export * from './video-most-liked.component'
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
import { immutableAssign } from '@app/helpers'
|
||||||
|
import { VideoService } from '@app/shared/shared-main'
|
||||||
|
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
||||||
|
import { VideoSortField } from '@shared/models'
|
||||||
|
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-videos-hot',
|
||||||
|
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||||
|
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
|
||||||
|
})
|
||||||
|
export class VideoHotComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
|
HeaderComponent = VideoTrendingHeaderComponent
|
||||||
|
titlePage: string
|
||||||
|
defaultSort: VideoSortField = '-hot'
|
||||||
|
|
||||||
|
useUserVideoPreferences = true
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected router: Router,
|
||||||
|
protected serverService: ServerService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notifier: Notifier,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected userService: UserService,
|
||||||
|
protected screenService: ScreenService,
|
||||||
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
|
private videoService: VideoService,
|
||||||
|
private hooks: HooksService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.headerComponentInjector = this.getInjector()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
super.ngOnInit()
|
||||||
|
|
||||||
|
this.generateSyndicationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
super.ngOnDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideosObservable (page: number) {
|
||||||
|
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||||
|
const params = {
|
||||||
|
videoPagination: newPagination,
|
||||||
|
sort: this.sort,
|
||||||
|
categoryOneOf: this.categoryOneOf,
|
||||||
|
languageOneOf: this.languageOneOf,
|
||||||
|
nsfwPolicy: this.nsfwPolicy,
|
||||||
|
skipCount: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hooks.wrapObsFun(
|
||||||
|
this.videoService.getVideos.bind(this.videoService),
|
||||||
|
params,
|
||||||
|
'common',
|
||||||
|
'filter:api.trending-videos.videos.list.params',
|
||||||
|
'filter:api.trending-videos.videos.list.result'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSyndicationList () {
|
||||||
|
this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInjector () {
|
||||||
|
return Injector.create({
|
||||||
|
providers: [{
|
||||||
|
provide: 'data',
|
||||||
|
useValue: {
|
||||||
|
model: this.defaultSort
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, Injector, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
@ -6,13 +6,15 @@ import { immutableAssign } from '@app/helpers'
|
||||||
import { VideoService } from '@app/shared/shared-main'
|
import { VideoService } from '@app/shared/shared-main'
|
||||||
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
||||||
import { VideoSortField } from '@shared/models'
|
import { VideoSortField } from '@shared/models'
|
||||||
|
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-most-liked',
|
selector: 'my-videos-most-liked',
|
||||||
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
|
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
|
||||||
})
|
})
|
||||||
export class VideoMostLikedComponent extends AbstractVideoList implements OnInit {
|
export class VideoMostLikedComponent extends AbstractVideoList implements OnInit {
|
||||||
|
HeaderComponent = VideoTrendingHeaderComponent
|
||||||
titlePage: string
|
titlePage: string
|
||||||
defaultSort: VideoSortField = '-likes'
|
defaultSort: VideoSortField = '-likes'
|
||||||
|
|
||||||
|
@ -27,19 +29,19 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
this.headerComponentInjector = this.getInjector()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
|
|
||||||
this.generateSyndicationList()
|
this.generateSyndicationList()
|
||||||
|
|
||||||
this.titlePage = $localize`Most liked videos`
|
|
||||||
this.titleTooltip = $localize`Videos that have the most likes.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideosObservable (page: number) {
|
getVideosObservable (page: number) {
|
||||||
|
@ -65,4 +67,15 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
|
||||||
generateSyndicationList () {
|
generateSyndicationList () {
|
||||||
this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
|
this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInjector () {
|
||||||
|
return Injector.create({
|
||||||
|
providers: [{
|
||||||
|
provide: 'data',
|
||||||
|
useValue: {
|
||||||
|
model: this.defaultSort
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
|
||||||
|
<label *ngFor="let button of buttons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
|
||||||
|
<my-global-icon [iconName]="button.iconName"></my-global-icon>
|
||||||
|
<input ngbButton type="radio" [value]="button.value"> {{ button.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
|
@ -0,0 +1,17 @@
|
||||||
|
.btn-group label {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 9999px !important;
|
||||||
|
padding: 5px 16px;
|
||||||
|
opacity: .8;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
height: 1rem;
|
||||||
|
margin-right: .1rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Component, Inject } from '@angular/core'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
|
||||||
|
import { GlobalIconName } from '@app/shared/shared-icons'
|
||||||
|
import { VideoSortField } from '@shared/models'
|
||||||
|
|
||||||
|
interface VideoTrendingHeaderItem {
|
||||||
|
label: string
|
||||||
|
iconName: GlobalIconName
|
||||||
|
value: VideoSortField
|
||||||
|
path: string
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'video-trending-title-page',
|
||||||
|
host: { 'class': 'title-page title-page-single' },
|
||||||
|
styleUrls: [ './video-trending-header.component.scss' ],
|
||||||
|
templateUrl: './video-trending-header.component.html'
|
||||||
|
})
|
||||||
|
export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
|
||||||
|
buttons: VideoTrendingHeaderItem[]
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
@Inject('data') public data: any,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
super(data)
|
||||||
|
|
||||||
|
this.buttons = [
|
||||||
|
{
|
||||||
|
label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
|
||||||
|
iconName: 'flame',
|
||||||
|
value: '-hot',
|
||||||
|
path: 'hot',
|
||||||
|
tooltip: $localize`Videos totalizing the most interactions for recent videos`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
|
||||||
|
iconName: 'trending',
|
||||||
|
value: '-trending',
|
||||||
|
path: 'trending',
|
||||||
|
tooltip: $localize`Videos totalizing the most views during the last 24 hours`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $localize`:a variant of Trending videos based on the number of likes:Likes`,
|
||||||
|
iconName: 'like',
|
||||||
|
value: '-likes',
|
||||||
|
path: 'most-liked',
|
||||||
|
tooltip: $localize`Videos that have the most likes`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
setSort () {
|
||||||
|
const path = this.buttons.find(b => b.value === this.data.model).path
|
||||||
|
this.router.navigate([ `/videos/${path}` ])
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
@ -6,13 +6,15 @@ import { immutableAssign } from '@app/helpers'
|
||||||
import { VideoService } from '@app/shared/shared-main'
|
import { VideoService } from '@app/shared/shared-main'
|
||||||
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
||||||
import { VideoSortField } from '@shared/models'
|
import { VideoSortField } from '@shared/models'
|
||||||
|
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-trending',
|
selector: 'my-videos-trending',
|
||||||
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
|
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
|
||||||
})
|
})
|
||||||
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
|
HeaderComponent = VideoTrendingHeaderComponent
|
||||||
titlePage: string
|
titlePage: string
|
||||||
defaultSort: VideoSortField = '-trending'
|
defaultSort: VideoSortField = '-trending'
|
||||||
|
|
||||||
|
@ -27,10 +29,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
this.headerComponentInjector = this.getInjector()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
@ -43,13 +48,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
const trendingDays = config.trending.videos.intervalDays
|
const trendingDays = config.trending.videos.intervalDays
|
||||||
|
|
||||||
if (trendingDays === 1) {
|
if (trendingDays === 1) {
|
||||||
this.titlePage = $localize`Trending for the last 24 hours`
|
|
||||||
this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last 24 hours`
|
this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last 24 hours`
|
||||||
return
|
} else {
|
||||||
|
this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last ${trendingDays} days`
|
||||||
}
|
}
|
||||||
|
|
||||||
this.titlePage = $localize`Trending for the last ${trendingDays} days`
|
this.headerComponentInjector = this.getInjector()
|
||||||
this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last ${trendingDays} days`
|
this.setHeader()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,4 +85,15 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
generateSyndicationList () {
|
generateSyndicationList () {
|
||||||
this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
|
this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInjector () {
|
||||||
|
return Injector.create({
|
||||||
|
providers: [{
|
||||||
|
provide: 'data',
|
||||||
|
useValue: {
|
||||||
|
model: this.defaultSort
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
@ -28,6 +28,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
@ -28,6 +28,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import { switchMap } from 'rxjs/operators'
|
import { switchMap } from 'rxjs/operators'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, LocalStorageService, Notifier, ScopedTokensService, ScreenService, ServerService, UserService } from '@app/core'
|
import { AuthService, LocalStorageService, Notifier, ScopedTokensService, ScreenService, ServerService, UserService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
|
@ -33,6 +33,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
private userSubscription: UserSubscriptionService,
|
private userSubscription: UserSubscriptionService,
|
||||||
|
protected cfr: ComponentFactoryResolver,
|
||||||
private hooks: HooksService,
|
private hooks: HooksService,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private scopedTokensService: ScopedTokensService
|
private scopedTokensService: ScopedTokensService
|
||||||
|
@ -102,7 +103,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
|
||||||
}
|
}
|
||||||
|
|
||||||
generateSyndicationList () {
|
generateSyndicationList () {
|
||||||
// not implemented yet
|
/* method disabled: the view provides its own */
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
}
|
}
|
||||||
|
|
||||||
activateCopiedMessage () {
|
activateCopiedMessage () {
|
||||||
|
|
|
@ -3,10 +3,11 @@ import { RouterModule, Routes } from '@angular/router'
|
||||||
import { LoginGuard } from '@app/core'
|
import { LoginGuard } from '@app/core'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
import { MetaGuard } from '@ngx-meta/core'
|
||||||
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
||||||
|
import { VideoHotComponent } from './video-list/trending/video-hot.component'
|
||||||
|
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
|
||||||
|
import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
|
||||||
import { VideoLocalComponent } from './video-list/video-local.component'
|
import { VideoLocalComponent } from './video-list/video-local.component'
|
||||||
import { VideoMostLikedComponent } from './video-list/video-most-liked.component'
|
|
||||||
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
||||||
import { VideoTrendingComponent } from './video-list/video-trending.component'
|
|
||||||
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
||||||
import { VideosComponent } from './videos.component'
|
import { VideosComponent } from './videos.component'
|
||||||
|
|
||||||
|
@ -38,6 +39,19 @@ const videosRoutes: Routes = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'hot',
|
||||||
|
component: VideoHotComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Hot videos`
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'hot-videos-list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'most-liked',
|
path: 'most-liked',
|
||||||
component: VideoMostLikedComponent,
|
component: VideoMostLikedComponent,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||||
|
@ -6,10 +7,12 @@ import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscripti
|
||||||
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
||||||
import { OverviewService } from './video-list'
|
import { OverviewService } from './video-list'
|
||||||
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
||||||
|
import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
|
||||||
|
import { VideoHotComponent } from './video-list/trending/video-hot.component'
|
||||||
|
import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
|
||||||
|
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
|
||||||
import { VideoLocalComponent } from './video-list/video-local.component'
|
import { VideoLocalComponent } from './video-list/video-local.component'
|
||||||
import { VideoMostLikedComponent } from './video-list/video-most-liked.component'
|
|
||||||
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
||||||
import { VideoTrendingComponent } from './video-list/video-trending.component'
|
|
||||||
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
||||||
import { VideosRoutingModule } from './videos-routing.module'
|
import { VideosRoutingModule } from './videos-routing.module'
|
||||||
import { VideosComponent } from './videos.component'
|
import { VideosComponent } from './videos.component'
|
||||||
|
@ -28,7 +31,9 @@ import { VideosComponent } from './videos.component'
|
||||||
declarations: [
|
declarations: [
|
||||||
VideosComponent,
|
VideosComponent,
|
||||||
|
|
||||||
|
VideoTrendingHeaderComponent,
|
||||||
VideoTrendingComponent,
|
VideoTrendingComponent,
|
||||||
|
VideoHotComponent,
|
||||||
VideoMostLikedComponent,
|
VideoMostLikedComponent,
|
||||||
VideoRecentlyAddedComponent,
|
VideoRecentlyAddedComponent,
|
||||||
VideoLocalComponent,
|
VideoLocalComponent,
|
||||||
|
|
|
@ -132,11 +132,6 @@
|
||||||
<ng-container i18n>Trending</ng-container>
|
<ng-container i18n>Trending</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a routerLink="/videos/most-liked" routerLinkActive="active">
|
|
||||||
<my-global-icon iconName="like" aria-hidden="true"></my-global-icon>
|
|
||||||
<ng-container i18n>Most liked</ng-container>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a routerLink="/videos/recently-added" routerLinkActive="active">
|
<a routerLink="/videos/recently-added" routerLinkActive="active">
|
||||||
<my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
|
||||||
<ng-container i18n>Recently added</ng-container>
|
<ng-container i18n>Recently added</ng-container>
|
||||||
|
|
|
@ -16,6 +16,7 @@ const icons = {
|
||||||
'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui
|
'playlist-add': require('!!raw-loader?!../../../assets/images/misc/playlist-add.svg').default, // material ui
|
||||||
'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui
|
'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui
|
||||||
'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui
|
'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui
|
||||||
|
'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default,
|
||||||
|
|
||||||
// feather icons
|
// feather icons
|
||||||
'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default,
|
'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default,
|
||||||
|
|
|
@ -11,7 +11,8 @@ import {
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgbTooltipModule
|
NgbTooltipModule,
|
||||||
|
NgbButtonsModule
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
||||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||||
|
@ -53,6 +54,7 @@ import { VideoChannelService } from './video-channel'
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
|
NgbButtonsModule,
|
||||||
|
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
|
|
||||||
|
@ -110,6 +112,7 @@ import { VideoChannelService } from './video-channel'
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
|
NgbButtonsModule,
|
||||||
|
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
<div class="margin-content">
|
<div class="margin-content">
|
||||||
<div class="videos-header">
|
<div class="videos-header">
|
||||||
<h1 *ngIf="titlePage" class="title-page title-page-single">
|
<ng-template #videoListHeader></ng-template>
|
||||||
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
|
|
||||||
{{ titlePage }}
|
|
||||||
</div>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="action-block">
|
<div class="action-block">
|
||||||
<my-feed *ngIf="titlePage" [syndicationItems]="syndicationItems"></my-feed>
|
<my-feed *ngIf="syndicationItems" [syndicationItems]="syndicationItems"></my-feed>
|
||||||
<ng-container *ngFor="let action of actions">
|
<ng-container *ngFor="let action of actions">
|
||||||
<a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active">
|
<a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active">
|
||||||
<ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
|
<ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
|
||||||
|
|
|
@ -5,16 +5,16 @@
|
||||||
|
|
||||||
$iconSize: 16px;
|
$iconSize: 16px;
|
||||||
|
|
||||||
|
::ng-deep .title-page.title-page-single {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.videos-header {
|
.videos-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.title-page.title-page-single {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-block {
|
.action-block {
|
||||||
::ng-deep my-feed {
|
::ng-deep my-feed {
|
||||||
my-global-icon {
|
my-global-icon {
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { fromEvent, Observable, Subject, Subscription } from 'rxjs'
|
import { fromEvent, Observable, Subject, Subscription } from 'rxjs'
|
||||||
import { debounceTime, switchMap, tap } from 'rxjs/operators'
|
import { debounceTime, switchMap, tap } from 'rxjs/operators'
|
||||||
import { Directive, OnDestroy, OnInit } from '@angular/core'
|
import {
|
||||||
|
AfterContentInit,
|
||||||
|
ComponentFactoryResolver,
|
||||||
|
Directive,
|
||||||
|
Injector,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Type,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef
|
||||||
|
} from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
AuthService,
|
AuthService,
|
||||||
|
@ -19,6 +29,7 @@ import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/mo
|
||||||
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
|
||||||
import { Syndication, Video } from '../shared-main'
|
import { Syndication, Video } from '../shared-main'
|
||||||
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
|
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
|
||||||
|
import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component'
|
||||||
|
|
||||||
enum GroupDate {
|
enum GroupDate {
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
|
@ -32,7 +43,12 @@ enum GroupDate {
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
// tslint:disable-next-line: directive-class-suffix
|
// tslint:disable-next-line: directive-class-suffix
|
||||||
export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
|
export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterContentInit, DisableForReuseHook {
|
||||||
|
@ViewChild('videoListHeader', { static: true, read: ViewContainerRef }) videoListHeader: ViewContainerRef
|
||||||
|
|
||||||
|
HeaderComponent: Type<GenericHeaderComponent> = VideoListHeaderComponent
|
||||||
|
headerComponentInjector: Injector
|
||||||
|
|
||||||
pagination: ComponentPaginationLight = {
|
pagination: ComponentPaginationLight = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 25
|
itemsPerPage: 25
|
||||||
|
@ -92,6 +108,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
||||||
protected abstract screenService: ScreenService
|
protected abstract screenService: ScreenService
|
||||||
protected abstract storageService: LocalStorageService
|
protected abstract storageService: LocalStorageService
|
||||||
protected abstract router: Router
|
protected abstract router: Router
|
||||||
|
protected abstract cfr: ComponentFactoryResolver
|
||||||
abstract titlePage: string
|
abstract titlePage: string
|
||||||
|
|
||||||
private resizeSubscription: Subscription
|
private resizeSubscription: Subscription
|
||||||
|
@ -153,6 +170,13 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
||||||
if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
|
if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterContentInit () {
|
||||||
|
if (this.videoListHeader) {
|
||||||
|
// some components don't use the header: they use their own template, like my-history.component.html
|
||||||
|
this.setHeader.apply(this, [ this.HeaderComponent, this.headerComponentInjector ])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disableForReuse () {
|
disableForReuse () {
|
||||||
this.disabled = true
|
this.disabled = true
|
||||||
}
|
}
|
||||||
|
@ -268,7 +292,27 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleModerationDisplay () {
|
toggleModerationDisplay () {
|
||||||
throw new Error('toggleModerationDisplay is not implemented')
|
throw new Error('toggleModerationDisplay ' + $localize`function is not implemented`)
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeader (
|
||||||
|
t: Type<any> = this.HeaderComponent,
|
||||||
|
i: Injector = this.headerComponentInjector
|
||||||
|
) {
|
||||||
|
const injector = i || Injector.create({
|
||||||
|
providers: [{
|
||||||
|
provide: 'data',
|
||||||
|
useValue: {
|
||||||
|
titlePage: this.titlePage,
|
||||||
|
titleTooltip: this.titleTooltip
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
const viewContainerRef = this.videoListHeader
|
||||||
|
viewContainerRef.clear()
|
||||||
|
|
||||||
|
const componentFactory = this.cfr.resolveComponentFactory(t)
|
||||||
|
viewContainerRef.createComponent(componentFactory, 0, injector)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On videos hook for children that want to do something
|
// On videos hook for children that want to do something
|
||||||
|
|
|
@ -3,5 +3,5 @@ export * from './video-actions-dropdown.component'
|
||||||
export * from './video-download.component'
|
export * from './video-download.component'
|
||||||
export * from './video-miniature.component'
|
export * from './video-miniature.component'
|
||||||
export * from './videos-selection.component'
|
export * from './videos-selection.component'
|
||||||
|
export * from './video-list-header.component'
|
||||||
export * from './shared-video-miniature.module'
|
export * from './shared-video-miniature.module'
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { VideoActionsDropdownComponent } from './video-actions-dropdown.componen
|
||||||
import { VideoDownloadComponent } from './video-download.component'
|
import { VideoDownloadComponent } from './video-download.component'
|
||||||
import { VideoMiniatureComponent } from './video-miniature.component'
|
import { VideoMiniatureComponent } from './video-miniature.component'
|
||||||
import { VideosSelectionComponent } from './videos-selection.component'
|
import { VideosSelectionComponent } from './videos-selection.component'
|
||||||
|
import { VideoListHeaderComponent } from './video-list-header.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -29,7 +30,8 @@ import { VideosSelectionComponent } from './videos-selection.component'
|
||||||
VideoActionsDropdownComponent,
|
VideoActionsDropdownComponent,
|
||||||
VideoDownloadComponent,
|
VideoDownloadComponent,
|
||||||
VideoMiniatureComponent,
|
VideoMiniatureComponent,
|
||||||
VideosSelectionComponent
|
VideosSelectionComponent,
|
||||||
|
VideoListHeaderComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component, Inject } from '@angular/core'
|
||||||
|
|
||||||
|
export abstract class GenericHeaderComponent {
|
||||||
|
constructor (@Inject('data') public data: any) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'h1',
|
||||||
|
host: { 'class': 'title-page title-page-single' },
|
||||||
|
template: `
|
||||||
|
<div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
|
||||||
|
{{ data.titlePage }}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class VideoListHeaderComponent extends GenericHeaderComponent {
|
||||||
|
constructor (@Inject('data') public data: any) {
|
||||||
|
super(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { Observable } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
AfterContentInit,
|
AfterContentInit,
|
||||||
Component,
|
Component,
|
||||||
|
ComponentFactoryResolver,
|
||||||
ContentChildren,
|
ContentChildren,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
|
@ -51,7 +52,8 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
|
||||||
protected userService: UserService,
|
protected userService: UserService,
|
||||||
protected screenService: ScreenService,
|
protected screenService: ScreenService,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
protected serverService: ServerService
|
protected serverService: ServerService,
|
||||||
|
protected cfr: ComponentFactoryResolver
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-78 0 512 512">
|
||||||
|
<defs/>
|
||||||
|
<path d="M178.2 512A178.9 178.9 0 010 332.8c0-43 14.7-72.3 31.6-106.2 9.5-18.8 19.2-38.2 28.2-63l9.4-25.9 23 5.6-3.7 27.3c-3 22.2 1 47.5 11.1 69.2 4.3 9.3 9.5 17.4 15.2 24.3a316 316 0 0111-104 288 288 0 0146.8-94.7c26.4-35.2 56.7-58.1 70.8-60L283.3 0l-26.9 30a74 74 0 00-18.8 49.5c0 35.3 21.6 60.4 46.8 89.5a359.4 359.4 0 0148.1 65.7c16 30 23.8 62.1 23.8 98.1 0 98.8-79.9 179.2-178.1 179.2zm0 0" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 502 B |
|
@ -72,7 +72,7 @@ const SORTABLE_COLUMNS = {
|
||||||
FOLLOWERS: [ 'createdAt', 'state', 'score' ],
|
FOLLOWERS: [ 'createdAt', 'state', 'score' ],
|
||||||
FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
|
FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
|
||||||
|
|
||||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending' ],
|
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending', 'hot' ],
|
||||||
|
|
||||||
// Don't forget to update peertube-search-index with the same values
|
// Don't forget to update peertube-search-index with the same values
|
||||||
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
||||||
|
|
|
@ -16,7 +16,7 @@ function setBlacklistSort (req: express.Request, res: express.Response, next: ex
|
||||||
// Set model we want to sort onto
|
// Set model we want to sort onto
|
||||||
if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' ||
|
if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' ||
|
||||||
req.query.sort === '-id' || req.query.sort === 'id') {
|
req.query.sort === '-id' || req.query.sort === 'id') {
|
||||||
// If we want to sort onto the BlacklistedVideos relation, we won't specify it in the query parameter ...
|
// If we want to sort onto the BlacklistedVideos relation, we won't specify it in the query parameter...
|
||||||
newSort.sortModel = undefined
|
newSort.sortModel = undefined
|
||||||
} else {
|
} else {
|
||||||
newSort.sortModel = 'Video'
|
newSort.sortModel = 'Video'
|
||||||
|
|
|
@ -32,6 +32,8 @@ export type BuildVideosQueryOptions = {
|
||||||
videoPlaylistId?: number
|
videoPlaylistId?: number
|
||||||
|
|
||||||
trendingDays?: number
|
trendingDays?: number
|
||||||
|
hot?: boolean
|
||||||
|
|
||||||
user?: MUserAccountId
|
user?: MUserAccountId
|
||||||
historyOfUser?: MUserId
|
historyOfUser?: MUserId
|
||||||
|
|
||||||
|
@ -239,14 +241,46 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't exclude results in this if so if we do a count we don't need to add this complex clauses
|
// We don't exclude results in this so if we do a count we don't need to add this complex clause
|
||||||
if (options.trendingDays && options.isCount !== true) {
|
if (options.trendingDays && options.isCount !== true) {
|
||||||
const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
|
const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
|
||||||
|
|
||||||
joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
|
joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
|
||||||
replacements.viewsGteDate = viewsGteDate
|
replacements.viewsGteDate = viewsGteDate
|
||||||
|
|
||||||
attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "videoViewsSum"')
|
attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
|
||||||
|
|
||||||
|
group = 'GROUP BY "video"."id"'
|
||||||
|
} else if (options.hot && options.isCount !== true) {
|
||||||
|
/**
|
||||||
|
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
|
||||||
|
* with fixed weights only applied to their log values.
|
||||||
|
*
|
||||||
|
* This algorithm gives little chance for an old video to have a good score,
|
||||||
|
* for which recent spikes in interactions could be a sign of "hotness" and
|
||||||
|
* justify a better score. However there are multiple ways to achieve that
|
||||||
|
* goal, which is left for later. Yes, this is a TODO :)
|
||||||
|
*
|
||||||
|
* note: weights and base score are in number of half-days.
|
||||||
|
* see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58
|
||||||
|
*/
|
||||||
|
const weights = {
|
||||||
|
like: 3,
|
||||||
|
dislike: 3,
|
||||||
|
view: 1 / 12,
|
||||||
|
comment: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
|
||||||
|
|
||||||
|
attributes.push(
|
||||||
|
`LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
|
||||||
|
`- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
|
||||||
|
`+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
|
||||||
|
`+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id") - 1)) * ${weights.comment} ` + // comments (+)
|
||||||
|
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
|
||||||
|
'AS "score"'
|
||||||
|
)
|
||||||
|
|
||||||
group = 'GROUP BY "video"."id"'
|
group = 'GROUP BY "video"."id"'
|
||||||
}
|
}
|
||||||
|
@ -372,8 +406,8 @@ function buildOrder (value: string) {
|
||||||
|
|
||||||
if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
|
if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
|
||||||
|
|
||||||
if (field.toLowerCase() === 'trending') { // Sort by aggregation
|
if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation
|
||||||
return `ORDER BY "videoViewsSum" ${direction}, "video"."views" ${direction}`
|
return `ORDER BY "score" ${direction}, "video"."views" ${direction}`
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstSort: string
|
let firstSort: string
|
||||||
|
|
|
@ -1090,6 +1090,7 @@ export class VideoModel extends Model {
|
||||||
const trendingDays = options.sort.endsWith('trending')
|
const trendingDays = options.sort.endsWith('trending')
|
||||||
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
|
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
|
||||||
: undefined
|
: undefined
|
||||||
|
const hot = options.sort.endsWith('hot')
|
||||||
|
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
|
@ -1119,6 +1120,7 @@ export class VideoModel extends Model {
|
||||||
user: options.user,
|
user: options.user,
|
||||||
historyOfUser: options.historyOfUser,
|
historyOfUser: options.historyOfUser,
|
||||||
trendingDays,
|
trendingDays,
|
||||||
|
hot,
|
||||||
search: options.search
|
search: options.search
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,4 +5,7 @@ export type VideoSortField =
|
||||||
'createdAt' | '-createdAt' |
|
'createdAt' | '-createdAt' |
|
||||||
'views' | '-views' |
|
'views' | '-views' |
|
||||||
'likes' | '-likes' |
|
'likes' | '-likes' |
|
||||||
'trending' | '-trending'
|
|
||||||
|
// trending sorts
|
||||||
|
'trending' | '-trending' |
|
||||||
|
'hot' | '-hot'
|
||||||
|
|
Loading…
Reference in New Issue