This commit is contained in:
Chocobozzz 2024-11-21 13:23:25 +01:00
parent c6821b689d
commit 5ce1470b9e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
86 changed files with 893 additions and 938 deletions

View File

@ -1,8 +1,8 @@
<div class="banner" *ngIf="instanceBannerUrl">
<img [src]="instanceBannerUrl" alt="Instance banner">
</div>
<div class="margin-content mt-4">
<div class="banner mb-4" *ngIf="instanceBannerUrl">
<img class="rounded" [src]="instanceBannerUrl" alt="Instance banner">
</div>
<div class="row ">
<div class="col-md-12 col-xl-6">

View File

@ -42,7 +42,7 @@
<div class="form-group" formGroupName="trending">
<ng-container formGroupName="videos">
<ng-container formGroupName="algorithms">
<label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
<label i18n for="trendingVideosAlgorithmsDefault">Default trending algorithm</label>
<div class="peertube-select-container">
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">

View File

@ -164,9 +164,7 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
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' }
{ label: $localize`Browse videos`, path: '/videos/browse' }
])
this.defaultLandingPageOptions = links.map(o => ({

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
import { Routes } from '@angular/router'
import { VideoChannelCreateComponent } from '@app/shared/standalone-channels/video-channel-create.component'
import { VideoChannelUpdateComponent } from '@app/shared/standalone-channels/video-channel-update.component'
export default [
{
path: 'create',
component: VideoChannelCreateComponent,
data: {
meta: {
title: $localize`Create a new video channel`
}
}
},
{
path: 'update/:videoChannelName',
component: VideoChannelUpdateComponent,
data: {
meta: {
title: $localize`Update video channel`
}
}
}
] satisfies Routes

View File

@ -39,18 +39,3 @@
}
}
}
@media screen and (min-width: $mobile-view) and (max-width: #{$small-view + $menu-width}) {
:host-context(.main-col:not(.expanded)) {
.header {
a {
font-size: 0;
padding: 0 13px;
}
.peertube-select-container {
width: auto !important;
}
}
}
}

View File

