Add ability to set a banner to the instance
This commit is contained in:
parent
1c0270ca8a
commit
7ee0efb57a
|
@ -1,28 +1,30 @@
|
|||
<div class="row">
|
||||
<h1 class="visually-hidden" i18n>Follows</h1>
|
||||
<div class="margin-content mt-4">
|
||||
<div class="row">
|
||||
<h1 class="visually-hidden" i18n>Follows</h1>
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Followers of {{ instanceName }} ({{ followersPagination.totalItems }})</h2>
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Followers of {{ instanceName }} ({{ followersPagination.totalItems }})</h2>
|
||||
|
||||
<div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">{{ instanceName }} does not have followers.</div>
|
||||
<div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">{{ instanceName }} does not have followers.</div>
|
||||
|
||||
<a *ngFor="let follower of followers" [href]="follower.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ follower.name }}
|
||||
</a>
|
||||
<a *ngFor="let follower of followers" [href]="follower.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ follower.name }}
|
||||
</a>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Subscriptions of {{ instanceName }} ({{ followingsPagination.totalItems }})</h2>
|
||||
|
||||
<div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">{{ instanceName }} does not have subscriptions.</div>
|
||||
|
||||
<a *ngFor="let following of followings" [href]="following.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ following.name }}
|
||||
</a>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
|
||||
</div>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Subscriptions of {{ instanceName }} ({{ followingsPagination.totalItems }})</h2>
|
||||
|
||||
<div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">{{ instanceName }} does not have subscriptions.</div>
|
||||
|
||||
<a *ngFor="let following of followings" [href]="following.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ following.name }}
|
||||
</a>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,226 +1,232 @@
|
|||
<div class="row">
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<div class="banner" *ngIf="instanceBannerUrl">
|
||||
<img [src]="instanceBannerUrl" alt="Instance banner">
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<h1 i18n class="fw-semibold fs-5">About {{ instanceName }}</h1>
|
||||
<div class="margin-content mt-4">
|
||||
<div class="row ">
|
||||
<div class="col-md-12 col-xl-6">
|
||||
|
||||
<a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="peertube-button-link orange-button h-100 d-flex align-items-center">Contact us</a>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h1 i18n class="fw-semibold fs-5">About {{ instanceName }}</h1>
|
||||
|
||||
<div class="mb-4" *ngIf="categories.length !== 0 || languages.length !== 0">
|
||||
<span *ngFor="let category of categories" class="pt-badge badge-primary">{{ category }}</span>
|
||||
<a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="peertube-button-link orange-button h-100 d-flex align-items-center">Contact us</a>
|
||||
</div>
|
||||
|
||||
<span *ngFor="let language of languages" class="pt-badge badge-secondary">{{ language }}</span>
|
||||
</div>
|
||||
<div class="mb-4" *ngIf="categories.length !== 0 || languages.length !== 0">
|
||||
<span *ngFor="let category of categories" class="pt-badge badge-primary">{{ category }}</span>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="block">{{ shortDescription }}</div>
|
||||
<span *ngFor="let language of languages" class="pt-badge badge-secondary">{{ language }}</span>
|
||||
</div>
|
||||
|
||||
<div i18n *ngIf="isNSFW" class="block mt-4 fw-semibold">This instance is dedicated to sensitive/NSFW content.</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="block">{{ shortDescription }}</div>
|
||||
|
||||
<div class="anchor" id="administrators-and-sustainability"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="administrators-and-sustainability"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)"
|
||||
>
|
||||
<h2 i18n class="middle-title">
|
||||
ADMINISTRATORS & SUSTAINABILITY
|
||||
</h2>
|
||||
</a>
|
||||
<div i18n *ngIf="isNSFW" class="block mt-4 fw-semibold">This instance is dedicated to sensitive/NSFW content.</div>
|
||||
</div>
|
||||
|
||||
<div class="block administrator" *ngIf="aboutHTML.administrator">
|
||||
<div class="anchor" id="administrators"></div>
|
||||
<div class="anchor" id="administrators-and-sustainability"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="administrators"
|
||||
fragment="administrators-and-sustainability"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Who we are</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.administrator"></div>
|
||||
</div>
|
||||
|
||||
<div class="block creation-reason" *ngIf="aboutHTML.creationReason">
|
||||
<div class="anchor" id="creation-reason"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="creation-reason"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Why we created this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.creationReason"></div>
|
||||
</div>
|
||||
|
||||
<div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime">
|
||||
<div class="anchor" id="maintenance-lifetime"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="maintenance-lifetime"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">How long we plan to maintain this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.maintenanceLifetime"></div>
|
||||
</div>
|
||||
|
||||
<div class="block business-model" *ngIf="aboutHTML.businessModel">
|
||||
<div class="anchor" id="business-model"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="business-model"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.businessModel"></div>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="information"></div>
|
||||
<a
|
||||
*ngIf="descriptionElement"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block description">
|
||||
<div class="anchor" id="description"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="description"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Description</h3>
|
||||
</a>
|
||||
|
||||
<my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-moderation">
|
||||
<div class="anchor" id="moderation"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
(click)="onClickCopyLink(anchorLink)"
|
||||
>
|
||||
<h2 i18n class="middle-title">
|
||||
MODERATION
|
||||
ADMINISTRATORS & SUSTAINABILITY
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block moderation-information" *ngIf="aboutHTML.moderationInformation">
|
||||
<div class="anchor" id="moderation-information"></div>
|
||||
<div class="block administrator" *ngIf="aboutHTML.administrator">
|
||||
<div class="anchor" id="administrators"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation-information"
|
||||
fragment="administrators"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Moderation information</h3>
|
||||
<h3 i18n class="section-title">Who we are</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.moderationInformation"></div>
|
||||
<div [innerHTML]="aboutHTML.administrator"></div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct">
|
||||
<div class="anchor" id="code-of-conduct"></div>
|
||||
<div class="block creation-reason" *ngIf="aboutHTML.creationReason">
|
||||
<div class="anchor" id="creation-reason"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="code-of-conduct"
|
||||
fragment="creation-reason"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Code of conduct</h3>
|
||||
<h3 i18n class="section-title">Why we created this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.codeOfConduct"></div>
|
||||
<div [innerHTML]="aboutHTML.creationReason"></div>
|
||||
</div>
|
||||
|
||||
<div class="block terms">
|
||||
<div class="anchor" id="terms"></div>
|
||||
<div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime">
|
||||
<div class="anchor" id="maintenance-lifetime"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="terms"
|
||||
fragment="maintenance-lifetime"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Terms</h3>
|
||||
<h3 i18n class="section-title">How long we plan to maintain this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.terms"></div>
|
||||
<div [innerHTML]="aboutHTML.maintenanceLifetime"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<div class="anchor" id="other-information"></div>
|
||||
<div class="block business-model" *ngIf="aboutHTML.businessModel">
|
||||
<div class="anchor" id="business-model"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="business-model"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.businessModel"></div>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="information"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.hardwareInformation"
|
||||
*ngIf="descriptionElement"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="other-information"
|
||||
fragment="information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
OTHER INFORMATION
|
||||
INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation">
|
||||
<div class="anchor" id="hardware-information"></div>
|
||||
<div class="block description">
|
||||
<div class="anchor" id="description"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="hardware-information"
|
||||
fragment="description"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Hardware information</h3>
|
||||
<h3 i18n class="section-title">Description</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.hardwareInformation"></div>
|
||||
<my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-moderation">
|
||||
<div class="anchor" id="moderation"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
MODERATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block moderation-information" *ngIf="aboutHTML.moderationInformation">
|
||||
<div class="anchor" id="moderation-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Moderation information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.moderationInformation"></div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct">
|
||||
<div class="anchor" id="code-of-conduct"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="code-of-conduct"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Code of conduct</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.codeOfConduct"></div>
|
||||
</div>
|
||||
|
||||
<div class="block terms">
|
||||
<div class="anchor" id="terms"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="terms"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Terms</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.terms"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<div class="anchor" id="other-information"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.hardwareInformation"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="other-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
OTHER INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation">
|
||||
<div class="anchor" id="hardware-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="hardware-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Hardware information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.hardwareInformation"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
|
||||
<h2 class="visually-hidden" i18n>FEATURES</h2>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
<div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
|
||||
<h2 class="visually-hidden" i18n>FEATURES</h2>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
|
||||
<div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
|
||||
<div class="anchor" id="statistics"></div>
|
||||
<div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
|
||||
<div class="anchor" id="statistics"></div>
|
||||
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="statistics"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">STATISTICS</h2>
|
||||
</a>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="statistics"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">STATISTICS</h2>
|
||||
</a>
|
||||
|
||||
<my-instance-statistics [serverStats]="serverStats"></my-instance-statistics>
|
||||
<my-instance-statistics [serverStats]="serverStats"></my-instance-statistics>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked {
|
|||
aboutHTML: AboutHTML
|
||||
descriptionElement: HTMLDivElement
|
||||
|
||||
instanceBannerUrl: string
|
||||
|
||||
languages: string[] = []
|
||||
categories: string[] = []
|
||||
shortDescription = ''
|
||||
|
@ -64,6 +66,10 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked {
|
|||
|
||||
this.shortDescription = about.instance.shortDescription
|
||||
|
||||
this.instanceBannerUrl = about.instance.banners.length !== 0
|
||||
? about.instance.banners[0].path
|
||||
: undefined
|
||||
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
this.route.data.subscribe(data => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="root">
|
||||
<div class="margin-content mt-4">
|
||||
<h1 i18n class="fs-3 text-center fw-semibold mb-3">
|
||||
This website is powered by PeerTube
|
||||
</h1>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.root {
|
||||
.margin-content {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div>
|
||||
<div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
|
||||
<div class="sub-menu mb-0" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
|
||||
<a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="sub-menu-entry">Instance</a>
|
||||
|
||||
<a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="sub-menu-entry">PeerTube</a>
|
||||
|
@ -7,7 +7,7 @@
|
|||
<a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="sub-menu-entry">Network</a>
|
||||
</div>
|
||||
|
||||
<div class="margin-content" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
|
||||
<div [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="root">
|
||||
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
|
||||
|
||||
<div class="margin-content" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
|
||||
<div class="margin-content" [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@use '_mixins' as *;
|
||||
|
||||
$form-base-input-width: 340px;
|
||||
$form-max-width: 500px;
|
||||
|
||||
form {
|
||||
padding-bottom: 1.5rem;
|
||||
|
@ -9,7 +10,7 @@ form {
|
|||
|
||||
my-markdown-textarea {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
||||
.homepage my-markdown-textarea {
|
||||
|
@ -156,3 +157,7 @@ my-user-real-quota-info {
|
|||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
<my-actor-banner-edit
|
||||
[previewImage]="false" class="d-block mb-4"
|
||||
[bannerUrl]="instanceBannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceName">Name</label>
|
||||
|
||||
|
|
|
@ -1,25 +1,80 @@
|
|||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
import { Notifier } from '@app/core'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import { InstanceService } from '@app/shared/shared-instance'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-instance-information',
|
||||
templateUrl: './edit-instance-information.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ]
|
||||
})
|
||||
export class EditInstanceInformationComponent {
|
||||
export class EditInstanceInformationComponent implements OnInit {
|
||||
@Input() form: FormGroup
|
||||
@Input() formErrors: any
|
||||
|
||||
@Input() languageItems: SelectOptionsItem[] = []
|
||||
@Input() categoryItems: SelectOptionsItem[] = []
|
||||
|
||||
constructor (private customMarkup: CustomMarkupService) {
|
||||
instanceBannerUrl: string
|
||||
|
||||
constructor (
|
||||
private customMarkup: CustomMarkupService,
|
||||
private configService: ConfigService,
|
||||
private notifier: Notifier,
|
||||
private instance: InstanceService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.resetBannerUrl()
|
||||
}
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
return this.customMarkup.getCustomMarkdownRenderer()
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.configService.updateInstanceBanner(formData)
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
|
||||
this.resetBannerUrl()
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
||||
})
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.configService.deleteInstanceBanner()
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
|
||||
this.resetBannerUrl()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private resetBannerUrl () {
|
||||
this.instance.getAbout()
|
||||
.subscribe(about => {
|
||||
const banners = about.instance.banners
|
||||
if (banners.length === 0) {
|
||||
this.instanceBannerUrl = undefined
|
||||
return
|
||||
}
|
||||
|
||||
this.instanceBannerUrl = banners[0].path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,4 +67,20 @@ export class ConfigService {
|
|||
return this.authHttp.put<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom', data)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateInstanceBanner (formData: FormData) {
|
||||
const url = ConfigService.BASE_APPLICATION_URL + '/instance-banner/pick'
|
||||
|
||||
return this.authHttp.post(url, formData)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
deleteInstanceBanner () {
|
||||
const url = ConfigService.BASE_APPLICATION_URL + '/instance-banner'
|
||||
|
||||
return this.authHttp.delete(url)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="root pt-4 margin-content">
|
||||
<div class="margin-content">
|
||||
<my-custom-markup-container [content]="homepageContent"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.margin-content {
|
||||
padding-top: 2rem;
|
||||
|
||||
::ng-deep .revert-home-padding-top {
|
||||
margin-top: -2rem;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
|||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
|
||||
|
||||
@Component({
|
||||
templateUrl: './home.component.html'
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: [ './home.component.scss' ]
|
||||
})
|
||||
|
||||
export class HomeComponent implements OnInit {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div class="col-12 col-lg-8 col-xl-9">
|
||||
<my-actor-banner-edit
|
||||
*ngIf="videoChannel" [previewImage]="isCreation()" class="d-block mb-4"
|
||||
[actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
[bannerUrl]="videoChannel?.bannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
|
|
|
@ -178,14 +178,6 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
|
|||
})
|
||||
}
|
||||
|
||||
get maxAvatarSize () {
|
||||
return this.serverConfig.avatar.file.size.max
|
||||
}
|
||||
|
||||
get avatarExtensions () {
|
||||
return this.serverConfig.avatar.file.extensions.join(',')
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="root">
|
||||
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
|
||||
|
||||
<div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
|
||||
<div class="margin-content pb-5" [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="root">
|
||||
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
|
||||
|
||||
<div class="margin-content pb-5" [ngClass]="{ 'offset-content': !isBroadcastMessageDisplayed }">
|
||||
<div class="margin-content pb-5" [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
--myGreyOwnerFontSize: 14px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
@include block-ratio('img', $banner-inverted-ratio);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
@include section-label-responsive;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="actor" *ngIf="actor">
|
||||
<div class="actor">
|
||||
<div class="actor-img-edit-container">
|
||||
<div class="banner-placeholder">
|
||||
<img *ngIf="hasBanner()" [src]="preview || actor.bannerUrl" alt="Banner" />
|
||||
<img *ngIf="hasBanner()" [src]="preview || bannerUrl" alt="Banner" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasBanner()" class="actor-img-edit-button button-focus-within" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { SafeResourceUrl } from '@angular/platform-browser'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { VideoChannel } from '@app/shared/shared-main'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
import { imageToDataURL } from '@root-helpers/images'
|
||||
|
@ -18,7 +17,7 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
@ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
|
||||
@ViewChild('bannerPopover') bannerPopover: NgbPopover
|
||||
|
||||
@Input() actor: VideoChannel
|
||||
@Input() bannerUrl: string
|
||||
@Input() previewImage = false
|
||||
|
||||
@Output() bannerChange = new EventEmitter<FormData>()
|
||||
|
@ -69,6 +68,6 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
}
|
||||
|
||||
hasBanner () {
|
||||
return !!this.preview || !!this.actor.bannerUrl
|
||||
return !!this.preview || !!this.bannerUrl
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
ChannelMiniatureMarkupData,
|
||||
ContainerMarkupData,
|
||||
EmbedMarkupData,
|
||||
InstanceBannerMarkupData,
|
||||
PlaylistMiniatureMarkupData,
|
||||
VideoMiniatureMarkupData,
|
||||
VideosListMarkupData
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
ChannelMiniatureMarkupComponent,
|
||||
EmbedMarkupComponent,
|
||||
InstanceBannerMarkupComponent,
|
||||
PlaylistMiniatureMarkupComponent,
|
||||
VideoMiniatureMarkupComponent,
|
||||
VideosListMarkupComponent
|
||||
|
@ -28,6 +30,7 @@ type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
|
|||
@Injectable()
|
||||
export class CustomMarkupService {
|
||||
private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
|
||||
'peertube-instance-banner': el => this.instanceBannerBuilder(el),
|
||||
'peertube-button': el => this.buttonBuilder(el),
|
||||
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
||||
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
||||
|
@ -160,6 +163,19 @@ export class CustomMarkupService {
|
|||
return component
|
||||
}
|
||||
|
||||
private instanceBannerBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as InstanceBannerMarkupData
|
||||
const component = this.dynamicElementService.createElement(InstanceBannerMarkupComponent)
|
||||
|
||||
const model = {
|
||||
revertHomePaddingTop: this.buildBoolean(data.revertHomePaddingTop) ?? true
|
||||
}
|
||||
|
||||
this.dynamicElementService.setModel(component, model)
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private videoMiniatureBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as VideoMiniatureMarkupData
|
||||
const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './button-markup.component'
|
||||
export * from './channel-miniature-markup.component'
|
||||
export * from './embed-markup.component'
|
||||
export * from './instance-banner-markup.component'
|
||||
export * from './playlist-miniature-markup.component'
|
||||
export * from './video-miniature-markup.component'
|
||||
export * from './videos-list-markup.component'
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div class="banner revert-margin-content" [ngClass]="{ 'revert-home-padding-top': revertHomePaddingTop }" *ngIf="instanceBannerUrl">
|
||||
<img [src]="instanceBannerUrl" alt="Instance banner">
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { CustomMarkupComponent } from './shared'
|
||||
import { InstanceService } from '@app/shared/shared-instance'
|
||||
import { finalize } from 'rxjs'
|
||||
|
||||
/*
|
||||
* Markup component that creates the img HTML element containing the instance banner
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-banner-markup',
|
||||
templateUrl: 'instance-banner-markup.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class InstanceBannerMarkupComponent implements OnInit, CustomMarkupComponent {
|
||||
@Input() revertHomePaddingTop: boolean
|
||||
|
||||
@Output() loaded = new EventEmitter<boolean>()
|
||||
|
||||
instanceBannerUrl: string
|
||||
|
||||
constructor (
|
||||
private cd: ChangeDetectorRef,
|
||||
private instance: InstanceService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.instance.getAbout()
|
||||
.pipe(finalize(() => this.loaded.emit(true)))
|
||||
.subscribe(about => {
|
||||
if (about.instance.banners.length === 0) return
|
||||
|
||||
this.instanceBannerUrl = about.instance.banners[0].path
|
||||
this.cd.markForCheck()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
ChannelMiniatureMarkupComponent,
|
||||
EmbedMarkupComponent,
|
||||
InstanceBannerMarkupComponent,
|
||||
PlaylistMiniatureMarkupComponent,
|
||||
VideoMiniatureMarkupComponent,
|
||||
VideosListMarkupComponent
|
||||
|
@ -39,7 +40,8 @@ import {
|
|||
VideosListMarkupComponent,
|
||||
ButtonMarkupComponent,
|
||||
CustomMarkupHelpComponent,
|
||||
CustomMarkupContainerComponent
|
||||
CustomMarkupContainerComponent,
|
||||
InstanceBannerMarkupComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -50,7 +52,8 @@ import {
|
|||
EmbedMarkupComponent,
|
||||
ButtonMarkupComponent,
|
||||
CustomMarkupHelpComponent,
|
||||
CustomMarkupContainerComponent
|
||||
CustomMarkupContainerComponent,
|
||||
InstanceBannerMarkupComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -140,7 +140,8 @@ code {
|
|||
}
|
||||
|
||||
.main-col {
|
||||
@include margin-left($menu-width);
|
||||
// Don't use rfs to get exact pixels
|
||||
margin-inline-start: $menu-width;
|
||||
|
||||
width: calc(100% - #{$menu-width});
|
||||
outline: none;
|
||||
|
@ -150,6 +151,10 @@ code {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.revert-margin-content {
|
||||
margin: 0 calc(#{pvar(--horizontalMarginContent)} * -1);
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
background-color: pvar(--submenuBackgroundColor);
|
||||
width: 100%;
|
||||
|
@ -168,10 +173,14 @@ code {
|
|||
}
|
||||
|
||||
// Use an appropriate offset top when sub-menu fixed
|
||||
.margin-content.offset-content {
|
||||
.sub-menu-offset-content {
|
||||
padding-top: $sub-menu-height + $sub-menu-margin-bottom;
|
||||
}
|
||||
|
||||
.sub-menu.mb-0 + .sub-menu-offset-content {
|
||||
padding-top: $sub-menu-height;
|
||||
}
|
||||
|
||||
// Override some properties if the main content is expanded (no menu on the left)
|
||||
&.expanded {
|
||||
--horizontalMarginContent: #{$expanded-horizontal-margins};
|
||||
|
@ -271,7 +280,7 @@ my-global-icon[iconName=external-link] {
|
|||
}
|
||||
|
||||
// Use an appropriate offset top when sub-menu fixed
|
||||
.margin-content.offset-content {
|
||||
.sub-menu-offset-content {
|
||||
padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
top: #{- ($header-height + 20px)};
|
||||
}
|
||||
|
||||
.offset-content {
|
||||
.sub-menu-offset-content {
|
||||
|
||||
// if sub-menu fixed
|
||||
.anchor {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.banner {
|
||||
@include block-ratio('img', $banner-inverted-ratio);
|
||||
}
|
||||
|
||||
.revert-margin-content.banner {
|
||||
width: calc(100% + 2 * #{pvar(--horizontalMarginContent)});
|
||||
}
|
|
@ -2,5 +2,6 @@
|
|||
@use './_common';
|
||||
@use './_custom-bootstrap-helpers';
|
||||
@use './_forms';
|
||||
@use './images';
|
||||
@use './_menu';
|
||||
@use './_text';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
type StringBoolean = 'true' | 'false'
|
||||
|
||||
export type EmbedMarkupData = {
|
||||
// Video or playlist uuid
|
||||
uuid: string
|
||||
|
@ -7,7 +9,7 @@ export type VideoMiniatureMarkupData = {
|
|||
// Video uuid
|
||||
uuid: string
|
||||
|
||||
onlyDisplayTitle?: string // boolean
|
||||
onlyDisplayTitle?: StringBoolean
|
||||
}
|
||||
|
||||
export type PlaylistMiniatureMarkupData = {
|
||||
|
@ -19,12 +21,12 @@ export type ChannelMiniatureMarkupData = {
|
|||
// Channel name (username)
|
||||
name: string
|
||||
|
||||
displayLatestVideo?: string // boolean
|
||||
displayDescription?: string // boolean
|
||||
displayLatestVideo?: StringBoolean
|
||||
displayDescription?: StringBoolean
|
||||
}
|
||||
|
||||
export type VideosListMarkupData = {
|
||||
onlyDisplayTitle?: string // boolean
|
||||
onlyDisplayTitle?: StringBoolean
|
||||
maxRows?: string // number
|
||||
|
||||
sort?: string
|
||||
|
@ -38,14 +40,14 @@ export type VideosListMarkupData = {
|
|||
|
||||
isLive?: string // number
|
||||
|
||||
onlyLocal?: string // boolean
|
||||
onlyLocal?: StringBoolean
|
||||
}
|
||||
|
||||
export type ButtonMarkupData = {
|
||||
theme: 'primary' | 'secondary'
|
||||
href: string
|
||||
label: string
|
||||
blankTarget?: string // boolean
|
||||
blankTarget?: StringBoolean
|
||||
}
|
||||
|
||||
export type ContainerMarkupData = {
|
||||
|
@ -56,3 +58,7 @@ export type ContainerMarkupData = {
|
|||
|
||||
justifyContent?: 'space-between' | 'normal' // default to 'space-between'
|
||||
}
|
||||
|
||||
export type InstanceBannerMarkupData = {
|
||||
revertHomePaddingTop?: StringBoolean // default to 'true'
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ActorImage } from '../index.js'
|
||||
|
||||
export interface About {
|
||||
instance: {
|
||||
name: string
|
||||
|
@ -16,5 +18,7 @@ export interface About {
|
|||
|
||||
languages: string[]
|
||||
categories: number[]
|
||||
|
||||
banners: ActorImage[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -295,6 +295,42 @@ export class ConfigCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateInstanceBanner (options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
}) {
|
||||
const { fixture } = options
|
||||
|
||||
const path = `/api/v1/config/instance-banner/pick`
|
||||
|
||||
return this.updateImageRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fixture,
|
||||
fieldname: 'bannerfile',
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
deleteInstanceBanner (options: OverrideCommandOptions = {}) {
|
||||
const path = `/api/v1/config/instance-banner`
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getCustomConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
|
|
|
@ -8,9 +8,11 @@ import {
|
|||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
|
||||
describe('Test config API validators', function () {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
@ -427,6 +429,82 @@ describe('Test config API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Updating instance banner', function () {
|
||||
const path = '/api/v1/config/instance-banner/pick'
|
||||
|
||||
it('Should fail with an incorrect input file', async function () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('video_short.mp4') }
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields: {}, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big file', async function () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar-big.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without the appropriate rights', async function () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deleting instance banner', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.config.deleteInstanceBanner({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without the appropriate rights', async function () {
|
||||
await server.config.deleteInstanceBanner({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.config.deleteInstanceBanner()
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js'
|
||||
import { basename } from 'path'
|
||||
|
||||
function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||
expect(data.instance.name).to.equal('PeerTube')
|
||||
|
@ -496,7 +498,8 @@ describe('Test static config', function () {
|
|||
})
|
||||
|
||||
describe('Test config', function () {
|
||||
let server: PeerTubeServer = null
|
||||
let server: PeerTubeServer
|
||||
let bannerPath: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
@ -595,23 +598,47 @@ describe('Test config', function () {
|
|||
})
|
||||
|
||||
it('Should fetch the about information', async function () {
|
||||
const data = await server.config.getAbout()
|
||||
const { instance } = await server.config.getAbout()
|
||||
|
||||
expect(data.instance.name).to.equal('PeerTube updated')
|
||||
expect(data.instance.shortDescription).to.equal('my short description')
|
||||
expect(data.instance.description).to.equal('my super description')
|
||||
expect(data.instance.terms).to.equal('my super terms')
|
||||
expect(data.instance.codeOfConduct).to.equal('my super coc')
|
||||
expect(instance.name).to.equal('PeerTube updated')
|
||||
expect(instance.shortDescription).to.equal('my short description')
|
||||
expect(instance.description).to.equal('my super description')
|
||||
expect(instance.terms).to.equal('my super terms')
|
||||
expect(instance.codeOfConduct).to.equal('my super coc')
|
||||
|
||||
expect(data.instance.creationReason).to.equal('my super creation reason')
|
||||
expect(data.instance.moderationInformation).to.equal('my super moderation information')
|
||||
expect(data.instance.administrator).to.equal('Kuja')
|
||||
expect(data.instance.maintenanceLifetime).to.equal('forever')
|
||||
expect(data.instance.businessModel).to.equal('my super business model')
|
||||
expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
|
||||
expect(instance.creationReason).to.equal('my super creation reason')
|
||||
expect(instance.moderationInformation).to.equal('my super moderation information')
|
||||
expect(instance.administrator).to.equal('Kuja')
|
||||
expect(instance.maintenanceLifetime).to.equal('forever')
|
||||
expect(instance.businessModel).to.equal('my super business model')
|
||||
expect(instance.hardwareInformation).to.equal('2vCore 3GB RAM')
|
||||
|
||||
expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
|
||||
expect(data.instance.categories).to.deep.equal([ 1, 2 ])
|
||||
expect(instance.languages).to.deep.equal([ 'en', 'es' ])
|
||||
expect(instance.categories).to.deep.equal([ 1, 2 ])
|
||||
|
||||
expect(instance.banners).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should update instance banner', async function () {
|
||||
await server.config.updateInstanceBanner({ fixture: 'banner.jpg' })
|
||||
|
||||
const { instance } = await server.config.getAbout()
|
||||
|
||||
expect(instance.banners).to.have.lengthOf(1)
|
||||
|
||||
bannerPath = instance.banners[0].path
|
||||
await testImage(server.url, 'banner-resized', bannerPath)
|
||||
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), true)
|
||||
})
|
||||
|
||||
it('Should remove instance banner', async function () {
|
||||
await server.config.deleteInstanceBanner()
|
||||
|
||||
const { instance } = await server.config.getAbout()
|
||||
|
||||
expect(instance.banners).to.have.lengthOf(0)
|
||||
|
||||
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), false)
|
||||
})
|
||||
|
||||
it('Should remove the custom configuration', async function () {
|
||||
|
|
|
@ -3,13 +3,25 @@ import { remove, writeJSON } from 'fs-extra/esm'
|
|||
import snakeCase from 'lodash-es/snakeCase.js'
|
||||
import validator from 'validator'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { About, CustomConfig, UserRight } from '@peertube/peertube-models'
|
||||
import { About, ActorImageType, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger.js'
|
||||
import { objectConverter } from '../../helpers/core-utils.js'
|
||||
import { CONFIG, reloadConfig } from '../../initializers/config.js'
|
||||
import { ClientHtml } from '../../lib/html/client-html.js'
|
||||
import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares/index.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
authenticate,
|
||||
ensureUserHasRight,
|
||||
openapiOperationDoc,
|
||||
updateBannerValidator
|
||||
} from '../../middlewares/index.js'
|
||||
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
|
||||
import { createReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { MIMETYPES } from '@server/initializers/constants.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '@server/lib/local-actor.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ActorImageModel } from '@server/models/actor/actor-image.js'
|
||||
|
||||
const configRouter = express.Router()
|
||||
|
||||
|
@ -24,7 +36,7 @@ configRouter.get('/',
|
|||
|
||||
configRouter.get('/about',
|
||||
openapiOperationDoc({ operationId: 'getAbout' }),
|
||||
getAbout
|
||||
asyncMiddleware(getAbout)
|
||||
)
|
||||
|
||||
configRouter.get('/custom',
|
||||
|
@ -51,13 +63,31 @@ configRouter.delete('/custom',
|
|||
asyncMiddleware(deleteCustomConfig)
|
||||
)
|
||||
|
||||
configRouter.post('/instance-banner/pick',
|
||||
authenticate,
|
||||
createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
|
||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateInstanceBanner)
|
||||
)
|
||||
|
||||
configRouter.delete('/instance-banner',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||
asyncMiddleware(deleteInstanceBanner)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getConfig (req: express.Request, res: express.Response) {
|
||||
const json = await ServerConfigManager.Instance.getServerConfig(req.ip)
|
||||
|
||||
return res.json(json)
|
||||
}
|
||||
|
||||
function getAbout (req: express.Request, res: express.Response) {
|
||||
async function getAbout (req: express.Request, res: express.Response) {
|
||||
const banners = await ActorImageModel.listByActor(await getServerActor(), ActorImageType.BANNER)
|
||||
|
||||
const about: About = {
|
||||
instance: {
|
||||
name: CONFIG.INSTANCE.NAME,
|
||||
|
@ -75,7 +105,9 @@ function getAbout (req: express.Request, res: express.Response) {
|
|||
businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
|
||||
|
||||
languages: CONFIG.INSTANCE.LANGUAGES,
|
||||
categories: CONFIG.INSTANCE.CATEGORIES
|
||||
categories: CONFIG.INSTANCE.CATEGORIES,
|
||||
|
||||
banners: banners.map(b => b.toFormattedJSON())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,6 +155,23 @@ async function updateCustomConfig (req: express.Request, res: express.Response)
|
|||
return res.json(data)
|
||||
}
|
||||
|
||||
async function updateInstanceBanner (req: express.Request, res: express.Response) {
|
||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
||||
|
||||
const accountServer = (await getServerActor()).Account
|
||||
await updateLocalActorImageFiles(accountServer, bannerPhysicalFile, ActorImageType.BANNER)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function deleteInstanceBanner (req: express.Request, res: express.Response) {
|
||||
const accountServer = (await getServerActor()).Account
|
||||
|
||||
await deleteLocalActorImageFile(accountServer, ActorImageType.BANNER)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
|
|
@ -18,10 +18,5 @@ const updateActorImageValidatorFactory = (fieldname: string) => ([
|
|||
}
|
||||
])
|
||||
|
||||
const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
|
||||
const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
|
||||
|
||||
export {
|
||||
updateAvatarValidator,
|
||||
updateBannerValidator
|
||||
}
|
||||
export const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
|
||||
export const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ActivityIconObject, ActorImage, ActorImageType, type ActorImageType_Type } from '@peertube/peertube-models'
|
||||
import { getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||
import { MActorImage, MActorImageFormattable } from '@server/types/models/index.js'
|
||||
import { MActorId, MActorImage, MActorImageFormattable } from '@server/types/models/index.js'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
|
@ -115,6 +115,17 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode
|
|||
return ActorImageModel.findOne(query)
|
||||
}
|
||||
|
||||
static listByActor (actor: MActorId, type: ActorImageType_Type) {
|
||||
const query = {
|
||||
where: {
|
||||
actorId: actor.id,
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
return ActorImageModel.findAll(query)
|
||||
}
|
||||
|
||||
static getImageUrl (image: MActorImage) {
|
||||
if (!image) return undefined
|
||||
|
||||
|
|
|
@ -936,6 +936,60 @@ paths:
|
|||
'200':
|
||||
description: successful operation
|
||||
|
||||
/api/v1/config/instance-banner/pick:
|
||||
post:
|
||||
summary: Update instance banner
|
||||
security:
|
||||
- OAuth2:
|
||||
- admin
|
||||
tags:
|
||||
- Config
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
banners:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ActorImage'
|
||||
'413':
|
||||
description: image file too large
|
||||
headers:
|
||||
X-File-Maximum-Size:
|
||||
schema:
|
||||
type: string
|
||||
format: Nginx size
|
||||
description: Maximum file size for the banner
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
bannerfile:
|
||||
description: The file to upload.
|
||||
type: string
|
||||
format: binary
|
||||
encoding:
|
||||
bannerfile:
|
||||
contentType: image/png, image/jpeg
|
||||
|
||||
'/api/v1/config/instance-banner':
|
||||
delete:
|
||||
summary: Delete instance banner
|
||||
security:
|
||||
- OAuth2:
|
||||
- admin
|
||||
tags:
|
||||
- Config
|
||||
responses:
|
||||
'204':
|
||||
description: successful operation
|
||||
|
||||
/api/v1/custom-pages/homepage/instance:
|
||||
get:
|
||||
summary: Get instance custom homepage
|
||||
|
|
Loading…
Reference in New Issue