Menu blocks

Chocobozzz 2024-11-08 10:36:22 +01:00
parent 8636708004
commit f2fb81fa80
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
48 changed files with 535 additions and 788 deletions

View File

@ -156,13 +156,24 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
}
buildLandingPageOptions () {
this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
.links
.map(o => ({
id: o.path,
label: o.label,
description: o.path
}))
let links: { label: string, path: string }[] = []
if (this.serverConfig.homepage.enabled) {
links.push({ label: $localize`Home`, path: '/home' })
}
links = links.concat([
{ label: $localize`Discover`, path: '/videos/overview' },
{ label: $localize`Trending`, path: '/videos/trending' },
{ label: $localize`Recently added`, path: '/videos/recently-added' },
{ label: $localize`Local videos`, path: '/videos/local' }
])
this.defaultLandingPageOptions = links.map(o => ({
id: o.path,
label: o.label,
description: o.path
}))
}
getDefaultThemeLabel () {

View File

@ -1,13 +1,10 @@
import { Routes } from '@angular/router'
import { ErrorPageComponent } from './error-page.component'
import { MenuGuards } from '@app/core'
export default [
{
path: '',
component: ErrorPageComponent,
canActivate: [ MenuGuards.close(true) ],
canDeactivate: [ MenuGuards.open(true) ],
data: {
meta: {
title: $localize`Not found`

View File

@ -45,15 +45,13 @@ export class MyLibraryComponent implements OnInit {
{
label: $localize`Channels`,
routerLink: '/my-library/video-channels'
}
]
},
if (this.user.canSeeVideosLink) {
this.menuEntries.push({
{
label: $localize`Videos`,
routerLink: '/my-library/videos'
})
}
}
]
this.menuEntries = this.menuEntries.concat([
{

View File

@ -6,7 +6,7 @@
}
</ng-template>
<ng-container *ngIf="user.isUploadDisabled()">
<ng-container *ngIf="user.hasUploadDisabled()">
<my-alert class="upload-message upload-disabled" type="warning">
<div>{{ uploadMessages?.noQuota }}</div>
<ng-template [ngTemplateOutlet]="AlertButtons"></ng-template>
@ -17,7 +17,7 @@
</div>
</ng-container>
<ng-container *ngIf="!user.isUploadDisabled()">
<ng-container *ngIf="!user.hasUploadDisabled()">
<my-alert rounded="false" *ngIf="user.isAutoBlocked(serverConfig)" class="upload-message auto-blocked" type="warning">
<div>{{ uploadMessages?.autoBlock }}</div>
<ng-template [ngTemplateOutlet]="AlertButtons" *ngIf="!hasNoQuotaLeft && !hasNoQuotaLeftDaily"></ng-template>
@ -42,7 +42,7 @@
</my-alert>
</ng-container>
<div *ngIf="!user.isUploadDisabled()" class="margin-content">
<div *ngIf="!user.hasUploadDisabled()" class="margin-content">
<my-user-quota *ngIf="!isInSecondStep() || secondStepType === 'go-live'"></my-user-quota>
<div class="title-page" *ngIf="isInSecondStep()">

View File

@ -8,14 +8,14 @@
class="peertube-container"
[ngClass]="{ 'user-logged-in': isUserLoggedIn(), 'user-not-logged-in': !isUserLoggedIn(), 'hotkeys-modal-opened': hotkeysModalOpened }"
>
<header class="root-header mb-2">
<header class="root-header">
<my-header class="w-100"></my-header>
</header>
<div class="sub-header-container">
<my-menu id="left-menu" role="navigation" aria-label="Main menu" i18n-ariaLabel [hidden]="!menu.isMenuDisplayed"></my-menu>
<my-menu id="left-menu" role="navigation" aria-label="Main menu" i18n-ariaLabel></my-menu>
<main #mainContent tabindex="-1" id="content" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }">
<main #mainContent tabindex="-1" id="content" class="main-col" [ngClass]="{ expanded: menu.isMenuCollapsed }">
<div class="main-row">

View File

@ -36,6 +36,7 @@
position: fixed;
top: 0;
width: 100%;
background: pvar(--mainBackgroundColor);
}
.broadcast-message {

View File

@ -196,7 +196,7 @@ export class AppComponent implements OnInit, AfterViewInit {
eventsObs.pipe(
filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
filter(() => this.screenService.isInSmallView() || this.screenService.isInTouchScreen())
).subscribe(() => this.menu.setMenuDisplay(false)) // User clicked on a link in the menu, change the page
).subscribe(() => this.menu.setMenuCollapsed(true)) // User clicked on a link in the menu, change the page
// Handle lazy loaded module
eventsObs.pipe(

View File

@ -1,5 +1,4 @@
import { Routes, UrlMatchResult, UrlSegment } from '@angular/router'
import { MenuGuards } from '@app/core/routing/menu-guard.service'
import { POSSIBLE_LOCALES } from '@peertube/peertube-core-utils'
import { MetaGuard } from './core'
import { EmptyComponent } from './empty.component'
@ -10,8 +9,6 @@ import { ActorRedirectGuard } from './shared/shared-main/router/actor-redirect-g
const routes: Routes = [
{
path: 'admin',
canActivate: [ MenuGuards.close() ],
canDeactivate: [ MenuGuards.open() ],
loadChildren: () => import('./+admin/routes'),
canActivateChild: [ MetaGuard ]
},

View File

@ -1,14 +1,11 @@
import { Observable, of } from 'rxjs'
import { map } from 'rxjs/operators'
import { User } from '@app/core/users/user.model'
import { hasUserRight } from '@peertube/peertube-core-utils'
import {
MyUser as ServerMyUserModel,
MyUserSpecialPlaylist,
MyUser as ServerMyUserModel,
User as ServerUserModel,
UserRightType,
UserRole,
UserVideoQuota
UserRole
} from '@peertube/peertube-models'
import { OAuthUserTokens } from '@root-helpers/users'
@ -16,8 +13,6 @@ export class AuthUser extends User implements ServerMyUserModel {
oauthTokens: OAuthUserTokens
specialPlaylists: MyUserSpecialPlaylist[]
canSeeVideosLink = true
constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<OAuthUserTokens>) {
super(userHash)
@ -54,26 +49,4 @@ export class AuthUser extends User implements ServerMyUserModel {
// I'm a moderator: I can only manage users
return user.role.id === UserRole.USER
}
computeCanSeeVideosLink (quotaObservable: Observable<UserVideoQuota>): Observable<boolean> {
if (!this.isUploadDisabled()) {
this.canSeeVideosLink = true
return of(this.canSeeVideosLink)
}
// Check if the user has videos
return quotaObservable.pipe(
map(({ videoQuotaUsed }) => {
if (videoQuotaUsed !== 0) {
// User already uploaded videos, so it can see the link
this.canSeeVideosLink = true
} else {
// No videos, no upload so the user don't need to see the videos link
this.canSeeVideosLink = false
}
return this.canSeeVideosLink
})
)
}
}

View File

@ -3,7 +3,7 @@ import { debounceTime } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { ScreenService } from '../wrappers'
import { LocalStorageService, ScreenService } from '../wrappers'
export type MenuLink = {
icon: GlobalIconName
@ -14,6 +14,8 @@ export type MenuLink = {
shortLabel: string
path: string
isPrimaryButton?: boolean // default false
}
export type MenuSection = {
@ -24,39 +26,45 @@ export type MenuSection = {
@Injectable()
export class MenuService {
isMenuDisplayed = true
private static LS_MENU_COLLAPSED = 'menu-collapsed'
isMenuCollapsed = false
isMenuChangedByUser = false
menuWidth = 240 // should be kept equal to $menu-width
constructor (
private screenService: ScreenService
private screenService: ScreenService,
private localStorageService: LocalStorageService
) {
// Do not display menu on small or touch screens
if (this.screenService.isInSmallView() || this.screenService.isInTouchScreen()) {
this.setMenuDisplay(false)
this.setMenuCollapsed(true)
}
this.handleWindowResize()
this.isMenuCollapsed = this.localStorageService.getItem(MenuService.LS_MENU_COLLAPSED) === 'true'
}
toggleMenu () {
this.setMenuDisplay(!this.isMenuDisplayed)
this.setMenuCollapsed(!this.isMenuCollapsed)
this.isMenuChangedByUser = true
this.localStorageService.setItem(MenuService.LS_MENU_COLLAPSED, this.isMenuCollapsed + '')
}
isDisplayed () {
return this.isMenuDisplayed
isCollapsed () {
return this.isMenuCollapsed
}
setMenuDisplay (display: boolean) {
this.isMenuDisplayed = display
setMenuCollapsed (collapsed: boolean) {
this.isMenuCollapsed = collapsed
if (!this.screenService.isInTouchScreen()) return
// On touch screens, lock body scroll and display content overlay when memu is opened
if (this.isMenuDisplayed) {
if (!this.isMenuCollapsed) {
document.body.classList.add('menu-open')
this.screenService.onFingerSwipe('left', () => this.setMenuDisplay(false))
this.screenService.onFingerSwipe('left', () => this.setMenuCollapsed(true))
return
}
@ -64,102 +72,10 @@ export class MenuService {
}
onResize () {
this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
this.isMenuCollapsed = window.innerWidth < 800 && !this.isMenuChangedByUser
}
buildLibraryLinks (userCanSeeVideosLink: boolean): MenuSection {
let links: MenuLink[] = []
if (userCanSeeVideosLink) {
links.push({
path: '/my-library/video-channels',
icon: 'channel' as GlobalIconName,
iconClass: 'channel-icon',
shortLabel: $localize`Channels`,
label: $localize`My channels`
})
links.push({
path: '/my-library/videos',
icon: 'videos' as GlobalIconName,
shortLabel: $localize`Videos`,
label: $localize`My videos`
})
}
links = links.concat([
{
path: '/my-library/video-playlists',
icon: 'playlists' as GlobalIconName,
shortLabel: $localize`Playlists`,
label: $localize`My playlists`
},
{
path: '/videos/subscriptions',
icon: 'subscriptions' as GlobalIconName,
shortLabel: $localize`Subscriptions`,
label: $localize`My subscriptions`
},
{
path: '/my-library/history/videos',
icon: 'history' as GlobalIconName,
shortLabel: $localize`History`,
label: $localize`My history`
}
])
return {
key: 'in-my-library',
title: $localize`In my library`,
links
}
}
buildCommonLinks (config: HTMLServerConfig): MenuSection {
let links: MenuLink[] = []
if (config.homepage.enabled) {
links.push({
icon: 'home' as 'home',
label: $localize`Home`,
shortLabel: $localize`Home`,
path: '/home'
})
}
links = links.concat([
{
icon: 'globe' as 'globe',
label: $localize`Discover videos`,
shortLabel: $localize`Discover`,
path: '/videos/overview'
},
{
icon: 'trending' as 'trending',
label: $localize`Trending videos`,
shortLabel: $localize`Trending`,
path: '/videos/trending'
},
{
icon: 'add' as 'add',
label: $localize`Recently added videos`,
shortLabel: $localize`Recently added`,
path: '/videos/recently-added'
},
{
icon: 'local' as 'local',
label: $localize`Local videos`,
shortLabel: $localize`Local videos`,
path: '/videos/local'
}
])
return {
key: 'on-instance',
title: $localize`ON ${config.instance.name}`,
links
}
}
// ---------------------------------------------------------------------------
private handleWindowResize () {
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay

View File

@ -2,7 +2,6 @@ export * from './can-deactivate-guard.service'
export * from './custom-reuse-strategy'
export * from './disable-for-reuse-hook'
export * from './login-guard.service'
export * from './menu-guard.service'
export * from './meta-guard.service'
export * from './peertube-router.service'
export * from './meta.service'

View File

@ -1,82 +0,0 @@
import { Injectable } from '@angular/core'
import { MenuService } from '../menu'
import { ScreenService } from '../wrappers'
abstract class MenuGuard {
canDeactivate = this.canActivate.bind(this)
constructor (protected menu: MenuService, protected screen: ScreenService, protected display: boolean) {
}
canActivate (): boolean {
// small screens already have the site-wide onResize from screenService
// > medium screens have enough space to fit the administrative menus
if (!this.screen.isInMobileView() && this.screen.isInMediumView()) {
this.menu.setMenuDisplay(this.display)
}
return true
}
}
@Injectable()
export class OpenMenuGuard extends MenuGuard {
constructor (menu: MenuService, screen: ScreenService) {
super(menu, screen, true)
}
}
@Injectable()
export class OpenMenuAlwaysGuard extends MenuGuard {
constructor (menu: MenuService, screen: ScreenService) {
super(menu, screen, true)
}
canActivate (): boolean {
this.menu.setMenuDisplay(this.display)
return true
}
}
@Injectable()
export class CloseMenuGuard extends MenuGuard {
constructor (menu: MenuService, screen: ScreenService) {
super(menu, screen, false)
}
}
@Injectable()
export class CloseMenuAlwaysGuard extends MenuGuard {
constructor (menu: MenuService, screen: ScreenService) {
super(menu, screen, false)
}
canActivate (): boolean {
this.menu.setMenuDisplay(this.display)
return true
}
}
@Injectable()
export class MenuGuards {
public static guards = [
OpenMenuGuard,
OpenMenuAlwaysGuard,
CloseMenuGuard,
CloseMenuAlwaysGuard
]
static open (always?: boolean) {
return always
? OpenMenuAlwaysGuard
: OpenMenuGuard
}
static close (always?: boolean) {
return always
? CloseMenuAlwaysGuard
: CloseMenuGuard
}
}

View File

@ -102,7 +102,7 @@ export class User implements UserServerModel {
else this.account.resetAvatar()
}
isUploadDisabled () {
hasUploadDisabled () {
return this.videoQuota === 0 || this.videoQuotaDaily === 0
}

View File

@ -8,7 +8,7 @@
<my-search-typeahead class="w-100 me-5"></my-search-typeahead>
@if (!isLoggedIn) {
<my-button icon="cog" (click)="openQuickSettings()"></my-button>
<my-button class="me-3" icon="cog" (click)="openQuickSettings()"></my-button>
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button peertube-button-link text-truncate d-block">
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>

View File

@ -28,6 +28,8 @@
width: 34px;
height: 34px;
background-repeat: no-repeat;
@include margin-right(10px);
}
}

View File

@ -1,87 +1,62 @@
<div class="menu-wrapper">
<nav [ngClass]="{ 'is-logged-in': isLoggedIn }">
<div class="menu-wrapper" [ngClass]="{ collapsed: collapsed }">
<div class="main-menu">
<div class="toggle-menu-container">
@if (collapsed) {
<button type="button" class="button-unstyle toggle-menu" i18n-title title="Display the lateral bar" (click)="toggleMenu()">
<my-global-icon class="transform-rotate-180" iconName="chevron-left"></my-global-icon>
</button>
} @else {
<button type="button" class="button-unstyle toggle-menu" i18n-title title="Hide the lateral bar" (click)="toggleMenu()">
<my-global-icon iconName="chevron-left"></my-global-icon>
</button>
}
</div>
<ng-container *ngFor="let menuSection of menuSections" >
<ul [ngClass]="[ menuSection.key, 'menu-block' ]">
<li i18n class="block-container">
<span class="block-title">{{ menuSection.title }}</span>
<nav>
<ng-container *ngFor="let menuSection of menuSections" >
<ul [ngClass]="[ menuSection.key, 'menu-block' ]">
<li i18n class="ellipsis">
<div class="block-title" [ngClass]="{ 'visually-hidden': collapsed }">{{ menuSection.title }}</div>
<ul class="mt-3">
<li *ngFor="let link of menuSection.links">
<a class="menu-link ps-0" [routerLink]="link.path" routerLinkActive="active">
<my-global-icon *ngIf="link.icon" [iconName]="link.icon" [ngClass]="link.iconClass" aria-hidden="true"></my-global-icon>
<ng-container>{{ link.shortLabel }}</ng-container>
</a>
</li>
</ul>
</li>
</ul>
</ng-container>
<div class="footer">
<ul class="footer-block">
<li>
<button *ngIf="!isLoggedIn" class="menu-link button-unstyle" (click)="openQuickSettings()">
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
<ng-container i18n>My settings</ng-container>
</button>
</li>
<li>
<a class="menu-link" routerLink="/about" routerLinkActive="active">
<my-global-icon iconName="help" aria-hidden="true"></my-global-icon>
<ng-container i18n>About</ng-container>
</a>
</li>
</ul>
<div class="footer-bottom">
<div class="footer-links">
<button *ngIf="isLoggedIn === false" (click)="openLanguageChooser()" class="button-unstyle" i18n>Interface: {{ currentInterfaceLanguage }}</button>
<ul class="d-flex flex-wrap">
<li>
<a i18n routerLink="/about/instance">Contact</a>
</li>
<li>
<a i18n href="https://joinpeertube.org/help" i18n-title title="Get help using PeerTube" target="_blank" rel="noopener noreferrer">Help</a>
</li>
<li>
<a i18n href="https://joinpeertube.org/faq" i18n-title title="FAQ (Frequently Asked Questions) - about PeerTube" target="_blank" rel="noopener noreferrer">FAQ</a>
</li>
<li>
<a i18n routerLink="/about/instance" fragment="statistics">Stats</a>
</li>
<li>
<a i18n href="https://docs.joinpeertube.org/api-rest-reference.html" i18n-title title="API documentation" target="_blank" rel="noopener noreferrer">API</a>
</li>
<li>
<button (click)="openHotkeysCheatSheet()" class="button-unstyle" i18n>Keyboard shortcuts</button>
</li>
</ul>
</div>
<ul class="d-flex footer-copyleft" i18n-title title="powered by PeerTube - CopyLeft 2015-2024">
<li>
<a href="https://joinpeertube.org" class="me-1" target="_blank" rel="noopener noreferrer" i18n>powered by PeerTube</a>
</li>
<li>
<a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" target="_blank" rel="noopener noreferrer">
<span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2024
</a>
<ul>
<li *ngFor="let link of menuSection.links">
@if (link.isPrimaryButton === true) {
<my-button class="d-block menu-button" theme="primary" [icon]="link.icon" [ariaLabel]="link.label">
@if (!collapsed) {
{{ link.label }}
}
</my-button>
} @else {
<a class="menu-link" [routerLink]="link.path" routerLinkActive="active">
<my-global-icon *ngIf="link.icon" [iconName]="link.icon" [ngClass]="link.iconClass" aria-hidden="true"></my-global-icon>
<span [ngClass]="{ 'visually-hidden': collapsed }">{{ link.shortLabel }}</span>
</a>
}
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</div>
</ng-container>
</nav>
<my-language-chooser #languageChooserModal></my-language-chooser>
<my-quick-settings #quickSettingsModal></my-quick-settings>
<div class="about">
<div [ngClass]="{ 'visually-hidden': collapsed }" class="block-title">About</div>
<div [ngClass]="{ 'visually-hidden': collapsed }" class="description">{{ shortDescription }}</div>
<my-button class="mt-2 d-block" theme="secondary" icon="help" i18n-ariaLabel aria-label="More info" i18n>
@if (!collapsed) {
More info
}
</my-button>
</div>
</div>
<div *ngIf="!collapsed" class="mt-3 mx-4">
<div class="fs-8" i18n>
Platform powered by <a class="" href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">PeerTube</a>
</div>
<a class="d-block fs-8" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer" i18n>Discover more platforms</a>
</div>
</div>

View File

@ -2,10 +2,6 @@
@use '_variables' as *;
@use '_mixins' as *;
$menu-link-icon-size: 22px;
$menu-link-icon-margin-right: 18px;
$footer-links-base-opacity: .8;
ul {
margin: 0;
padding: 0;
@ -15,11 +11,119 @@ ul {
}
}
.footer-bottom {
li {
.menu-wrapper {
--menuXPadding: 1.5rem;
position: fixed;
height: calc(100vh - #{$header-height});
z-index: z(menu);
scrollbar-color: pvar(--actionButtonColor) pvar(--menuBackgroundColor);
width: calc(#{$menu-width} - 2rem);
@include margin-left(2rem);
&.collapsed {
--menuXPadding: 0.25rem;
width: auto;
display: inline-flex;
@include margin-left(0);
}
@media screen and (max-width: $mobile-view) {
width: 100% !important;
.main-menu {
overflow-y: auto !important;
}
}
}
.toggle-menu {
color: pvar(--menuForegroundColor);
width: 24px;
height: 24px;
border-radius: 100%;
background-color: #3C2E2E;
color: #C1B0B0;
@include button-with-icon(20px, 0, -1px, 1px);
}
.menu-wrapper:not(.collapsed) .toggle-menu {
position: absolute;
top: 20px;
@include right(24px);
}
.collapsed .toggle-menu my-global-icon {
right: -1px;
top: 0;
}
.main-menu {
position: relative;
padding-top: 1.5rem;
padding-bottom: 1.5rem;
border-radius: 14px;
background-color: pvar(--menuBackgroundColor);
color: pvar(--menuForegroundColor);
overflow-y: auto;
scrollbar-color: transparent transparent;
max-height: calc(100% - 50px); // Space for links below the menu
&:focus,
&:hover {
scrollbar-color: auto;
}
@media not all and (hover: hover) and (pointer: fine) {
scrollbar-color: auto;
}
}
.collapsed .main-menu {
max-height: calc(100% - 10px);
border-start-start-radius: 0;
border-end-start-radius: 0;
padding-top: 1rem;
padding-bottom: 1rem;
}
.menu-link,
.menu-button,
.block-title,
.about .description,
.about my-button {
@include padding-left(var(--menuXPadding));
@include padding-right(var(--menuXPadding));
}
.menu-block,
.collapsed .toggle-menu-container {
&::after {
content: '';
display: block;
height: 2px;
background: #3C2E2E;
margin: 1rem var(--menuXPadding);
}
}
.collapsed .toggle-menu-container {
text-align: center;
}
.block-title {
font-weight: $font-bold;
font-size: 14px;
margin-bottom: 0.5rem;
}
.menu-link {
@ -27,16 +131,14 @@ ul {
align-items: center;
color: pvar(--menuForegroundColor);
cursor: pointer;
white-space: normal;
word-break: break-word;
transition: background-color .1s ease-in-out;
line-height: $line-height-normal;
width: 100%;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@include disable-default-a-behaviour;
@include padding-left($menu-lateral-padding);
@include padding-right(20px);
&.active {
background-color: rgba(255, 255, 255, 0.15);
@ -48,118 +150,28 @@ ul {
}
my-global-icon {
display: flex;
width: $menu-link-icon-size;
height: $menu-link-icon-size;
width: 22px;
height: 22px;
position: relative;
top: -1px;
@include apply-svg-color(#808080);
@include margin-right($menu-link-icon-margin-right);
}
}
.menu-wrapper {
position: fixed;
height: calc(100vh - #{$header-height});
padding: 0;
width: $menu-width;
z-index: z(menu);
scrollbar-color: pvar(--actionButtonColor) pvar(--menuBackgroundColor);
}
nav {
background-color: pvar(--menuBackgroundColor);
color: pvar(--menuForegroundColor);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
@include ellipsis;
&:focus,
&:hover {
overflow-y: auto;
}
@media not all and (hover: hover) and (pointer: fine) {
overflow-y: auto;
}
}
.menu-block,
.footer-block {
margin-bottom: 15px;
.block-container {
@include margin-left(26px);
@include margin-right(15px);
@include ellipsis;
}
.block-title {
text-transform: uppercase;
font-weight: $font-bold;
font-size: 13px;
}
a,
button {
min-height: 40px;
}
}
.footer {
padding-bottom: 15px;
}
.footer-bottom {
padding: 0 $menu-lateral-padding;
}
.footer-links {
a,
button {
color: pvar(--menuForegroundColor);
opacity: $footer-links-base-opacity;
white-space: nowrap;
font-size: 0.75rem;
line-height: 1.4rem;
font-weight: $font-semibold;
@include margin-right(8px);
@include disable-default-a-behaviour;
&:hover {
opacity: $footer-links-base-opacity + .2;
+ *:not(.visually-hidden) {
@include margin-left(12px);
}
}
}
.footer-copyleft a {
color: pvar(--menuForegroundColor);
opacity: $footer-links-base-opacity - .2;
font-size: 0.85rem;
@include disable-default-a-behaviour;
&:hover {
opacity: $footer-links-base-opacity;
}
.menu-wrapper.collapsed .menu-link {
justify-content: center;
}
@media screen and (max-width: $mobile-view) {
.menu-wrapper {
width: 100% !important;
nav {
overflow-y: auto;
}
}
.menu-button {
margin-top: 0.5rem;
}
.footer {
width: 100% !important;
.about {
.description {
font-size: 14px;
}
}

View File

@ -1,31 +1,28 @@
import { CommonModule, ViewportScroller } from '@angular/common'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { Router, RouterLink, RouterLinkActive } from '@angular/router'
import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { RouterLink, RouterLinkActive } from '@angular/router'
import {
AuthService,
AuthStatus,
AuthUser,
HooksService,
HotkeysService,
MenuLink,
MenuSection,
MenuService,
RedirectService,
ScreenService,
ServerService,
UserService
} from '@app/core'
import { scrollToTop } from '@app/helpers'
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
import { InputSwitchComponent } from '@app/shared/shared-forms/input-switch.component'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
import { LoginLinkComponent } from '@app/shared/shared-main/users/login-link.component'
import { SignupLabelComponent } from '@app/shared/shared-main/users/signup-label.component'
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { HTMLServerConfig, ServerConfig, UserRight, UserRightType, VideoConstant } from '@peertube/peertube-models'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { ServerConfig, UserRight } from '@peertube/peertube-models'
import debug from 'debug'
import { forkJoin, Subscription } from 'rxjs'
import { first, switchMap } from 'rxjs/operators'
import { of, Subscription } from 'rxjs'
import { first, map, switchMap } from 'rxjs/operators'
import { LanguageChooserComponent } from './language-chooser.component'
import { NotificationDropdownComponent } from './notification-dropdown.component'
import { QuickSettingsModalComponent } from './quick-settings-modal.component'
@ -49,308 +46,218 @@ const debugLogger = debug('peertube:menu:MenuComponent')
GlobalIconComponent,
RouterLink,
RouterLinkActive,
NgbDropdownModule
NgbDropdownModule,
ButtonComponent
]
})
export class MenuComponent implements OnInit, OnDestroy {
@ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent
@ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent
@ViewChild('dropdown') dropdown: NgbDropdown
user: AuthUser
isLoggedIn: boolean
userHasAdminAccess = false
helpVisible = false
videoLanguages: string[] = []
nsfwPolicy: string
currentInterfaceLanguage: string
menuSections: MenuSection[] = []
private languages: VideoConstant<string>[] = []
private isLoggedIn: boolean
private user: AuthUser
private canSeeVideoMakerBlock: boolean
private htmlServerConfig: HTMLServerConfig
private serverConfig: ServerConfig
private routesPerRight: { [role in UserRightType]?: string } = {
[UserRight.MANAGE_USERS]: '/admin/users',
[UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
[UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses',
[UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks',
[UserRight.MANAGE_JOBS]: '/admin/jobs',
[UserRight.MANAGE_CONFIGURATION]: '/admin/config'
}
private languagesSub: Subscription
private modalSub: Subscription
private hotkeysSub: Subscription
private authSub: Subscription
constructor (
private viewportScroller: ViewportScroller,
private authService: AuthService,
private userService: UserService,
private serverService: ServerService,
private redirectService: RedirectService,
private hotkeysService: HotkeysService,
private screenService: ScreenService,
private menuService: MenuService,
private modalService: PeertubeModalService,
private router: Router,
private hooks: HooksService
private hooks: HooksService,
private menu: MenuService
) { }
get isInMobileView () {
return this.screenService.isInMobileView()
get shortDescription () {
return this.serverConfig.instance.shortDescription
}
get language () {
return this.languageChooserModal.getCurrentLanguage()
}
get requiresApproval () {
return this.serverConfig.signup.requiresApproval
get collapsed () {
return this.menu.isCollapsed()
}
ngOnInit () {
this.htmlServerConfig = this.serverService.getHTMLConfig()
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
this.isLoggedIn = this.authService.isLoggedIn()
this.updateUserState()
this.buildMenuSections()
this.onUserStateChange()
this.authSub = this.authService.loginChangedSource.subscribe(status => {
if (status === AuthStatus.LoggedIn) {
this.isLoggedIn = true
} else if (status === AuthStatus.LoggedOut) {
this.isLoggedIn = false
}
if (status === AuthStatus.LoggedIn) this.isLoggedIn = true
else if (status === AuthStatus.LoggedOut) this.isLoggedIn = false
this.updateUserState()
this.buildMenuSections()
})
this.hotkeysSub = this.hotkeysService.cheatSheetToggle
.subscribe(isOpen => this.helpVisible = isOpen)
this.languagesSub = forkJoin([
this.serverService.getVideoLanguages(),
this.authService.userInformationLoaded.pipe(first())
]).subscribe(([ languages ]) => {
this.languages = languages
this.buildUserLanguages()
this.onUserStateChange()
})
this.serverService.getConfig()
.subscribe(config => this.serverConfig = config)
this.modalSub = this.modalService.openQuickSettingsSubject
.subscribe(() => this.openQuickSettings())
}
ngOnDestroy () {
if (this.modalSub) this.modalSub.unsubscribe()
if (this.languagesSub) this.languagesSub.unsubscribe()
if (this.hotkeysSub) this.hotkeysSub.unsubscribe()
if (this.authSub) this.authSub.unsubscribe()
}
isRegistrationAllowed () {
if (!this.serverConfig) return false
// ---------------------------------------------------------------------------
return this.serverConfig.signup.allowed &&
this.serverConfig.signup.allowedForCurrentIP
toggleMenu () {
this.menu.toggleMenu()
}
getFirstAdminRightAvailable () {
const user = this.authService.getUser()
if (!user) return undefined
// ---------------------------------------------------------------------------
const adminRights = [
UserRight.MANAGE_USERS,
UserRight.MANAGE_SERVER_FOLLOW,
UserRight.MANAGE_ABUSES,
UserRight.MANAGE_VIDEO_BLACKLIST,
UserRight.MANAGE_JOBS,
UserRight.MANAGE_CONFIGURATION
]
private async buildMenuSections () {
this.menuSections = []
for (const adminRight of adminRights) {
if (user.hasRight(adminRight)) {
return adminRight
for (const section of [ this.buildLibraryLinks(), this.buildVideoMakerLinks(), this.buildAdminLinks() ]) {
if (section.links.length !== 0) {
this.menuSections.push(section)
}
}
return undefined
this.menuSections = await this.hooks.wrapObject(this.menuSections, 'common', 'filter:left-menu.links.create.result')
}
getFirstAdminRouteAvailable () {
const right = this.getFirstAdminRightAvailable()
return this.routesPerRight[right]
}
logout (event: Event) {
event.preventDefault()
this.authService.logout()
// Redirect to home page
this.redirectService.redirectToHomepage()
}
openLanguageChooser () {
this.languageChooserModal.show()
}
openHotkeysCheatSheet () {
this.hotkeysService.cheatSheetToggle.next(!this.helpVisible)
}
openQuickSettings () {
this.quickSettingsModal.show()
}
toggleUseP2P () {
if (!this.user) return
this.user.p2pEnabled = !this.user.p2pEnabled
this.userService.updateMyProfile({ p2pEnabled: this.user.p2pEnabled })
.subscribe(() => this.authService.refreshUserInformation())
}
langForLocale (localeId: string) {
if (localeId === '_unknown') return $localize`Unknown`
return this.languages.find(lang => lang.id === localeId).label
}
onActiveLinkScrollToAnchor (link: HTMLAnchorElement) {
const linkURL = link.getAttribute('href')
const linkHash = link.getAttribute('fragment')
// On same url without fragment restore top scroll position
if (!linkHash && this.router.url.includes(linkURL)) {
scrollToTop('smooth')
}
// On same url with fragment restore anchor scroll position
if (linkHash && this.router.url === linkURL) {
this.viewportScroller.scrollToAnchor(linkHash)
}
if (this.screenService.isInSmallView()) {
this.menuService.toggleMenu()
}
}
// Lock menu scroll when menu scroll to avoid fleeing / detached dropdown
onMenuScrollEvent () {
document.querySelector('nav').scrollTo(0, 0)
}
onDropdownOpenChange (opened: boolean) {
if (this.screenService.isInMobileView()) return
// Close dropdown when window scroll to avoid dropdown quick jump for re-position
const onWindowScroll = () => {
this.dropdown?.close()
window.removeEventListener('scroll', onWindowScroll)
}
if (opened) {
window.addEventListener('scroll', onWindowScroll)
document.querySelector('nav').scrollTo(0, 0) // Reset menu scroll to easy lock
// eslint-disable-next-line @typescript-eslint/unbound-method
document.querySelector('nav').addEventListener('scroll', this.onMenuScrollEvent)
} else {
// eslint-disable-next-line @typescript-eslint/unbound-method
document.querySelector('nav').removeEventListener('scroll', this.onMenuScrollEvent)
}
}
private async buildMenuSections () {
const menuSections = []
private buildLibraryLinks (): MenuSection {
let links: MenuLink[] = []
if (this.isLoggedIn) {
menuSections.push(
this.menuService.buildLibraryLinks(this.user?.canSeeVideosLink)
)
links = links.concat([
{
path: '/my-library/video-playlists',
icon: 'playlists' as GlobalIconName,
shortLabel: $localize`Playlists`,
label: $localize`My Playlists`
},
{
path: '/videos/subscriptions',
icon: 'subscriptions' as GlobalIconName,
shortLabel: $localize`Subscriptions`,
label: $localize`My Subscriptions`
},
{
path: '/my-library/history/videos',
icon: 'history' as GlobalIconName,
shortLabel: $localize`History`,
label: $localize`My History`
}
])
}
menuSections.push(
this.menuService.buildCommonLinks(this.htmlServerConfig)
)
this.menuSections = await this.hooks.wrapObject(menuSections, 'common', 'filter:left-menu.links.create.result')
return {
key: 'my-library',
title: $localize`My library`,
links
}
}
private buildUserLanguages () {
if (!this.user) {
this.videoLanguages = []
return
private buildVideoMakerLinks (): MenuSection {
let links: MenuLink[] = []
if (this.isLoggedIn && this.canSeeVideoMakerBlock) {
links = links.concat([
{
path: '/my-library/video-channels',
icon: 'channel' as GlobalIconName,
iconClass: 'channel-icon',
shortLabel: $localize`Channels`,
label: $localize`My channels`
},
{
path: '/my-library/videos',
icon: 'videos' as GlobalIconName,
shortLabel: $localize`Videos`,
label: $localize`My videos`
},
{
path: '/videos/upload',
icon: 'upload' as GlobalIconName,
shortLabel: $localize`Publish`,
label: $localize`Publish`,
isPrimaryButton: true
}
])
}
if (!this.user.videoLanguages) {
this.videoLanguages = [ $localize`any language` ]
return
return {
key: 'my-video-space',
title: $localize`My video space`,
links
}
}
private buildAdminLinks (): MenuSection {
const links: MenuLink[] = []
if (this.isLoggedIn) {
if (this.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
links.push({
path: '/admin/videos/list',
icon: 'overview' as GlobalIconName,
shortLabel: $localize`Overview`,
label: $localize`Overview`
})
}
if (this.user.hasRight(UserRight.MANAGE_ABUSES)) {
links.push({
path: '/admin/moderation/abuses/list',
icon: 'moderation' as GlobalIconName,
shortLabel: $localize`Moderation`,
label: $localize`Moderation`
})
}
if (this.user.hasRight(UserRight.MANAGE_CONFIGURATION)) {
links.push({
path: '/admin/config/edit-custom',
icon: 'config' as GlobalIconName,
shortLabel: $localize`Advanced parameters`,
label: $localize`Advanced parameters`
})
}
}
this.videoLanguages = this.user.videoLanguages
.map(locale => this.langForLocale(locale))
.map(value => value === undefined ? '?' : value)
return {
key: 'admin',
title: $localize`Administration`,
links
}
}
private computeAdminAccess () {
const right = this.getFirstAdminRightAvailable()
// ---------------------------------------------------------------------------
this.userHasAdminAccess = right !== undefined
}
private computeCanSeeVideoMakerBlock () {
if (!this.isLoggedIn) return of(false)
if (!this.user.hasUploadDisabled()) return of(true)
private computeVideosLink () {
if (!this.isLoggedIn) return
this.authService.userInformationLoaded
return this.authService.userInformationLoaded
.pipe(
switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed()))
).subscribe(res => {
if (res === true) debugLogger('User can see videos link.')
else debugLogger('User cannot see videos link.')
})
first(),
switchMap(() => this.userService.getMyVideoQuotaUsed()),
map(({ videoQuotaUsed }) => {
// User already uploaded videos, so it can see the link
if (videoQuotaUsed !== 0) return true
// No videos, no upload so the user don't need to see the videos link
return false
})
)
}
private computeNSFWPolicy () {
if (!this.user) {
this.nsfwPolicy = null
return
}
switch (this.user.nsfwPolicy) {
case 'do_not_list':
this.nsfwPolicy = $localize`hide`
break
case 'blur':
this.nsfwPolicy = $localize`blur`
break
case 'display':
this.nsfwPolicy = $localize`display`
break
}
}
private updateUserState () {
private onUserStateChange () {
this.user = this.isLoggedIn
? this.authService.getUser()
: undefined
this.computeAdminAccess()
this.computeNSFWPolicy()
this.computeVideosLink()
this.computeCanSeeVideoMakerBlock()
.subscribe(res => {
this.canSeeVideoMakerBlock = res
if (this.canSeeVideoMakerBlock) debugLogger('User can see videos link.')
else debugLogger('User cannot see videos link.')
this.buildMenuSections()
})
}
}

View File

@ -10,23 +10,26 @@ const icons = {
'support': require('../../../assets/images/misc/support.svg'),
'peertube-x': require('../../../assets/images/misc/peertube-x.svg'),
'robot': require('../../../assets/images/misc/miscellaneous-services.svg'), // material ui
'videos': require('../../../assets/images/misc/video-library.svg'), // material ui
'history': require('../../../assets/images/misc/history.svg'), // material ui
'subscriptions': require('../../../assets/images/misc/subscriptions.svg'), // material ui
'playlist-add': require('../../../assets/images/misc/playlist-add.svg'), // material ui
'follower': require('../../../assets/images/misc/account-arrow-left.svg'), // material ui
'following': require('../../../assets/images/misc/account-arrow-right.svg'), // material ui
'tip': require('../../../assets/images/misc/tip.svg'), // material ui
'flame': require('../../../assets/images/misc/flame.svg'),
'local': require('../../../assets/images/misc/local.svg'),
// feather/lucide icons
'history': require('../../../assets/images/feather/history.svg'),
'subscriptions': require('../../../assets/images/feather/subscriptions.svg'),
'videos': require('../../../assets/images/feather/videos.svg'),
'add': require('../../../assets/images/feather/plus-circle.svg'),
'alert': require('../../../assets/images/feather/alert.svg'),
'overview': require('../../../assets/images/feather/overview.svg'),
'moderation': require('../../../assets/images/feather/moderation.svg'),
'config': require('../../../assets/images/feather/config.svg'),
'award': require('../../../assets/images/feather/award.svg'),
'bell': require('../../../assets/images/feather/bell.svg'),
'channel': require('../../../assets/images/feather/tv.svg'),
'channel': require('../../../assets/images/feather/channel.svg'),
'chevrons-up': require('../../../assets/images/feather/chevrons-up.svg'),
'chevron-left': require('../../../assets/images/feather/chevron-left.svg'),
'circle-tick': require('../../../assets/images/feather/check-circle.svg'),
'clock-arrow-down': require('../../../assets/images/feather/clock-arrow-down.svg'),
'clock': require('../../../assets/images/feather/clock.svg'),
@ -64,7 +67,7 @@ const icons = {
'ownership-change': require('../../../assets/images/feather/share.svg'),
'p2p': require('../../../assets/images/feather/airplay.svg'),
'play': require('../../../assets/images/feather/play.svg'),
'playlists': require('../../../assets/images/feather/list.svg'),
'playlists': require('../../../assets/images/feather/playlists.svg'),
'refresh': require('../../../assets/images/feather/refresh-cw.svg'),
'repeat': require('../../../assets/images/feather/repeat.svg'),
'search': require('../../../assets/images/feather/search.svg'),
@ -94,7 +97,7 @@ export type GlobalIconName = keyof typeof icons
standalone: true
})
export class GlobalIconComponent implements OnInit {
@Input() iconName: GlobalIconName
@Input({ required: true }) iconName: GlobalIconName
constructor (
private el: ElementRef,

View File

@ -1,16 +1,22 @@
<button *ngIf="!ptRouterLink" type="button" class="action-button" [ngClass]="classes" [disabled]="disabled" [ngbTooltip]="title">
<ng-container *ngTemplateOutlet="content"></ng-container>
</button>
<a *ngIf="ptRouterLink" class="action-button" [ngClass]="classes" [ngbTooltip]="title" [routerLink]="ptRouterLink">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
@if (ptRouterLink) {
<a class="action-button" [ngClass]="classes" [ngbTooltip]="title" [routerLink]="ptRouterLink">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
} @else {
<button type="button" class="action-button" [ngClass]="classes" [disabled]="disabled" [ngbTooltip]="title">
<ng-container *ngTemplateOutlet="content"></ng-container>
</button>
}
<ng-template #content>
<my-loader size="sm" [ngClass]="{ displayed: loading }" [loading]="loading"></my-loader>
<my-global-icon *ngIf="icon && !loading" [iconName]="icon"></my-global-icon>
<span *ngIf="label" class="button-label">{{ label }}</span>
<ng-content></ng-content>
<span class="button-label ellipsis" #labelContent>
@if (label) {
{{ label }}
} @else {
<ng-content></ng-content>
}
</span>
</ng-template>

View File

@ -1,31 +1,10 @@
@use '_variables' as *;
@use '_mixins' as *;
@mixin responsive-label {
.action-button {
padding: 0 13px;
}
.button-label {
display: none;
}
my-global-icon {
margin: 0 !important;
}
}
:host {
display: inline-block;
}
a[class$=-button],
span[class$=-button] {
> span {
@include margin-left(5px);
}
}
.action-button {
width: 100%; // useful for ellipsis, allow to define a max-width on host component
@ -38,16 +17,22 @@ span[class$=-button] {
}
&.has-icon {
@include button-with-icon(21px);
@include button-with-icon(21px, 0);
}
&.icon-only my-global-icon {
margin: 0 !important;
&.icon-only {
padding: 6px 8px !important;
}
}
.button-label {
@include ellipsis;
@mixin responsive-label {
.action-button {
padding: 6px 8px !important;
}
.button-label {
display: none;
}
}
// In a table, try to minimize the space taken by this button

View File

@ -1,5 +1,5 @@
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input, OnChanges, booleanAttribute } from '@angular/core'
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, ViewChild, booleanAttribute } from '@angular/core'
import { RouterLink } from '@angular/router'
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
@ -15,9 +15,9 @@ import { LoaderComponent } from '../common/loader.component'
imports: [ NgIf, NgClass, NgbTooltip, NgTemplateOutlet, RouterLink, LoaderComponent, GlobalIconComponent ]
})
export class ButtonComponent implements OnChanges {
export class ButtonComponent implements OnChanges, AfterViewInit {
@Input() label = ''
@Input() theme: 'orange' | 'grey' = 'grey'
@Input() theme: 'orange' | 'grey' | 'primary' | 'secondary' = 'grey'
@Input() icon: GlobalIconName
@Input() ptRouterLink: string[] | string
@Input() title: string
@ -25,22 +25,28 @@ export class ButtonComponent implements OnChanges {
@Input({ transform: booleanAttribute }) disabled = false
@Input({ transform: booleanAttribute }) responsiveLabel = false
@ViewChild('labelContent') labelContent: ElementRef
classes: { [id: string]: boolean } = {}
ngOnChanges () {
this.buildClasses()
}
private buildClasses () {
console.log('build classes')
ngAfterViewInit () {
this.buildClasses()
}
private buildClasses () {
this.classes = {
'peertube-button': !this.ptRouterLink,
'peertube-button-link': !!this.ptRouterLink,
'orange-button': this.theme === 'orange',
'grey-button': this.theme === 'grey',
'icon-only': !this.label,
'primary-button': this.theme === 'primary',
'secondary-button': this.theme === 'secondary',
'has-icon': !!this.icon,
'icon-only': !(this.labelContent?.nativeElement as HTMLElement)?.innerText,
'responsive-label': this.responsiveLabel
}
}

View File

@ -1,13 +1,13 @@
import { Subscription } from 'rxjs'
import { filter } from 'rxjs/operators'
import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, Input, OnChanges, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { NavigationEnd, Router, RouterLinkActive, RouterLink } from '@angular/router'
import { MenuService, ScreenService } from '@app/core'
import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'
import { ScreenService } from '@app/core'
import { scrollToTop } from '@app/helpers'
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { NgbDropdown, NgbModal, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem } from '@ng-bootstrap/ng-bootstrap'
import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { filter } from 'rxjs/operators'
import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
import { NgClass, NgFor, NgIf } from '@angular/common'
export type TopMenuDropdownParam = {
label: string
@ -57,17 +57,11 @@ export class TopMenuDropdownComponent implements OnInit, OnChanges, OnDestroy {
constructor (
private router: Router,
private modalService: NgbModal,
private screen: ScreenService,
private menuService: MenuService
private screen: ScreenService
) { }
get isInSmallView () {
let marginLeft = 0
if (this.menuService.isMenuDisplayed) {
marginLeft = this.menuService.menuWidth
}
return this.screen.isInSmallView(marginLeft)
return this.screen.isInSmallView()
}
get isBroadcastMessageDisplayed () {

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.82353 7H21.1765C22.1003 7 23 7.84905 23 9.09091V19.9091C23 21.151 22.1003 22 21.1765 22H2.82353C1.89966 22 1 21.151 1 19.9091V9.09091C1 7.84905 1.89966 7 2.82353 7Z" stroke="currentColor" stroke-width="2"/>
<path d="M6 1L11 6.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 1L13 6.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7001 6.3C14.5169 6.48692 14.4142 6.73824 14.4142 7C14.4142 7.26175 14.5169 7.51307 14.7001 7.7L16.3001 9.3C16.487 9.48322 16.7383 9.58585 17.0001 9.58585C17.2619 9.58585 17.5132 9.48322 17.7001 9.3L21.4701 5.53C21.9729 6.64118 22.1252 7.87923 21.9066 9.07914C21.6879 10.2791 21.1088 11.3838 20.2464 12.2463C19.3839 13.1087 18.2792 13.6878 17.0792 13.9065C15.8793 14.1251 14.6413 13.9728 13.5301 13.47L6.6201 20.38C6.22227 20.7778 5.68271 21.0013 5.1201 21.0013C4.55749 21.0013 4.01792 20.7778 3.6201 20.38C3.22227 19.9822 2.99878 19.4426 2.99878 18.88C2.99878 18.3174 3.22227 17.7778 3.6201 17.38L10.5301 10.47C10.0273 9.35881 9.87502 8.12076 10.0936 6.92085C10.3123 5.72094 10.8914 4.61615 11.7538 3.75372C12.6163 2.89128 13.721 2.31216 14.921 2.09354C16.1209 1.87491 17.3589 2.02716 18.4701 2.52999L14.7101 6.29L14.7001 6.3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.519 2 22 6.481 22 12C22 17.519 17.519 22 12 22C6.481 22 2 17.519 2 12C2 6.481 6.481 2 12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M9.09009 9C9.57509 7.62 10.9851 6.791 12.4271 7.039C13.8691 7.286 14.9221 8.537 14.9201 10C14.9201 11.234 13.4941 12 12.5001 12C12.0001 12 12.0001 14 12.0001 14" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M12 15.355V16.355V17.355" stroke="currentColor" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12.2432V12.2432C4 7.13834 8.13834 3 13.2432 3V3V3C18.0795 3 22 6.92054 22 11.7568V12.2432V12.2432C22 17.0795 18.0795 21 13.2432 21V21C13.2432 21 13.2432 21 13.2432 21C13.2432 21 8.86486 21 6.91892 18.0811" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7V11.4296C12 12.0983 12.3342 12.7228 12.8906 13.0937L16.5 15.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.93939 11.5606L4.00012 14.5L1.0607 11.5606" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 482 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13H12.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

View File

@ -1 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8.875C2.48325 8.875 2.875 8.48325 2.875 8C2.875 7.51675 2.48325 7.125 2 7.125C1.51675 7.125 1.125 7.51675 1.125 8C1.125 8.48325 1.51675 8.875 2 8.875Z" fill="currentColor"/>
<path d="M2 8.875C2.48325 8.875 2.875 8.48325 2.875 8C2.875 7.51675 2.48325 7.125 2 7.125C1.51675 7.125 1.125 7.51675 1.125 8C1.125 8.48325 1.51675 8.875 2 8.875Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
<path d="M2 2.75C2.48325 2.75 2.875 2.35825 2.875 1.875C2.875 1.39175 2.48325 1 2 1C1.51675 1 1.125 1.39175 1.125 1.875C1.125 2.35825 1.51675 2.75 2 2.75Z" fill="currentColor"/>
<path d="M2 2.75C2.48325 2.75 2.875 2.35825 2.875 1.875C2.875 1.39175 2.48325 1 2 1C1.51675 1 1.125 1.39175 1.125 1.875C1.125 2.35825 1.51675 2.75 2 2.75Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
<path d="M2 15C2.48325 15 2.875 14.6082 2.875 14.125C2.875 13.6418 2.48325 13.25 2 13.25C1.51675 13.25 1.125 13.6418 1.125 14.125C1.125 14.6082 1.51675 15 2 15Z" fill="currentColor"/>
<path d="M2 15C2.48325 15 2.875 14.6082 2.875 14.125C2.875 13.6418 2.48325 13.25 2 13.25C1.51675 13.25 1.125 13.6418 1.125 14.125C1.125 14.6082 1.51675 15 2 15Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4H18C18.5304 4 19.0391 4.21071 19.4142 4.58579C19.7893 4.96086 20 5.46957 20 6V20C20 20.5304 19.7893 21.0391 19.4142 21.4142C19.0391 21.7893 18.5304 22 18 22H6C5.46957 22 4.96086 21.7893 4.58579 21.4142C4.21071 21.0391 4 20.5304 4 20V6C4 5.46957 4.21071 4.96086 4.58579 4.58579C4.96086 4.21071 5.46957 4 6 4H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 2H9C8.44772 2 8 2.44772 8 3V5C8 5.55228 8.44772 6 9 6H15C15.5523 6 16 5.55228 16 5V3C16 2.44772 15.5523 2 15 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.3335 4H23.0002" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 4H1.61111" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.3335 20H23.0002" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 20H1.61111" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.33301 12H22.9997" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 12H1.61111" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5.93121H21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M6 1.91663H18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M2.82353 10H21.1765C22.1556 10 23 10.8264 23 11.9091V22.0909C23 23.1736 22.1556 24 21.1765 24H2.82353C1.84435 24 1 23.1736 1 22.0909V11.9091C1 10.8264 1.84435 10 2.82353 10Z" stroke="currentColor" stroke-width="2"/>
<path d="M9 13.6526V20.6526L15 17.1532L9 13.6526Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tv"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>

Before

Width:  |  Height:  |  Size: 320 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8C1.89543 8 1 8.97683 1 10.1818V17.8182C1 19.0232 1.89543 20 3 20H16C17.1046 20 18 19 18 17.8182" stroke="currentColor" stroke-width="2"/>
<path d="M6.35294 4H21.6471C22.3217 4 23 4.61642 23 5.54545V14.4545C23 15.3836 22.3217 16 21.6471 16H6.35294C5.67833 16 5 15.3836 5 14.4545V5.54545C5 4.61642 5.67833 4 6.35294 4Z" stroke="currentColor" stroke-width="2"/>
<path d="M12 7V12.3846L16.3077 9.69277L12 7Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="material" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>

Before

Width:  |  Height:  |  Size: 403 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.4666667 8.4666667" x="0px" y="0px" class="misc">
<path fill="currentColor" d="m 4.2330754,3.0330699e-4 c -1.9062912,0 -3.45664101,1.55086299301 -3.45663572,3.45715399301 0,1.041342 0.84545222,2.220339 1.65622812,3.201355 0.8107786,0.981014 1.6190225,1.736328 1.6190225,1.736328 a 0.26460984,0.26460984 0 0 0 0.3612197,0 c 0,0 0.8082439,-0.755314 1.6190224,-1.736328 0.810776,-0.981016 1.6582946,-2.160013 1.6582946,-3.201355 0,-1.906291 -1.5508605,-3.45715399301 -3.4571516,-3.45715399301 z m 0,0.52968500301 c 1.6203083,0 2.9279876,1.30716399 2.9279849,2.92746899 0,0.721961 -0.7497154,1.914917 -1.5353056,2.865459 -0.6952271,0.8412 -1.2416102,1.3482 -1.3926793,1.491898 C 4.0825513,7.6716453 3.5360226,7.1646033 2.840396,6.3229163 2.0548058,5.3723743 1.3035373,4.1794183 1.3035373,3.4574573 1.3035347,1.8371523 2.6127671,0.52998831 4.2330754,0.52998831 Z m 0.00878,0.91518899 a 0.26460979,0.26460979 0 0 0 -0.026355,5.16e-4 0.26460979,0.26460979 0 0 0 -0.1405599,0.05116 L 2.444037,2.6998813 a 0.26474432,0.26474432 0 1 0 0.3147086,0.425813 l 0.056327,-0.04134 v 1.224733 a 0.26460979,0.26460979 0 0 0 0.2640673,0.265615 h 2.30632 a 0.26460979,0.26460979 0 0 0 0.2656152,-0.265615 v -1.223698 l 0.054777,0.04031 A 0.2647471,0.2647471 0 1 0 6.0205633,2.6998813 L 5.5513406,2.3536473 a 0.26460979,0.26460979 0 0 0 -0.00775,-0.0057 L 4.3896558,1.4968523 a 0.26460979,0.26460979 0 0 0 -0.1477963,-0.05168 z m -0.00878,0.594278 0.8888333,0.655775 v 0.217556 1.132747 H 4.4971428 v -0.437697 a 0.26460984,0.26460984 0 0 0 -0.2676843,-0.267684 0.26460984,0.26460984 0 0 0 -0.262001,0.267684 v 0.437697 H 3.344758 v -1.132747 -0.219107 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs/>
<path fill="none" fill-rule="evenodd" stroke="#333" d="M3.5 7c0-.3.2-.5.5-.5h16c.3 0 .5.2.5.5s-.2.5-.5.5H4a.5.5 0 01-.5-.5zm0 5c0-.3.2-.5.5-.5h16c.3 0 .5.2.5.5s-.2.5-.5.5H4a.5.5 0 01-.5-.5zm0 5c0-.3.2-.5.5-.5h16c.3 0 .5.2.5.5s-.2.5-.5.5H4a.5.5 0 01-.5-.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="material" width="24px" height="24px"><path d="M20 8H4V6h16v2zm-2-6H6v2h12V2zm4 10v8c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2v-8c0-1.1.9-2 2-2h16c1.1 0 2 .9 2 2zm-6 4l-6-3.27v6.53L16 16z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 310 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="material" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/></svg>

Before

Width:  |  Height:  |  Size: 313 B

View File

@ -16,7 +16,6 @@ import { AppComponent } from './app/app.component'
import routes from './app/app.routes'
import {
CustomReuseStrategy,
MenuGuards,
PluginService,
PreloadSelectedModulesList,
RedirectService,
@ -73,7 +72,6 @@ const bootstrap = () => bootstrapApplication(AppComponent, {
getFormProviders(),
PreloadSelectedModulesList,
...MenuGuards.guards,
{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy },
provideRouter(routes,

View File

@ -194,9 +194,9 @@ code {
--horizontalMarginContent: #{$expanded-horizontal-margins};
--mainColWidth: 100vw;
width: 100%;
width: calc(100% - 48px);
@include margin-left(0);
@include margin-left(48px);
}
&.lock-scroll .main-row > router-outlet + * { /* stylelint-disable-line selector-max-compound-selectors */

View File

@ -16,7 +16,8 @@
@include peertube-button-big-link;
}
.orange-button {
.orange-button,
.primary-button {
@include orange-button;
}
@ -24,7 +25,8 @@
@include orange-button-inverted;
}
.grey-button {
.grey-button,
.secondary-button {
@include grey-button;
}

View File

@ -143,3 +143,9 @@
.outline-0 {
outline: none;
}
// ---------------------------------------------------------------------------
.transform-rotate-180 {
transform: rotate(180deg);
}

View File

@ -264,12 +264,12 @@
}
@mixin peertube-button {
padding: 4px 13px;
padding: 6px 20px;
border: 0;
font-weight: $font-semibold;
border-radius: 3px;
border-radius: 4px;
text-align: center;
cursor: pointer;
@ -278,9 +278,9 @@
@include rounded-line-height-1-5($button-font-size);
my-global-icon + * {
@include margin-right(4px);
@include margin-left(4px);
my-global-icon + *:not(:empty) {
@include margin-right(8px);
@include margin-left(8px);
}
}
@ -318,12 +318,13 @@
@include peertube-button;
}
@mixin button-with-icon($width: 20px, $margin-right: 3px, $top: -1px) {
@mixin button-with-icon($width: 20px, $margin-right: 3px, $top: -1px, $right: 0) {
my-global-icon {
position: relative;
display: inline-block;
width: $width;
top: $top;
right: $right;
@include margin-right($margin-right);
}

View File

@ -42,10 +42,9 @@ $button-font-size: 15px;
$header-height: 106px;
$menu-background: #000;
$menu-color: #fff;
$menu-width: 240px;
$menu-lateral-padding: 26px;
$menu-background: #221A1A;
$menu-color: #E9DFDF;
$menu-width: 248px;
$sub-menu-background-color: #F7F7F7;
$sub-menu-height: 81px;

View File

@ -92,8 +92,8 @@ $playlist-menu-width: 350px;
}
.vjs-playlist-icon {
mask-image: url('#{$assets-path}/images/feather/list.svg');
-webkit-mask-image: url('#{$assets-path}/images/feather/list.svg');
mask-image: url('#{$assets-path}/images/feather/playlists.svg');
-webkit-mask-image: url('#{$assets-path}/images/feather/playlists.svg');
mask-size: cover;
-webkit-mask-size: cover;