Menu blocks
|
@ -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 () {
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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([
|
||||
{
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: pvar(--mainBackgroundColor);
|
||||
}
|
||||
|
||||
.broadcast-message {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ]
|
||||
},
|
||||
|
|
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -102,7 +102,7 @@ export class User implements UserServerModel {
|
|||
else this.account.resetAvatar()
|
||||
}
|
||||
|
||||
isUploadDisabled () {
|
||||
hasUploadDisabled () {
|
||||
return this.videoQuota === 0 || this.videoQuotaDaily === 0
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
width: 34px;
|
||||
height: 34px;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@include margin-right(10px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)">©</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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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,
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -143,3 +143,9 @@
|
|||
.outline-0 {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.transform-rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|