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)
|
||||
* `video-language` by Rigel Kent (CC-BY)
|
||||
* `peertube-x` by Solen DP (CC-BY)
|
||||
* `flame` by Freepik (Flaticon License)
|
||||
|
||||
|
||||
# Contributors to our 2020 crowdfunding :heart:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Subscription } from 'rxjs'
|
||||
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 { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
|
@ -11,9 +11,7 @@ import { VideoFilter } from '@shared/models'
|
|||
@Component({
|
||||
selector: 'my-account-search',
|
||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html',
|
||||
styleUrls: [
|
||||
'../../shared/shared-video-miniature/abstract-video-list.scss'
|
||||
]
|
||||
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ]
|
||||
})
|
||||
export class AccountSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||
titlePage: string
|
||||
|
@ -35,6 +33,7 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
|
|||
protected confirmService: ConfirmService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private accountService: AccountService,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
|
@ -99,6 +98,7 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
|
|||
}
|
||||
|
||||
generateSyndicationList () {
|
||||
/* disable syndication */
|
||||
/* method disabled */
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Subscription } from 'rxjs'
|
||||
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 { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
|
@ -35,7 +35,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
|||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
private accountService: AccountService,
|
||||
private videoService: VideoService
|
||||
private videoService: VideoService,
|
||||
protected cfr: ComponentFactoryResolver
|
||||
) {
|
||||
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 {
|
||||
AuthService,
|
||||
|
@ -42,7 +42,8 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
|
|||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
private confirmService: ConfirmService,
|
||||
private userHistoryService: UserHistoryService
|
||||
private userHistoryService: UserHistoryService,
|
||||
protected cfr: ComponentFactoryResolver
|
||||
) {
|
||||
super()
|
||||
|
||||
|
@ -95,6 +96,7 @@ export class MyHistoryComponent extends AbstractVideoList implements OnInit, OnD
|
|||
}
|
||||
|
||||
generateSyndicationList () {
|
||||
/* method disabled */
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Subscription } from 'rxjs'
|
||||
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 { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
|
@ -34,6 +34,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
|
|||
protected confirmService: ConfirmService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private videoService: VideoService
|
||||
) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export * from './overview'
|
||||
export * from './trending'
|
||||
export * from './video-local.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 { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
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 { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
||||
import { VideoSortField } from '@shared/models'
|
||||
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-most-liked',
|
||||
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
|
||||
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
|
||||
})
|
||||
export class VideoMostLikedComponent extends AbstractVideoList implements OnInit {
|
||||
HeaderComponent = VideoTrendingHeaderComponent
|
||||
titlePage: string
|
||||
defaultSort: VideoSortField = '-likes'
|
||||
|
||||
|
@ -27,19 +29,19 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
|
|||
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()
|
||||
|
||||
this.titlePage = $localize`Most liked videos`
|
||||
this.titleTooltip = $localize`Videos that have the most likes.`
|
||||
}
|
||||
|
||||
getVideosObservable (page: number) {
|
||||
|
@ -65,4 +67,15 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
|
|||
generateSyndicationList () {
|
||||
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 { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
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 { AbstractVideoList } from '@app/shared/shared-video-miniature'
|
||||
import { VideoSortField } from '@shared/models'
|
||||
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-trending',
|
||||
styleUrls: [ '../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||
templateUrl: '../../shared/shared-video-miniature/abstract-video-list.html'
|
||||
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
|
||||
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
|
||||
})
|
||||
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||
HeaderComponent = VideoTrendingHeaderComponent
|
||||
titlePage: string
|
||||
defaultSort: VideoSortField = '-trending'
|
||||
|
||||
|
@ -27,10 +29,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
protected userService: UserService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private videoService: VideoService,
|
||||
private hooks: HooksService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.headerComponentInjector = this.getInjector()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -43,13 +48,13 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
const trendingDays = config.trending.videos.intervalDays
|
||||
|
||||
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`
|
||||
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.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last ${trendingDays} days`
|
||||
this.headerComponentInjector = this.getInjector()
|
||||
this.setHeader()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -80,4 +85,15 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
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, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, ComponentFactoryResolver, 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'
|
||||
|
@ -28,6 +28,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
|
|||
protected userService: UserService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private videoService: VideoService,
|
||||
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 { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
|
@ -28,6 +28,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
|
|||
protected userService: UserService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private videoService: VideoService,
|
||||
private hooks: HooksService
|
||||
) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
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 { AuthService, LocalStorageService, Notifier, ScopedTokensService, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
|
@ -33,6 +33,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
|
|||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
private userSubscription: UserSubscriptionService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private hooks: HooksService,
|
||||
private videoService: VideoService,
|
||||
private scopedTokensService: ScopedTokensService
|
||||
|
@ -102,7 +103,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
|
|||
}
|
||||
|
||||
generateSyndicationList () {
|
||||
// not implemented yet
|
||||
/* method disabled: the view provides its own */
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
activateCopiedMessage () {
|
||||
|
|
|
@ -3,10 +3,11 @@ import { RouterModule, Routes } from '@angular/router'
|
|||
import { LoginGuard } from '@app/core'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
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 { VideoMostLikedComponent } from './video-list/video-most-liked.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 { 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',
|
||||
component: VideoMostLikedComponent,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
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 { OverviewService } from './video-list'
|
||||
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 { VideoMostLikedComponent } from './video-list/video-most-liked.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 { VideosRoutingModule } from './videos-routing.module'
|
||||
import { VideosComponent } from './videos.component'
|
||||
|
@ -28,7 +31,9 @@ import { VideosComponent } from './videos.component'
|
|||
declarations: [
|
||||
VideosComponent,
|
||||
|
||||
VideoTrendingHeaderComponent,
|
||||
VideoTrendingComponent,
|
||||
VideoHotComponent,
|
||||
VideoMostLikedComponent,
|
||||
VideoRecentlyAddedComponent,
|
||||
VideoLocalComponent,
|
||||
|
|
|
@ -132,11 +132,6 @@
|
|||
<ng-container i18n>Trending</ng-container>
|
||||
</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">
|
||||
<my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
|
||||
<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
|
||||
'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
|
||||
'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default,
|
||||
|
||||
// feather icons
|
||||
'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default,
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
NgbModalModule,
|
||||
NgbNavModule,
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule
|
||||
NgbTooltipModule,
|
||||
NgbButtonsModule
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||
|
@ -53,6 +54,7 @@ import { VideoChannelService } from './video-channel'
|
|||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgbCollapseModule,
|
||||
NgbButtonsModule,
|
||||
|
||||
ClipboardModule,
|
||||
|
||||
|
@ -110,6 +112,7 @@ import { VideoChannelService } from './video-channel'
|
|||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgbCollapseModule,
|
||||
NgbButtonsModule,
|
||||
|
||||
ClipboardModule,
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
<div class="margin-content">
|
||||
<div class="videos-header">
|
||||
<h1 *ngIf="titlePage" class="title-page title-page-single">
|
||||
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
|
||||
{{ titlePage }}
|
||||
</div>
|
||||
</h1>
|
||||
<ng-template #videoListHeader></ng-template>
|
||||
|
||||
<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">
|
||||
<a *ngIf="action.routerLink" class="ml-2" [routerLink]="action.routerLink" routerLinkActive="active">
|
||||
<ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
|
||||
$iconSize: 16px;
|
||||
|
||||
::ng-deep .title-page.title-page-single {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.videos-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title-page.title-page-single {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.action-block {
|
||||
::ng-deep my-feed {
|
||||
my-global-icon {
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import { fromEvent, Observable, Subject, Subscription } from 'rxjs'
|
||||
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 {
|
||||
AuthService,
|
||||
|
@ -19,6 +29,7 @@ import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/mo
|
|||
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
|
||||
import { Syndication, Video } from '../shared-main'
|
||||
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
|
||||
import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component'
|
||||
|
||||
enum GroupDate {
|
||||
UNKNOWN = 0,
|
||||
|
@ -32,7 +43,12 @@ enum GroupDate {
|
|||
|
||||
@Directive()
|
||||
// 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 = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 25
|
||||
|
@ -92,6 +108,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
|||
protected abstract screenService: ScreenService
|
||||
protected abstract storageService: LocalStorageService
|
||||
protected abstract router: Router
|
||||
protected abstract cfr: ComponentFactoryResolver
|
||||
abstract titlePage: string
|
||||
|
||||
private resizeSubscription: Subscription
|
||||
|
@ -153,6 +170,13 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
|||
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 () {
|
||||
this.disabled = true
|
||||
}
|
||||
|
@ -268,7 +292,27 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -3,5 +3,5 @@ export * from './video-actions-dropdown.component'
|
|||
export * from './video-download.component'
|
||||
export * from './video-miniature.component'
|
||||
export * from './videos-selection.component'
|
||||
|
||||
export * from './video-list-header.component'
|
||||
export * from './shared-video-miniature.module'
|
||||
|
|
|
@ -12,6 +12,7 @@ import { VideoActionsDropdownComponent } from './video-actions-dropdown.componen
|
|||
import { VideoDownloadComponent } from './video-download.component'
|
||||
import { VideoMiniatureComponent } from './video-miniature.component'
|
||||
import { VideosSelectionComponent } from './videos-selection.component'
|
||||
import { VideoListHeaderComponent } from './video-list-header.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -29,7 +30,8 @@ import { VideosSelectionComponent } from './videos-selection.component'
|
|||
VideoActionsDropdownComponent,
|
||||
VideoDownloadComponent,
|
||||
VideoMiniatureComponent,
|
||||
VideosSelectionComponent
|
||||
VideosSelectionComponent,
|
||||
VideoListHeaderComponent
|
||||
],
|
||||
|
||||
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 {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
|
@ -51,7 +52,8 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
|
|||
protected userService: UserService,
|
||||
protected screenService: ScreenService,
|
||||
protected storageService: LocalStorageService,
|
||||
protected serverService: ServerService
|
||||
protected serverService: ServerService,
|
||||
protected cfr: ComponentFactoryResolver
|
||||
) {
|
||||
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' ],
|
||||
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
|
||||
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
|
||||
if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' ||
|
||||
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
|
||||
} else {
|
||||
newSort.sortModel = 'Video'
|
||||
|
|
|
@ -32,6 +32,8 @@ export type BuildVideosQueryOptions = {
|
|||
videoPlaylistId?: number
|
||||
|
||||
trendingDays?: number
|
||||
hot?: boolean
|
||||
|
||||
user?: MUserAccountId
|
||||
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) {
|
||||
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')
|
||||
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"'
|
||||
}
|
||||
|
@ -372,8 +406,8 @@ function buildOrder (value: string) {
|
|||
|
||||
if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
|
||||
|
||||
if (field.toLowerCase() === 'trending') { // Sort by aggregation
|
||||
return `ORDER BY "videoViewsSum" ${direction}, "video"."views" ${direction}`
|
||||
if ([ 'trending', 'hot' ].includes(field.toLowerCase())) { // Sort by aggregation
|
||||
return `ORDER BY "score" ${direction}, "video"."views" ${direction}`
|
||||
}
|
||||
|
||||
let firstSort: string
|
||||
|
|
|
@ -1090,6 +1090,7 @@ export class VideoModel extends Model {
|
|||
const trendingDays = options.sort.endsWith('trending')
|
||||
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
|
||||
: undefined
|
||||
const hot = options.sort.endsWith('hot')
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
|
@ -1119,6 +1120,7 @@ export class VideoModel extends Model {
|
|||
user: options.user,
|
||||
historyOfUser: options.historyOfUser,
|
||||
trendingDays,
|
||||
hot,
|
||||
search: options.search
|
||||
}
|
||||
|
||||
|
|
|
@ -5,4 +5,7 @@ export type VideoSortField =
|
|||
'createdAt' | '-createdAt' |
|
||||
'views' | '-views' |
|
||||
'likes' | '-likes' |
|
||||
'trending' | '-trending'
|
||||
|
||||
// trending sorts
|
||||
'trending' | '-trending' |
|
||||
'hot' | '-hot'
|
||||
|
|
Loading…
Reference in New Issue