modularize abstract video list header and implement video hotness recommendation variant

This commit is contained in:
Rigel Kent 2021-01-22 00:12:44 +01:00 committed by Chocobozzz
parent 7a4994873c
commit 5bcbcbe338
34 changed files with 396 additions and 63 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,4 +5,7 @@ export type VideoSortField =
'createdAt' | '-createdAt' |
'views' | '-views' |
'likes' | '-likes' |
'trending' | '-trending'
// trending sorts
'trending' | '-trending' |
'hot' | '-hot'