@ -37,7 +37,7 @@
</div>
<div class="video-channel-buttons">
<my-edit-button label [ptRouterLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
<my-edit-button label [ptRouterLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]"></my-edit-button>
<my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
</div>

View File

@ -123,9 +123,7 @@ my-edit-button {
}
@media screen and (min-width: $small-view) {
:host-context(.expanded) {
.video-channel-buttons {
float: right;
}
.video-channel-buttons {
float: right;
}
}

View File

@ -1,6 +1,8 @@
<div class="root" *ngIf="videoChannel">
<div class="margin-content banner" *ngIf="videoChannel.bannerUrl">
<img [src]="videoChannel.bannerUrl" alt="Channel banner">
<div class="margin-content">
<div class="banner" *ngIf="videoChannel.bannerUrl">
<img [src]="videoChannel.bannerUrl" alt="Channel banner">
</div>
</div>
<div class="margin-content channel-info">

View File

@ -5,7 +5,7 @@
@use '_button-mixins' as *;
.root {
--co-global-top-padding: 60px;
--co-global-top-padding: 2rem;
--co-channel-img-margin: 30px;
--co-font-size: 16px;
--co-channel-handle-font-size: 16px;
@ -15,6 +15,7 @@
.actor-info {
min-width: 1px;
width: 100%;
align-items: flex-start;
> h4,
> .actor-handle {
@ -89,6 +90,7 @@
padding: 30px;
width: 300px;
font-size: var(--co-font-size);
border-radius: 5px;
.avatar-row {
display: flex;
@ -222,7 +224,7 @@ my-copy-button {
@media screen and (max-width: $mobile-view) {
.root {
--co-global-top-padding: 15px;
--co-global-top-padding: 1rem;
--co-font-size: 14px;
--co-channel-handle-font-size: 13px;
--co-owner-handle-font-size: 13px;

View File

@ -89,9 +89,3 @@ $nav-link-height: 40px;
@media screen and (max-width: $small-view) {
@include nav-scroll();
}
@media screen and (max-width: #{$small-view + $menu-width}) {
:host-context(.main-col:not(.expanded)) {
@include nav-scroll();
}
}

View File

@ -69,7 +69,7 @@
}
}
@media screen and (max-width: 450px) {
@media screen and (max-width: $small-view) {
.action-button .icon-text {
display: none !important;
}

View File

@ -28,7 +28,6 @@ form {
min-height: calc(#{$peertube-textarea-height} - 15px * 2);
@include peertube-textarea(100%, $peertube-textarea-height);
@include button-focus(pvar(--primary-100));
@include padding-right($markdown-icon-width + 15px !important);
@media screen and (max-width: 600px) {

View File

@ -7,7 +7,7 @@
</ng-container>
</span>
<a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about/peertube#privacy">More information</a>
<a class="link-orange" i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about/peertube#privacy">More information</a>
</div>
<button i18n class="ms-2 peertube-button primary-button" (click)="acceptedPrivacyConcern()">

View File

@ -5,10 +5,11 @@
position: fixed;
bottom: 0;
width: calc(100% - #{$menu-width});
width: calc(100% - #{$menu-width} - (#{pvar(--x-margin-content)} * 2));
z-index: z(privacymsg);
padding: 5px 15px;
border-radius: 5px 5px 0 0;
display: flex;
flex-wrap: nowrap;
@ -16,27 +17,8 @@
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.9);
color: #fff;
}
// If the view is expanded
:host-context(.expanded) {
.privacy-concerns {
width: 100%;
}
}
// Avoid higher z-index when overlay on touchscreens
:host-context(.menu-open) {
.privacy-concerns {
z-index: z(overlay) - 1;
}
}
// Or if we are in the small view
@media screen and (max-width: $small-view) {
.privacy-concerns {
width: 100%;
}
@include margin-left(pvar(--x-margin-content));
}
.privacy-concerns-text {
@ -44,23 +26,21 @@
}
a {
color: pvar(--primary);
transition: color 0.3s;
@include disable-default-a-behaviour;
&:hover {
color: pvar(--bg);
}
color: #fff;
}
@media screen and (max-width: 1300px) {
:host-context(.main-col.expanded) {
.privacy-concerns {
font-size: 12px;
padding: 2px 5px;
}
.privacy-concerns-text {
margin: 0;
width: calc(100% - #{$menu-collapsed-width} - (#{pvar(--x-margin-content)} * 2));
}
}
// Or if we are in the small view
@media screen and (max-width: $mobile-view) {
.privacy-concerns {
width: 100% !important;
border-radius: 0 !important;
@include margin-left(0 !important);
}
}

View File

@ -13,7 +13,7 @@ import { NgIf } from '@angular/common'
imports: [ NgIf ]
})
export class PrivacyConcernsComponent implements OnInit {
private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
private static LS_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
@Input() video: Video
@ -34,7 +34,7 @@ export class PrivacyConcernsComponent implements OnInit {
}
acceptedPrivacyConcern () {
peertubeLocalStorage.setItem(PrivacyConcernsComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
peertubeLocalStorage.setItem(PrivacyConcernsComponent.LS_PRIVACY_CONCERN_KEY, 'true')
this.display = false
}
@ -46,6 +46,6 @@ export class PrivacyConcernsComponent implements OnInit {
}
private alreadyAccepted () {
return peertubeLocalStorage.getItem(PrivacyConcernsComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
return peertubeLocalStorage.getItem(PrivacyConcernsComponent.LS_PRIVACY_CONCERN_KEY) === 'true'
}
}

View File

@ -1,38 +1,40 @@
<div class="root" [ngClass]="{ 'theater-enabled': theaterEnabled }" [hidden]="!playlist && !video">
<!-- We need the video container for videojs so we just hide it -->
<div id="video-wrapper">
<div *ngIf="remoteServerDown" class="remote-server-down">
<ng-container i18n>Sorry, but this video did not load because the remote instance did not respond.</ng-container>
<div class="margin-content player-margin-content">
<!-- We need the video container for videojs so we just hide it -->
<div id="video-wrapper">
<div *ngIf="remoteServerDown" class="remote-server-down">
<ng-container i18n>Sorry, but this video did not load because the remote instance did not respond.</ng-container>
<br />
<br />
<ng-container i18n>Please try refreshing the page, or try again later.</ng-container>
<ng-container i18n>Please try refreshing the page, or try again later.</ng-container>
</div>
<div id="videojs-wrapper">
<video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video>
</div>
<div class="player-widget-component">
<my-video-watch-playlist
#videoWatchPlaylist [playlist]="playlist"
[hidden]="transcriptionWidgetOpened"
(noVideoFound)="onPlaylistNoVideoFound()" (videoFound)="onPlaylistVideoFound($event)"
></my-video-watch-playlist>
@if (transcriptionWidgetOpened) {
<my-video-transcription
[video]="video" [captions]="videoCaptions" [currentTime]="getCurrentTime()"
(segmentClicked)="handleTimestampClicked($event)" (closeTranscription)="transcriptionWidgetOpened = false"
></my-video-transcription>
}
</div>
<my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder>
</div>
<div id="videojs-wrapper">
<video #playerElement class="video-js vjs-peertube-skin" playsinline="true"></video>
</div>
<div class="player-widget-component">
<my-video-watch-playlist
#videoWatchPlaylist [playlist]="playlist"
[hidden]="transcriptionWidgetOpened"
(noVideoFound)="onPlaylistNoVideoFound()" (videoFound)="onPlaylistVideoFound($event)"
></my-video-watch-playlist>
@if (transcriptionWidgetOpened) {
<my-video-transcription
[video]="video" [captions]="videoCaptions" [currentTime]="getCurrentTime()"
(segmentClicked)="handleTimestampClicked($event)" (closeTranscription)="transcriptionWidgetOpened = false"
></my-video-transcription>
}
</div>
<my-plugin-placeholder pluginId="player-next"></my-plugin-placeholder>
<my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>
</div>
<my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>
<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
<div class="video-info">

View File

@ -5,7 +5,7 @@
@use '_miniature' as *;
$video-default-height: 66vh;
$video-max-height: calc(100vh - #{$header-height} - #{$theater-bottom-space});
$video-max-height: calc(100vh - #{pvar(--header-height)} - #{$theater-bottom-space});
@mixin player-widget-below-player {
width: 100% !important;
@ -53,6 +53,7 @@ $video-max-height: calc(100vh - #{$header-height} - #{$theater-bottom-space});
background-color: #000;
display: flex;
justify-content: center;
border-radius:5px;
#videojs-wrapper {
display: flex;
@ -256,7 +257,7 @@ my-video-comments {
--co-player-portrait-mode: 1;
--co-player-height: calc(100vw / var(--co-player-ratio)) !important;
max-height: calc(100vh - #{$header-height} - #{$player-portrait-bottom-space});
max-height: calc(100vh - #{pvar(--header-height)} - #{$player-portrait-bottom-space});
}
}
@ -275,7 +276,11 @@ my-video-comments {
}
}
@media screen and (max-width: 450px) {
@media screen and (max-width: $mobile-view) {
.margin-content.player-margin-content {
margin: 0 !important;
}
.video-info-name {
font-size: 18px;
}

View File

@ -1,45 +1,43 @@
<h1 class="visually-hidden" i18n>Discover</h1>
<div class="margin-content">
@if (notResults) {
<div class="no-results" i18n>No results.</div>
} @else {
<div class="quick-access mt-3">
<div #quickAccessContent class="quick-access-links" [ngClass]="{ 'see-all-quick-links': seeAllQuickLinks }">
<span class="me-2 fg-100">Quick access:</span>
@if (notResults) {
<div class="margin-content no-results" i18n>No results.</div>
} @else {
<div class="margin-content quick-access mt-3">
<div #quickAccessContent class="quick-access-links" [ngClass]="{ 'see-all-quick-links': seeAllQuickLinks }">
<span class="me-2 fg-100">Quick access:</span>
@for (quickAccess of quickAccessLinks; track quickAccess.label) {
<a class="me-2" [routerLink]="quickAccess.routerLink" [queryParams]="quickAccess.queryParams">{{ quickAccess.label }}</a>
}
</div>
<button *ngIf="!seeAllQuickLinks && quickAccessOverflow" type="button" class="peertube-button tertiary-button" (click)="seeAllQuickLinks = true" i18n>More</button>
@for (quickAccess of quickAccessLinks; track quickAccess.label) {
<a class="me-2" [routerLink]="quickAccess.routerLink" [queryParams]="quickAccess.queryParams">{{ quickAccess.label }}</a>
}
</div>
<div
myInfiniteScroller (nearOfBottom)="onNearOfBottom()"
[dataObservable]="onDataSubject.asObservable()" setAngularState="true" [parentDisabled]="disabled"
>
<div class="section videos" *ngFor="let object of objects">
<button *ngIf="!seeAllQuickLinks && quickAccessOverflow" type="button" class="peertube-button tertiary-button" (click)="seeAllQuickLinks = true" i18n>More</button>
</div>
<div class="section-header d-flex justify-content-between align-items-start">
<h1 class="section-title">
<my-actor-avatar *ngIf="object.channel" size="40px" actorType="channel" [actor]="object.channel"></my-actor-avatar>
<div
class="margin-content videos-margin-content"
myInfiniteScroller (nearOfBottom)="onNearOfBottom()"
[dataObservable]="onDataSubject.asObservable()" setAngularState="true" [parentDisabled]="disabled"
>
<div class="section videos" *ngFor="let object of objects">
<a class="text-fg border-highlight text-decoration-none" routerLink="/search" [queryParams]="object.queryParams">{{ object.label }}</a>
<div class="section-header d-flex flex-wrap justify-content-between align-items-start mb-3">
<h1 class="section-title">
<my-actor-avatar *ngIf="object.channel" size="40px" actorType="channel" [actor]="object.channel"></my-actor-avatar>
<span class="fg-100 fs-7 mx-2">·</span>
<span i18n class="fg-100 fs-7">{{ object.type }}</span>
</h1>
<a class="text-fg border-highlight text-decoration-none" routerLink="/search" [queryParams]="object.queryParams">{{ object.label }}</a>
<my-button theme="primary" [routerLink]="object.routerLink" [queryParams]="object.queryParams">{{ object.buttonLabel }}</my-button>
</div>
<span class="fg-100 fs-7 mx-2">·</span>
<span i18n class="fg-100 fs-7">{{ object.type }}</span>
</h1>
<div class="video-wrapper" *ngFor="let video of object.videos">
<my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"></my-video-miniature>
</div>
<my-button class="ms-2 d-none-mw" theme="primary" [routerLink]="object.routerLink" [queryParams]="object.queryParams">{{ object.buttonLabel }}</my-button>
</div>
<div class="video-wrapper" *ngFor="let video of object.videos">
<my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"></my-video-miniature>
</div>
</div>
}
</div>
</div>
}

View File

@ -18,7 +18,7 @@
@include margin-bottom(2rem);
}
.margin-content {
.margin-content.videos-margin-content {
@include grid-videos-miniature-layout-with-margins($rows-limit: 2);
}
@ -60,7 +60,7 @@
overflow: initial;
.section-title {
@include margin-left(10px);
@include margin-left(pvar(--x-margin-content));
}
}
}

View File

@ -15,7 +15,7 @@
<div class="sub-header-container">
<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.isMenuCollapsed() }">
<main #mainContent tabindex="-1" id="content" class="main-col" [ngClass]="{ expanded: menu.isCollapsed() }">
<div class="main-row">

View File

@ -22,17 +22,17 @@
}
.main-row {
min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin});
min-height: calc(100vh - #{pvar(--header-height)} - #{$footer-height} - #{$footer-margin});
}
.sub-header-container {
margin-top: $header-height;
margin-top: pvar(--header-height);
background-color: pvar(--bg);
width: 100%;
}
.root-header {
height: $header-height;
height: pvar(--header-height);
position: fixed;
top: 0;
width: 100%;

View File

@ -56,9 +56,9 @@ const routes: Routes = [
},
{
path: 'manage/update',
pathMatch: 'prefix',
redirectTo: '/my-library/video-channels/update'
path: 'manage/update/:channel',
pathMatch: 'full',
redirectTo: '/my-library/video-channels/update/:channel'
},
{

View File

@ -27,7 +27,7 @@ export class AuthService {
private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token'
private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-token'
private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me'
private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
private static LS_OAUTH_CLIENT_KEYS = {
CLIENT_ID: 'client_id',
CLIENT_SECRET: 'client_secret'
}
@ -37,8 +37,8 @@ export class AuthService {
tokensRefreshed = new ReplaySubject<void>(1)
loggedInHotkeys: Hotkey[]
private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
private clientId: string = peertubeLocalStorage.getItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_ID)
private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
private loginChanged: Subject<AuthStatus>
private user: AuthUser = null
private refreshingTokenObservable: Observable<void>
@ -89,8 +89,8 @@ export class AuthService {
this.clientId = res.client_id
this.clientSecret = res.client_secret
peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID, this.clientId)
peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET, this.clientSecret)
peertubeLocalStorage.setItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_ID, this.clientId)
peertubeLocalStorage.setItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_SECRET, this.clientSecret)
logger.info('Client credentials loaded.')
},

View File

@ -17,16 +17,12 @@ export class MenuService {
// Do not display menu on small or touch screens
if (this.screenService.isInSmallView() || this.screenService.isInTouchScreen()) {
this.setMenuCollapsed(true)
} else {
this.setMenuCollapsed(this.localStorageService.getItem(MenuService.LS_MENU_COLLAPSED) === 'true')
this.menuChangedByUser = this.menuCollapsed
}
this.handleWindowResize()
this.menuCollapsed = this.localStorageService.getItem(MenuService.LS_MENU_COLLAPSED) === 'true'
this.menuChangedByUser = this.menuCollapsed
}
isMenuCollapsed () {
return this.menuCollapsed
}
toggleMenu () {
@ -43,21 +39,16 @@ export class MenuService {
setMenuCollapsed (collapsed: boolean) {
this.menuCollapsed = collapsed
if (!this.screenService.isInTouchScreen()) return
// On touch screens, lock body scroll and display content overlay when memu is opened
if (!this.menuCollapsed) {
if (this.menuCollapsed) {
document.body.classList.remove('menu-open')
} else {
document.body.classList.add('menu-open')
this.screenService.onFingerSwipe('left', () => this.setMenuCollapsed(true))
return
}
document.body.classList.remove('menu-open')
}
onResize () {
if (this.screenService.isInSmallView() && !this.menuChangedByUser) {
this.menuCollapsed = true
this.setMenuCollapsed(true)
}
}

View File

@ -1,34 +1,35 @@
<div class="root py-4 px-4 w-100 d-flex justify-content-between">
<a class="peertube-title me-3" [routerLink]="getDefaultRoute()" [queryParams]="getDefaultRouteQuery()">
<div class="root" [hidden]="!loaded || (loggedIn && !user.account)">
<a class="peertube-title" [routerLink]="getDefaultRoute()" [queryParams]="getDefaultRouteQuery()">
<span class="icon-logo"></span>
<span class="instance-name">{{ instanceName }}</span>
</a>
<div class="d-flex align-items-center" [hidden]="!loaded || (loggedIn && !user.account)">
<my-search-typeahead class="w-100 me-5"></my-search-typeahead>
<my-search-typeahead></my-search-typeahead>
<div class="d-flex align-items-center">
@if (!loggedIn) {
<my-button theme="tertiary" rounded="true" class="me-3" icon="cog" (click)="openQuickSettings()"></my-button>
<my-button theme="tertiary" rounded="true" class="margin-button" icon="cog" (click)="openQuickSettings()"></my-button>
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link secondary-button w-100 text-truncate me-3">
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link secondary-button w-100 ellipsis margin-button">
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
</a>
<my-login-link className="peertube-button-link primary-button w-100 text-truncate"></my-login-link>
<my-login-link class="login-label" className="peertube-button-link primary-button w-100 ellipsis"></my-login-link>
<my-login-link class="login-icon" className="peertube-button-link primary-button icon-only w-100 ellipsis" icon="true" label=""></my-login-link>
} @else {
<my-notification-dropdown class="me-3" (navigate)="onActiveLinkScrollToAnchor($event)"></my-notification-dropdown>
<my-notification-dropdown class="margin-button"></my-notification-dropdown>
<my-button theme="tertiary" rounded="true" class="me-3" icon="cog" ptRouterLink="/my-account"></my-button>
<my-button theme="tertiary" rounded="true" class="margin-button" icon="cog" ptRouterLink="/my-account"></my-button>
<div
class="logged-in-container" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left auto"
container="body" (openChange)="onDropdownOpenChange($event)"
container="body"
>
<button class="tertiary-button" ngbDropdownToggle>
<my-actor-avatar [actor]="user.account" actorType="account" size="34" class="me-2"></my-actor-avatar>
<my-actor-avatar [actor]="user.account" actorType="account" size="34" responseSize="true"></my-actor-avatar>
<div class="logged-in-info text-start">
<div class="logged-in-info text-start ms-2">
<div class="display-name ellipsis">{{ user.account?.displayName }}</div>
<div class="username ellipsis fs-8">&#64;{{ user.username }}</div>
@ -37,8 +38,8 @@
<div ngbDropdownMenu>
<a
*ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
#profile (click)="onActiveLinkScrollToAnchor(profile)"
*ngIf="user.account" ngbDropdownItem class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
#profile
>
<my-global-icon iconName="user" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
</a>
@ -46,8 +47,8 @@
<div class="dropdown-divider"></div>
<a
*ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account"
#manageAccount (click)="onActiveLinkScrollToAnchor(manageAccount)"
*ngIf="user.account" ngbDropdownItem class="dropdown-item" routerLink="/my-account"
#manageAccount
>
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> <ng-container i18n>Manage my account</ng-container>
</a>
@ -76,6 +77,8 @@
</div>
}
</div>
<my-button theme="tertiary" rounded="true" class="menu-button margin-button" icon="menu" (click)="toggleMenu()"></my-button>
</div>
<my-language-chooser #languageChooserModal></my-language-chooser>

View File

@ -1,48 +1,72 @@
@use 'sass:math';
@use '_variables' as *;
@use '_mixins' as *;
@use '_button-mixins' as *;
.root {
--co-logo-size: 34px;
background-color: pvar(--bg);
padding: 1.5rem;
width: 100%;
display: flex;
align-items: center;
}
.peertube-title {
flex-shrink: 1;
min-width: var(--co-logo-size);
flex-grow: 1;
font-size: 24px;
font-weight: $font-bold;
color: inherit !important;
display: flex;
align-items: center;
overflow: hidden;
@include padding-left(18px);
@include margin-right(0.5rem);
@include disable-default-a-behaviour;
}
.instance-name {
width: 100%;
.instance-name {
width: 100%;
@include ellipsis;
@include ellipsis;
}
@media screen and (max-width: $mobile-view) {
display: none;
}
}
.icon-logo {
display: inline-block;
width: var(--co-logo-size);
height: var(--co-logo-size);
min-width: var(--co-logo-size);
max-width: var(--co-logo-size);
.icon-logo {
display: inline-block;
width: 34px;
height: 34px;
min-width: 34px;
max-width: 34px;
background-repeat: no-repeat;
background-size: contain;
background-repeat: no-repeat;
@include margin-left(18px);
@include margin-right(10px);
}
@include margin-left(18px);
@include margin-right(10px);
}
my-search-typeahead {
max-width: 270px;
@include margin-right(1.5rem);
}
.margin-button {
@include margin-right(0.75rem);
}
.menu-button {
display: none;
justify-self: end;
}
.dropdown {
z-index: #{z('menu') + 1} !important;
z-index: #{z('header') + 1} !important;
}
.dropdown-item {
@ -64,11 +88,11 @@
}
.logged-in-container {
flex: 1;
border-radius: 25px;
transition: all .1s ease-in-out;
cursor: pointer;
max-width: 250px;
height: 100%;
.display-name {
font-weight: $font-bold;
@ -90,3 +114,95 @@
}
}
}
my-actor-avatar {
width: 34px;
height: 34px;
}
.login-icon {
display: none;
}
@media screen and (max-width: $menu-overlay-view) {
.peertube-title {
@include padding-left(0);
}
.icon-logo {
@include margin-left(0);
}
}
@media screen and (max-width: $small-view) {
.root {
padding: 1rem;
}
my-search-typeahead {
@include margin-right(0.5rem);
}
.margin-button.tertiary-button {
@include margin-right(0);
}
my-actor-avatar {
width: 24px;
height: 24px;
}
.dropdown-toggle {
@include peertube-button;
@include rounded-icon-button;
&::after {
display: none;
}
}
.logged-in-info {
display: none;
}
.login-icon {
display: inline-block;
}
.login-label {
display: none;
}
}
@media screen and (max-width: $mobile-view) {
.root {
--co-logo-size: 48px;
padding: 1rem;
display: grid;
row-gap: 0.5rem;
justify-content: space-between;
> * {
grid-row: 1;
}
}
.menu-button {
display: block;
position: relative;
right: -10px;
}
my-search-typeahead {
grid-row: 2 !important;
grid-column: 1 / 4;
max-width: none;
@include margin-right(0);
}
.instance-name {
display: none;
}
}

View File

@ -1,4 +1,4 @@
import { CommonModule, ViewportScroller } from '@angular/common'
import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { Router, RouterLink } from '@angular/router'
import {
@ -9,11 +9,9 @@ import {
MenuService,
RedirectService,
ScreenService,
ServerService,
UserService
ServerService
} from '@app/core'
import { NotificationDropdownComponent } from '@app/header/notification-dropdown.component'
import { scrollToTop } from '@app/helpers'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
import { QuickSettingsModalComponent } from '@app/menu/quick-settings-modal.component'
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
@ -73,22 +71,16 @@ export class HeaderComponent implements OnInit, OnDestroy {
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 router: Router,
private menu: MenuService
) { }
get isInMobileView () {
return this.screenService.isInMobileView()
}
get language () {
return this.languageChooserModal.getCurrentLanguage()
}
@ -101,6 +93,14 @@ export class HeaderComponent implements OnInit, OnDestroy {
return this.serverConfig.instance.name
}
isInMobileView () {
return this.screenService.isInMobileView()
}
isInSmallView () {
return this.screenService.isInSmallView()
}
ngOnInit () {
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
@ -170,57 +170,14 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.quickSettingsModal.show()
}
// FIXME: needed?
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)
}
}
// Lock menu scroll when menu scroll to avoid fleeing / detached dropdown
// FIXME: needed?
onMenuScrollEvent () {
document.querySelector('nav').scrollTo(0, 0)
}
// FIXME: needed?
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()
}
}
openHotkeysCheatSheet () {
this.hotkeysService.cheatSheetToggle.next(!this.hotkeysHelpVisible)
}
toggleMenu () {
this.menu.toggleMenu()
}
private updateUserState () {
this.user = this.loggedIn
? this.authService.getUser()

View File

@ -12,21 +12,23 @@
</ng-template>
@if (isInMobileView) {
<div i18n-title title="View your notifications" class="peertube-button tertiary-button rounded-icon-button">
<a
i18n-title title="View your notifications"
class="peertube-button tertiary-button rounded-icon-button notification-inbox-link"
routerLink="/my-account/notifications" routerLinkActive="active" #link (click)="onNavigate(link)"
>
<ng-container *ngTemplateOutlet="notificationNumber"></ng-container>
<a routerLink="/my-account/notifications" routerLinkActive="active" #link (click)="onNavigate(link)">
<ng-container *ngTemplateOutlet="notificationIcon"></ng-container>
</a>
</div>
<ng-container *ngTemplateOutlet="notificationIcon"></ng-container>
</a>
} @else {
<div
ngbDropdown autoClose="outside" placement="bottom" container="body" dropdownClass="dropdown-notifications"
#dropdown="ngbDropdown" (shown)="onDropdownShown()" (hidden)="onDropdownHidden()"
>
<button
i18n-title title="View your notifications" class="peertube-button tertiary-button rounded-icon-button disable-dropdown-caret"
[ngClass]="{ 'notification-inbox-dropdown': true, 'shown': opened, 'hidden': isInMobileView }"
i18n-title title="View your notifications" class="peertube-button tertiary-button rounded-icon-button disable-dropdown-caret notification-inbox-dropdown"
[ngClass]="{ 'shown': opened, 'hidden': isInMobileView }"
ngbDropdownToggle
>
<ng-container *ngTemplateOutlet="notificationNumber"></ng-container>

View File

@ -81,6 +81,7 @@
.notification-inbox-link {
cursor: pointer;
position: relative;
display: inline-block;
.unread-notifications {
@include margin-left(20px);
@ -91,24 +92,14 @@
top: 6px;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: pvar(--primary);
color: pvar(--on-primary);
font-weight: $font-bold;
font-size: 10px;
border-radius: 16px;
width: 16px;
height: 16px;
line-height: 1;
@media screen and (max-width: $mobile-view) {
top: -4px;
left: -2px;
}
line-height: 16px;
padding: 0 3px;
}
}

View File

@ -5,7 +5,7 @@
#search-video {
text-overflow: ellipsis;
@include peertube-input-text(270px);
@include peertube-input-text(100%);
& {
padding-inline-start: 42px; // For the search icon
@ -15,14 +15,6 @@
&::placeholder {
color: pvar(--input-placeholder);
}
@media screen and (max-width: $small-view) {
width: 200px;
}
@media screen and (max-width: $mobile-view) {
width: 150px;
}
}
.search-button {

View File

@ -1,21 +1,43 @@
<div class="menu-container" [ngClass]="{ collapsed: collapsed }">
<ng-template #moreInfoButton>
<my-button i18n class="mt-2 d-block" theme="secondary" icon="help" i18n-ariaLabel aria-label="More info" i18n ptRouterLink="/about" ptRouterLinkActive="active">
@if (!collapsed) {
More info
}
</my-button>
</ng-template>
<div class="menu-container" [ngClass]="{ collapsed: collapsed, 'logged-in': loggedIn }">
<div class="main-menu-wrapper">
<div class="main-menu-scrollbar">
<div class="main-menu">
<div class="mobile-controls">
<span class="icon-logo"></span>
<my-button rounded="true" icon="cross" theme="tertiary" (click)="toggleMenu()"></my-button>
</div>
<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()">
<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>
<div *ngIf="!loggedIn" class="about about-top">
<div i18n [ngClass]="{ 'visually-hidden': collapsed }" class="block-title me-2">{{ instanceName }}</div>
<div [ngClass]="{ 'visually-hidden': collapsed }" class="description">{{ shortDescription }}</div>
<ng-container *ngTemplateOutlet="moreInfoButton"></ng-container>
</div>
<nav>
<ng-container *ngFor="let menuSection of menuSections" >
<ul class="ul-unstyle" [ngClass]="[ menuSection.key, 'menu-block' ]">
@ -43,16 +65,10 @@
</ng-container>
</nav>
<div class="about">
<div [ngClass]="{ 'visually-hidden': collapsed }" class="block-title">About {{ instanceName }}</div>
<div *ngIf="loggedIn" class="about">
<div [ngClass]="{ 'visually-hidden': collapsed }" class="block-title">{{ instanceName }}</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 ptRouterLink="/about">
@if (!collapsed) {
More info
}
</my-button>
<ng-container *ngTemplateOutlet="moreInfoButton"></ng-container>
</div>
</div>
</div>
@ -66,3 +82,6 @@
<a class="d-block fs-8" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer" i18n>Discover more platforms</a>
</div>
</div>
<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->
<div class="menu-overlay" [ngClass]="{ 'menu-collapsed': collapsed }" (click)="toggleMenu()"></div>

View File

@ -9,9 +9,9 @@
z-index: z(menu);
width: calc(#{$menu-width} - 2rem);
position: fixed;
height: calc(100vh - #{$header-height});
height: calc(100vh - #{pvar(--header-height)});
@include margin-left(2rem);
@include margin-left(pvar(--menu-margin-left));
&.collapsed {
--co-menu-x-padding: 0.25rem;
@ -20,14 +20,6 @@
@include margin-left(0);
}
@media screen and (max-width: $mobile-view) {
width: 100% !important;
.main-menu {
overflow-y: auto !important;
}
}
}
.toggle-menu {
@ -102,8 +94,10 @@
@include padding-right(var(--co-menu-x-padding));
}
.menu-block,
.collapsed .toggle-menu-container {
.menu-block:not(:last-child),
.logged-in .menu-block,
.collapsed .toggle-menu-container,
.about-top {
&::after {
content: '';
display: block;
@ -113,11 +107,14 @@
}
}
.collapsed .menu-block,
.collapsed .toggle-menu-container {
&::after {
width: 28px;
margin: 1rem auto;
.collapsed {
.menu-block,
.toggle-menu-container,
.about {
&::after {
width: 28px;
margin: 1rem auto;
}
}
}
@ -148,6 +145,9 @@
color: pvar-fallback(--menu-fg, --fg-350);
font-size: 14px;
margin-bottom: 0.5rem;
max-width: 180px;
@include ellipsis;
}
.menu-link {
@ -201,3 +201,87 @@
my-button[theme=secondary] ::ng-deep my-global-icon {
color: pvar(--secondary-icon-color) !important;
}
.menu-overlay {
background-color: #000;
width: 100vw;
height: 100vh;
opacity: 0.75;
content: '';
display: none;
position: fixed;
z-index: z(overlay);
}
.mobile-controls {
display: none;
padding: 0 calc(var(--co-menu-x-padding) - 1rem) 2rem var(--co-menu-x-padding);
align-items: center;
justify-content: space-between;
.icon-logo {
display: inline-block;
width: 48px;
height: 48px;
min-width: 48px;
max-width: 48px;
background-repeat: no-repeat;
background-size: contain;
}
}
@media screen and (max-width: $menu-overlay-view) {
// On small screen use a menu overlay
.menu-container {
&:not(.collapsed) {
--menu-margin-left: 0;
border-radius: 0;
background-color: pvar-fallback(--menu-bg, --bg-secondary-400);
overflow: auto;
.main-menu-wrapper {
height: auto;
max-height: unset;
}
}
}
.menu-overlay:not(.menu-collapsed) {
display: block;
}
}
@media screen and (max-width: $mobile-view) {
.menu-container {
height: 100vh;
margin-top: calc(#{pvar(--header-height)} * -1);
z-index: z(root-header) + 1;
&.collapsed {
display: none;
}
&:not(.collapsed) {
width: 100vw !important;
}
}
.main-menu {
padding-top: 0.75rem;
}
.menu-overlay {
display: none !important;
}
.mobile-controls {
display: flex;
}
.toggle-menu-container {
display: none;
}
}

View File

@ -7,6 +7,7 @@ import {
AuthUser,
HooksService,
MenuService,
RedirectService,
ServerService,
UserService
} from '@app/core'
@ -53,8 +54,8 @@ const debugLogger = debug('peertube:menu:MenuComponent')
})
export class MenuComponent implements OnInit, OnDestroy {
menuSections: MenuSection[] = []
loggedIn: boolean
private isLoggedIn: boolean
private user: AuthUser
private canSeeVideoMakerBlock: boolean
@ -67,7 +68,8 @@ export class MenuComponent implements OnInit, OnDestroy {
private userService: UserService,
private serverService: ServerService,
private hooks: HooksService,
private menu: MenuService
private menu: MenuService,
private redirectService: RedirectService
) { }
get shortDescription () {
@ -82,13 +84,17 @@ export class MenuComponent implements OnInit, OnDestroy {
return this.menu.isCollapsed()
}
get isOverlay () {
return this.menu.isCollapsed()
}
ngOnInit () {
this.isLoggedIn = this.authService.isLoggedIn()
this.loggedIn = this.authService.isLoggedIn()
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.loggedIn = true
else if (status === AuthStatus.LoggedOut) this.loggedIn = false
this.onUserStateChange()
})
@ -127,9 +133,14 @@ export class MenuComponent implements OnInit, OnDestroy {
title: $localize`Quick access`,
links: [
{
path: '/home',
path: this.redirectService.getDefaultRoute(),
icon: 'home' as GlobalIconName,
label: $localize`Home`
},
{
path: '/videos/subscriptions',
icon: 'subscriptions' as GlobalIconName,
label: $localize`Subscriptions`
}
]
}
@ -138,18 +149,13 @@ export class MenuComponent implements OnInit, OnDestroy {
private buildLibraryLinks (): MenuSection {
let links: MenuLink[] = []
if (this.isLoggedIn) {
if (this.loggedIn) {
links = links.concat([
{
path: '/my-library/video-playlists',
icon: 'playlists' as GlobalIconName,
label: $localize`Playlists`
},
{
path: '/videos/subscriptions',
icon: 'subscriptions' as GlobalIconName,
label: $localize`Subscriptions`
},
{
path: '/my-library/history/videos',
icon: 'history' as GlobalIconName,
@ -168,7 +174,7 @@ export class MenuComponent implements OnInit, OnDestroy {
private buildVideoMakerLinks (): MenuSection {
let links: MenuLink[] = []
if (this.isLoggedIn && this.canSeeVideoMakerBlock) {
if (this.loggedIn && this.canSeeVideoMakerBlock) {
links = links.concat([
{
path: '/my-library/video-channels',
@ -202,7 +208,7 @@ export class MenuComponent implements OnInit, OnDestroy {
private buildAdminLinks (): MenuSection {
const links: MenuLink[] = []
if (this.isLoggedIn) {
if (this.loggedIn) {
if (this.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
links.push({
path: '/admin/overview',
@ -238,7 +244,7 @@ export class MenuComponent implements OnInit, OnDestroy {
// ---------------------------------------------------------------------------
private computeCanSeeVideoMakerBlock () {
if (!this.isLoggedIn) return of(false)
if (!this.loggedIn) return of(false)
if (!this.user.hasUploadDisabled()) return of(true)
return this.authService.userInformationLoaded
@ -256,7 +262,7 @@ export class MenuComponent implements OnInit, OnDestroy {
}
private onUserStateChange () {
this.user = this.isLoggedIn
this.user = this.loggedIn
? this.authService.getUser()
: undefined

View File

@ -24,7 +24,7 @@ export class AccountSetupWarningModalComponent {
user: User
private LOCAL_STORAGE_KEYS = {
private LS_KEYS = {
NO_ACCOUNT_SETUP_WARNING_MODAL: 'no_account_setup_warning_modal'
}
@ -49,7 +49,7 @@ export class AccountSetupWarningModalComponent {
shouldOpen (user: User) {
if (user.noAccountSetupWarningModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL) === 'true') return false
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL) === 'true') return false
if (this.hasAccountAvatar(user) && this.hasAccountDescription(user)) return false
if (this.userService.hasSignupInThisSession()) return false
@ -75,7 +75,7 @@ export class AccountSetupWarningModalComponent {
}
private doNotOpenAgain () {
peertubeLocalStorage.setItem(this.LOCAL_STORAGE_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL, 'true')
peertubeLocalStorage.setItem(this.LS_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL, 'true')
this.userService.updateMyProfile({ noAccountSetupWarningModal: true })
.subscribe({

View File

@ -15,7 +15,7 @@ import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
export class AdminWelcomeModalComponent {
@ViewChild('modal', { static: true }) modal: ElementRef
private LOCAL_STORAGE_KEYS = {
private LS_KEYS = {
NO_WELCOME_MODAL: 'no_welcome_modal'
}
@ -27,7 +27,7 @@ export class AdminWelcomeModalComponent {
shouldOpen (user: User) {
if (user.noWelcomeModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_WELCOME_MODAL) === 'true') return false
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_WELCOME_MODAL) === 'true') return false
return true
}
@ -42,7 +42,7 @@ export class AdminWelcomeModalComponent {
}
doNotOpenAgain () {
peertubeLocalStorage.setItem(this.LOCAL_STORAGE_KEYS.NO_WELCOME_MODAL, 'true')
peertubeLocalStorage.setItem(this.LS_KEYS.NO_WELCOME_MODAL, 'true')
this.userService.updateMyProfile({ noWelcomeModal: true })
.subscribe({

View File

@ -22,7 +22,7 @@ export class InstanceConfigWarningModalComponent {
stopDisplayModal = false
about: About
private LOCAL_STORAGE_KEYS = {
private LS_KEYS = {
NO_INSTANCE_CONFIG_WARNING_MODAL: 'no_instance_config_warning_modal'
}
@ -35,7 +35,7 @@ export class InstanceConfigWarningModalComponent {
shouldOpenByUser (user: User) {
if (user.noInstanceConfigWarningModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL) === 'true') return false
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL) === 'true') return false
return true
}
@ -66,7 +66,7 @@ export class InstanceConfigWarningModalComponent {
}
private doNotOpenAgain () {
peertubeLocalStorage.setItem(this.LOCAL_STORAGE_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL, 'true')
peertubeLocalStorage.setItem(this.LS_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL, 'true')
this.userService.updateMyProfile({ noInstanceConfigWarningModal: true })
.subscribe({

View File

@ -40,7 +40,7 @@
}
.button-focus-within:focus-within {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
.dropdown-item {

View File

@ -35,7 +35,7 @@
}
.button-focus-within:focus-within {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
.dropdown-item {

View File

@ -81,13 +81,13 @@ $input-border-radius: 3px;
&.maximized {
z-index: #{z(root-header) - 1};
position: fixed;
top: $header-height;
top: pvar(--header-height);
left: $menu-width;
max-height: none !important;
max-width: none !important;
width: calc(100% - #{$menu-width});
height: calc(100vh - #{$header-height});
height: calc(100vh - #{pvar(--header-height)});
display: grid;
grid-template-rows: auto 1fr;

View File

@ -9,7 +9,7 @@ p-inputmask {
&:focus-within,
&:focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
&:disabled {

View File

@ -16,6 +16,7 @@ const icons = {
'flame': require('../../../assets/images/misc/flame.svg'),
// feather/lucide icons
'menu': require('../../../assets/images/feather/menu.svg'),
'history': require('../../../assets/images/feather/history.svg'),
'registry': require('../../../assets/images/feather/registry.svg'),
'subscriptions': require('../../../assets/images/feather/subscriptions.svg'),

View File

@ -3,13 +3,13 @@
<table *ngIf="serverConfig">
<caption i18n>Features found on this instance</caption>
<tr>
<th i18n class="label" scope="row">PeerTube version</th>
<th i18n class="t-label" scope="row">PeerTube version</th>
<td class="value">{{ getServerVersionAndCommit() }}</td>
</tr>
<tr>
<th class="label" scope="row">
<th class="t-label" scope="row">
<div i18n>Default NSFW/sensitive videos policy</div>
<span i18n class="fs-7 fw-normal fst-italic">can be redefined by the users</span>
</th>
@ -18,31 +18,31 @@
</tr>
<tr>
<th i18n class="label" scope="row">User registration</th>
<th i18n class="t-label" scope="row">User registration</th>
<td class="value">{{ buildRegistrationLabel() }}</td>
</tr>
<tr>
<th i18n class="label" colspan="2">Video uploads</th>
<th i18n class="t-label" colspan="2">Video uploads</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Transcoding in multiple resolutions</th>
<th i18n class="t-sub-label" scope="row">Transcoding in multiple resolutions</th>
<td>
<my-feature-boolean [value]="serverConfig.transcoding.enabledResolutions.length !== 0"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Automatic transcription</th>
<th i18n class="t-sub-label" scope="row">Automatic transcription</th>
<td>
<my-feature-boolean [value]="serverConfig.videoTranscription.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Video uploads</th>
<th i18n class="t-sub-label" scope="row">Video uploads</th>
<td>
<span i18n *ngIf="serverConfig.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span>
<span i18n *ngIf="!serverConfig.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
@ -50,7 +50,7 @@
</tr>
<tr>
<th i18n class="sub-label" scope="row">Video quota</th>
<th i18n class="t-sub-label" scope="row">Video quota</th>
<td class="value">
<ng-container *ngIf="initialUserVideoQuota !== -1">
@ -70,90 +70,90 @@
</tr>
<tr>
<th i18n class="label" colspan="2">Live streaming</th>
<th i18n class="t-label" colspan="2">Live streaming</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Live streaming enabled</th>
<th i18n class="t-sub-label" scope="row">Live streaming enabled</th>
<td>
<my-feature-boolean [value]="serverConfig.live.enabled"></my-feature-boolean>
</td>
</tr>
<tr *ngIf="serverConfig.live.enabled">
<th i18n class="sub-label" scope="row">Transcode live video in multiple resolutions</th>
<th i18n class="t-sub-label" scope="row">Transcode live video in multiple resolutions</th>
<td>
<my-feature-boolean [value]="serverConfig.live.transcoding.enabled && serverConfig.live.transcoding.enabledResolutions.length > 1"></my-feature-boolean>
</td>
</tr>
<tr *ngIf="serverConfig.live.enabled">
<th i18n class="sub-label" scope="row">Max parallel lives</th>
<th i18n class="t-sub-label" scope="row">Max parallel lives</th>
<td i18n>
{{ maxUserLives }} per user / {{ maxInstanceLives }} per instance
</td>
</tr>
<tr>
<th i18n class="label" colspan="2">Video Import</th>
<th i18n class="t-label" colspan="2">Video Import</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">HTTP import (YouTube, Vimeo, direct URL...)</th>
<th i18n class="t-sub-label" scope="row">HTTP import (YouTube, Vimeo, direct URL...)</th>
<td>
<my-feature-boolean [value]="serverConfig.import.videos.http.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Torrent import</th>
<th i18n class="t-sub-label" scope="row">Torrent import</th>
<td>
<my-feature-boolean [value]="serverConfig.import.videos.torrent.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Channel synchronization with other platforms (YouTube, Vimeo, ...)</th>
<th i18n class="t-sub-label" scope="row">Channel synchronization with other platforms (YouTube, Vimeo, ...)</th>
<td>
<my-feature-boolean [value]="serverConfig.import.videoChannelSynchronization.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="label" colspan="2">User Import/Export</th>
<th i18n class="t-label" colspan="2">User Import/Export</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Users can export their data</th>
<th i18n class="t-sub-label" scope="row">Users can export their data</th>
<td>
<my-feature-boolean [value]="serverConfig.export.users.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Users can import their data</th>
<th i18n class="t-sub-label" scope="row">Users can import their data</th>
<td>
<my-feature-boolean [value]="serverConfig.import.users.enabled"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="label" colspan="2">Search</th>
<th i18n class="t-label" colspan="2">Search</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Users can resolve distant content</th>
<th i18n class="t-sub-label" scope="row">Users can resolve distant content</th>
<td>
<my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean>
</td>
</tr>
<tr>
<th i18n class="label" colspan="2">Plugins & Themes</th>
<th i18n class="t-label" colspan="2">Plugins & Themes</th>
</tr>
<tr>
<th i18n class="sub-label" scope="row">Available themes</th>
<th i18n class="t-sub-label" scope="row">Available themes</th>
<td>
<span class="theme" *ngFor="let theme of serverConfig.theme.registered">
{{ theme.name }}
@ -162,7 +162,7 @@
</tr>
<tr>
<th i18n class="sub-label" scope="row">Plugins enabled</th>
<th i18n class="t-sub-label" scope="row">Plugins enabled</th>
<td>
<span class="plugin" *ngFor="let plugin of serverConfig.plugin.registered">
{{ plugin.name }}

View File

@ -6,13 +6,13 @@ table {
color: pvar(--fg);
width: 100%;
.label,
.sub-label {
&.label {
.t-label,
.t-sub-label {
&.t-label {
font-weight: $font-semibold;
}
&.sub-label {
&.t-sub-label {
font-weight: $font-regular;
@include padding-left(30px);

View File

@ -22,7 +22,7 @@ export class InstanceFollowService {
getFollowing (options: {
pagination: RestPagination
sort: SortMeta
sort?: SortMeta
search?: string
actorType?: ActivityPubActorType
state?: FollowState

View File

@ -2,7 +2,7 @@
<a
class="action-button"
[ngClass]="classes" [ngbTooltip]="title"
[routerLink]="ptRouterLink" [queryParams]="ptQueryParams" [queryParamsHandling]="ptQueryParamsHandling"
[routerLink]="ptRouterLink" [queryParams]="ptQueryParams" [queryParamsHandling]="ptQueryParamsHandling" [routerLinkActive]="ptRouterLinkActive"
>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>

View File

@ -34,7 +34,8 @@ const debugLogger = debug('peertube:button')
RouterLink,
LoaderComponent,
GlobalIconComponent,
ObserversModule
ObserversModule,
RouterLinkActive
]
})
@ -46,6 +47,7 @@ export class ButtonComponent implements OnChanges, AfterViewInit {
@Input() ptRouterLink: string[] | string
@Input() ptQueryParams: Params
@Input() ptQueryParamsHandling: QueryParamsHandling
@Input() ptRouterLinkActive = ''
@Input() title: string
@Input({ transform: booleanAttribute }) active = false

View File

@ -5,6 +5,4 @@ my-global-icon {
width: 32px;
display: inline-block;
margin-bottom: 0.75rem;
color: pvar(--on-primary);
}

View File

@ -1,4 +1,6 @@
<ng-template #content>
<my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon>
<ng-content></ng-content>
</ng-template>

View File

@ -1,13 +1,14 @@
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
import { Component, Input, OnInit } from '@angular/core'
import { RouterLink } from '@angular/router'
import { NgIf, NgClass, NgTemplateOutlet } from '@angular/common'
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
@Component({
selector: 'my-link',
styleUrls: [ './link.component.scss' ],
templateUrl: './link.component.html',
standalone: true,
imports: [ NgIf, RouterLink, NgClass, NgTemplateOutlet ]
imports: [ NgIf, RouterLink, NgClass, NgTemplateOutlet, GlobalIconComponent ]
})
export class LinkComponent implements OnInit {
@Input() internalLink?: string | any[]
@ -24,6 +25,8 @@ export class LinkComponent implements OnInit {
@Input() ariaLabel: string
@Input() icon: GlobalIconName
builtClasses: string
ngOnInit () {

View File

@ -14,10 +14,10 @@
</ng-template>
<div class="parent-container">
<my-list-overflow [items]="menuEntries" [itemTemplate]="entryTemplate"></my-list-overflow>
<my-list-overflow [items]="menuEntries" [itemTemplate]="entryTemplate" hasBorder="true"></my-list-overflow>
</div>
@if (children) {
@if (children && children.length !== 0) {
<div class="children-container">
<my-list-overflow [items]="children" [itemTemplate]="entryTemplate"></my-list-overflow>
</div>

View File

@ -20,8 +20,6 @@ h1 {
}
.parent-container {
border-bottom: 1px solid pvar(--fg-200);
.entry {
color: pvar(--fg-100);
display: inline-block;
@ -62,16 +60,20 @@ h1 {
@include rfs(margin-bottom, 2.5rem);
::ng-deep li:not(:last-child)::after {
content: '';
::ng-deep li:not(:last-child) {
white-space: nowrap;
color: pvar(--secondary-icon-color);
&::after {
content: '';
display: inline-block;
margin: 0 0.5rem;
color: pvar(--secondary-icon-color);
position: relative;
top: -1px;
display: inline-block;
margin: 0 0.5rem;
position: relative;
top: -1px;
}
}
.entry {

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'
import { NavigationEnd, Router, RouterModule } from '@angular/router'
import { ActivatedRoute, NavigationEnd, Router, RouterModule } from '@angular/router'
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { logger } from '@root-helpers/logger'
import { filter, Subscription } from 'rxjs'
@ -51,7 +51,7 @@ export class HorizontalMenuComponent implements OnInit, OnChanges, OnDestroy {
private routerSub: Subscription
constructor (private router: Router) {
constructor (private router: Router, private route: ActivatedRoute) {
}
@ -74,8 +74,15 @@ export class HorizontalMenuComponent implements OnInit, OnChanges, OnDestroy {
this.activeParent = undefined
const currentUrl = window.location.pathname
const currentComponentPath = this.route.snapshot.pathFromRoot.reduce((a, c) => {
if (c.url.length === 0) return a
return a + '/' + c.url[0].path
}, '')
const entry = this.menuEntries.find(parent => {
if (currentUrl.startsWith(parent.routerLink)) return true
if (!parent.routerLink.startsWith('/') && `${currentComponentPath}/${parent.routerLink}` === currentUrl) return true
if (parent.children) return parent.children.some(child => currentUrl.startsWith(child.routerLink))
@ -84,7 +91,7 @@ export class HorizontalMenuComponent implements OnInit, OnChanges, OnDestroy {
if (!entry) {
if (this.menuEntries.length !== 0) {
logger.info(`Unable to find entry for ${currentUrl}`, { menuEntries: this.menuEntries })
logger.info(`Unable to find entry for ${currentUrl} or ${currentComponentPath}`, { menuEntries: this.menuEntries })
}
return

View File

@ -1,54 +1,56 @@
<div #itemsParent class="list-overflow-parent" [ngClass]="{ 'opacity-0': !initialized }">
<div class="root">
<div #itemsParent class="list-overflow-parent" [ngClass]="{ 'opacity-0': !initialized, 'has-border': hasBorder }">
<ul class="ul-unstyle d-flex">
@for (item of items; track item.label; let id = $index) {
@if (!item.isDisplayed || item.isDisplayed()) {
<li class="d-inline-block" [id]="getId(id)" #itemsRendered>
<ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
</li>
}
}
</ul>
@if (isMenuDisplayed()) {
@if (isInMobileView) {
<button type="button" class="peertube-button tertiary-button list-overflow-menu" (click)="toggleModal()">
<span class="chevron-down"></span>
</button>
} @else {
<div class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown">
<button class="peertube-button tertiary-button p-0" ngbDropdownToggle type="button">
<span class="chevron-down"></span>
</button>
<ul class="ul-unstyle" ngbDropdownMenu>
@for (item of items | slice:showItemsUntilIndexExcluded:items.length; track item.label) {
@if (!item.isDisplayed || item.isDisplayed()) {
<li>
<a [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
{{ item.label }}
</a>
</li>
}
}
</ul>
</div>
}
}
</div >
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-body">
<ul class="ul-unstyle">
@for (item of items | slice:showItemsUntilIndexExcluded:items.length; track item.label) {
<ul class="ul-unstyle d-flex">
@for (item of items; track item.label; let id = $index) {
@if (!item.isDisplayed || item.isDisplayed()) {
<li>
<a [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
{{ item.label }}
</a>
<li class="d-inline-block" [id]="getId(id)" #itemsRendered>
<ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
</li>
}
}
</ul>
</div>
</ng-template>
@if (isMenuDisplayed()) {
@if (isInMobileView) {
<button type="button" class="peertube-button tertiary-button p-0 list-overflow-menu" (click)="toggleModal()">
<span class="chevron-down"></span>
</button>
} @else {
<div class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown">
<button class="peertube-button tertiary-button p-0" ngbDropdownToggle type="button">
<span class="chevron-down"></span>
</button>
<ul class="ul-unstyle" ngbDropdownMenu>
@for (item of items | slice:showItemsUntilIndexExcluded:items.length; track item.label) {
@if (!item.isDisplayed || item.isDisplayed()) {
<li>
<a [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
{{ item.label }}
</a>
</li>
}
}
</ul>
</div>
}
}
</div >
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-body">
<ul class="ul-unstyle">
@for (item of items | slice:showItemsUntilIndexExcluded:items.length; track item.label) {
@if (!item.isDisplayed || item.isDisplayed()) {
<li>
<a [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
{{ item.label }}
</a>
</li>
}
}
</ul>
</div>
</ng-template>
</div>

View File

@ -10,13 +10,22 @@ li {
padding-top: 2px;
}
.root {
overflow: hidden;
max-width: calc(100vw - 30px);
padding-bottom: 3px;
}
.list-overflow-parent {
display: flex;
align-items: center;
// For the menu icon
position: relative;
max-width: calc(100vw - 30px);
&.has-border {
border-bottom: 1px solid pvar(--fg-200);
}
}
.list-overflow-menu {

View File

@ -1,6 +1,7 @@
import { NgClass, NgFor, NgIf, NgTemplateOutlet, SlicePipe } from '@angular/common'
import { NgClass, NgTemplateOutlet, SlicePipe } from '@angular/common'
import {
AfterViewInit,
booleanAttribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -46,6 +47,7 @@ export interface ListOverflowItem {
export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit {
@Input() items: T[]
@Input() itemTemplate: TemplateRef<{ item: T }>
@Input({ transform: booleanAttribute }) hasBorder = false
@ViewChild('modal', { static: true }) modal: ElementRef
@ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement>

View File

@ -1,63 +0,0 @@
<ul class="sub-menu ul-unstyle" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed, 'no-scroll': isModalOpened }">
<ng-container *ngFor="let menuEntry of menuEntries; index as id">
@if (isDisplayed(menuEntry)) {
@if (menuEntry.routerLink) {
<li>
<a
class="sub-menu-entry" [routerLink]="menuEntry.routerLink" routerLinkActive="active"
#routerLink (click)="onActiveLinkScrollToTop(routerLink)" ariaCurrentWhenActive="page"
>{{ menuEntry.label }}</a>
</li>
} @else if (isInSmallView) { <!-- On mobile, use a modal to display sub menu items -->
<li>
<button class="sub-menu-entry" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" (click)="openModal(id)">
{{ menuEntry.label }}
<span class="chevron-down"></span>
</button>
</li>
} @else {
<!-- On desktop, use a classic dropdown -->
<li ngbDropdown #dropdown="ngbDropdown" autoClose="true" container="body">
<button ngbDropdownToggle class="sub-menu-entry" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }">{{ menuEntry.label }}</button>
<ul class="ul-unstyle" ngbDropdownMenu>
<li *ngFor="let menuChild of menuEntry.children">
<a
*ngIf="isDisplayed(menuChild)" ngbDropdownItem
routerLinkActive="active" ariaCurrentWhenActive="page"
[routerLink]="menuChild.routerLink" #routerLink (click)="onActiveLinkScrollToTop(routerLink)"
[queryParams]="menuChild.queryParams"
>
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
{{ menuChild.label }}
</a>
</li>
</ul>
</li>
}
}
</ng-container>
</ul>
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-body">
<ng-container *ngFor="let menuEntry of menuEntries; index as id">
<div [ngClass]="{ hidden: id !== currentMenuEntryIndex }">
<ng-container *ngFor="let menuChild of menuEntry.children">
<a
*ngIf="isDisplayed(menuChild)" [ngClass]="{ icon: hasIcons }" [routerLink]="menuChild.routerLink" routerLinkActive="active"
#routerLink (click)="dismissOtherModals(); onActiveLinkScrollToTop(routerLink)" ariaCurrentWhenActive="page"
>
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
{{ menuChild.label }}
</a>
</ng-container>
</div>
</ng-container>
</div>
</ng-template>

View File

@ -1,50 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
.sub-menu ::ng-deep .dropdown-toggle::after {
position: relative;
top: 2px;
}
.sub-menu ::ng-deep .dropdown-menu {
margin-top: 0 !important;
}
.dropdown-menu .dropdown-item {
top: -1px;
@include dropdown-with-icon-item;
}
.sub-menu.no-scroll {
overflow-x: hidden;
}
.modal-body {
.hidden {
display: none;
}
my-global-icon {
@include margin-right(10px);
}
a {
color: currentColor;
box-sizing: border-box;
display: block;
font-size: 1.2rem;
padding: 9px 12px;
text-align: initial;
text-transform: unset;
width: 100%;
@include disable-default-a-behaviour;
&.active {
color: pvar(--on-primary) !important;
background-color: pvar(--primary-450);
opacity: .9;
}
}
}

View File

@ -1,138 +0,0 @@
import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, Input, OnChanges, OnDestroy, OnInit, ViewChild } from '@angular/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, 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'
export type TopMenuDropdownParam = {
label: string
routerLink?: string
isDisplayed?: () => boolean // Default: () => true
children?: {
label: string
routerLink: string
queryParams?: { [id: string]: string }
iconName: GlobalIconName
isDisplayed?: () => boolean // Default: () => true
}[]
}
@Component({
selector: 'my-top-menu-dropdown',
templateUrl: './top-menu-dropdown.component.html',
styleUrls: [ './top-menu-dropdown.component.scss' ],
standalone: true,
imports: [
NgClass,
NgFor,
NgIf,
RouterLinkActive,
RouterLink,
NgbDropdown,
NgbDropdownToggle,
NgbDropdownMenu,
NgbDropdownItem,
GlobalIconComponent
]
})
export class TopMenuDropdownComponent implements OnInit, OnChanges, OnDestroy {
@Input() menuEntries: TopMenuDropdownParam[] = []
@ViewChild('modal', { static: true }) modal: NgbModal
suffixLabels: { [ parentLabel: string ]: string }
hasIcons = false
isModalOpened = false
currentMenuEntryIndex: number
private routeSub: Subscription
constructor (
private router: Router,
private modalService: NgbModal,
private screen: ScreenService
) { }
get isInSmallView () {
return this.screen.isInSmallView()
}
get isBroadcastMessageDisplayed () {
return this.screen.isBroadcastMessageDisplayed
}
ngOnInit () {
this.updateChildLabels(window.location.pathname)
this.routeSub = this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => this.updateChildLabels(window.location.pathname))
}
ngOnChanges () {
this.updateChildLabels(window.location.pathname)
}
ngOnDestroy () {
if (this.routeSub) this.routeSub.unsubscribe()
}
dropdownAnchorClicked (dropdown: NgbDropdown) {
return dropdown.toggle()
}
openModal (index: number) {
this.currentMenuEntryIndex = index
this.isModalOpened = true
this.modalService.open(this.modal, {
centered: true,
beforeDismiss: () => {
this.onModalDismiss()
return true
}
})
}
onModalDismiss () {
this.isModalOpened = false
}
onActiveLinkScrollToTop (link: HTMLAnchorElement) {
if (!this.isBroadcastMessageDisplayed && this.router.url.includes(link.getAttribute('href'))) {
scrollToTop('smooth')
}
}
dismissOtherModals () {
this.modalService.dismissAll()
}
isDisplayed (obj: { isDisplayed?: () => boolean }) {
if (typeof obj.isDisplayed !== 'function') return true
return obj.isDisplayed()
}
private updateChildLabels (path: string) {
this.suffixLabels = {}
for (const entry of this.menuEntries) {
if (!entry.children) continue
for (const child of entry.children) {
if (path.startsWith(child.routerLink)) {
this.suffixLabels[entry.label] = child.label
}
}
}
}
}

View File

@ -1 +1,3 @@
<my-link i18n internalLink="/login" [href]="getExternalLoginHref()" [className]="className">{{ label }}</my-link>
<my-link class="d-flex" internalLink="/login" [href]="getExternalLoginHref()" [className]="className" [icon]="icon ? 'sign-in' : undefined">
{{ label }}
</my-link>

View File

@ -0,0 +1,6 @@
@use '_variables' as *;
@use '_mixins' as *;
my-global-icon {
@include global-icon-size(24px);
}

View File

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'
import { booleanAttribute, Component, Input } from '@angular/core'
import { ServerService } from '@app/core'
import { PluginsManager } from '@root-helpers/plugins-manager'
import { environment } from 'src/environments/environment'
@ -7,11 +7,13 @@ import { LinkComponent } from '../common/link.component'
@Component({
selector: 'my-login-link',
templateUrl: './login-link.component.html',
styleUrls: [ './login-link.component.scss' ],
standalone: true,
imports: [ LinkComponent ]
})
export class LoginLinkComponent {
@Input() label = $localize`Login`
@Input({ transform: booleanAttribute }) icon = false
@Input() className?: string

View File

@ -47,7 +47,11 @@ export class VideoThumbnailComponent {
}
isLiveStreaming () {
return this.video.isLive && this.video.state?.id === VideoState.PUBLISHED
// In non moderator mode we only display published live
// If in moderator mode, the server adds the state info to the object
if (!this.video.isLive) return false
return !this.video.state || this.video.state?.id === VideoState.PUBLISHED
}
isEndedLive () {

View File

@ -1,7 +1,16 @@
<div class="root" [formGroup]="form">
<div class="first-row">
<div>
<div class="scope-row" *ngIf="totalFollowing && !hideScope">
@if (filters.scope === 'local') {
<ng-container i18n>Videos on {{ instanceName }}</ng-container>
} @else {
<ng-container i18n>Videos on {{ instanceName }} and {{ totalFollowing }} other platforms</ng-container>
}
</div>
<div class="filters-row">
<div class="d-flex flex-wrap">
<div class="d-flex flex-wrap me-2">
@for (quickFilter of quickFilters; track quickFilter) {
<my-button
@ -37,25 +46,15 @@
</div>
</div>
<div class="d-flex flex-wrap align-items-center ms-3" *ngIf="!hideScope">
<label for="scope" i18n class="select-label">Display videos of:</label>
<div class="d-flex flex-wrap align-items-center ms-3" >
<label for="sort-videos" i18n class="select-label">Sort by:</label>
<my-select-options inputId="scope" class="scope-select" formControlName="scope" [items]="availableScopes"></my-select-options>
<my-select-options inputId="sort-videos" class="d-inline-block me-2" formControlName="sort" [items]="sortItems"></my-select-options>
</div>
</div>
<div [ngbCollapse]="areFiltersCollapsed" [animation]="true">
<div class="filters">
<div class="section">
<div class="section-title" i18n>Display order</div>
<div class="form-group">
<label for="sort-videos" i18n class="select-label">Sort by:</label>
<my-select-options inputId="sort-videos" class="d-inline-block me-2" formControlName="sort" [items]="sortItems"></my-select-options>
</div>
</div>
<div class="section">
<div class="section-title">
<ng-container i18n>Content preferences</ng-container>
@ -90,6 +89,24 @@
</div>
</div>
<div class="section" *ngIf="totalFollowing && !hideScope">
<div class="section-title">
<ng-container i18n>Platforms order</ng-container>
<div class="with-description">
<div i18n>{{ instanceName }} platform subscribes to content from <a routerLink="/about/follows" target="_blank">{{ totalFollowing }} other platforms</a>.</div>
<div i18n>Set your display preferences here.</div>
</div>
</div>
<div class="form-group" >
<label for="scope" i18n>Displayed videos</label>
<my-select-options inputId="scope" class="scope-select" formControlName="scope" [items]="availableScopes"></my-select-options>
</div>
</div>
<div class="section">
<div class="section-title" i18n>Content type</div>

View File

@ -10,7 +10,13 @@ $filters-background: pvar(--bg-secondary-400);
margin-bottom: 45px;
}
.first-row {
.scope-row {
font-size: 20px;
color: pvar(--fg-350);
margin-bottom: 0.5rem;
}
.filters-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
@ -128,7 +134,7 @@ my-select-categories {
}
@media screen and (max-width: $small-view) {
.first-row {
.filters-row {
flex-direction: column;
}
}

View File

@ -17,6 +17,7 @@ import { GlobalIconComponent, GlobalIconName } from '../shared-icons/global-icon
import { ButtonComponent } from '../shared-main/buttons/button.component'
import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
import { VideoFilterActive, VideoFilters } from './video-filters.model'
import { InstanceFollowService } from '../shared-instance/instance-follow.service'
const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent')
@ -45,7 +46,8 @@ type QuickFilter = {
PeertubeCheckboxComponent,
SelectOptionsComponent,
ButtonComponent
]
],
providers: [ InstanceFollowService ]
})
export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
@Input() filters: VideoFilters
@ -63,6 +65,9 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
quickFilters: QuickFilter[] = []
instanceName: string
totalFollowing: number
private videoCategories: VideoConstant<number>[] = []
private videoLanguages: VideoConstant<string>[] = []
@ -74,11 +79,15 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
private fb: FormBuilder,
private modalService: PeertubeModalService,
private redirectService: RedirectService,
private route: ActivatedRoute
private route: ActivatedRoute,
private server: ServerService,
private followService: InstanceFollowService
) {
}
ngOnInit () {
this.instanceName = this.server.getHTMLConfig().instance.name
this.form = this.fb.group({
sort: [ '' ],
nsfw: [ '' ],
@ -113,9 +122,12 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
this.serverService.getVideoLanguages()
.subscribe(languages => this.videoLanguages = languages)
this.followService.getFollowing({ pagination: { count: 1, start: 0 }, state: 'accepted' })
.subscribe(({ total }) => this.totalFollowing = total)
this.availableScopes = [
{ id: 'local', label: $localize`This platform only` },
{ id: 'federated', label: $localize`All platforms` }
{ id: 'local', label: $localize`Only videos from this platform` },
{ id: 'federated', label: $localize`Videos from all platforms` }
]
this.buildSortItems()

View File

@ -1,6 +1,6 @@
import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, booleanAttribute } from '@angular/core'
import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router'
import { ActivatedRoute } from '@angular/router'
import {
AuthService,
ComponentPaginationLight,
@ -11,7 +11,6 @@ import {
UserService
} from '@app/core'
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@peertube/peertube-core-utils'
import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
@ -20,7 +19,6 @@ import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs
import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators'
import { ButtonComponent } from '../shared-main/buttons/button.component'
import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive'
import { FeedComponent } from '../shared-main/feeds/feed.component'
import { Syndication } from '../shared-main/feeds/syndication.model'
import { Video } from '../shared-main/video/video.model'
import { VideoFiltersHeaderComponent } from './video-filters-header.component'
@ -52,14 +50,9 @@ enum GroupDate {
standalone: true,
imports: [
NgIf,
NgbTooltip,
NgClass,
FeedComponent,
NgFor,
RouterLinkActive,
RouterLink,
ButtonComponent,
NgTemplateOutlet,
ButtonComponent,
VideoFiltersHeaderComponent,
InfiniteScrollerDirective,

View File

@ -223,37 +223,10 @@ my-video-thumbnail,
}
@media screen and (min-width: $small-view) {
:host-context(.expanded) {
@include more-dropdown-control();
}
}
@media screen and (max-width: $small-view) {
:host-context(.expanded) {
@include edit-button-control();
}
@include more-dropdown-control();
@include edit-button-control();
}
@media screen and (max-width: $mobile-view) {
:host-context(.expanded) {
@include edit-button-in-mobile-view();
}
}
@media screen and (min-width: #{$small-view + $menu-width}) {
:host-context(.main-col:not(.expanded)) {
@include more-dropdown-control();
}
}
@media screen and (max-width: #{$small-view + $menu-width}) {
:host-context(.main-col:not(.expanded)) {
@include edit-button-control();
}
}
@media screen and (max-width: #{$mobile-view + $menu-width}) {
:host-context(.main-col:not(.expanded)) {
@include edit-button-in-mobile-view();
}
@include edit-button-in-mobile-view();
}

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-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -11,6 +11,13 @@
@use './z-index';
@use './class-helpers/index.scss';
@mixin main-col-expanded {
--main-col-width: 100vw;
width: calc(100% - #{$menu-collapsed-width});
margin-inline-start: $menu-collapsed-width;
}
/* clears the X from Chrome */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
@ -67,6 +74,14 @@ body {
--alert-primary-bg: #{pvar(--primary-200)};
--alert-primary-border-color: #{pvar(--primary-300)};
--menu-margin-left: #{$menu-margin-left};
--header-height: #{$header-height};
@media screen and (max-width: $mobile-view) {
--header-height: #{$header-height-mobile-view};
}
// Light theme
&[data-pt-theme=peertube-core-light],
&[data-pt-theme=default] {
@ -104,8 +119,8 @@ body {
--bg: hsl(0 14% 7%);
--bg-secondary: hsl(0 14% 22%);
--alert-primary-fg: #{pvar(--on-primary)};
--alert-primary-bg: #{pvar(--primary-200)};
--alert-primary-fg: #{pvar(--primary-650)};
--alert-primary-bg: #{pvar(--primary-100)};
--alert-primary-border-color: #{pvar(--primary-300)};
}
@ -126,7 +141,7 @@ body {
}
::selection {
color: pvar(--bg);
color: pvar(--on-primary);
background-color: pvar(--primary-450);
}
@ -201,17 +216,6 @@ code {
margin: 0 calc(#{pvar(--x-margin-content)} * -1);
}
.sub-menu {
background-color: pvar(--bg-secondary-400);
width: 100%;
display: flex;
align-items: center;
padding: 0 pvar(--x-margin-content);
height: $sub-menu-height;
margin-bottom: $sub-menu-margin-bottom;
overflow-x: auto;
}
.skip-to-content-sub-menu {
display: block;
z-index: z(modal);
@ -223,24 +227,21 @@ code {
// Override some properties if the main content is expanded (no menu on the left)
&.expanded {
--main-col-width: 100vw;
width: calc(100% - #{$collapsed-menu-width});
@include margin-left($collapsed-menu-width);
@include main-col-expanded();
}
&.lock-scroll .main-row > router-outlet + * { /* stylelint-disable-line selector-max-compound-selectors */
// Lock and hide body scrollbars
position: fixed;
// Lock and hide sub-menu scrollbars
.sub-menu { /* stylelint-disable-line */
overflow-x: hidden;
}
}
}
.modal-open,
.main-col.expanded,
.menu-open {
overflow: hidden !important;
}
my-global-icon[iconName=external-link] {
margin: 0 0.3em;
width: 0.9em;
@ -256,54 +257,49 @@ my-global-icon[iconName=external-link] {
/* the following applies from 500px to 900px and is partially overridden from 500px to 800px by changes below to $small-view */
.main-col,
.main-col.expanded {
.sub-menu {
padding: 0 50px;
.title-page {
font-size: 17px;
}
.title-page {
font-size: 17px;
}
}
}
@media screen and (min-width: $mobile-view) and (max-width: $small-view) {
@media screen and (max-width: $menu-overlay-view) {
.main-col {
width: 100%;
@include main-col-expanded();
}
}
@media screen and (max-width: $small-view) {
.main-col {
--x-margin-content: 1rem;
}
my-markdown-textarea {
.root {
max-width: 100% !important;
}
}
input[type=text],
input[type=password],
input[type=email],
textarea,
.peertube-select-container {
flex-grow: 1;
}
.caption input[type=text] {
width: unset !important;
flex-grow: 1;
}
}
@media screen and (max-width: $mobile-view) {
.main-col,
.main-col.expanded {
--x-margin-content: 15px;
width: 100%;
@include margin-left(0);
.sub-menu {
width: 100vw;
padding: 0 15px;
margin-bottom: $sub-menu-margin-bottom-small-view;
overflow-x: auto;
}
my-markdown-textarea {
.root {
max-width: 100% !important;
}
}
input[type=text],
input[type=password],
input[type=email],
textarea,
.peertube-select-container {
flex-grow: 1;
}
.caption input[type=text] {
width: unset !important;
flex-grow: 1;
}
}
}

View File

@ -199,30 +199,6 @@ body {
width: 100vw; // Make sure the content fits all the available width
}
// On touchscreen devices, simply overflow: hidden to avoid detached overlay on scroll
@media (hover: none) and (pointer: coarse) {
.modal-open,
.menu-open {
overflow: hidden !important;
}
// On touchscreen devices display content overlay when opened menu
.menu-open {
.main-col {
&::before {
background-color: #000;
width: 100vw;
height: 100vh;
opacity: 0.75;
content: '';
display: block;
position: fixed;
z-index: z(overlay);
}
}
}
}
// ---------------------------------------------------------------------------
// Nav
// ---------------------------------------------------------------------------

View File

@ -81,7 +81,7 @@
.anchor {
position: relative;
top: #{- ($header-height + 20px)};
top: -calc(#{pvar(--header-height)} + 20px);
}
// ---------------------------------------------------------------------------
@ -97,7 +97,12 @@
}
.secondary-button {
@include secondary-button($fg: pvar(--alert-primary-fg));
@include secondary-button(
$fg: pvar(--alert-primary-fg),
$active-bg: rgba(0, 0, 0, 0.4),
$hover-bg: rgba(0, 0, 0, 0.3),
$border-color: pvar(--alert-primary-fg)
);
}
}
}

View File

@ -3,6 +3,10 @@
.banner {
@include block-ratio('img', $banner-inverted-ratio);
img {
border-radius: 15px;
}
}
.revert-margin-content.banner {

View File

@ -47,3 +47,15 @@
.min-width-0 {
min-width: 0;
}
.d-none-mw {
@include on-mobile-main-col {
display: none !important;
}
}
.d-none-sw {
@include on-small-main-col {
display: none !important;
}
}

View File

@ -15,7 +15,7 @@
}
&.badge-primary {
color: pvar(--bg);
color: pvar(--on-primary);
background-color: pvar(--primary);
}

View File

@ -42,7 +42,7 @@ $input-focus-bg: pvar(--input-bg);
$input-btn-focus-width: 0;
$input-btn-focus-color: inherit;
$input-focus-border-color: pvar(--input-border-color);
$input-focus-box-shadow: 0 0 0 0.25rem pvar(--primary-100);
$input-focus-box-shadow: #{$focus-box-shadow-form};
$input-group-addon-color: pvar(--fg);
$input-group-addon-bg: pvar(--bg-secondary-500);
@ -64,7 +64,7 @@ $dropdown-bg: pvar(--bg);
$accordion-button-active-bg: pvar(--primary-50);
$accordion-button-active-color: pvar(--fg);
$accordion-button-focus-border-color: pvar(--primary-100);
$accordion-button-focus-box-shadow: 0 0 0 .15rem pvar(--primary-100);
$accordion-button-focus-box-shadow: #{$focus-box-shadow-form};
$accordion-border-color: pvar(--input-border-color);
$card-color: pvar(--fg);

View File

@ -3,11 +3,16 @@
@use '_variables' as *;
@use '_mixins' as *;
@mixin secondary-button ($fg: pvar(--fg), $active-bg: pvar(--bg-secondary-400), $hover-bg: pvar(--bg-secondary-450)) {
@mixin secondary-button (
$fg: pvar(--fg),
$active-bg: pvar(--bg-secondary-500),
$hover-bg: pvar(--bg-secondary-450),
$border-color: pvar(--bg-secondary-500)
) {
& {
color: $fg;
background-color: transparent;
border: 1px solid pvar(--bg-secondary-500) !important;
border: 1px solid $border-color !important;
}
&:active,
@ -16,7 +21,7 @@
&:focus-visible {
color: $fg;
background-color: $active-bg;
border-color: pvar(--bg-secondary-500);
border-color: $border-color;
}
// Override bootstrap
@ -25,7 +30,7 @@
&.btn.show {
color: $fg !important;
background-color: $active-bg !important;
border-color: pvar(--bg-secondary-500) !important;
border-color: $border-color !important;
}
&:hover {
@ -215,7 +220,7 @@
@mixin button-focus($color) {
&:focus,
&:focus-visible {
box-shadow: #{$focus-box-shadow-form} $color;
box-shadow: #{$focus-box-shadow-dimensions} $color;
}
}

View File

@ -28,9 +28,12 @@
&:focus,
&:focus-visible {
box-shadow: $focus-box-shadow-form;
outline: 0;
border-color: pvar(--fg);
box-shadow: none;
}
&[disabled] {
opacity: 0.4;
}
@media screen and (max-width: calc(#{$width} + 40px)) {
@ -95,21 +98,29 @@
&:focus,
&:focus-visible {
outline: 0;
border-color: pvar(--fg);
box-shadow: none;
box-shadow: $focus-box-shadow-form;
}
&[disabled] {
opacity: 0.4;
}
}
}
// Thanks: https://codepen.io/manabox/pen/raQmpL
@mixin peertube-radio-container {
&[disabled] {
opacity: 0.4;
}
label {
font-size: $form-input-font-size;
}
[type=radio]:focus-visible + label::before {
outline: 2px solid;
[type=radio]:focus-visible,[type=radio]:focus {
& + label::before {
box-shadow: $focus-box-shadow-form;
}
}
[type=radio]:checked,
@ -129,6 +140,7 @@
line-height: 20px;
display: inline-block;
font-weight: $font-regular;
color: pvar(--fg);
}
[type=radio]:checked + label::before,
@ -146,12 +158,12 @@
[type=radio]:checked + label::after,
[type=radio]:not(:checked) + label::after {
content: '';
width: 12px;
height: 12px;
background: pvar(--border-primary);
width: 8px;
height: 8px;
background: pvar(--primary);
position: absolute;
top: 3px;
left: 3px;
top: 5px;
left: 5px;
border-radius: 100%;
transition: all 0.2s ease;
}
@ -161,7 +173,7 @@
}
[type=radio]:checked + label::before {
border: 4px solid pvar(--fg-400);
border: 2px solid pvar(--fg);
}
[type=radio]:checked + label::after {
@ -184,7 +196,7 @@
position: absolute;
&:focus + span {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
+ span {
@ -200,10 +212,10 @@
&::after {
content: '';
position: absolute;
top: 1px;
left: 5px;
width: 6px;
height: 12px;
top: 3px;
left: 6px;
width: 5px;
height: 8px;
opacity: 0;
transform: rotate(45deg) scale(0);
border-right: 2px solid pvar(--on-primary);
@ -212,10 +224,21 @@
}
&:checked + span {
border-color: transparent;
background: pvar(--border-primary);
border-color: pvar(--fg);
background: pvar(--fg);
animation: jelly 0.6s ease;
&::before {
content: '';
width: 100%;
height: 100%;
background: pvar(--primary);
position: absolute;
transition: all 0.2s ease;
border: 2px solid pvar(--on-primary);
border-radius: 2px;
}
&::after {
opacity: 1;
transform: rotate(45deg) scale(1);
@ -232,7 +255,7 @@
&[disabled] + span,
&[disabled] + span + span {
opacity: 0.5;
opacity: 0.4;
cursor: default;
}
}

View File

@ -240,30 +240,14 @@
}
@mixin on-small-main-col () {
:host-context(.main-col:not(.expanded)) {
@media screen and (max-width: #{$small-view + $menu-width}) {
@content;
}
}
:host-context(.main-col.expanded) {
@media screen and (max-width: $small-view) {
@content;
}
@media screen and (max-width: $small-view) {
@content;
}
}
@mixin on-mobile-main-col () {
:host-context(.main-col:not(.expanded)) {
@media screen and (max-width: #{$mobile-view + $menu-width}) {
@content;
}
}
:host-context(.main-col.expanded) {
@media screen and (max-width: $mobile-view) {
@content;
}
@media screen and (max-width: $mobile-view) {
@content;
}
}

View File

@ -21,9 +21,12 @@ $white: #fff;
$button-font-size: 1rem;
$header-height: 94px;
$header-height-mobile-view: 144px;
$menu-width: 248px;
$collapsed-menu-width: 48px;
$menu-collapsed-width: 50px;
$menu-margin-left: 2rem;
$menu-overlay-view: 1200px;
$sub-menu-height: 81px;
@ -53,7 +56,8 @@ $player-portrait-bottom-space: 50px;
$sub-menu-margin-bottom: 30px;
$sub-menu-margin-bottom-small-view: 10px;
$focus-box-shadow-form: 0 0 0 .2rem;
$focus-box-shadow-dimensions: 0 0 0 .2rem;
$form-input-font-size: 16px;
$video-watch-info-margin-left: 44px;
@ -71,6 +75,7 @@ $variables: (
--x-margin-content: var(--x-margin-content),
--x-videos-margin-content: var(--x-videos-margin-content),
--main-col-width: var(--main-col-width),
--header-height: var(--header-height),
--fg: var(--fg),
--bg: var(--bg),
@ -121,6 +126,8 @@ $variables: (
--on-primary: var(--on-primary),
--primary: var(--primary),
--primary-700: var(--primary-700),
--primary-650: var(--primary-650),
--primary-600: var(--primary-600),
--primary-550: var(--primary-550),
--primary-500: var(--primary-500),
@ -140,6 +147,8 @@ $variables: (
--alert-primary-bg: var(--alert-primary-bg),
--alert-primary-border-color: var(--alert-primary-border-color),
--menu-margin-left: var(--menu-margin-left),
// Optional variables
--menu-fg: var(--menu-fg),
--menu-bg: var(--menu-bg)
@ -168,7 +177,6 @@ $variables: (
$zindex: (
miniature : 10,
sub-menu : 12500,
overlay : 12550,
menu : 12600,
search-typeahead: 12650,
@ -193,3 +201,4 @@ $zindex: (
// ---------------------------------------------------------------------------
$separator-border-color: pvar(--bg-secondary-400);
$focus-box-shadow-form: #{$focus-box-shadow-dimensions} #{pvar(--fg-100)};

View File

@ -80,7 +80,7 @@ body .p-paginator .p-paginator-next:focus,
body .p-paginator .p-paginator-last:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
body .p-paginator .p-paginator-current {
color: pvar(--fg);
@ -134,7 +134,7 @@ body .p-paginator .p-paginator-pages .p-paginator-page:not(.p-highlight):hover {
body .p-paginator .p-paginator-pages .p-paginator-page:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
body .p-paginator .p-dropdown {
@include margin-left(0.5em);
@ -157,7 +157,7 @@ body .p-dropdown:not(.p-disabled):hover {
body .p-dropdown:not(.p-disabled).p-focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
border-color: pvar(--input-border-color);
}
body .p-dropdown.p-dropdown-clearable .p-dropdown-label {
@ -281,7 +281,7 @@ body .p-datepicker:not(.p-disabled) .p-datepicker-header .p-datepicker-prev:focu
body .p-datepicker:not(.p-disabled) .p-datepicker-header .p-datepicker-next:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
body .p-datepicker .p-datepicker-header {
padding: 0.429em 0.857em 0.429em 0.857em;
@ -311,7 +311,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select {
body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
body .p-datepicker .p-datepicker-header .p-datepicker-title .p-datepicker-month {
@include margin-right(0.5rem);
@ -349,7 +349,7 @@ body .p-datepicker table td > a {
body .p-datepicker table td > a:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
body .p-datepicker table td.p-datepicker-today > a,
body .p-datepicker table td.p-datepicker-today > span {
@ -478,7 +478,7 @@ body p-autocomplete.ng-dirty.ng-invalid > .p-autocomplete > .p-inputtext {
.p-chips:not(.p-disabled).p-focus .p-chips-multiple-container {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.25rem pvar(--primary-300);
box-shadow: $focus-box-shadow-form;
}
.p-chips .p-chips-multiple-container {
padding: pvar(--input-y-padding) pvar(--input-x-padding);
@ -534,7 +534,7 @@ p-chips.p-chips-clearable .p-chips-clear-icon {
.p-multiselect:not(.p-disabled).p-focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.25rem pvar(--primary-300);
box-shadow: $focus-box-shadow-form;
}
.p-multiselect .p-multiselect-label {
padding: pvar(--input-y-padding) pvar(--input-x-padding);
@ -620,14 +620,14 @@ p-chips.p-chips-clearable .p-chips-clear-icon {
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:enabled:hover {
color: pvar(--primary);
color: pvar(--fg-400);
border-color: transparent;
background: transparent;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:focus-visible {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2rem pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
.p-multiselect-panel .p-multiselect-items {
padding: 0;
@ -708,7 +708,7 @@ p-multiselect.ng-dirty.ng-invalid > .p-multiselect {
.p-inputtext:enabled:focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.25rem pvar(--primary-300);
box-shadow: $focus-box-shadow-form;
}
.p-inputtext.ng-dirty.ng-invalid {
border-color: pvar(--red);
@ -924,7 +924,7 @@ p-table {
}
&.p-focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
.p-label {
@ -962,7 +962,7 @@ p-table {
&.focus-within,
&:focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100);
box-shadow: $focus-box-shadow-form;
}
&.p-disabled:hover {
@ -998,7 +998,7 @@ p-table {
&.focus-within,
&:focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100) !important;
box-shadow: #{$focus-box-shadow-form} !important;
}
&.p-highlight {

View File

@ -3,7 +3,7 @@ import { OAuthUserTokens, objectToUrlEncoded } from '../../../root-helpers'
import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage'
export class AuthHTTP {
private readonly LOCAL_STORAGE_OAUTH_CLIENT_KEYS = {
private readonly LS_OAUTH_CLIENT_KEYS = {
CLIENT_ID: 'client_id',
CLIENT_SECRET: 'client_secret'
}
@ -44,8 +44,8 @@ export class AuthHTTP {
if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
const refreshingTokenPromise = new Promise<void>((resolve, reject) => {
const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
const clientSecret: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
const clientId: string = peertubeLocalStorage.getItem(this.LS_OAUTH_CLIENT_KEYS.CLIENT_ID)
const clientSecret: string = peertubeLocalStorage.getItem(this.LS_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
const headers = new Headers()
headers.set('Content-Type', 'application/x-www-form-urlencoded')

View File

@ -938,7 +938,7 @@ instance:
# - 17 # Kids
# - 18 # Food
default_client_route: '/videos/trending'
default_client_route: '/videos/browse'
# Whether or not the instance is dedicated to NSFW content
# Enabling it will allow other administrators to know that you are mainly federating sensitive content