Global client redesign

* Split "my library" into "video space (channels, videos...)" and "my library (playlists, history...)"
 * Split "admin" into "overview (users, videos...)", "moderation (abuses, blocks, registrations...)" and "settings (configuration, runners...)"
 * Reorganize the header and the left menu: account settings/notifications are now in the header
 * Add instance information context in the left menu
 * Merge dedicated videos pages for "recently added", "trending", "local videos" into a "browse videos" page that includes quick filters
 * Clean up entire CSS
 * Clean CSS variables so it's easier to theme PeerTube (some new variables fallback to old variables to limit currnet themes breakages)
 * Replace the current light theme into a new one (beige)
 * Add a dark (brown) theme (included in PeerTube core)
 * Fix accessibility issues with old light theme colors (white on orange button for example)
 * Redesign the left menu, the horizontal menu, form controls and buttons, "Discover videos" page and common video filters panel
 * Replace/remove/add some global icon
This commit is contained in:
Chocobozzz 2024-11-05 10:03:40 +01:00
parent 064a44ec4d
commit f83674c143
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
525 changed files with 6861 additions and 6725 deletions

View File

@ -8,7 +8,7 @@ export class AdminConfigPage {
'basic-configuration': 'APPEARANCE', 'basic-configuration': 'APPEARANCE',
'instance-information': 'INSTANCE' 'instance-information': 'INSTANCE'
} }
await go('/admin/config/edit-custom#' + tab) await go('/admin/settings/config/edit-custom#' + tab)
await $('h2=' + waitTitles[tab]).waitForDisplayed() await $('h2=' + waitTitles[tab]).waitForDisplayed()
} }

View File

