mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-09 17:33:08 +03:00
Add new name/terms/description config options
This commit is contained in:
parent
81ebea48bf
commit
66b16cafb3
@ -2,6 +2,41 @@
|
||||
|
||||
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
|
||||
<div class="inner-form-title">Instance</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="instanceName">Name</label>
|
||||
<input
|
||||
type="text" id="instanceName"
|
||||
formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.instanceName" class="form-error">
|
||||
{{ formErrors.instanceName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="instanceDescription">Description (markdown)</label>
|
||||
<my-markdown-textarea
|
||||
id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
|
||||
[classes]="{ 'input-error': formErrors['instanceDescription'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instanceDescription" class="form-error">
|
||||
{{ formErrors.instanceDescription }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="instanceTerms">Terms (markdown)</label>
|
||||
<my-markdown-textarea
|
||||
id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
|
||||
[ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instanceTerms" class="form-error">
|
||||
{{ formErrors.instanceTerms }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-form-title">Cache</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -4,7 +4,13 @@ import { Router } from '@angular/router'
|
||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
||||
import { ServerService } from '@app/core/server/server.service'
|
||||
import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
|
||||
import { ADMIN_EMAIL, CACHE_PREVIEWS_SIZE, SIGNUP_LIMIT, TRANSCODING_THREADS } from '@app/shared/forms/form-validators/custom-config'
|
||||
import {
|
||||
ADMIN_EMAIL,
|
||||
CACHE_PREVIEWS_SIZE,
|
||||
INSTANCE_NAME,
|
||||
SIGNUP_LIMIT,
|
||||
TRANSCODING_THREADS
|
||||
} from '@app/shared/forms/form-validators/custom-config'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
|
||||
|
||||
@ -36,6 +42,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
|
||||
form: FormGroup
|
||||
formErrors = {
|
||||
instanceName: '',
|
||||
instanceDescription: '',
|
||||
instanceTerms: '',
|
||||
cachePreviewsSize: '',
|
||||
signupLimit: '',
|
||||
adminEmail: '',
|
||||
@ -43,6 +52,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
transcodingThreads: ''
|
||||
}
|
||||
validationMessages = {
|
||||
instanceName: INSTANCE_NAME.MESSAGES,
|
||||
cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
|
||||
signupLimit: SIGNUP_LIMIT.MESSAGES,
|
||||
adminEmail: ADMIN_EMAIL.MESSAGES,
|
||||
@ -65,6 +75,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
|
||||
buildForm () {
|
||||
const formGroupData = {
|
||||
instanceName: [ '', INSTANCE_NAME.VALIDATORS ],
|
||||
instanceDescription: [ '' ],
|
||||
instanceTerms: [ '' ],
|
||||
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
|
||||
signupEnabled: [ ],
|
||||
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
|
||||
@ -109,6 +122,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
|
||||
formValidated () {
|
||||
const data = {
|
||||
instance: {
|
||||
name: this.form.value['instanceName'],
|
||||
description: this.form.value['instanceDescription'],
|
||||
terms: this.form.value['instanceTerms']
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: this.form.value['cachePreviewsSize']
|
||||
@ -146,6 +164,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
this.serverService.loadConfig()
|
||||
|
||||
this.updateForm()
|
||||
|
||||
this.notificationsService.success('Success', 'Configuration updated.')
|
||||
},
|
||||
|
||||
err => this.notificationsService.error('Error', err.message)
|
||||
@ -154,6 +174,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
|
||||
private updateForm () {
|
||||
const data = {
|
||||
instanceName: this.customConfig.instance.name,
|
||||
instanceDescription: this.customConfig.instance.description,
|
||||
instanceTerms: this.customConfig.instance.terms,
|
||||
cachePreviewsSize: this.customConfig.cache.previews.size,
|
||||
signupEnabled: this.customConfig.signup.enabled,
|
||||
signupLimit: this.customConfig.signup.limit,
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { Validators } from '@angular/forms'
|
||||
|
||||
export const INSTANCE_NAME = {
|
||||
VALIDATORS: [ Validators.required ],
|
||||
MESSAGES: {
|
||||
'required': 'Instance name is required.',
|
||||
}
|
||||
}
|
||||
|
||||
export const CACHE_PREVIEWS_SIZE = {
|
||||
VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
|
||||
MESSAGES: {
|
||||
|
12
client/src/app/shared/forms/markdown-textarea.component.html
Normal file
12
client/src/app/shared/forms/markdown-textarea.component.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="root" [ngStyle]="{ 'flex-direction': flexDirection }">
|
||||
<textarea
|
||||
[(ngModel)]="description" (ngModelChange)="onModelChange()"
|
||||
[ngClass]="classes" [ngStyle]="{ width: textareaWidth, height: textareaHeight, 'margin-right': textareaMarginRight }"
|
||||
id="description" name="description">
|
||||
</textarea>
|
||||
|
||||
<tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews">
|
||||
<tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
|
||||
<tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
|
||||
</tabset>
|
||||
</div>
|
27
client/src/app/shared/forms/markdown-textarea.component.scss
Normal file
27
client/src/app/shared/forms/markdown-textarea.component.scss
Normal file
@ -0,0 +1,27 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 150px);
|
||||
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/deep/ {
|
||||
.nav-link {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 30px !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 75px;
|
||||
padding: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,25 +3,33 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/distinctUntilChanged'
|
||||
import { isInMobileView } from '@app/shared/misc/utils'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
import { MarkdownService } from '../../shared'
|
||||
import truncate from 'lodash-es/truncate'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-description',
|
||||
templateUrl: './video-description.component.html',
|
||||
styleUrls: [ './video-description.component.scss' ],
|
||||
selector: 'my-markdown-textarea',
|
||||
templateUrl: './markdown-textarea.component.html',
|
||||
styleUrls: [ './markdown-textarea.component.scss' ],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => VideoDescriptionComponent),
|
||||
useExisting: forwardRef(() => MarkdownTextareaComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
|
||||
export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
||||
@Input() description = ''
|
||||
@Input() classes: string[] = []
|
||||
@Input() textareaWidth = '100%'
|
||||
@Input() textareaHeight = '150px'
|
||||
@Input() previewColumn = false
|
||||
@Input() truncate: number
|
||||
|
||||
textareaMarginRight = '0'
|
||||
flexDirection = 'column'
|
||||
truncatedDescriptionHTML = ''
|
||||
descriptionHTML = ''
|
||||
|
||||
@ -36,6 +44,11 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
|
||||
.subscribe(() => this.updateDescriptionPreviews())
|
||||
|
||||
this.descriptionChanged.next(this.description)
|
||||
|
||||
if (this.previewColumn) {
|
||||
this.flexDirection = 'row'
|
||||
this.textareaMarginRight = '15px'
|
||||
}
|
||||
}
|
||||
|
||||
propagateChange = (_: any) => { /* empty */ }
|
||||
@ -65,9 +78,9 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit {
|
||||
}
|
||||
|
||||
private updateDescriptionPreviews () {
|
||||
if (!this.description) return
|
||||
if (this.description === null || this.description === undefined) return
|
||||
|
||||
this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 }))
|
||||
this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: this.truncate }))
|
||||
this.descriptionHTML = this.markdownService.markdownToHTML(this.description)
|
||||
}
|
||||
}
|
@ -3,10 +3,13 @@ import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||
|
||||
import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
|
||||
import { ModalModule } from 'ngx-bootstrap/modal'
|
||||
import { TabsModule } from 'ngx-bootstrap/tabs'
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll'
|
||||
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
||||
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
||||
@ -40,7 +43,8 @@ import { VideoService } from './video/video.service'
|
||||
|
||||
PrimeSharedModule,
|
||||
InfiniteScrollModule,
|
||||
NgPipesModule
|
||||
NgPipesModule,
|
||||
TabsModule.forRoot()
|
||||
],
|
||||
|
||||
declarations: [
|
||||
@ -50,7 +54,8 @@ import { VideoService } from './video/video.service'
|
||||
DeleteButtonComponent,
|
||||
EditButtonComponent,
|
||||
NumberFormatterPipe,
|
||||
FromNowPipe
|
||||
FromNowPipe,
|
||||
MarkdownTextareaComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
@ -74,6 +79,7 @@ import { VideoService } from './video/video.service'
|
||||
VideoMiniatureComponent,
|
||||
DeleteButtonComponent,
|
||||
EditButtonComponent,
|
||||
MarkdownTextareaComponent,
|
||||
|
||||
NumberFormatterPipe,
|
||||
FromNowPipe
|
||||
@ -86,7 +92,8 @@ import { VideoService } from './video/video.service'
|
||||
VideoAbuseService,
|
||||
VideoBlacklistService,
|
||||
UserService,
|
||||
VideoService
|
||||
VideoService,
|
||||
MarkdownService
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
@ -42,10 +42,10 @@ export class VideoService {
|
||||
}
|
||||
|
||||
updateVideo (video: VideoEdit) {
|
||||
const language = video.language || undefined
|
||||
const licence = video.licence || undefined
|
||||
const category = video.category || undefined
|
||||
const description = video.description || undefined
|
||||
const language = video.language || null
|
||||
const licence = video.licence || null
|
||||
const category = video.category || null
|
||||
const description = video.description || null
|
||||
|
||||
const body: VideoUpdate = {
|
||||
name: video.name,
|
||||
|
@ -1,9 +0,0 @@
|
||||
<textarea
|
||||
[(ngModel)]="description" (ngModelChange)="onModelChange()"
|
||||
id="description" name="description">
|
||||
</textarea>
|
||||
|
||||
<tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews">
|
||||
<tab heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
|
||||
<tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
|
||||
</tabset>
|
@ -1,24 +0,0 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 150px);
|
||||
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/deep/ {
|
||||
.nav-link {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 30px !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 75px;
|
||||
padding: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,14 @@
|
||||
<div class="form-group">
|
||||
<label class="label-tags">Tags</label> <span>(press Enter to add)</span>
|
||||
<tag-input
|
||||
[ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
|
||||
formControlName="tags" maxItems="5" modelAsStrings="true"
|
||||
[ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
|
||||
formControlName="tags" maxItems="5" modelAsStrings="true"
|
||||
></tag-input>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<my-video-description formControlName="description"></my-video-description>
|
||||
<my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.description" class="form-error">
|
||||
{{ formErrors.description }}
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { TagInputModule } from 'ngx-chips'
|
||||
import { TabsModule } from 'ngx-bootstrap/tabs'
|
||||
|
||||
import { MarkdownService } from '../../shared'
|
||||
import { TagInputModule } from 'ngx-chips'
|
||||
import { SharedModule } from '../../../shared'
|
||||
import { VideoDescriptionComponent } from './video-description.component'
|
||||
import { VideoEditComponent } from './video-edit.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
TagInputModule,
|
||||
TabsModule.forRoot(),
|
||||
|
||||
SharedModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
VideoDescriptionComponent,
|
||||
VideoEditComponent
|
||||
],
|
||||
|
||||
@ -25,12 +19,9 @@ import { VideoEditComponent } from './video-edit.component'
|
||||
TagInputModule,
|
||||
TabsModule,
|
||||
|
||||
VideoDescriptionComponent,
|
||||
VideoEditComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
MarkdownService
|
||||
]
|
||||
providers: []
|
||||
})
|
||||
export class VideoEditModule { }
|
||||
|
@ -14,6 +14,17 @@ export class MarkdownService {
|
||||
.enable('link')
|
||||
.enable('newline')
|
||||
|
||||
this.setTargetToLinks()
|
||||
}
|
||||
|
||||
markdownToHTML (markdown: string) {
|
||||
const html = this.markdownIt.render(markdown)
|
||||
|
||||
// Avoid linkify truncated links
|
||||
return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...')
|
||||
}
|
||||
|
||||
private setTargetToLinks () {
|
||||
// Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
|
||||
const defaultRender = this.markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options)
|
||||
@ -33,11 +44,4 @@ export class MarkdownService {
|
||||
return defaultRender(tokens, idx, options, env, self)
|
||||
}
|
||||
}
|
||||
|
||||
markdownToHTML (markdown: string) {
|
||||
const html = this.markdownIt.render(markdown)
|
||||
|
||||
// Avoid linkify truncated links
|
||||
return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...')
|
||||
}
|
||||
}
|
||||
|
@ -69,3 +69,8 @@ transcoding:
|
||||
480p: true
|
||||
720p: true
|
||||
1080p: true
|
||||
|
||||
instance:
|
||||
name: 'PeerTube'
|
||||
description: '' # Support markdown
|
||||
terms: '' # Support markdown
|
||||
|
@ -69,3 +69,8 @@ transcoding:
|
||||
480p: true
|
||||
720p: true
|
||||
1080p: true
|
||||
|
||||
instance:
|
||||
name: 'PeerTube'
|
||||
description: '' # Support markdown
|
||||
terms: '' # Support markdown
|
||||
|
@ -105,6 +105,11 @@ export {
|
||||
|
||||
function customConfig (): CustomConfig {
|
||||
return {
|
||||
instance: {
|
||||
name: CONFIG.INSTANCE.NAME,
|
||||
description: CONFIG.INSTANCE.DESCRIPTION,
|
||||
terms: CONFIG.INSTANCE.TERMS
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CONFIG.CACHE.PREVIEWS.SIZE
|
||||
|
@ -23,7 +23,8 @@ function checkMissedConfig () {
|
||||
'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password',
|
||||
'storage.videos', 'storage.logs', 'storage.thumbnails', 'storage.previews', 'storage.torrents', 'storage.cache', 'log.level',
|
||||
'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
|
||||
'user.video_quota', 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address'
|
||||
'user.video_quota', 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
|
||||
'instance.name', 'instance.description', 'instance.terms'
|
||||
]
|
||||
const miss: string[] = []
|
||||
|
||||
|
@ -154,6 +154,11 @@ const CONFIG = {
|
||||
PREVIEWS: {
|
||||
get SIZE () { return config.get<number>('cache.previews.size') }
|
||||
}
|
||||
},
|
||||
INSTANCE: {
|
||||
get NAME () { return config.get<string>('instance.name') },
|
||||
get DESCRIPTION () { return config.get<string>('instance.description') },
|
||||
get TERMS () { return config.get<string>('instance.terms') }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,11 @@ describe('Test config API validators', function () {
|
||||
let server: ServerInfo
|
||||
let userAccessToken: string
|
||||
const updateParams: CustomConfig = {
|
||||
instance: {
|
||||
name: 'PeerTube updated',
|
||||
description: 'my super description',
|
||||
terms: 'my super terms'
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
|
@ -49,6 +49,9 @@ describe('Test config', function () {
|
||||
const res = await getCustomConfig(server.url, server.accessToken)
|
||||
const data = res.body
|
||||
|
||||
expect(data.instance.name).to.equal('PeerTube')
|
||||
expect(data.instance.description).to.be.empty
|
||||
expect(data.instance.terms).to.be.empty
|
||||
expect(data.cache.previews.size).to.equal(1)
|
||||
expect(data.signup.enabled).to.be.true
|
||||
expect(data.signup.limit).to.equal(4)
|
||||
@ -65,6 +68,11 @@ describe('Test config', function () {
|
||||
|
||||
it('Should update the customized configuration', async function () {
|
||||
const newCustomConfig = {
|
||||
instance: {
|
||||
name: 'PeerTube updated',
|
||||
description: 'my super description',
|
||||
terms: 'my super terms'
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
@ -97,7 +105,9 @@ describe('Test config', function () {
|
||||
const res = await getCustomConfig(server.url, server.accessToken)
|
||||
const data = res.body
|
||||
|
||||
expect(data.cache.previews.size).to.equal(2)
|
||||
expect(data.instance.name).to.equal('PeerTube updated')
|
||||
expect(data.instance.description).to.equal('my super description')
|
||||
expect(data.instance.terms).to.equal('my super terms')
|
||||
expect(data.signup.enabled).to.be.false
|
||||
expect(data.signup.limit).to.equal(5)
|
||||
expect(data.admin.email).to.equal('superadmin1@example.com')
|
||||
|
@ -373,7 +373,7 @@ async function completeVideoCheck (
|
||||
expect(dateIsValid(video.createdAt)).to.be.true
|
||||
expect(dateIsValid(video.updatedAt)).to.be.true
|
||||
|
||||
const res = await getVideo(url, video.id)
|
||||
const res = await getVideo(url, video.uuid)
|
||||
const videoDetails = res.body
|
||||
|
||||
expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
|
||||
|
@ -1,4 +1,10 @@
|
||||
export interface CustomConfig {
|
||||
instance: {
|
||||
name: string
|
||||
description: string
|
||||
terms: string
|
||||
}
|
||||
|
||||
cache: {
|
||||
previews: {
|
||||
size: number
|
||||
|
Loading…
Reference in New Issue
Block a user