add default trending page choice, revert comments count for hot strategy
This commit is contained in:
parent
28eeb811c4
commit
3da68f0a78
|
@ -269,9 +269,13 @@
|
||||||
<div class="peertube-select-container">
|
<div class="peertube-select-container">
|
||||||
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
|
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
|
||||||
<option i18n value="/videos/overview">Discover videos</option>
|
<option i18n value="/videos/overview">Discover videos</option>
|
||||||
<option i18n value="/videos/trending">Trending videos</option>
|
<optgroup i18n-label label="Trending pages">
|
||||||
<option i18n value="/videos/hot">Hot videos</option>
|
<option i18n value="/videos/trending">Default trending page</option>
|
||||||
<option i18n value="/videos/most-liked">Most liked videos</option>
|
<option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
|
||||||
|
<option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
|
||||||
|
<option i18n value="/videos/most-viewed">Most viewed videos</option>
|
||||||
|
<option i18n value="/videos/most-liked">Most liked videos</option>
|
||||||
|
</optgroup>
|
||||||
<option i18n value="/videos/recently-added">Recently added videos</option>
|
<option i18n value="/videos/recently-added">Recently added videos</option>
|
||||||
<option i18n value="/videos/local">Local videos</option>
|
<option i18n value="/videos/local">Local videos</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -279,6 +283,19 @@
|
||||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="instance">
|
||||||
|
<label i18n for="instanceDefaultTrendingRoute">Default trending page</label>
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select id="instanceDefaultTrendingRoute" formControlName="defaultTrendingRoute" class="form-control">
|
||||||
|
<option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
|
||||||
|
<option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
|
||||||
|
<option i18n value="/videos/trending">Most viewed videos</option>
|
||||||
|
<option i18n value="/videos/most-liked">Most liked videos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="formErrors.instance.defaultTrendingRoute" class="form-error">{{ formErrors.instance.defaultTrendingRoute }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -186,6 +186,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
|
||||||
languages: null,
|
languages: null,
|
||||||
|
|
||||||
defaultClientRoute: null,
|
defaultClientRoute: null,
|
||||||
|
defaultTrendingRoute: null,
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: null,
|
javascript: null,
|
||||||
|
@ -364,6 +370,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
|
||||||
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTrendingHotEnabled () {
|
||||||
|
return this.form.value['instance']['pages']['hot']['enabled'] === true
|
||||||
|
}
|
||||||
|
|
||||||
async formValidated () {
|
async formValidated () {
|
||||||
const value: CustomConfig = this.form.getRawValue()
|
const value: CustomConfig = this.form.getRawValue()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from './video-trending-header.component'
|
export * from './video-trending-header.component'
|
||||||
export * from './video-trending.component'
|
|
||||||
export * from './video-hot.component'
|
export * from './video-hot.component'
|
||||||
|
export * from './video-most-viewed.component'
|
||||||
export * from './video-most-liked.component'
|
export * from './video-most-liked.component'
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { VideoSortField } from '@shared/models'
|
||||||
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-trending',
|
selector: 'my-videos-most-viewed',
|
||||||
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 VideoMostViewedComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||||
HeaderComponent = VideoTrendingHeaderComponent
|
HeaderComponent = VideoTrendingHeaderComponent
|
||||||
titlePage: string
|
titlePage: string
|
||||||
defaultSort: VideoSortField = '-trending'
|
defaultSort: VideoSortField = '-trending'
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
|
<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">
|
<label *ngFor="let button of visibleButtons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
|
||||||
<my-global-icon [iconName]="button.iconName"></my-global-icon>
|
<my-global-icon [iconName]="button.iconName"></my-global-icon>
|
||||||
<input ngbButton type="radio" [value]="button.value"> {{ button.label }}
|
<input ngbButton type="radio" [value]="button.value"> {{ button.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Component, Inject } from '@angular/core'
|
import { Component, Inject, OnInit } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
|
import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
|
||||||
import { GlobalIconName } from '@app/shared/shared-icons'
|
import { GlobalIconName } from '@app/shared/shared-icons'
|
||||||
import { VideoSortField } from '@shared/models'
|
import { VideoSortField } from '@shared/models'
|
||||||
|
import { ServerService } from '@app/core/server/server.service'
|
||||||
|
|
||||||
interface VideoTrendingHeaderItem {
|
interface VideoTrendingHeaderItem {
|
||||||
label: string
|
label: string
|
||||||
|
@ -10,6 +11,7 @@ interface VideoTrendingHeaderItem {
|
||||||
value: VideoSortField
|
value: VideoSortField
|
||||||
path: string
|
path: string
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
|
hidden?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -18,12 +20,13 @@ interface VideoTrendingHeaderItem {
|
||||||
styleUrls: [ './video-trending-header.component.scss' ],
|
styleUrls: [ './video-trending-header.component.scss' ],
|
||||||
templateUrl: './video-trending-header.component.html'
|
templateUrl: './video-trending-header.component.html'
|
||||||
})
|
})
|
||||||
export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
|
export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit {
|
||||||
buttons: VideoTrendingHeaderItem[]
|
buttons: VideoTrendingHeaderItem[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@Inject('data') public data: any,
|
@Inject('data') public data: any,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private serverService: ServerService
|
||||||
) {
|
) {
|
||||||
super(data)
|
super(data)
|
||||||
|
|
||||||
|
@ -34,16 +37,17 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
|
||||||
value: '-hot',
|
value: '-hot',
|
||||||
path: 'hot',
|
path: 'hot',
|
||||||
tooltip: $localize`Videos totalizing the most interactions for recent videos`,
|
tooltip: $localize`Videos totalizing the most interactions for recent videos`,
|
||||||
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
|
label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
|
||||||
iconName: 'trending',
|
iconName: 'trending',
|
||||||
value: '-trending',
|
value: '-trending',
|
||||||
path: 'trending',
|
path: 'most-viewed',
|
||||||
tooltip: $localize`Videos totalizing the most views during the last 24 hours`,
|
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`,
|
label: $localize`:A variant of Trending videos based on the number of likes:Likes`,
|
||||||
iconName: 'like',
|
iconName: 'like',
|
||||||
value: '-likes',
|
value: '-likes',
|
||||||
path: 'most-liked',
|
path: 'most-liked',
|
||||||
|
@ -52,6 +56,21 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.serverService.getConfig()
|
||||||
|
.subscribe(config => {
|
||||||
|
// don't filter if auto-blacklist is not enabled as this will be the only list
|
||||||
|
if (config.instance.pages.hot.enabled) {
|
||||||
|
const index = this.buttons.findIndex(b => b.path === 'hot')
|
||||||
|
this.buttons[index].hidden = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get visibleButtons () {
|
||||||
|
return this.buttons.filter(b => !b.hidden)
|
||||||
|
}
|
||||||
|
|
||||||
setSort () {
|
setSort () {
|
||||||
const path = this.buttons.find(b => b.value === this.data.model).path
|
const path = this.buttons.find(b => b.value === this.data.model).path
|
||||||
this.router.navigate([ `/videos/${path}` ])
|
this.router.navigate([ `/videos/${path}` ])
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { LoginGuard } from '@app/core'
|
import { LoginGuard, TrendingGuard } 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 { VideoHotComponent } from './video-list/trending/video-hot.component'
|
||||||
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
|
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
|
||||||
import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
|
import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
|
||||||
import { VideoLocalComponent } from './video-list/video-local.component'
|
import { VideoLocalComponent } from './video-list/video-local.component'
|
||||||
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
||||||
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
||||||
|
@ -28,16 +28,7 @@ const videosRoutes: Routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'trending',
|
path: 'trending',
|
||||||
component: VideoTrendingComponent,
|
canActivate: [ TrendingGuard ]
|
||||||
data: {
|
|
||||||
meta: {
|
|
||||||
title: $localize`Trending videos`
|
|
||||||
},
|
|
||||||
reuse: {
|
|
||||||
enabled: true,
|
|
||||||
key: 'trending-videos-list'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'hot',
|
path: 'hot',
|
||||||
|
@ -52,6 +43,19 @@ const videosRoutes: Routes = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'most-viewed',
|
||||||
|
component: VideoMostViewedComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Most viewed videos`
|
||||||
|
},
|
||||||
|
reuse: {
|
||||||
|
enabled: true,
|
||||||
|
key: 'most-viewed-videos-list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'most-liked',
|
path: 'most-liked',
|
||||||
component: VideoMostLikedComponent,
|
component: VideoMostLikedComponent,
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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 { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
|
||||||
import { VideoHotComponent } from './video-list/trending/video-hot.component'
|
import { VideoHotComponent } from './video-list/trending/video-hot.component'
|
||||||
import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
|
import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
|
||||||
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.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 { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
|
||||||
|
@ -32,7 +32,7 @@ import { VideosComponent } from './videos.component'
|
||||||
VideosComponent,
|
VideosComponent,
|
||||||
|
|
||||||
VideoTrendingHeaderComponent,
|
VideoTrendingHeaderComponent,
|
||||||
VideoTrendingComponent,
|
VideoMostViewedComponent,
|
||||||
VideoHotComponent,
|
VideoHotComponent,
|
||||||
VideoMostLikedComponent,
|
VideoMostLikedComponent,
|
||||||
VideoRecentlyAddedComponent,
|
VideoRecentlyAddedComponent,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||||
import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
|
import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
|
|
||||||
import { AuthService } from './auth'
|
import { AuthService } from './auth'
|
||||||
import { ConfirmService } from './confirm'
|
import { ConfirmService } from './confirm'
|
||||||
import { CheatSheetComponent } from './hotkeys'
|
import { CheatSheetComponent } from './hotkeys'
|
||||||
|
@ -16,7 +15,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
|
||||||
import { Notifier } from './notification'
|
import { Notifier } from './notification'
|
||||||
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
|
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
|
||||||
import { RestExtractor, RestService } from './rest'
|
import { RestExtractor, RestService } from './rest'
|
||||||
import { LoginGuard, RedirectService, UserRightGuard } from './routing'
|
import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard, TrendingGuard } from './routing'
|
||||||
import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
|
import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
|
||||||
import { ServerConfigResolver } from './routing/server-config-resolver.service'
|
import { ServerConfigResolver } from './routing/server-config-resolver.service'
|
||||||
import { ScopedTokensService } from './scoped-tokens'
|
import { ScopedTokensService } from './scoped-tokens'
|
||||||
|
@ -57,6 +56,7 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
|
||||||
LoginGuard,
|
LoginGuard,
|
||||||
UserRightGuard,
|
UserRightGuard,
|
||||||
UnloggedGuard,
|
UnloggedGuard,
|
||||||
|
TrendingGuard,
|
||||||
|
|
||||||
PluginService,
|
PluginService,
|
||||||
HooksService,
|
HooksService,
|
||||||
|
|
|
@ -8,3 +8,4 @@ export * from './redirect.service'
|
||||||
export * from './server-config-resolver.service'
|
export * from './server-config-resolver.service'
|
||||||
export * from './unlogged-guard.service'
|
export * from './unlogged-guard.service'
|
||||||
export * from './user-right-guard.service'
|
export * from './user-right-guard.service'
|
||||||
|
export * from './trending-guard.service'
|
||||||
|
|
|
@ -7,11 +7,14 @@ export class RedirectService {
|
||||||
// Default route could change according to the instance configuration
|
// Default route could change according to the instance configuration
|
||||||
static INIT_DEFAULT_ROUTE = '/videos/trending'
|
static INIT_DEFAULT_ROUTE = '/videos/trending'
|
||||||
static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
||||||
|
static INIT_DEFAULT_TRENDING_ROUTE = '/videos/most-viewed'
|
||||||
|
static DEFAULT_TRENDING_ROUTE = RedirectService.INIT_DEFAULT_TRENDING_ROUTE
|
||||||
|
|
||||||
private previousUrl: string
|
private previousUrl: string
|
||||||
private currentUrl: string
|
private currentUrl: string
|
||||||
|
|
||||||
private redirectingToHomepage = false
|
private redirectingToHomepage = false
|
||||||
|
private redirectingToTrending = false
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
@ -19,18 +22,28 @@ export class RedirectService {
|
||||||
) {
|
) {
|
||||||
// The config is first loaded from the cache so try to get the default route
|
// The config is first loaded from the cache so try to get the default route
|
||||||
const tmpConfig = this.serverService.getTmpConfig()
|
const tmpConfig = this.serverService.getTmpConfig()
|
||||||
if (tmpConfig && tmpConfig.instance && tmpConfig.instance.defaultClientRoute) {
|
if (tmpConfig && tmpConfig.instance) {
|
||||||
RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
|
if (tmpConfig.instance.defaultClientRoute) {
|
||||||
|
RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
|
||||||
|
}
|
||||||
|
if (tmpConfig.instance.defaultTrendingRoute) {
|
||||||
|
RedirectService.DEFAULT_TRENDING_ROUTE = tmpConfig.instance.defaultTrendingRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load default route
|
// Load default route
|
||||||
this.serverService.getConfig()
|
this.serverService.getConfig()
|
||||||
.subscribe(config => {
|
.subscribe(config => {
|
||||||
const defaultRouteConfig = config.instance.defaultClientRoute
|
const defaultRouteConfig = config.instance.defaultClientRoute
|
||||||
|
const defaultTrendingConfig = config.instance.defaultTrendingRoute
|
||||||
|
|
||||||
if (defaultRouteConfig) {
|
if (defaultRouteConfig) {
|
||||||
RedirectService.DEFAULT_ROUTE = defaultRouteConfig
|
RedirectService.DEFAULT_ROUTE = defaultRouteConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (defaultTrendingConfig) {
|
||||||
|
RedirectService.DEFAULT_TRENDING_ROUTE = defaultTrendingConfig
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Track previous url
|
// Track previous url
|
||||||
|
@ -57,6 +70,15 @@ export class RedirectService {
|
||||||
return this.redirectToHomepage()
|
return this.redirectToHomepage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redirectToTrending () {
|
||||||
|
if (this.redirectingToTrending) return
|
||||||
|
|
||||||
|
this.redirectingToTrending = true
|
||||||
|
|
||||||
|
this.router.navigate([ RedirectService.DEFAULT_TRENDING_ROUTE ])
|
||||||
|
.then(() => this.redirectingToTrending = false)
|
||||||
|
}
|
||||||
|
|
||||||
redirectToHomepage (skipLocationChange = false) {
|
redirectToHomepage (skipLocationChange = false) {
|
||||||
if (this.redirectingToHomepage) return
|
if (this.redirectingToHomepage) return
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'
|
||||||
|
import { RedirectService } from './redirect.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TrendingGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor (private redirectService: RedirectService) {}
|
||||||
|
|
||||||
|
canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
this.redirectService.redirectToTrending()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,9 +36,15 @@ export class ServerService {
|
||||||
name: 'PeerTube',
|
name: 'PeerTube',
|
||||||
shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' +
|
shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' +
|
||||||
'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.',
|
'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.',
|
||||||
defaultClientRoute: '',
|
|
||||||
isNSFW: false,
|
isNSFW: false,
|
||||||
defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
|
defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
|
||||||
|
defaultClientRoute: '',
|
||||||
|
defaultTrendingRoute: '',
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: '',
|
javascript: '',
|
||||||
css: ''
|
css: ''
|
||||||
|
|
|
@ -127,10 +127,14 @@
|
||||||
<ng-container i18n>Discover</ng-container>
|
<ng-container i18n>Discover</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a routerLink="/videos/trending" routerLinkActive="active">
|
<a routerLink="/videos/trending" routerLinkActive="active" [ngClass]="{ 'active': hot.isActive || mostViewed.isActive || mostLiked.isActive }">
|
||||||
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
|
||||||
<ng-container i18n>Trending</ng-container>
|
<ng-container i18n>Trending</ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
<a routerLink="/videos/hot" routerLinkActive #hot="routerLinkActive" hidden></a>
|
||||||
|
<a routerLink="/videos/most-viewed" routerLinkActive #mostViewed="routerLinkActive" hidden></a>
|
||||||
|
<a routerLink="/videos/most-liked" routerLinkActive #mostLiked="routerLinkActive" hidden></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>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
$iconSize: 16px;
|
$iconSize: 16px;
|
||||||
|
|
||||||
::ng-deep .title-page.title-page-single {
|
::ng-deep my-video-list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1 class="title-page title-page-single">
|
||||||
|
<div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
|
||||||
|
{{ data.titlePage }}
|
||||||
|
</div>
|
||||||
|
</h1>
|
|
@ -1,17 +1,13 @@
|
||||||
import { Component, Inject } from '@angular/core'
|
import { Component, Inject, ViewEncapsulation } from '@angular/core'
|
||||||
|
|
||||||
export abstract class GenericHeaderComponent {
|
export abstract class GenericHeaderComponent {
|
||||||
constructor (@Inject('data') public data: any) {}
|
constructor (@Inject('data') public data: any) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'h1',
|
selector: 'my-video-list-header',
|
||||||
host: { 'class': 'title-page title-page-single' },
|
encapsulation: ViewEncapsulation.None,
|
||||||
template: `
|
templateUrl: './video-list-header.component.html'
|
||||||
<div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
|
|
||||||
{{ data.titlePage }}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class VideoListHeaderComponent extends GenericHeaderComponent {
|
export class VideoListHeaderComponent extends GenericHeaderComponent {
|
||||||
constructor (@Inject('data') public data: any) {
|
constructor (@Inject('data') public data: any) {
|
||||||
|
|
|
@ -366,6 +366,10 @@ instance:
|
||||||
# - 18 # Food
|
# - 18 # Food
|
||||||
|
|
||||||
default_client_route: '/videos/trending'
|
default_client_route: '/videos/trending'
|
||||||
|
default_trending_route: '/videos/most-viewed'
|
||||||
|
pages:
|
||||||
|
hot:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# Whether or not the instance is dedicated to NSFW content
|
# Whether or not the instance is dedicated to NSFW content
|
||||||
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
|
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
|
||||||
|
|
|
@ -380,6 +380,10 @@ instance:
|
||||||
# - 18 # Food
|
# - 18 # Food
|
||||||
|
|
||||||
default_client_route: '/videos/trending'
|
default_client_route: '/videos/trending'
|
||||||
|
default_trending_route: '/videos/most-viewed'
|
||||||
|
pages:
|
||||||
|
hot:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# Whether or not the instance is dedicated to NSFW content
|
# Whether or not the instance is dedicated to NSFW content
|
||||||
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
|
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
|
||||||
|
|
|
@ -65,9 +65,15 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
instance: {
|
instance: {
|
||||||
name: CONFIG.INSTANCE.NAME,
|
name: CONFIG.INSTANCE.NAME,
|
||||||
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
|
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
|
||||||
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
|
|
||||||
isNSFW: CONFIG.INSTANCE.IS_NSFW,
|
isNSFW: CONFIG.INSTANCE.IS_NSFW,
|
||||||
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
||||||
|
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
|
||||||
|
defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
|
||||||
|
}
|
||||||
|
},
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
|
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
|
||||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
||||||
|
@ -362,8 +368,16 @@ function customConfig (): CustomConfig {
|
||||||
categories: CONFIG.INSTANCE.CATEGORIES,
|
categories: CONFIG.INSTANCE.CATEGORIES,
|
||||||
|
|
||||||
isNSFW: CONFIG.INSTANCE.IS_NSFW,
|
isNSFW: CONFIG.INSTANCE.IS_NSFW,
|
||||||
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
|
|
||||||
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
||||||
|
|
||||||
|
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
|
||||||
|
defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
|
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
|
||||||
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
|
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
|
||||||
|
|
|
@ -230,6 +230,7 @@ const customConfigKeysToKeep = [
|
||||||
'instance-description',
|
'instance-description',
|
||||||
'instance-terms',
|
'instance-terms',
|
||||||
'instance-defaultClientRoute',
|
'instance-defaultClientRoute',
|
||||||
|
'instance-defaultTrendingRoute',
|
||||||
'instance-defaultNSFWPolicy',
|
'instance-defaultNSFWPolicy',
|
||||||
'instance-customizations-javascript',
|
'instance-customizations-javascript',
|
||||||
'instance-customizations-css',
|
'instance-customizations-css',
|
||||||
|
|
|
@ -278,8 +278,16 @@ const CONFIG = {
|
||||||
get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
|
get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
|
||||||
|
|
||||||
get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
|
get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
|
||||||
get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
|
|
||||||
get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
|
get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
|
||||||
|
|
||||||
|
get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
|
||||||
|
get DEFAULT_TRENDING_ROUTE () { return config.get<string>('instance.default_trending_route') },
|
||||||
|
PAGES: {
|
||||||
|
HOT: {
|
||||||
|
get ENABLED () { return config.get<boolean>('instance.pages.hot.enabled') }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
CUSTOMIZATIONS: {
|
CUSTOMIZATIONS: {
|
||||||
get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
|
get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
|
||||||
get CSS () { return config.get<string>('instance.customizations.css') }
|
get CSS () { return config.get<string>('instance.customizations.css') }
|
||||||
|
|
|
@ -15,8 +15,9 @@ const customConfigUpdateValidator = [
|
||||||
body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
|
body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
|
||||||
body('instance.description').exists().withMessage('Should have a valid instance description'),
|
body('instance.description').exists().withMessage('Should have a valid instance description'),
|
||||||
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
|
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
|
||||||
body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
|
|
||||||
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
|
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
|
||||||
|
body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
|
||||||
|
body('instance.defaultTrendingRoute').exists().withMessage('Should have a valid instance default trending route'),
|
||||||
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
|
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
|
||||||
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
|
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
|
||||||
|
|
||||||
|
|
|
@ -242,64 +242,49 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't exclude results in this so if we do a count we don't need to add this complex clause
|
// 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.isCount !== true) {
|
||||||
const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
|
if (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 "score"')
|
attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
|
||||||
|
|
||||||
group = 'GROUP BY "video"."id"'
|
group = 'GROUP BY "video"."id"'
|
||||||
} else if (options.hot && options.isCount !== true) {
|
} else if (options.hot) {
|
||||||
/**
|
/**
|
||||||
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
|
* "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
|
||||||
* with fixed weights only applied to their log values.
|
* with fixed weights only applied to their log values.
|
||||||
*
|
*
|
||||||
* This algorithm gives little chance for an old video to have a good score,
|
* 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
|
* 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
|
* justify a better score. However there are multiple ways to achieve that
|
||||||
* goal, which is left for later. Yes, this is a TODO :)
|
* goal, which is left for later. Yes, this is a TODO :)
|
||||||
*
|
*
|
||||||
* note: weights and base score are in number of half-days.
|
* 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
|
* see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58
|
||||||
*/
|
*/
|
||||||
const weights = {
|
const weights = {
|
||||||
like: 3,
|
like: 3,
|
||||||
dislike: 3,
|
dislike: 3,
|
||||||
view: 1 / 12,
|
view: 1 / 12,
|
||||||
comment: 2 // a comment takes more time than a like to do, but can be done multiple times
|
comment: 2 // a comment takes more time than a like to do, but can be done multiple times
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))) * ${weights.comment} ` + // comments (+)
|
||||||
|
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
|
||||||
|
'AS "score"'
|
||||||
|
)
|
||||||
|
|
||||||
|
group = 'GROUP BY "video"."id"'
|
||||||
}
|
}
|
||||||
|
|
||||||
cte.push( // TODO: exclude blocklisted comments
|
|
||||||
'"totalCommentsWithoutVideoAuthor" AS (' +
|
|
||||||
'SELECT "video"."id", ' +
|
|
||||||
'COUNT("replies"."id") - (' +
|
|
||||||
'SELECT COUNT("authorReplies"."id") ' +
|
|
||||||
'FROM "videoComment" AS "authorReplies" ' +
|
|
||||||
'LEFT JOIN "account" ON "account"."id" = "authorReplies"."accountId" ' +
|
|
||||||
'LEFT JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
|
|
||||||
'WHERE "video"."channelId" = "videoChannel"."id" ' +
|
|
||||||
') as "value" ' +
|
|
||||||
'FROM "videoComment" AS "replies" ' +
|
|
||||||
'LEFT JOIN "video" ON "video"."id" = "replies"."videoId" ' +
|
|
||||||
'WHERE "replies"."videoId" = "video"."id" ' +
|
|
||||||
'GROUP BY "video"."id"' +
|
|
||||||
')'
|
|
||||||
)
|
|
||||||
|
|
||||||
joins.push('LEFT JOIN "totalCommentsWithoutVideoAuthor" ON "video"."id" = "totalCommentsWithoutVideoAuthor"."id"')
|
|
||||||
|
|
||||||
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, "totalCommentsWithoutVideoAuthor"."value")) * ${weights.comment} ` + // comments (+)
|
|
||||||
'+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
|
|
||||||
'AS "score"'
|
|
||||||
)
|
|
||||||
|
|
||||||
group = 'GROUP BY "video"."id", "totalCommentsWithoutVideoAuthor"."value"'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.historyOfUser) {
|
if (options.historyOfUser) {
|
||||||
|
|
|
@ -41,8 +41,16 @@ describe('Test config API validators', function () {
|
||||||
categories: [ 1, 2 ],
|
categories: [ 1, 2 ],
|
||||||
|
|
||||||
isNSFW: true,
|
isNSFW: true,
|
||||||
defaultClientRoute: '/videos/recently-added',
|
|
||||||
defaultNSFWPolicy: 'blur',
|
defaultNSFWPolicy: 'blur',
|
||||||
|
|
||||||
|
defaultClientRoute: '/videos/recently-added',
|
||||||
|
defaultTrendingRoute: '/videos/trending',
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: 'alert("coucou")',
|
javascript: 'alert("coucou")',
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
|
|
|
@ -272,9 +272,17 @@ describe('Test config', function () {
|
||||||
languages: [ 'en', 'es' ],
|
languages: [ 'en', 'es' ],
|
||||||
categories: [ 1, 2 ],
|
categories: [ 1, 2 ],
|
||||||
|
|
||||||
defaultClientRoute: '/videos/recently-added',
|
|
||||||
isNSFW: true,
|
isNSFW: true,
|
||||||
defaultNSFWPolicy: 'blur' as 'blur',
|
defaultNSFWPolicy: 'blur' as 'blur',
|
||||||
|
|
||||||
|
defaultClientRoute: '/videos/recently-added',
|
||||||
|
defaultTrendingRoute: '/videos/trending',
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: 'alert("coucou")',
|
javascript: 'alert("coucou")',
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
|
|
|
@ -308,8 +308,8 @@ describe('Test a client controllers', function () {
|
||||||
shortDescription: 'my short description',
|
shortDescription: 'my short description',
|
||||||
description: 'my super description',
|
description: 'my super description',
|
||||||
terms: 'my super terms',
|
terms: 'my super terms',
|
||||||
defaultClientRoute: '/videos/recently-added',
|
|
||||||
defaultNSFWPolicy: 'blur',
|
defaultNSFWPolicy: 'blur',
|
||||||
|
defaultClientRoute: '/videos/recently-added',
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: 'alert("coucou")',
|
javascript: 'alert("coucou")',
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
|
|
|
@ -65,9 +65,17 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
|
||||||
languages: [ 'en', 'es' ],
|
languages: [ 'en', 'es' ],
|
||||||
categories: [ 1, 2 ],
|
categories: [ 1, 2 ],
|
||||||
|
|
||||||
defaultClientRoute: '/videos/recently-added',
|
|
||||||
isNSFW: true,
|
isNSFW: true,
|
||||||
defaultNSFWPolicy: 'blur',
|
defaultNSFWPolicy: 'blur',
|
||||||
|
|
||||||
|
defaultClientRoute: '/videos/recently-added',
|
||||||
|
defaultTrendingRoute: '/videos/trending',
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: 'alert("coucou")',
|
javascript: 'alert("coucou")',
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
|
|
|
@ -30,8 +30,16 @@ export interface CustomConfig {
|
||||||
categories: number[]
|
categories: number[]
|
||||||
|
|
||||||
isNSFW: boolean
|
isNSFW: boolean
|
||||||
defaultClientRoute: string
|
|
||||||
defaultNSFWPolicy: NSFWPolicyType
|
defaultNSFWPolicy: NSFWPolicyType
|
||||||
|
|
||||||
|
defaultClientRoute: string
|
||||||
|
defaultTrendingRoute: string
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript?: string
|
javascript?: string
|
||||||
css?: string
|
css?: string
|
||||||
|
|
|
@ -36,9 +36,15 @@ export interface ServerConfig {
|
||||||
instance: {
|
instance: {
|
||||||
name: string
|
name: string
|
||||||
shortDescription: string
|
shortDescription: string
|
||||||
defaultClientRoute: string
|
|
||||||
isNSFW: boolean
|
isNSFW: boolean
|
||||||
defaultNSFWPolicy: NSFWPolicyType
|
defaultNSFWPolicy: NSFWPolicyType
|
||||||
|
defaultClientRoute: string
|
||||||
|
defaultTrendingRoute: string
|
||||||
|
pages: {
|
||||||
|
hot: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: string
|
javascript: string
|
||||||
css: string
|
css: string
|
||||||
|
|
Loading…
Reference in New Issue