@ -3,7 +3,7 @@ import { browserSleep, go } from '../utils'
export class AdminPluginPage { export class AdminPluginPage {
async navigateToPluginSearch () { async navigateToPluginSearch () {
await go('/admin/plugins/search') await go('/admin/settings/plugins/search')
await $('my-plugin-search').waitForDisplayed() await $('my-plugin-search').waitForDisplayed()
} }

View File

@ -77,7 +77,7 @@ export class MyAccountPage {
async countVideos (names: string[]) { async countVideos (names: string[]) {
const elements = await $$('.video').filter(async e => { const elements = await $$('.video').filter(async e => {
const t = await e.$('.video-miniature-name').getText() const t = await e.$('.video-name').getText()
return names.some(n => t.includes(n)) return names.some(n => t.includes(n))
}) })
@ -140,7 +140,7 @@ export class MyAccountPage {
private async getVideoElement (name: string) { private async getVideoElement (name: string) {
const video = async () => { const video = async () => {
const videos = await $$('.video').filter(async e => { const videos = await $$('.video').filter(async e => {
const t = await e.$('.video-miniature-name').getText() const t = await e.$('.video-name').getText()
return t.includes(name) return t.includes(name)
}) })

View File

@ -66,25 +66,25 @@ export class VideoListPage {
} }
async getVideosListName () { async getVideosListName () {
const elems = await $$('.videos .video-miniature .video-miniature-name') const elems = await $$('.videos .video-miniature .video-name')
const texts = await elems.map(e => e.getText()) const texts = await elems.map(e => e.getText())
return texts.map(t => t.trim()) return texts.map(t => t.trim())
} }
videoExists (name: string) { videoExists (name: string) {
return $('.video-miniature-name=' + name).isDisplayed() return $('.video-name=' + name).isDisplayed()
} }
async videoIsBlurred (name: string) { async videoIsBlurred (name: string) {
const filter = await $('.video-miniature-name=' + name).getCSSProperty('filter') const filter = await $('.video-name=' + name).getCSSProperty('filter')
return filter.value !== 'none' return filter.value !== 'none'
} }
async clickOnVideo (videoName: string) { async clickOnVideo (videoName: string) {
const video = async () => { const video = async () => {
const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => { const videos = await $$('.videos .video-miniature .video-name').filter(async e => {
const t = await e.getText() const t = await e.getText()
return t === videoName return t === videoName
@ -106,7 +106,7 @@ export class VideoListPage {
async clickOnFirstVideo () { async clickOnFirstVideo () {
const video = () => $('.videos .video-miniature .video-thumbnail') const video = () => $('.videos .video-miniature .video-thumbnail')
const videoName = () => $('.videos .video-miniature .video-miniature-name') const videoName = () => $('.videos .video-miniature .video-name')
await video().waitForClickable() await video().waitForClickable()
@ -119,7 +119,7 @@ export class VideoListPage {
} }
private waitForList () { private waitForList () {
return $('.videos .video-miniature .video-miniature-name').waitForDisplayed() return $('.videos .video-miniature .video-name').waitForDisplayed()
} }
private waitForTitle (title: string) { private waitForTitle (title: string) {

View File

@ -199,7 +199,7 @@ export class VideoWatchPage {
await textarea.setValue(comment) await textarea.setValue(comment)
const confirmButton = await $('.comment-buttons .orange-button') const confirmButton = await $('.comment-buttons .primary-button')
await confirmButton.waitForClickable() await confirmButton.waitForClickable()
await confirmButton.click() await confirmButton.click()
@ -218,7 +218,7 @@ export class VideoWatchPage {
await textarea.waitForClickable() await textarea.waitForClickable()
await textarea.setValue(comment) await textarea.setValue(comment)
const confirmButton = await $('my-video-comment .comment-buttons .orange-button') const confirmButton = await $('my-video-comment .comment-buttons .primary-button')
await confirmButton.waitForClickable() await confirmButton.waitForClickable()
await confirmButton.click() await confirmButton.click()

View File

@ -89,6 +89,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"chart.js": "^4.3.0", "chart.js": "^4.3.0",
"chartjs-plugin-zoom": "~2.0.1", "chartjs-plugin-zoom": "~2.0.1",
"color-bits": "^1.0.4",
"core-js": "^3.22.8", "core-js": "^3.22.8",
"debug": "^4.3.1", "debug": "^4.3.1",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",

View File

@ -11,7 +11,7 @@
{{ follower.name }} {{ follower.name }}
</a> </a>
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> <button i18n class="peertube-button-link secondary-button mt-1" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
</div> </div>
<div class="col-xl-6 col-md-12"> <div class="col-xl-6 col-md-12">
@ -23,7 +23,7 @@
{{ following.name }} {{ following.name }}
</a> </a>
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button> <button i18n class="peertube-button-link secondary-button mt-1" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
</div> </div>
</div> </div>

View File

@ -1,15 +1,15 @@
<div class="banner" *ngIf="instanceBannerUrl"> <div class="margin-content mt-4">
<img [src]="instanceBannerUrl" alt="Instance banner"> <div class="banner mb-4" *ngIf="instanceBannerUrl">
<img class="rounded" [src]="instanceBannerUrl" alt="Instance banner">
</div> </div>
<div class="margin-content mt-4">
<div class="row "> <div class="row ">
<div class="col-md-12 col-xl-6"> <div class="col-md-12 col-xl-6">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h1 i18n class="fw-semibold fs-5">About {{ instanceName }}</h1> <h1 i18n class="fw-semibold fs-5">About {{ instanceName }}</h1>
<a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="peertube-button-link orange-button h-100 d-flex align-items-center">Contact us</a> <a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="peertube-button-link primary-button h-100 d-flex align-items-center">Contact us</a>
</div> </div>
<div class="mb-4" *ngIf="categories.length !== 0 || languages.length !== 0"> <div class="mb-4" *ngIf="categories.length !== 0 || languages.length !== 0">

View File

@ -15,8 +15,11 @@
.middle-title { .middle-title {
margin-top: 0; margin-top: 0;
text-transform: uppercase;
color: pvar(--fg);
font-weight: $font-bold;
@include in-content-small-title; @include font-size(22px);
@include margin-bottom(1.5rem); @include margin-bottom(1.5rem);
} }
@ -42,9 +45,6 @@
.middle-title, .middle-title,
.section-title { .section-title {
display: inline-block; display: inline-block;
} color: pvar(--fg-400);
.section-title {
color: var(--mainForegroundColor);
} }
} }

View File

@ -50,11 +50,11 @@
<div class="form-group inputs"> <div class="form-group inputs">
<input <input
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" type="button" role="button" i18n-value value="Cancel" class="peertube-button secondary-button"
(click)="hide()" (key.enter)="hide()" (click)="hide()" (key.enter)="hide()"
> >
<input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid" /> <input type="submit" i18n-value value="Submit" class="peertube-button primary-button" [disabled]="!form.valid" />
</div> </div>
</form> </form>

View File

@ -1,5 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
.modal-subtitle { .modal-subtitle {
line-height: 1rem; line-height: 1rem;

View File

@ -11,12 +11,12 @@
</p> </p>
<p i18n> <p i18n>
It is free and open-source software, under <a class="link-orange" href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3 It is free and open-source software, under <a class="link-primary" href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3
licence</a>. licence</a>.
</p> </p>
<p i18n> <p i18n>
For more information, please visit <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>. For more information, please visit <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>.
</p> </p>
</div> </div>
@ -25,7 +25,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/setup-account">Use PeerTube documentation</a> <a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/setup-account">Use PeerTube documentation</a>
</div> </div>
<div i18n class="card-text"> <div i18n class="card-text">
@ -37,7 +37,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/third-party-application">PeerTube Applications</a> <a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/third-party-application">PeerTube Applications</a>
</div> </div>
<div i18n class="card-text"> <div i18n class="card-text">
@ -49,7 +49,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute/getting-started">Contribute on PeerTube</a> <a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute/getting-started">Contribute on PeerTube</a>
</div> </div>
<div i18n class="card-text"> <div i18n class="card-text">
@ -112,7 +112,7 @@
Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker. Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker.
When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers
to forward the information to. to forward the information to.
See <a class="link-orange" href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information See <a class="link-primary" href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
</li> </li>
</ul> </ul>

View File

@ -1,13 +1,7 @@
<div> <div>
<div class="sub-menu mb-0" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }"> <div class="margin-content">
<a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="sub-menu-entry">Instance</a> <my-horizontal-menu [menuEntries]="menuEntries"></my-horizontal-menu>
<a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="sub-menu-entry">PeerTube</a>
<a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="sub-menu-entry">Network</a>
</div> </div>
<div [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</div>

View File

@ -1,22 +1,30 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { ScreenService } from '@app/core' import { RouterOutlet } from '@angular/router'
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router' import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { PluginSelectorDirective } from '../shared/shared-main/plugins/plugin-selector.directive'
import { NgClass } from '@angular/common'
@Component({ @Component({
selector: 'my-about', selector: 'my-about',
templateUrl: './about.component.html', templateUrl: './about.component.html',
standalone: true, standalone: true,
imports: [ NgClass, PluginSelectorDirective, RouterLink, RouterLinkActive, RouterOutlet ] imports: [ RouterOutlet, HorizontalMenuComponent ]
}) })
export class AboutComponent { export class AboutComponent {
constructor ( menuEntries: HorizontalMenuEntry[] = [
private screenService: ScreenService {
) { } label: $localize`Platform`,
routerLink: '/about/instance',
get isBroadcastMessageDisplayed () { pluginSelectorId: 'about-menu-instance'
return this.screenService.isBroadcastMessageDisplayed },
{
label: $localize`PeerTube`,
routerLink: '/about/peertube',
pluginSelectorId: 'about-menu-peertube'
},
{
label: $localize`Network`,
routerLink: '/about/follows',
pluginSelectorId: 'about-menu-network'
} }
]
} }

View File

@ -17,7 +17,7 @@
></my-actor-avatar> ></my-actor-avatar>
<h2 class="fs-5 lh-1 fw-bold m-0"> <h2 class="fs-5 lh-1 fw-bold m-0">
<a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> <a class="text-decoration-none" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
{{ videoChannel.displayName }} {{ videoChannel.displayName }}
</a> </a>
</h2> </h2>
@ -35,7 +35,7 @@
<my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button>
<a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a> <a i18n class="button-show-channel peertube-button-link primary-button" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
<div class="videos-overflow-workaround"> <div class="videos-overflow-workaround">
<div class="videos"> <div class="videos">
@ -47,7 +47,7 @@
></my-video-miniature> ></my-video-miniature>
<div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel"> <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
<a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a> <a class="link-primary" i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
.channel { .channel {
max-width: $max-channels-width; max-width: $max-channels-width;
background-color: pvar(--channelBackgroundColor); background-color: pvar(--bg-secondary-350);
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@ -36,7 +36,7 @@
} }
a { a {
color: pvar(--mainForegroundColor); color: pvar(--fg);
@include peertube-word-wrap; @include peertube-word-wrap;
} }
@ -60,7 +60,7 @@
max-height: 80px; max-height: 80px;
@include fade-text(50px, pvar(--channelBackgroundColor)); @include fade-text(50px, pvar(--bg-secondary-350));
} }
} }
@ -95,14 +95,9 @@ my-subscribe-button {
height: 100%; height: 100%;
position: absolute; position: absolute;
right: 0; right: 0;
background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px); background: linear-gradient(90deg, transparent 0, pvar(--bg-secondary-350) 45px);
padding: (math.div($video-thumbnail-medium-height, 2) - 10px) 15px 0 60px; padding: (math.div($video-thumbnail-medium-height, 2) - 10px) 15px 0 60px;
z-index: z(miniature) + 1; z-index: z(miniature) + 1;
a {
color: pvar(--mainColor);
font-weight: $font-semibold;
}
} }
.button-show-channel { .button-show-channel {
@ -130,11 +125,6 @@ my-subscribe-button {
} }
} }
.show-channel a {
@include peertube-button-link;
@include orange-button-inverted;
}
.videos { .videos {
display: none; display: none;
} }

View File

@ -1,11 +1,10 @@
<h1 class="visually-hidden" i18n>Videos</h1>
<my-videos-list <my-videos-list
#videosList #videosList
*ngIf="account" *ngIf="account"
[title]="title"
displayTitle="false"
[getVideosObservableFunction]="getVideosObservableFunction" [getVideosObservableFunction]="getVideosObservableFunction"
[getSyndicationItemsFunction]="getSyndicationItemsFunction" [getSyndicationItemsFunction]="getSyndicationItemsFunction"

View File

@ -21,7 +21,6 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
getVideosObservableFunction = this.getVideosObservable.bind(this) getVideosObservableFunction = this.getVideosObservable.bind(this)
getSyndicationItemsFunction = this.getSyndicationItems.bind(this) getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
title = $localize`Videos`
defaultSort = '-publishedAt' as VideoSortField defaultSort = '-publishedAt' as VideoSortField
account: Account account: Account

View File

@ -1,5 +1,5 @@
<div *ngIf="account" class="root"> <div *ngIf="account" class="root">
<div class="account-info d-md-grid d-block"> <div class="margin-content account-info d-md-grid d-block">
<div class="account-avatar-row"> <div class="account-avatar-row">
<my-actor-avatar [size]="getAccountAvatarSize()" actorType="account" [actor]="account"></my-actor-avatar> <my-actor-avatar [size]="getAccountAvatarSize()" actorType="account" [actor]="account"></my-actor-avatar>
@ -48,7 +48,7 @@
<div class="description-html" [innerHTML]="accountDescriptionHTML"></div> <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
</div> </div>
<button *ngIf="hasShowMoreDescription()" class="show-more d-md-none d-block button-unstyle" <button *ngIf="hasShowMoreDescription()" class="show-more peertube-button-like-link d-md-none d-block"
(click)="accountDescriptionExpanded = !accountDescriptionExpanded" (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
title="Show the complete description" i18n-title i18n title="Show the complete description" i18n-title i18n
> >
@ -56,7 +56,7 @@
</button> </button>
<div class="buttons"> <div class="buttons">
<a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n> <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link primary-button" i18n>
Manage account Manage account
</a> </a>
@ -64,14 +64,15 @@
</div> </div>
</div> </div>
<div class="links" [ngClass]="{ 'on-channel-page': isOnChannelPage() }"> <div class="margin-content horizontal-menu mb-3">
<ng-template #linkTemplate let-item="item"> <ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="sub-menu-entry">{{ item.label }}</a> <a [routerLink]="item.routerLink" routerLinkActive="active" class="sub-menu-entry">{{ item.label }}</a>
</ng-template> </ng-template>
<my-list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow> <my-horizontal-menu [hidden]="hideMenu" [menuEntries]="links"></my-horizontal-menu>
<my-simple-search-input <my-simple-search-input
class="ms-auto"
[alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)" [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
(inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos" (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
i18n-iconTitle icon-title="Search account videos" i18n-iconTitle icon-title="Search account videos"

View File

@ -4,28 +4,21 @@
@use '_miniature' as *; @use '_miniature' as *;
.root { .root {
--myFontSize: 1rem; --co-font-size: 1rem;
--myGreyFontSize: 1rem; --co-secondary-font-size: 1rem;
} }
.section-label { my-horizontal-menu {
@include section-label-responsive; flex-grow: 1;
@include margin-right(3rem);
} }
.links { .horizontal-menu {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-wrap: wrap;
@include grid-videos-miniature-margins;
&.on-channel-page {
max-width: $max-channels-width;
}
simple-search-input {
@include margin-left(auto);
}
} }
my-copy-button { my-copy-button {
@ -36,17 +29,12 @@ my-copy-button {
grid-template-columns: 1fr min-content; grid-template-columns: 1fr min-content;
grid-template-rows: auto auto; grid-template-rows: auto auto;
background-color: pvar(--submenuBackgroundColor);
@include grid-videos-miniature-margins(false, 15px);
@include padding-top(3.75rem);
@include padding-bottom(3.75rem);
@include margin-bottom(3rem); @include margin-bottom(3rem);
@include font-size(1rem); @include font-size(1rem);
} }
.account-avatar-row { .account-avatar-row {
@include avatar-row-responsive(2rem, var(--myGreyFontSize)); @include avatar-row-responsive(2rem, var(--co-secondary-font-size));
} }
.actor-display-name { .actor-display-name {
@ -93,7 +81,7 @@ my-copy-button {
.description:not(.expanded) { .description:not(.expanded) {
max-height: 70px; max-height: 70px;
@include fade-text(30px, pvar(--submenuBackgroundColor)); @include fade-text(30px, pvar(--bg));
} }
.buttons { .buttons {
@ -103,8 +91,8 @@ my-copy-button {
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {
.root { .root {
--myFontSize: 14px; --co-font-size: 14px;
--myGreyFontSize: 13px; --co-secondary-font-size: 13px;
} }
.links { .links {

View File

@ -7,6 +7,7 @@ import { AccountService } from '@app/shared/shared-main/account/account.service'
import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component' import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component'
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model' import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service' import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { VideoService } from '@app/shared/shared-main/video/video.service' import { VideoService } from '@app/shared/shared-main/video/video.service'
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service' import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
import { AccountReportComponent } from '@app/shared/shared-moderation/report-modals' import { AccountReportComponent } from '@app/shared/shared-moderation/report-modals'
@ -16,7 +17,6 @@ import { Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
import { ActorAvatarComponent } from '../shared/shared-actor-image/actor-avatar.component' import { ActorAvatarComponent } from '../shared/shared-actor-image/actor-avatar.component'
import { CopyButtonComponent } from '../shared/shared-main/buttons/copy-button.component' import { CopyButtonComponent } from '../shared/shared-main/buttons/copy-button.component'
import { ListOverflowComponent, ListOverflowItem } from '../shared/shared-main/menu/list-overflow.component'
import { SimpleSearchInputComponent } from '../shared/shared-main/search/simple-search-input.component' import { SimpleSearchInputComponent } from '../shared/shared-main/search/simple-search-input.component'
import { AccountBlockBadgesComponent } from '../shared/shared-moderation/account-block-badges.component' import { AccountBlockBadgesComponent } from '../shared/shared-moderation/account-block-badges.component'
import { UserModerationDropdownComponent } from '../shared/shared-moderation/user-moderation-dropdown.component' import { UserModerationDropdownComponent } from '../shared/shared-moderation/user-moderation-dropdown.component'
@ -37,11 +37,12 @@ import { SubscribeButtonComponent } from '../shared/shared-user-subscription/sub
RouterLink, RouterLink,
SubscribeButtonComponent, SubscribeButtonComponent,
RouterLinkActive, RouterLinkActive,
ListOverflowComponent, HorizontalMenuComponent,
SimpleSearchInputComponent, SimpleSearchInputComponent,
RouterOutlet, RouterOutlet,
AccountReportComponent, AccountReportComponent,
DatePipe DatePipe,
HorizontalMenuComponent
] ]
}) })
export class AccountsComponent implements OnInit, OnDestroy { export class AccountsComponent implements OnInit, OnDestroy {
@ -52,7 +53,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
videoChannels: VideoChannel[] = [] videoChannels: VideoChannel[] = []
links: ListOverflowItem[] = [] links: HorizontalMenuEntry[] = []
hideMenu = false hideMenu = false
accountVideosCount: number accountVideosCount: number

View File

@ -0,0 +1,11 @@
<div class="root">
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-moderation-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
<div class="margin-content">
<my-horizontal-menu i18n-h1 h1="Moderation" h1Icon="moderation" [menuEntries]="menuEntries"></my-horizontal-menu>
</div>
<div #adminContent tabindex="-1" id="admin-moderation-content" class="margin-content outline-0">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,87 @@
import { Component, OnInit } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { AuthService, ServerService } from '@app/core'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { UserRight, UserRightType } from '@peertube/peertube-models'
@Component({
templateUrl: './admin-moderation.component.html',
standalone: true,
imports: [ HorizontalMenuComponent, RouterOutlet ]
})
export class AdminModerationComponent implements OnInit {
menuEntries: HorizontalMenuEntry[] = []
constructor (
private auth: AuthService,
private server: ServerService
) { }
ngOnInit () {
this.server.configReloaded.subscribe(() => this.buildMenu())
this.buildMenu()
}
private buildMenu () {
this.menuEntries = []
if (this.hasRight(UserRight.MANAGE_ABUSES)) {
this.menuEntries.push({
label: $localize`Reports`,
routerLink: '/admin/moderation/abuses/list'
})
}
if (this.hasRight(UserRight.MANAGE_REGISTRATIONS)) {
this.menuEntries.push({
label: $localize`Registrations`,
routerLink: '/admin/moderation/registrations/list'
})
}
if (this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
this.menuEntries.push({
label: $localize`Video blocks`,
routerLink: '/admin/moderation/video-blocks/list'
})
}
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST) || this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
const item: HorizontalMenuEntry = {
label: $localize`Mutes`,
routerLink: '',
children: []
}
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
item.children.push({
label: $localize`Muted accounts`,
routerLink: '/admin/moderation/blocklist/accounts'
})
}
if (this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
item.children.push({
label: $localize`Muted servers`,
routerLink: '/admin/moderation/blocklist/servers'
})
}
item.routerLink = item.children[0].routerLink
this.menuEntries.push(item)
}
if (this.hasRight(UserRight.MANAGE_INSTANCE_WATCHED_WORDS)) {
this.menuEntries.push({
label: $localize`Watched words`,
routerLink: '/admin/moderation/watched-words/list'
})
}
}
private hasRight (right: UserRightType) {
return this.auth.getUser().hasRight(right)
}
}

View File

@ -0,0 +1,11 @@
<div class="root">
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-overview-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
<div class="margin-content">
<my-horizontal-menu i18n-h1 h1="Overview" h1Icon="overview" [menuEntries]="menuEntries"></my-horizontal-menu>
</div>
<div #adminContent tabindex="-1" id="admin-overview-content" class="margin-content outline-0">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,54 @@
import { Component, OnInit } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { AuthService } from '@app/core'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { UserRight, UserRightType } from '@peertube/peertube-models'
@Component({
templateUrl: './admin-overview.component.html',
standalone: true,
imports: [ HorizontalMenuComponent, RouterOutlet ]
})
export class AdminOverviewComponent implements OnInit {
menuEntries: HorizontalMenuEntry[] = []
constructor (
private auth: AuthService
) { }
ngOnInit () {
this.buildMenu()
}
private buildMenu () {
this.menuEntries = []
if (this.hasRight(UserRight.MANAGE_USERS)) {
this.menuEntries.push({
label: $localize`Users`,
routerLink: '/admin/overview/users'
})
}
if (this.hasRight(UserRight.SEE_ALL_VIDEOS)) {
this.menuEntries.push({
label: $localize`Videos`,
routerLink: '/admin/overview/videos',
queryParams: {
search: 'isLocal:true'
}
})
}
if (this.hasRight(UserRight.SEE_ALL_COMMENTS)) {
this.menuEntries.push({
label: $localize`Comments`,
routerLink: '/admin/overview/comments'
})
}
}
private hasRight (right: UserRightType) {
return this.auth.getUser().hasRight(right)
}
}

View File

@ -0,0 +1,11 @@
<div class="root">
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-settings-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
<div class="margin-content">
<my-horizontal-menu i18n-h1 h1="Settings" h1Icon="config" [menuEntries]="menuEntries"></my-horizontal-menu>
</div>
<div #adminContent tabindex="-1" id="admin-settings-content" class="margin-content outline-0">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,179 @@
import { Component, OnInit } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { AuthService, ServerService } from '@app/core'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
import { PluginType, UserRight, UserRightType } from '@peertube/peertube-models'
@Component({
templateUrl: './admin-settings.component.html',
standalone: true,
imports: [ HorizontalMenuComponent, RouterOutlet ]
})
export class AdminSettingsComponent implements OnInit {
menuEntries: HorizontalMenuEntry[] = []
constructor (
private auth: AuthService,
private server: ServerService
) { }
ngOnInit () {
this.server.configReloaded.subscribe(() => this.buildMenu())
this.buildMenu()
}
private buildMenu () {
this.menuEntries = []
this.buildConfigurationItems()
this.buildFederationItems()
this.buildPluginItems()
this.buildRunnerItems()
this.buildSystemItems()
}
private buildFederationItems () {
if (!this.hasRight(UserRight.MANAGE_SERVER_FOLLOW)) return
this.menuEntries.push({
label: $localize`Federation`,
routerLink: '/admin/settings/follows/following-list',
children: [
{
label: $localize`Following`,
routerLink: '/admin/settings/follows/following-list'
},
{
label: $localize`Followers`,
routerLink: '/admin/settings/follows/followers-list'
},
{
label: $localize`Video redundancies`,
routerLink: '/admin/settings/follows/video-redundancies-list'
}
]
})
}
private buildConfigurationItems () {
if (this.hasRight(UserRight.MANAGE_CONFIGURATION)) {
this.menuEntries.push({ label: $localize`Configuration`, routerLink: '/admin/settings/config' })
}
}
private buildPluginItems () {
if (this.hasRight(UserRight.MANAGE_PLUGINS)) {
this.menuEntries.push({
label: $localize`Plugins/Themes`,
routerLink: '/admin/settings/plugins',
queryParams: {
pluginType: PluginType.PLUGIN
},
children: [
{
label: 'Installed plugins',
routerLink: '/admin/settings/plugins/list-installed',
queryParams: {
pluginType: PluginType.PLUGIN
}
},
{
label: 'Search plugins',
routerLink: '/admin/settings/plugins/search',
queryParams: {
pluginType: PluginType.PLUGIN
}
},
{
label: 'Installed themes',
routerLink: '/admin/settings/plugins/list-installed',
queryParams: {
pluginType: PluginType.THEME
}
},
{
label: 'Search themes',
routerLink: '/admin/settings/plugins/search',
queryParams: {
pluginType: PluginType.THEME
}
}
]
})
}
}
private buildRunnerItems () {
if (!this.isRemoteRunnersEnabled() || !this.hasRight(UserRight.MANAGE_RUNNERS)) return
this.menuEntries.push({
label: $localize`Runners`,
routerLink: '/admin/settings/system/runners/runners-list',
children: [
{
label: $localize`Remote runners`,
routerLink: '/admin/settings/system/runners/runners-list'
},
{
label: $localize`Runner jobs`,
routerLink: '/admin/settings/system/runners/jobs-list'
},
{
label: $localize`Registration tokens`,
routerLink: '/admin/settings/system/runners/registration-tokens-list'
}
]
})
}
private buildSystemItems () {
const systemItems: HorizontalMenuEntry = {
label: $localize`System`,
routerLink: '',
children: []
}
if (this.hasRight(UserRight.MANAGE_JOBS)) {
systemItems.children.push({
label: $localize`Local jobs`,
routerLink: '/admin/settings/system/jobs'
})
}
if (this.hasRight(UserRight.MANAGE_LOGS)) {
systemItems.children.push({
label: $localize`Logs`,
routerLink: '/admin/settings/system/logs'
})
}
if (this.hasRight(UserRight.MANAGE_DEBUG)) {
systemItems.children.push({
label: $localize`Debug`,
routerLink: '/admin/settings/system/debug'
})
}
if (systemItems.children.length === 0) return
systemItems.routerLink = systemItems.children[0].routerLink
this.menuEntries.push(systemItems)
}
private hasRight (right: UserRightType) {
return this.auth.getUser().hasRight(right)
}
private isRemoteRunnersEnabled () {
const config = this.server.getHTMLConfig()
return config.transcoding.remoteRunners.enabled ||
config.live.transcoding.remoteRunners.enabled ||
config.videoStudio.remoteRunners.enabled ||
config.videoTranscription.remoteRunners.enabled
}
}

View File

@ -1,9 +0,0 @@
<div class="root">
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
<div #adminContent tabindex="-1" id="admin-content" class="margin-content outline-0" [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -1,10 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
my-top-menu-dropdown {
flex-grow: 1;
}
.root {
@include sub-menu-h1;
}

View File

@ -1,240 +0,0 @@
import { NgClass } from '@angular/common'
import { Component, OnInit } from '@angular/core'
import { RouterOutlet } from '@angular/router'
import { AuthService, ScreenService, ServerService } from '@app/core'
import { ListOverflowItem } from '@app/shared/shared-main/menu/list-overflow.component'
import { TopMenuDropdownParam } from '@app/shared/shared-main/menu/top-menu-dropdown.component'
import { UserRight, UserRightType } from '@peertube/peertube-models'
import { TopMenuDropdownComponent } from '../shared/shared-main/menu/top-menu-dropdown.component'
@Component({
templateUrl: './admin.component.html',
styleUrls: [ './admin.component.scss' ],
standalone: true,
imports: [ TopMenuDropdownComponent, NgClass, RouterOutlet ]
})
export class AdminComponent implements OnInit {
items: ListOverflowItem[] = []
menuEntries: TopMenuDropdownParam[] = []
constructor (
private auth: AuthService,
private screen: ScreenService,
private server: ServerService
) { }
get isBroadcastMessageDisplayed () {
return this.screen.isBroadcastMessageDisplayed
}
ngOnInit () {
this.server.configReloaded.subscribe(() => this.buildMenu())
this.buildMenu()
}
private buildMenu () {
this.menuEntries = []
this.buildOverviewItems()
this.buildFederationItems()
this.buildModerationItems()
this.buildConfigurationItems()
this.buildPluginItems()
this.buildSystemItems()
}
private buildOverviewItems () {
const overviewItems: TopMenuDropdownParam = {
label: $localize`Overview`,
children: []
}
if (this.hasRight(UserRight.MANAGE_USERS)) {
overviewItems.children.push({
label: $localize`Users`,
routerLink: '/admin/users',
iconName: 'user'
})
}
if (this.hasRight(UserRight.SEE_ALL_VIDEOS)) {
overviewItems.children.push({
label: $localize`Videos`,
routerLink: '/admin/videos',
queryParams: {
search: 'isLocal:true'
},
iconName: 'videos'
})
}
if (this.hasRight(UserRight.SEE_ALL_COMMENTS)) {
overviewItems.children.push({
label: $localize`Comments`,
routerLink: '/admin/comments',
iconName: 'message-circle'
})
}
if (overviewItems.children.length !== 0) {
this.menuEntries.push(overviewItems)
}
}
private buildFederationItems () {
if (!this.hasRight(UserRight.MANAGE_SERVER_FOLLOW)) return
this.menuEntries.push({
label: $localize`Federation`,
children: [
{
label: $localize`Following`,
routerLink: '/admin/follows/following-list',
iconName: 'following'
},
{
label: $localize`Followers`,
routerLink: '/admin/follows/followers-list',
iconName: 'follower'
},
{
label: $localize`Video redundancies`,
routerLink: '/admin/follows/video-redundancies-list',
iconName: 'videos'
}
]
})
}
private buildModerationItems () {
const moderationItems: TopMenuDropdownParam = {
label: $localize`Moderation`,
children: []
}
if (this.hasRight(UserRight.MANAGE_REGISTRATIONS)) {
moderationItems.children.push({
label: $localize`Registrations`,
routerLink: '/admin/moderation/registrations/list',
iconName: 'user'
})
}
if (this.hasRight(UserRight.MANAGE_ABUSES)) {
moderationItems.children.push({
label: $localize`Reports`,
routerLink: '/admin/moderation/abuses/list',
iconName: 'flag'
})
}
if (this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
moderationItems.children.push({
label: $localize`Video blocks`,
routerLink: '/admin/moderation/video-blocks/list',
iconName: 'cross'
})
}
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
moderationItems.children.push({
label: $localize`Muted accounts`,
routerLink: '/admin/moderation/blocklist/accounts',
iconName: 'user-x'
})
}
if (this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
moderationItems.children.push({
label: $localize`Muted servers`,
routerLink: '/admin/moderation/blocklist/servers',
iconName: 'peertube-x'
})
}
if (this.hasRight(UserRight.MANAGE_INSTANCE_WATCHED_WORDS)) {
moderationItems.children.push({
label: $localize`Watched words`,
routerLink: '/admin/moderation/watched-words/list',
iconName: 'eye-open'
})
}
if (moderationItems.children.length !== 0) this.menuEntries.push(moderationItems)
}
private buildConfigurationItems () {
if (this.hasRight(UserRight.MANAGE_CONFIGURATION)) {
this.menuEntries.push({ label: $localize`Configuration`, routerLink: '/admin/config' })
}
}
private buildPluginItems () {
if (this.hasRight(UserRight.MANAGE_PLUGINS)) {
this.menuEntries.push({ label: $localize`Plugins/Themes`, routerLink: '/admin/plugins' })
}
}
private buildSystemItems () {
const systemItems: TopMenuDropdownParam = {
label: $localize`System`,
children: []
}
if (this.isRemoteRunnersEnabled() && this.hasRight(UserRight.MANAGE_RUNNERS)) {
systemItems.children.push({
label: $localize`Remote runners`,
iconName: 'codesandbox',
routerLink: '/admin/system/runners/runners-list'
})
systemItems.children.push({
label: $localize`Runner jobs`,
iconName: 'globe',
routerLink: '/admin/system/runners/jobs-list'
})
}
if (this.hasRight(UserRight.MANAGE_JOBS)) {
systemItems.children.push({
label: $localize`Local jobs`,
iconName: 'circle-tick',
routerLink: '/admin/system/jobs'
})
}
if (this.hasRight(UserRight.MANAGE_LOGS)) {
systemItems.children.push({
label: $localize`Logs`,
iconName: 'playlists',
routerLink: '/admin/system/logs'
})
}
if (this.hasRight(UserRight.MANAGE_DEBUG)) {
systemItems.children.push({
label: $localize`Debug`,
iconName: 'cog',
routerLink: '/admin/system/debug'
})
}
if (systemItems.children.length !== 0) {
this.menuEntries.push(systemItems)
}
}
private hasRight (right: UserRightType) {
return this.auth.getUser().hasRight(right)
}
private isRemoteRunnersEnabled () {
const config = this.server.getHTMLConfig()
return config.transcoding.remoteRunners.enabled ||
config.live.transcoding.remoteRunners.enabled ||
config.videoStudio.remoteRunners.enabled ||
config.videoTranscription.remoteRunners.enabled
}
}

View File

@ -3,7 +3,7 @@ import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config
import { UserRightGuard } from '@app/core' import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models' import { UserRight } from '@peertube/peertube-models'
export const ConfigRoutes: Routes = [ export const configRoutes: Routes = [
{ {
path: 'config', path: 'config',
canActivate: [ UserRightGuard ], canActivate: [ UserRightGuard ],

View File

@ -4,7 +4,7 @@
<h2 i18n>APPEARANCE</h2> <h2 i18n>APPEARANCE</h2>
<div i18n class="inner-form-description"> <div i18n class="inner-form-description">
Use <a class="link-orange" routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-orange" routerLink="/admin/config/edit-custom" fragment="advanced-configuration">customizations</a>. Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-primary" routerLink="/admin/settings/config/edit-custom" fragment="advanced-configuration">customizations</a>.
</div> </div>
</div> </div>
@ -14,13 +14,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="themeDefault">Theme</label> <label i18n for="themeDefault">Theme</label>
<div class="peertube-select-container"> <my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
<select formControlName="default" id="themeDefault" class="form-control">
<option i18n value="default">{{ getDefaultThemeLabel() }}</option>
<option *ngFor="let theme of availableThemes" [value]="theme.id">{{ theme.label }}</option>
</select>
</div>
</div> </div>
</ng-container> </ng-container>
@ -42,7 +36,7 @@
<div class="form-group" formGroupName="trending"> <div class="form-group" formGroupName="trending">
<ng-container formGroupName="videos"> <ng-container formGroupName="videos">
<ng-container formGroupName="algorithms"> <ng-container formGroupName="algorithms">
<label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label> <label i18n for="trendingVideosAlgorithmsDefault">Default trending algorithm</label>
<div class="peertube-select-container"> <div class="peertube-select-container">
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
@ -151,7 +145,7 @@
<div class="title-col"> <div class="title-col">
<h2 i18n>NEW USERS</h2> <h2 i18n>NEW USERS</h2>
<div i18n class="inner-form-description"> <div i18n class="inner-form-description">
Manage <a class="link-orange" routerLink="/admin/users">users</a> to set their quota individually. Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to set their quota individually.
</div> </div>
</div> </div>
@ -296,7 +290,7 @@
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)" i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n>⚠️ If enabled, we recommend to use <a class="link-orange" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span> <span i18n>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
</ng-container> </ng-container>
</my-peertube-checkbox> </my-peertube-checkbox>
</div> </div>
@ -393,7 +387,7 @@
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n> <span i18n>
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process transcription tasks. Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.
Remote runners has to register on your instance first. Remote runners has to register on your instance first.
</span> </span>
</ng-container> </ng-container>
@ -472,7 +466,7 @@
<div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div> <div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
<div i18n> <div i18n>
You should only use moderated search indexes in production, or <a class="link-orange" href="https://framagit.org/framasoft/peertube/search-index">host your own</a>. You should only use moderated search indexes in production, or <a class="link-primary" href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
</div> </div>
</ng-container> </ng-container>
@ -595,7 +589,7 @@
<div class="title-col"> <div class="title-col">
<h2 i18n>FEDERATION</h2> <h2 i18n>FEDERATION</h2>
<div i18n class="inner-form-description"> <div i18n class="inner-form-description">
Manage <a class="link-orange" routerLink="/admin/follows">relations</a> with other instances. Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other instances.
</div> </div>
</div> </div>
@ -646,7 +640,7 @@
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div> <div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
<span i18n> <span i18n>
See <a class="link-orange" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
</span> </span>
</ng-container> </ng-container>

View File

@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { MenuService, ThemeService } from '@app/core' import { ThemeService } from '@app/core'
import { AlertComponent } from '@app/shared/shared-main/common/alert.component' import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
import { HTMLServerConfig } from '@peertube/peertube-models' import { HTMLServerConfig } from '@peertube/peertube-models'
import { pairwise } from 'rxjs/operators' import { pairwise } from 'rxjs/operators'
@ -12,7 +12,6 @@ import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component' import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component' import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component' import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component' import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
import { ConfigService } from '../shared/config.service' import { ConfigService } from '../shared/config.service'
@ -34,7 +33,6 @@ import { ConfigService } from '../shared/config.service'
NgClass, NgClass,
UserRealQuotaInfoComponent, UserRealQuotaInfoComponent,
SelectOptionsComponent, SelectOptionsComponent,
PeerTubeTemplateDirective,
AlertComponent AlertComponent
] ]
}) })
@ -53,7 +51,6 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
constructor ( constructor (
private configService: ConfigService, private configService: ConfigService,
private menuService: MenuService,
private themeService: ThemeService private themeService: ThemeService
) {} ) {}
@ -62,7 +59,11 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
this.checkSignupField() this.checkSignupField()
this.checkImportSyncField() this.checkImportSyncField()
this.availableThemes = this.themeService.buildAvailableThemes() this.availableThemes = [
this.themeService.getDefaultThemeItem(),
...this.themeService.buildAvailableThemes()
]
this.exportExpirationOptions = [ this.exportExpirationOptions = [
{ id: 1000 * 3600 * 24, label: $localize`1 day` }, { id: 1000 * 3600 * 24, label: $localize`1 day` },
@ -156,19 +157,24 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
} }
buildLandingPageOptions () { buildLandingPageOptions () {
this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) let links: { label: string, path: string }[] = []
.links
.map(o => ({ if (this.serverConfig.homepage.enabled) {
links.push({ label: $localize`Home`, path: '/home' })
}
links = links.concat([
{ label: $localize`Discover`, path: '/videos/overview' },
{ label: $localize`Browse videos`, path: '/videos/browse' }
])
this.defaultLandingPageOptions = links.map(o => ({
id: o.path, id: o.path,
label: o.label, label: o.label,
description: o.path description: o.path
})) }))
} }
getDefaultThemeLabel () {
return this.themeService.getDefaultThemeLabel()
}
private checkImportSyncField () { private checkImportSyncField () {
const importSyncControl = this.form.get('import.videoChannelSynchronization.enabled') const importSyncControl = this.form.get('import.videoChannelSynchronization.enabled')
const importVideosHttpControl = this.form.get('import.videos.http.enabled') const importVideosHttpControl = this.form.get('import.videos.http.enabled')

View File

@ -87,6 +87,7 @@
</span> </span>
<input <input
class="peertube-button primary-button"
(click)="formValidated()" type="submit" i18n-value value="Update configuration" (click)="formValidated()" type="submit" i18n-value value="Update configuration"
[disabled]="!form.valid || !hasConsistentOptions() || !isUpdateAllowed()" [disabled]="!form.valid || !hasConsistentOptions() || !isUpdateAllowed()"
> >

View File

@ -1,5 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
$form-base-input-width: 340px; $form-base-input-width: 340px;
$form-max-width: 500px; $form-max-width: 500px;
@ -33,8 +34,8 @@ input[type=number] {
input[type=number] + span { input[type=number] + span {
position: absolute; position: absolute;
top: 0.2em; top: 0.4em;
right: 2.5rem; right: 3em;
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {
display: none; display: none;
@ -42,13 +43,13 @@ input[type=number] {
} }
input[disabled] { input[disabled] {
background-color: #f9f9f9; opacity: 0.8;
pointer-events: none; pointer-events: none;
} }
} }
input[type=checkbox] { input[type=checkbox] {
@include peertube-checkbox(1px); @include peertube-checkbox;
} }
.peertube-select-container { .peertube-select-container {
@ -65,8 +66,6 @@ my-select-custom-value {
input[type=submit] { input[type=submit] {
display: flex; display: flex;
@include peertube-button;
@include orange-button;
@include margin-left(auto); @include margin-left(auto);
+ .form-error { + .form-error {
@ -131,18 +130,6 @@ ngb-tabset:not(.previews) ::ng-deep {
height: 0; height: 0;
width: 100%; width: 100%;
justify-content: right; justify-content: right;
.callout-link {
position: relative;
right: 3.3em;
top: .3em;
font-size: 90%;
color: pvar(--mainColor);
background-color: pvar(--mainBackgroundColor);
padding: 0 .3em;
@include peertube-button-link;
}
} }
my-actor-banner-edit { my-actor-banner-edit {

View File

@ -106,7 +106,7 @@
<div class="title-col"> <div class="title-col">
<h2 i18n>MODERATION & NSFW</h2> <h2 i18n>MODERATION & NSFW</h2>
<div i18n class="inner-form-description"> <div i18n class="inner-form-description">
Manage <a class="link-orange" routerLink="/admin/users">users</a> to build a moderation team. Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to build a moderation team.
</div> </div>
</div> </div>

View File

@ -166,7 +166,7 @@
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n> <span i18n>
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process live transcoding. Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process live transcoding.
Remote runners has to register on your instance first. Remote runners has to register on your instance first.
</span> </span>
</ng-container> </ng-container>

View File

@ -4,7 +4,7 @@
<div class="title-col"></div> <div class="title-col"></div>
<div class="content-col"> <div class="content-col">
<div class="callout callout-orange"> <div class="callout callout-primary">
<span i18n> <span i18n>
Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically. Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
</span> </span>
@ -12,7 +12,7 @@
<br /> <br />
<span i18n> <span i18n>
However, you may want to read <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin/configuration#vod-transcoding">our guidelines</a> before tweaking the following values. However, you may want to read <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin/configuration#vod-transcoding">our guidelines</a> before tweaking the following values.
</span> </span>
</div> </div>
</div> </div>
@ -192,7 +192,7 @@
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n> <span i18n>
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process VOD transcoding. Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process VOD transcoding.
Remote runners has to register on your instance first. Remote runners has to register on your instance first.
</span> </span>
</ng-container> </ng-container>
@ -278,7 +278,7 @@
> >
<ng-container ngProjectAs="description"> <ng-container ngProjectAs="description">
<span i18n> <span i18n>
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process studio transcoding tasks. Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process studio transcoding tasks.
Remote runners has to register on your instance first. Remote runners has to register on your instance first.
</span> </span>
</ng-container> </ng-container>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
<h1 i18n>Followers of your instance</h1>
</div>
</div>
<p-table <p-table
[value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
@ -16,7 +9,7 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>

View File

@ -1,22 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
a {
display: inline-block;
@include disable-default-a-behaviour;
&,
&:hover {
color: pvar(--mainForegroundColor);
}
span {
font-size: 80%;
color: pvar(--inputPlaceholderColor);
}
}
.action-cell { .action-cell {
my-button:first-child { my-button:first-child {
@include margin-right(10px); @include margin-right(10px);

View File

@ -32,11 +32,11 @@
<div class="form-group inputs"> <div class="form-group inputs">
<input <input
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" type="button" role="button" i18n-value value="Cancel" class="peertube-button secondary-button"
(click)="hide()" (key.enter)="hide()" (click)="hide()" (key.enter)="hide()"
> >
<input type="submit" i18n-value value="Follow" class="peertube-button orange-button" [disabled]="!form.valid" /> <input type="submit" i18n-value value="Follow" class="peertube-button primary-button" [disabled]="!form.valid" />
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
<h1 i18n>Subscriptions of your instance</h1>
</div>
</div>
<p-table <p-table
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
@ -16,7 +9,7 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>
@ -27,7 +20,7 @@
</button> </button>
</div> </div>
<div class="ms-auto d-flex gap-1"> <div class="ms-auto d-flex gap-1 flex-wrap">
<my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter> <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>

View File

@ -2,19 +2,7 @@
@use '_mixins' as *; @use '_mixins' as *;
a { a {
display: inline-block; color: pvar(--fg);
@include disable-default-a-behaviour;
&,
&:hover {
color: pvar(--mainForegroundColor);
}
span {
font-size: 80%;
color: pvar(--inputPlaceholderColor);
}
} }
my-delete-button { my-delete-button {

View File

@ -5,7 +5,7 @@ import { UserRight } from '@peertube/peertube-models'
import { FollowersListComponent } from './followers-list' import { FollowersListComponent } from './followers-list'
import { FollowingListComponent } from './following-list/following-list.component' import { FollowingListComponent } from './following-list/following-list.component'
export const FollowsRoutes: Routes = [ export const followsRoutes: Routes = [
{ {
path: 'follows', path: 'follows',
canActivate: [ UserRightGuard ], canActivate: [ UserRightGuard ],

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
<h1 i18n>Videos redundancies</h1>
</div>
</div>
<div class="admin-sub-header"> <div class="admin-sub-header">
<div class="select-filter-block"> <div class="select-filter-block">
<label for="displayType" i18n>Display</label> <label for="displayType" i18n>Display</label>

View File

@ -1,20 +1,9 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
a { a {
display: inline-block; color: pvar(--fg);
@include disable-default-a-behaviour;
&,
&:hover {
color: pvar(--mainForegroundColor);
}
span {
font-size: 80%;
color: pvar(--inputPlaceholderColor);
}
} }
.expansion-block { .expansion-block {

View File

@ -39,7 +39,7 @@ import { VideoRedundancyInformationComponent } from './video-redundancy-informat
] ]
}) })
export class VideoRedundanciesListComponent extends RestTable implements OnInit { export class VideoRedundanciesListComponent extends RestTable implements OnInit {
private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type' private static LS_DISPLAY_TYPE = 'video-redundancies-list-display-type'
videoRedundancies: VideoRedundancy[] = [] videoRedundancies: VideoRedundancy[] = []
totalRecords = 0 totalRecords = 0
@ -211,12 +211,12 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
} }
private loadSelectLocalStorage () { private loadSelectLocalStorage () {
const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE) const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LS_DISPLAY_TYPE)
if (displayType) this.displayType = displayType as VideoRedundanciesTarget if (displayType) this.displayType = displayType as VideoRedundanciesTarget
} }
private saveSelectLocalStorage () { private saveSelectLocalStorage () {
peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType) peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LS_DISPLAY_TYPE, this.displayType)
} }
private bytesToHuman (bytes: number) { private bytesToHuman (bytes: number) {

View File

@ -1,8 +1 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<h1 i18n>Reports</h1>
</div>
</div>
<my-abuse-list-table viewType="admin"></my-abuse-list-table> <my-abuse-list-table viewType="admin"></my-abuse-list-table>

View File

@ -7,7 +7,7 @@ import { UserRight } from '@peertube/peertube-models'
import { RegistrationListComponent } from './registration-list' import { RegistrationListComponent } from './registration-list'
import { WatchedWordsListAdminComponent } from './watched-words-list/watched-words-list-admin.component' import { WatchedWordsListAdminComponent } from './watched-words-list/watched-words-list-admin.component'
export const ModerationRoutes: Routes = [ export const moderationRoutes: Routes = [
{ {
path: 'moderation', path: 'moderation',
children: [ children: [
@ -90,7 +90,7 @@ export const ModerationRoutes: Routes = [
}, },
{ {
path: 'video-comments/list', path: 'video-comments/list',
redirectTo: '/admin/comments/list', redirectTo: '/admin/overview/comments/list',
pathMatch: 'full' pathMatch: 'full'
}, },

View File

@ -66,11 +66,11 @@
<div class="modal-footer inputs"> <div class="modal-footer inputs">
<input <input
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" type="button" role="button" i18n-value value="Cancel" class="peertube-button secondary-button"
(click)="hide()" (key.enter)="hide()" (click)="hide()" (key.enter)="hide()"
> >
<input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid"> <input type="submit" [value]="getSubmitValue()" class="peertube-button primary-button" [disabled]="!form.valid">
</div> </div>
</form> </form>
</ng-template> </ng-template>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
<h1 i18n>Registration requests</h1>
</div>
</div>
<p-table <p-table
[value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
@ -16,7 +9,7 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>
@ -62,7 +55,7 @@
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
i18n-label label="Actions" [actions]="registrationActions" [entry]="registration" i18n-label label="Actions" [actions]="registrationActions" [entry]="registration" buttonSize="small"
></my-action-dropdown> ></my-action-dropdown>
</td> </td>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="cross" aria-hidden="true"></my-global-icon>
<h1 i18n>Video blocks</h1>
</div>
</div>
<p-table <p-table
[value]="blocklist" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="blocklist" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
@ -48,7 +41,7 @@
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
[ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body" [ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
i18n-label label="Actions" [actions]="videoBlocklistActions" [entry]="videoBlock" i18n-label label="Actions" [actions]="videoBlocklistActions" [entry]="videoBlock" buttonSize="small"
></my-action-dropdown> ></my-action-dropdown>
</td> </td>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="eye-open" aria-hidden="true"></my-global-icon>
<h1 i18n>Instance watched words lists</h1>
</div>
</div>
<em class="d-block" i18n>Video name/description and comments that contain any of the watched words are automatically tagged with the name of the list.</em> <em class="d-block" i18n>Video name/description and comments that contain any of the watched words are automatically tagged with the name of the list.</em>
<em class="d-block mb-3" i18n>These automatic tags can be used to filter comments and videos.</em> <em class="d-block mb-3" i18n>These automatic tags can be used to filter comments and videos.</em>

View File

@ -1,2 +1 @@
export * from './video-comment-list.component' export * from './video-comment-list.component'
export * from './video-comment.routes'

View File

@ -1,12 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon class="top--1px" iconName="message-circle" aria-hidden="true"></my-global-icon>
<h1 i18n>Video comments</h1>
<my-feed [syndicationItems]="syndicationItems"></my-feed>
</div>
</div>
<em i18n>This view also shows comments from muted accounts.</em> <em i18n>This view also shows comments from muted accounts.</em>
<my-video-comment-list-admin-owner mode="admin"></my-video-comment-list-admin-owner> <my-video-comment-list-admin-owner mode="admin"></my-video-comment-list-admin-owner>

View File

@ -1,30 +0,0 @@
import { Routes } from '@angular/router'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models'
import { VideoCommentListComponent } from './video-comment-list.component'
export const commentRoutes: Routes = [
{
path: 'comments',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.SEE_ALL_COMMENTS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: VideoCommentListComponent,
data: {
meta: {
title: $localize`Comments list`
}
}
}
]
}
]

View File

@ -1,10 +1,132 @@
import { Routes } from '@angular/router' import { Routes, UrlSegment } from '@angular/router'
import { commentRoutes } from './comments' import { VideoCommentListComponent } from './comments'
import { usersRoutes } from './users' import { UserCreateComponent, UserListComponent, UserUpdateComponent } from './users'
import { videosRoutes } from './videos' import { VideoListComponent } from './videos'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models'
export const OverviewRoutes: Routes = [ function basePathRedirect ({ url }: { url: UrlSegment[] }) {
...commentRoutes, return `/admin/overview/${url.map(u => u.path).join('/')}`
...usersRoutes, }
...videosRoutes
export const overviewRoutes: Routes = [
{
path: 'comments',
pathMatch: 'prefix',
redirectTo: basePathRedirect
},
{
path: 'videos',
pathMatch: 'prefix',
redirectTo: basePathRedirect
},
{
path: 'users',
pathMatch: 'prefix',
redirectTo: basePathRedirect
},
{
path: 'overview',
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'users'
},
{
path: 'comments',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.SEE_ALL_COMMENTS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: VideoCommentListComponent,
data: {
meta: {
title: $localize`Comments list`
}
}
}
]
},
{
path: 'users',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.MANAGE_USERS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: UserListComponent,
data: {
meta: {
title: $localize`Users list`
}
}
},
{
path: 'create',
component: UserCreateComponent,
data: {
meta: {
title: $localize`Create a user`
}
}
},
{
path: 'update/:id',
component: UserUpdateComponent,
data: {
meta: {
title: $localize`Update a user`
}
}
}
]
},
{
path: 'videos',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.SEE_ALL_VIDEOS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: VideoListComponent,
data: {
meta: {
title: $localize`Videos list`
}
}
}
]
}
]
}
] ]

View File

@ -1,3 +1,2 @@
export * from './user-edit' export * from './user-edit'
export * from './user-list' export * from './user-list'
export * from './users.routes'

View File

@ -108,7 +108,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
.subscribe({ .subscribe({
next: () => { next: () => {
this.notifier.success($localize`User ${userCreate.username} created.`) this.notifier.success($localize`User ${userCreate.username} created.`)
this.router.navigate([ '/admin/users/list' ]) this.router.navigate([ '/admin/overview/users/list' ])
}, },
error: err => { error: err => {

View File

@ -1,15 +1,15 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="pt-breadcrumb"> <ol class="pt-breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a routerLink="/admin/users" i18n>Users</a> <a routerLink="/admin/overview/users" i18n>Users</a>
</li> </li>
@if (isCreation()) { @if (isCreation()) {
<li class="breadcrumb-item active" i18n>Create</li> <li class="breadcrumb-item" i18n>Create</li>
} @else { } @else {
<li class="breadcrumb-item active" i18n>Edit</li> <li class="breadcrumb-item" i18n>Edit</li>
<li class="breadcrumb-item active" aria-current="page"> <li class="breadcrumb-item" aria-current="page">
<a *ngIf="user" [routerLink]="[ '/a', user?.username ]">{{ user?.username }}</a> <a *ngIf="user" [routerLink]="[ '/a', user?.username ]">{{ user?.username }}</a>
</li> </li>
} }
@ -207,7 +207,7 @@
</my-peertube-checkbox> </my-peertube-checkbox>
</div> </div>
<input class="peertube-button orange-button" type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> <input class="peertube-button primary-button" type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form> </form>
<div class="d-none d-xxl-block col-7"> <div class="d-none d-xxl-block col-7">

View File

@ -1,6 +1,8 @@
@use 'sass:math'; @use 'sass:math';
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use 'form-mixins' as *;
@use '_button-mixins' as *;
$form-base-input-width: 340px; $form-base-input-width: 340px;
@ -54,7 +56,7 @@ my-select-custom-value {
> a, > a,
> div { > div {
padding: 20px; padding: 20px;
background: pvar(--submenuBackgroundColor); background: pvar(--bg-secondary-400);
border-radius: 4px; border-radius: 4px;
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
@ -65,14 +67,14 @@ my-select-custom-value {
.dashboard-text { .dashboard-text {
text-align: center; text-align: center;
font-size: 130%; font-size: 130%;
color: pvar(--mainForegroundColor); color: pvar(--fg);
line-height: 30px; line-height: 30px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.dashboard-label { .dashboard-label {
font-size: 90%; font-size: 90%;
color: pvar(--inputPlaceholderColor); color: pvar(--fg-300);
text-align: center; text-align: center;
} }
} }

View File

@ -6,9 +6,12 @@
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
autocomplete="new-password" autocomplete="new-password"
> >
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> <button class="btn btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
<ng-container *ngIf="!showPassword" i18n>Show</ng-container> @if (showPassword) {
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> <ng-container i18n>Hide</ng-container>
} @else {
<ng-container i18n>Show</ng-container>
}
</button> </button>
</div> </div>

View File

@ -1,5 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
input[type=text], input[type=text],
input[type=password] { input[type=password] {

View File

@ -126,7 +126,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
.subscribe({ .subscribe({
next: () => { next: () => {
this.notifier.success($localize`User ${this.user.username} updated.`) this.notifier.success($localize`User ${this.user.username} updated.`)
this.router.navigate([ '/admin/users/list' ]) this.router.navigate([ '/admin/overview/users/list' ])
}, },
error: err => { error: err => {

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
<h1 i18n>Users</h1>
</div>
</div>
<p-table <p-table
[value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
@ -15,12 +8,12 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>
<a *ngIf="!isInSelectionMode()" class="peertube-create-button" routerLink="/admin/users/create"> <a *ngIf="!isInSelectionMode()" class="peertube-create-button" routerLink="/admin/overview/users/create">
<my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
<ng-container i18n>Create user</ng-container> <ng-container i18n>Create user</ng-container>
</a> </a>
@ -97,7 +90,7 @@
<td class="action-cell"> <td class="action-cell">
<my-user-moderation-dropdown <my-user-moderation-dropdown
*ngIf="!isInSelectionMode()" [user]="user" [account]="user.accountMutedStatus" [displayOptions]="userModerationDisplayOptions" *ngIf="!isInSelectionMode()" [user]="user" [account]="user.accountMutedStatus" [displayOptions]="userModerationDisplayOptions"
container="body" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> container="body" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()" buttonSize="small">
</my-user-moderation-dropdown> </my-user-moderation-dropdown>
</td> </td>

View File

@ -3,11 +3,6 @@
@use '_mixins' as *; @use '_mixins' as *;
@use 'bootstrap/scss/functions' as *; @use 'bootstrap/scss/functions' as *;
.sub-title-container my-global-icon {
position: relative;
top: -2px;
}
.banned-info { .banned-info {
font-style: italic; font-style: italic;
} }

View File

@ -71,7 +71,7 @@ type UserForList = User & {
] ]
}) })
export class UserListComponent extends RestTable <User> implements OnInit { export class UserListComponent extends RestTable <User> implements OnInit {
private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' private static readonly LS_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
@ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
@ -184,7 +184,7 @@ export class UserListComponent extends RestTable <User> implements OnInit {
} }
loadSelectedColumns () { loadSelectedColumns () {
const result = this.peertubeLocalStorage.getItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY) const result = this.peertubeLocalStorage.getItem(UserListComponent.LS_SELECTED_COLUMNS_KEY)
if (result) { if (result) {
try { try {
@ -201,7 +201,7 @@ export class UserListComponent extends RestTable <User> implements OnInit {
} }
saveSelectedColumns () { saveSelectedColumns () {
this.peertubeLocalStorage.setItem(UserListComponent.LOCAL_STORAGE_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns)) this.peertubeLocalStorage.setItem(UserListComponent.LS_SELECTED_COLUMNS_KEY, JSON.stringify(this.selectedColumns))
} }
getIdentifier () { getIdentifier () {

View File

@ -1,49 +0,0 @@
import { Routes } from '@angular/router'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models'
import { UserCreateComponent, UserUpdateComponent } from './user-edit'
import { UserListComponent } from './user-list'
export const usersRoutes: Routes = [
{
path: 'users',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.MANAGE_USERS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: UserListComponent,
data: {
meta: {
title: $localize`Users list`
}
}
},
{
path: 'create',
component: UserCreateComponent,
data: {
meta: {
title: $localize`Create a user`
}
}
},
{
path: 'update/:id',
component: UserUpdateComponent,
data: {
meta: {
title: $localize`Update a user`
}
}
}
]
}
]

View File

@ -1,3 +1,2 @@
export * from './video-admin.service' export * from './video-admin.service'
export * from './video-list.component' export * from './video-list.component'
export * from './video.routes'

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
<h1 i18n>Videos</h1>
</div>
</div>
<p-table <p-table
[value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
@ -16,7 +9,7 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>
@ -63,7 +56,7 @@
<td class="action-cell"> <td class="action-cell">
<my-video-actions-dropdown <my-video-actions-dropdown
placement="bottom auto" buttonDirection="horizontal" buttonStyled="true" [video]="video" [displayOptions]="videoActionsOptions" placement="bottom auto" buttonDirection="horizontal" buttonStyled="true" [video]="video" [displayOptions]="videoActionsOptions"
(videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()" (transcodingCreated)="reloadData()" (videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()" (transcodingCreated)="reloadData()" buttonSize="small"
></my-video-actions-dropdown> ></my-video-actions-dropdown>
</td> </td>

View File

@ -29,6 +29,8 @@ my-embed {
.right-form { .right-form {
display: flex; display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
> *:not(:last-child) { > *:not(:last-child) {
@include margin-right(10px); @include margin-right(10px);

View File

@ -1,30 +0,0 @@
import { Routes } from '@angular/router'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models'
import { VideoListComponent } from './video-list.component'
export const videosRoutes: Routes = [
{
path: 'videos',
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.SEE_ALL_VIDEOS
},
children: [
{
path: '',
redirectTo: 'list',
pathMatch: 'full'
},
{
path: 'list',
component: VideoListComponent,
data: {
meta: {
title: $localize`Videos list`
}
}
}
]
}
]

View File

@ -1,5 +1,3 @@
<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation>
<div class="no-results" *ngIf="pagination.totalItems === 0"> <div class="no-results" *ngIf="pagination.totalItems === 0">
{{ getNoResultMessage() }} {{ getNoResultMessage() }}
</div> </div>
@ -8,10 +6,10 @@
<ng-container *ngFor="let plugin of plugins"> <ng-container *ngFor="let plugin of plugins">
<my-plugin-card [plugin]="plugin" [version]="plugin.version" [pluginType]="pluginType"> <my-plugin-card [plugin]="plugin" [version]="plugin.version" [pluginType]="pluginType">
<div ngProjectAs="buttons"> <div ngProjectAs="buttons">
<my-edit-button <my-button
*ngIf="!isTheme(plugin)" [ptRouterLink]="getShowRouterLink(plugin)" label="Settings" i18n-label *ngIf="!isTheme(plugin)" [ptRouterLink]="getShowRouterLink(plugin)" label="Settings" i18n-label
[responsiveLabel]="true" [responsiveLabel]="true" icon="config"
></my-edit-button> ></my-button>
<my-button <my-button
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)" class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"

View File

@ -1,4 +1,4 @@
import { Subject } from 'rxjs' import { NgFor, NgIf } from '@angular/common'
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
@ -6,13 +6,11 @@ import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@ap
import { PluginService } from '@app/core/plugins/plugin.service' import { PluginService } from '@app/core/plugins/plugin.service'
import { compareSemVer } from '@peertube/peertube-core-utils' import { compareSemVer } from '@peertube/peertube-core-utils'
import { PeerTubePlugin, PluginType, PluginType_Type } from '@peertube/peertube-models' import { PeerTubePlugin, PluginType, PluginType_Type } from '@peertube/peertube-models'
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component' import { Subject } from 'rxjs'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component' import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
import { PluginCardComponent } from '../shared/plugin-card.component'
import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive' import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive'
import { NgIf, NgFor } from '@angular/common' import { PluginCardComponent } from '../shared/plugin-card.component'
import { PluginNavigationComponent } from '../shared/plugin-navigation.component'
@Component({ @Component({
selector: 'my-plugin-list-installed', selector: 'my-plugin-list-installed',
@ -20,12 +18,10 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component
styleUrls: [ './plugin-list-installed.component.scss' ], styleUrls: [ './plugin-list-installed.component.scss' ],
standalone: true, standalone: true,
imports: [ imports: [
PluginNavigationComponent,
NgIf, NgIf,
InfiniteScrollerDirective, InfiniteScrollerDirective,
NgFor, NgFor,
PluginCardComponent, PluginCardComponent,
EditButtonComponent,
ButtonComponent, ButtonComponent,
DeleteButtonComponent DeleteButtonComponent
] ]
@ -194,7 +190,7 @@ export class PluginListInstalledComponent implements OnInit {
} }
getShowRouterLink (plugin: PeerTubePlugin) { getShowRouterLink (plugin: PeerTubePlugin) {
return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ] return [ '/admin', 'settings', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ]
} }
getPluginOrThemeHref (name: string) { getPluginOrThemeHref (name: string) {

View File

@ -1,23 +1,27 @@
<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation>
<my-alert class="mt-3" type="primary" i18n *ngIf="pluginInstalled"> <my-alert class="mt-3" type="primary" i18n *ngIf="pluginInstalled">
To load your new installed plugins or themes, refresh the page. To load your new installed plugins or themes, refresh the page.
</my-alert> </my-alert>
<div class="result-and-search"> <div class="result-and-search">
<ng-container *ngIf="!search"> @if (!search) {
<div>
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon> <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
<ng-container *ngIf="!isThemeSearch()" i18n>Popular plugins</ng-container>
<ng-container *ngIf="isThemeSearch()" i18n>Popular themes</ng-container>
</ng-container>
<ng-container *ngIf="search && !isSearching"> @if (isThemeSearch()) {
<ng-container i18n>Popular themes</ng-container>
} @else {
<ng-container i18n>Popular plugins</ng-container>
}
</div>
}
@if (search && !isSearching) {
<my-global-icon iconName="search"></my-global-icon> <my-global-icon iconName="search"></my-global-icon>
<ng-container i18n> <ng-container i18n>
{{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}"
</ng-container> </ng-container>
</ng-container> }
<div class="search-bar"> <div class="search-bar">
<input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." myAutofocus /> <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." myAutofocus />
@ -34,13 +38,15 @@
<div ngProjectAs="badges"> <div ngProjectAs="badges">
<span i18n *ngIf="plugin.installed" class="pt-badge badge-success">Installed</span> <span i18n *ngIf="plugin.installed" class="pt-badge badge-success">Installed</span>
<span *ngIf="plugin.official" class="pt-badge badge-primary" i18n i18n-title title="This plugin is developed by Framasoft, the not-for-profit that develops PeerTube"> <span
Official *ngIf="plugin.official" class="pt-badge badge-primary"
</span> i18n i18n-title title="This plugin is developed by Framasoft, the not-for-profit that develops PeerTube"
>Official</span>
<span *ngIf="plugin.recommended" class="pt-badge badge-primary" i18n i18n-title title="This plugin is recommended by Framasoft, the not-for-profit that develops PeerTube"> <span
Recommended *ngIf="plugin.recommended" class="pt-badge badge-primary"
</span> i18n i18n-title title="This plugin is recommended by Framasoft, the not-for-profit that develops PeerTube"
>Recommended</span>
</div> </div>
<div ngProjectAs="buttons"> <div ngProjectAs="buttons">

View File

@ -1,20 +1,23 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
.result-and-search { .result-and-search {
font-size: 22px; font-size: 22px;
font-weight: 600; font-weight: 600;
margin: 30px 0 15px; margin: 30px 0 15px;
display: flex; display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
justify-content: space-between;
my-global-icon { my-global-icon {
@include global-icon-size(24px);
@include margin-right(5px); @include margin-right(5px);
} }
} }
.search-bar { .search-bar {
@include margin-left(auto);
input { input {
@include peertube-input-text(500px); @include peertube-input-text(500px);
} }

View File

@ -14,7 +14,6 @@ import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-bu
import { AutofocusDirective } from '../../../shared/shared-main/common/autofocus.directive' import { AutofocusDirective } from '../../../shared/shared-main/common/autofocus.directive'
import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive' import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive'
import { PluginCardComponent } from '../shared/plugin-card.component' import { PluginCardComponent } from '../shared/plugin-card.component'
import { PluginNavigationComponent } from '../shared/plugin-navigation.component'
@Component({ @Component({
selector: 'my-plugin-search', selector: 'my-plugin-search',
@ -22,7 +21,6 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component
styleUrls: [ './plugin-search.component.scss' ], styleUrls: [ './plugin-search.component.scss' ],
standalone: true, standalone: true,
imports: [ imports: [
PluginNavigationComponent,
NgIf, NgIf,
GlobalIconComponent, GlobalIconComponent,
AutofocusDirective, AutofocusDirective,
@ -138,7 +136,7 @@ export class PluginSearchComponent implements OnInit {
} }
getShowRouterLink (plugin: PeerTubePluginIndex) { getShowRouterLink (plugin: PeerTubePluginIndex) {
return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, this.pluginType) ] return [ '/admin', 'settings', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, this.pluginType) ]
} }
isThemeSearch () { isThemeSearch () {

View File

@ -1,6 +1,6 @@
<ng-container *ngIf="plugin"> <ng-container *ngIf="plugin">
<h2> <h2 class="mb-4">
<ng-container>{{ pluginTypeLabel }}</ng-container> <ng-container>{{ pluginTypeLabel }}</ng-container>
{{ plugin.name }} {{ plugin.name }}
</h2> </h2>
@ -10,7 +10,7 @@
<my-dynamic-form-field [hidden]="isSettingHidden(setting)" [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field> <my-dynamic-form-field [hidden]="isSettingHidden(setting)" [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
</div> </div>
<input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid"> <input class="peertube-button primary-button mt-3" type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
</form> </form>
<div *ngIf="!hasRegisteredSettings()" i18n class="no-settings"> <div *ngIf="!hasRegisteredSettings()" i18n class="no-settings">

View File

@ -1,14 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
h2 {
margin-bottom: 20px;
}
input[type=submit],
button {
margin-top: 10px;
@include peertube-button;
@include orange-button;
}

View File

@ -15,7 +15,6 @@ import { BuildFormArgument } from '@app/shared/form-validators/form-validator.mo
@Component({ @Component({
selector: 'my-plugin-show-installed', selector: 'my-plugin-show-installed',
templateUrl: './plugin-show-installed.component.html', templateUrl: './plugin-show-installed.component.html',
styleUrls: [ './plugin-show-installed.component.scss' ],
standalone: true, standalone: true,
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgFor, DynamicFormFieldComponent ] imports: [ NgIf, FormsModule, ReactiveFormsModule, NgFor, DynamicFormFieldComponent ]
}) })

View File

@ -5,7 +5,7 @@ import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-in
import { UserRightGuard } from '@app/core' import { UserRightGuard } from '@app/core'
import { UserRight } from '@peertube/peertube-models' import { UserRight } from '@peertube/peertube-models'
export const PluginsRoutes: Routes = [ export const pluginsRoutes: Routes = [
{ {
path: 'plugins', path: 'plugins',
canActivate: [ UserRightGuard ], canActivate: [ UserRightGuard ],
@ -15,7 +15,7 @@ export const PluginsRoutes: Routes = [
children: [ children: [
{ {
path: '', path: '',
redirectTo: 'list-installed', redirectTo: 'list-installed?pluginType=1',
pathMatch: 'full' pathMatch: 'full'
}, },
{ {

View File

@ -5,12 +5,12 @@
<span class="plugin-version">{{ version }}</span> <span class="plugin-version">{{ version }}</span>
<a class="plugin-icon" target="_blank" rel="noopener noreferrer" [href]="plugin.homepage" i18n-title title="Plugin homepage (new window)"> <a class="plugin-icon" target="_blank" rel="noopener noreferrer" [href]="plugin.homepage" i18n-title title="Homepage (new window)">
<my-global-icon iconName="home"></my-global-icon> <my-global-icon iconName="home"></my-global-icon>
</a> </a>
<a class="plugin-icon" target="_blank" rel="noopener noreferrer" [href]="getPluginOrThemeHref(plugin.name)" i18n-title title="Plugin homepage (new window)"> <a class="plugin-icon" target="_blank" rel="noopener noreferrer" [href]="getPluginOrThemeHref(plugin.name)" i18n-title title="NPM page (new window)">
<my-global-icon iconName="npm"></my-global-icon> <my-global-icon iconName="registry"></my-global-icon>
</a> </a>
<ng-content select="badges"></ng-content> <ng-content select="badges"></ng-content>

View File

@ -3,7 +3,7 @@
.plugin { .plugin {
margin: 15px 0; margin: 15px 0;
background-color: pvar(--submenuBackgroundColor); background-color: pvar(--bg-secondary-400);
} }
.first-row { .first-row {
@ -25,10 +25,10 @@
@include margin-left(10px); @include margin-left(10px);
my-global-icon { my-global-icon {
@include apply-svg-color(pvar(--greyForegroundColor)); color: pvar(--fg-400);
&[iconName=npm] { &[iconName=npm] {
@include fill-svg-color(pvar(--greyForegroundColor)); @include fill-svg-color(pvar(--fg-400));
} }
} }
} }
@ -51,11 +51,6 @@
} }
} }
.action-button {
@include peertube-button-link;
@include button-with-icon(21px, 0, -2px);
}
@media screen and (max-width: $small-view) { @media screen and (max-width: $small-view) {
.first-row { .first-row {
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,11 +0,0 @@
<div class="root">
<div class="btn-group select-button" role="group" i18n-aria-label aria-label="Navigate between installed plugins and themes or find new ones">
<a i18n routerLink="/admin/plugins/list-installed" [queryParams]="{ pluginType: pluginType }" routerLinkActive="active">Installed</a>
<a i18n routerLink="/admin/plugins/search" [queryParams]="{ pluginType: pluginType }" routerLinkActive="active">Search</a>
</div>
<div class="btn-group select-button" role="group" i18n-aria-label aria-label="Navigate between plugins and themes">
<a i18n [ngClass]="{ active: pluginType === 1 }" routerLink="." [queryParams]="{ pluginType: 1 }" queryParamsHandling="merge" class="">Plugins</a>
<a i18n [ngClass]="{ active: pluginType === 2 }" routerLink="." [queryParams]="{ pluginType: 2 }" queryParamsHandling="merge" class="">Themes</a>
</div>
</div>

View File

@ -1,11 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
.root {
display: flex;
justify-content: center;
}
.btn-group:not(:last-child) {
@include margin-right(15px);
}

View File

@ -1,15 +0,0 @@
import { Component, Input } from '@angular/core'
import { PluginType_Type } from '@peertube/peertube-models'
import { NgClass } from '@angular/common'
import { RouterLink, RouterLinkActive } from '@angular/router'
@Component({
selector: 'my-plugin-navigation',
templateUrl: './plugin-navigation.component.html',
styleUrls: [ './plugin-navigation.component.scss' ],
standalone: true,
imports: [ RouterLink, RouterLinkActive, NgClass ]
})
export class PluginNavigationComponent {
@Input() pluginType: PluginType_Type
}

View File

@ -1,18 +1,12 @@
import { Routes } from '@angular/router' import { Route, Routes, UrlSegment } from '@angular/router'
import { ConfigRoutes, EditConfigurationService } from '@app/+admin/config' import { configRoutes, EditConfigurationService } from '@app/+admin/config'
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' import { moderationRoutes } from '@app/+admin/moderation/moderation.routes'
import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' import { pluginsRoutes } from '@app/+admin/plugins/plugins.routes'
import { DebugService, JobService, LogsService, RunnerService, SystemRoutes } from '@app/+admin/system' import { DebugService, JobService, LogsService, RunnerService, systemRoutes } from '@app/+admin/system'
import { AdminComponent } from './admin.component'
import { FollowsRoutes } from './follows'
import { OverviewRoutes, VideoAdminService } from './overview'
import { AdminRegistrationService } from './moderation/registration-list'
import { PluginApiService } from './plugins/shared/plugin-api.service'
import { ConfigService } from './config/shared/config.service'
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service' import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
import { DynamicElementService } from '@app/shared/shared-custom-markup/dynamic-element.service' import { DynamicElementService } from '@app/shared/shared-custom-markup/dynamic-element.service'
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service' import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
import { AbuseService } from '@app/shared/shared-moderation/abuse.service' import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service' import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
import { BulkService } from '@app/shared/shared-moderation/bulk.service' import { BulkService } from '@app/shared/shared-moderation/bulk.service'
@ -24,11 +18,17 @@ import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service' import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service' import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
import { WatchedWordsListService } from '@app/shared/standalone-watched-words/watched-words-list.service' import { WatchedWordsListService } from '@app/shared/standalone-watched-words/watched-words-list.service'
import { AdminModerationComponent } from './admin-moderation.component'
import { AdminOverviewComponent } from './admin-overview.component'
import { AdminSettingsComponent } from './admin-settings.component'
import { ConfigService } from './config/shared/config.service'
import { followsRoutes } from './follows'
import { AdminRegistrationService } from './moderation/registration-list'
import { overviewRoutes, VideoAdminService } from './overview'
import { PluginApiService } from './plugins/shared/plugin-api.service'
export default [ const commonConfig = {
{
path: '', path: '',
component: AdminComponent,
providers: [ providers: [
BlocklistService, BlocklistService,
UserAdminService, UserAdminService,
@ -55,20 +55,108 @@ export default [
SearchService, SearchService,
VideoPlaylistService, VideoPlaylistService,
WatchedWordsListService WatchedWordsListService
]
}
function isOverviewRoute (segments: UrlSegment[]) {
if (segments.length === 0) return false
const rootPath = segments[0].path
return overviewRoutes.some(r => r.path === rootPath || r.path.startsWith(`${rootPath}/`))
}
function isModerationRoute (segments: UrlSegment[]) {
if (segments.length === 0) return false
const rootPath = segments[0].path
return moderationRoutes.some(r => r.path === rootPath || r.path.startsWith(`${rootPath}/`))
}
function baseSettingsPathRedirect ({ url }: { url: UrlSegment[] }) {
return `/admin/settings/${url.map(u => u.path).join('/')}`
}
export default [
{
path: '',
pathMatch: 'full',
redirectTo: 'overview'
},
{
...commonConfig,
component: AdminModerationComponent,
canMatch: [
(_route: Route, segments: UrlSegment[]) => {
return isModerationRoute(segments)
}
], ],
children: moderationRoutes
},
{
...commonConfig,
component: AdminOverviewComponent,
canMatch: [
(_route: Route, segments: UrlSegment[]) => {
return isOverviewRoute(segments)
}
],
children: overviewRoutes
},
{
path: 'config',
pathMatch: 'prefix',
redirectTo: baseSettingsPathRedirect
},
{
path: 'follows',
pathMatch: 'prefix',
redirectTo: baseSettingsPathRedirect
},
{
path: 'system',
pathMatch: 'prefix',
redirectTo: baseSettingsPathRedirect
},
{
path: 'plugins',
pathMatch: 'prefix',
redirectTo: baseSettingsPathRedirect
},
{
...commonConfig,
path: 'settings',
component: AdminSettingsComponent,
canMatch: [
(_route: Route, segments: UrlSegment[]) => {
return !isOverviewRoute(segments) && !isModerationRoute(segments)
}
],
children: [ children: [
{ {
path: '', path: '',
redirectTo: 'users', redirectTo: 'config',
pathMatch: 'full' pathMatch: 'full'
}, },
...FollowsRoutes, ...configRoutes,
...OverviewRoutes, ...followsRoutes,
...ModerationRoutes, ...systemRoutes,
...SystemRoutes, ...pluginsRoutes
...ConfigRoutes,
...PluginsRoutes
] ]
} }
] satisfies Routes ] satisfies Routes

View File

@ -2,7 +2,7 @@
@use '_mixins' as *; @use '_mixins' as *;
a { a {
color: pvar(--mainForegroundColor); color: pvar(--fg);
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<h1 i18n>Debug</h1>
</div>
</div>
<h2 class="fs-5" i18n>IP address</h2> <h2 class="fs-5" i18n>IP address</h2>
<p i18n>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p> <p i18n>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="circle-tick" aria-hidden="true"></my-global-icon>
<h1 i18n>Local jobs</h1>
</div>
</div>
<div class="admin-sub-header"> <div class="admin-sub-header">
<div class="select-filter-block"> <div class="select-filter-block">
<label for="jobType" i18n>Job type</label> <label for="jobType" i18n>Job type</label>

View File

@ -1,5 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use 'form-mixins' as *;
.select-job-state { .select-job-state {
display: block; display: block;

View File

@ -38,8 +38,8 @@ import { JobService } from './job.service'
] ]
}) })
export class JobsComponent extends RestTable implements OnInit { export class JobsComponent extends RestTable implements OnInit {
private static LOCAL_STORAGE_STATE = 'jobs-list-state' private static LS_STATE = 'jobs-list-state'
private static LOCAL_STORAGE_TYPE = 'jobs-list-type' private static LS_TYPE = 'jobs-list-type'
jobState?: JobStateClient jobState?: JobStateClient
jobStates: JobStateClient[] = [ 'all', 'active', 'completed', 'failed', 'waiting', 'delayed' ] jobStates: JobStateClient[] = [ 'all', 'active', 'completed', 'failed', 'waiting', 'delayed' ]
@ -175,15 +175,15 @@ export class JobsComponent extends RestTable implements OnInit {
} }
private loadJobStateAndType () { private loadJobStateAndType () {
const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE) const state = peertubeLocalStorage.getItem(JobsComponent.LS_STATE)
if (state) this.jobState = state as JobState if (state) this.jobState = state as JobState
const jobType = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE) const jobType = peertubeLocalStorage.getItem(JobsComponent.LS_TYPE)
if (jobType) this.jobType = jobType as JobType if (jobType) this.jobType = jobType as JobType
} }
private saveJobStateAndType () { private saveJobStateAndType () {
peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState) peertubeLocalStorage.setItem(JobsComponent.LS_STATE, this.jobState)
peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType) peertubeLocalStorage.setItem(JobsComponent.LS_TYPE, this.jobType)
} }
} }

View File

@ -1,10 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
<h1 i18n>Logs</h1>
</div>
</div>
<div class="header"> <div class="header">
<div> <div>

View File

@ -1,5 +1,6 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_form-mixins' as *;
.logs { .logs {
font-family: monospace; font-family: monospace;
@ -92,7 +93,7 @@
my-copy-button { my-copy-button {
position: absolute; position: absolute;
right: 5px; right: 5px;
background: pvar(--mainBackgroundHoverColor); background: pvar(--bg-secondary-300);
} }
@include on-small-main-col { @include on-small-main-col {

View File

@ -31,7 +31,7 @@ import { LogsService } from './logs.service'
] ]
}) })
export class LogsComponent implements OnInit { export class LogsComponent implements OnInit {
private static LOCAL_STORAGE_LOG_TYPE_CHOICE_KEY = 'admin-logs-log-type-choice' private static LS_LOG_TYPE_CHOICE_KEY = 'admin-logs-log-type-choice'
@ViewChild('logsElement', { static: true }) logsElement: ElementRef<HTMLElement> @ViewChild('logsElement', { static: true }) logsElement: ElementRef<HTMLElement>
@ViewChild('logsContent', { static: true }) logsContent: ElementRef<HTMLElement> @ViewChild('logsContent', { static: true }) logsContent: ElementRef<HTMLElement>
@ -69,7 +69,7 @@ export class LogsComponent implements OnInit {
refresh () { refresh () {
this.logs = [] this.logs = []
this.localStorage.setItem(LogsComponent.LOCAL_STORAGE_LOG_TYPE_CHOICE_KEY, this.logType) this.localStorage.setItem(LogsComponent.LS_LOG_TYPE_CHOICE_KEY, this.logType)
this.load() this.load()
} }
@ -175,7 +175,7 @@ export class LogsComponent implements OnInit {
} }
private loadPreviousChoices () { private loadPreviousChoices () {
this.logType = this.localStorage.getItem(LogsComponent.LOCAL_STORAGE_LOG_TYPE_CHOICE_KEY) this.logType = this.localStorage.getItem(LogsComponent.LS_LOG_TYPE_CHOICE_KEY)
if (this.logType !== 'standard' && this.logType !== 'audit') this.logType = 'audit' if (this.logType !== 'standard' && this.logType !== 'audit') this.logType = 'audit'
} }

View File

@ -1,17 +1,3 @@
<div class="sub-title-container">
<div class="sub-title">
<my-global-icon class="top--1px" iconName="globe" aria-hidden="true"></my-global-icon>
<h1 i18n>Runner jobs</h1>
</div>
<div>
<a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
<ng-container i18n>Remote runners</ng-container>
</a>
</div>
</div>
<p-table <p-table
[value]="runnerJobs" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="runnerJobs" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
@ -46,13 +32,13 @@
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-action-dropdown <my-action-dropdown
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
[actions]="bulkActions" [entry]="selectedRows" [actions]="bulkActions" [entry]="selectedRows"
> >
</my-action-dropdown> </my-action-dropdown>
</div> </div>
<div class="ms-auto d-flex"> <div class="ms-auto d-flex flex-wrap gap-2">
<my-advanced-input-filter class="me-2" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> <my-advanced-input-filter class="me-2" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
@ -73,7 +59,7 @@
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
placement="bottom-right top-right left auto" container="body" placement="bottom-right top-right left auto" container="body"
i18n-label label="Actions" [actions]="actions" [entry]="runnerJob" i18n-label label="Actions" [actions]="actions" [entry]="runnerJob" buttonSize="small"
></my-action-dropdown> ></my-action-dropdown>
</td> </td>

View File

@ -1,17 +1,3 @@
<div class="sub-title-container">
<span class="sub-title">
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
<h1 i18n>Remote runners</h1>
</span>
<div>
<a routerLink="/admin/system/runners/registration-tokens-list" class="peertube-button-link peertube-button-icon grey-button">
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<ng-container i18n>Runner registration tokens</ng-container>
</a>
</div>
</div>
<p-table <p-table
[value]="runners" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="runners" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
@ -36,7 +22,7 @@
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
placement="bottom-right top-right left auto" container="body" placement="bottom-right top-right left auto" container="body"
i18n-label label="Actions" [actions]="actions" [entry]="runner" i18n-label label="Actions" [actions]="actions" [entry]="runner" buttonSize="small"
></my-action-dropdown> ></my-action-dropdown>
</td> </td>

View File

@ -1,17 +1,3 @@
<div class="sub-title-container">
<span class="sub-title">
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<h1 i18n>Runner registration tokens</h1>
</span>
<div>
<a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
<ng-container i18n>Remote runners</ng-container>
</a>
</div>
</div>
<p-table <p-table
[value]="registrationTokens" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" [value]="registrationTokens" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
@ -32,7 +18,7 @@
<ng-template pTemplate="caption"> <ng-template pTemplate="caption">
<div class="caption"> <div class="caption">
<div class="left-buttons"> <div class="left-buttons">
<my-button theme="orange" i18n-label label="Generate token" icon="add" (click)="generateToken()"></my-button> <my-button theme="primary" i18n-label label="Generate token" icon="add" (click)="generateToken()"></my-button>
</div> </div>
</div> </div>
</ng-template> </ng-template>
@ -42,7 +28,7 @@
<td class="action-cell"> <td class="action-cell">
<my-action-dropdown <my-action-dropdown
placement="bottom-right top-right left auto" container="body" placement="bottom-right top-right left auto" container="body"
i18n-label label="Actions" [actions]="actions" [entry]="registrationToken" i18n-label label="Actions" [actions]="actions" [entry]="registrationToken" buttonSize="small"
></my-action-dropdown> ></my-action-dropdown>
</td> </td>

View File

@ -6,7 +6,7 @@ import { JobsComponent } from './jobs/jobs.component'
import { LogsComponent } from './logs' import { LogsComponent } from './logs'
import { RunnersRoutes } from './runners' import { RunnersRoutes } from './runners'
export const SystemRoutes: Routes = [ export const systemRoutes: Routes = [
{ {
path: 'system', path: 'system',
children: [ children: [

View File

@ -12,7 +12,7 @@
} }
</div> </div>
<my-login-link className="peertube-button-big-link orange-button mt-5"></my-login-link> <my-login-link className="peertube-button-big-link primary-button mt-5"></my-login-link>
</div> </div>
} @else if (status === 403) { } @else if (status === 403) {
<div class="box"> <div class="box">

Some files were not shown because too many files have changed in this diff Show More