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="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="row ">
<div class="col-md-12 col-xl-6"> <div class="col-md-12 col-xl-6">

View File

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

View File

@ -164,9 +164,7 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
links = links.concat([ links = links.concat([
{ label: $localize`Discover`, path: '/videos/overview' }, { label: $localize`Discover`, path: '/videos/overview' },
{ label: $localize`Trending`, path: '/videos/trending' }, { label: $localize`Browse videos`, path: '/videos/browse' }
{ label: $localize`Recently added`, path: '/videos/recently-added' },
{ label: $localize`Local videos`, path: '/videos/local' }
]) ])
this.defaultLandingPageOptions = links.map(o => ({ this.defaultLandingPageOptions = links.map(o => ({

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
<div class="video-channel-buttons"> <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> <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
</ng-container> </ng-container>
</span> </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> </div>
<button i18n class="ms-2 peertube-button primary-button" (click)="acceptedPrivacyConcern()"> <button i18n class="ms-2 peertube-button primary-button" (click)="acceptedPrivacyConcern()">

View File

@ -5,10 +5,11 @@
position: fixed; position: fixed;
bottom: 0; bottom: 0;
width: calc(100% - #{$menu-width}); width: calc(100% - #{$menu-width} - (#{pvar(--x-margin-content)} * 2));
z-index: z(privacymsg); z-index: z(privacymsg);
padding: 5px 15px; padding: 5px 15px;
border-radius: 5px 5px 0 0;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -16,27 +17,8 @@
justify-content: space-between; justify-content: space-between;
background-color: rgba(0, 0, 0, 0.9); background-color: rgba(0, 0, 0, 0.9);
color: #fff; color: #fff;
}
// If the view is expanded @include margin-left(pvar(--x-margin-content));
: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%;
}
} }
.privacy-concerns-text { .privacy-concerns-text {
@ -44,23 +26,21 @@
} }
a { a {
color: pvar(--primary); color: #fff;
transition: color 0.3s;
@include disable-default-a-behaviour;
&:hover {
color: pvar(--bg);
}
} }
@media screen and (max-width: 1300px) { :host-context(.main-col.expanded) {
.privacy-concerns { .privacy-concerns {
font-size: 12px; width: calc(100% - #{$menu-collapsed-width} - (#{pvar(--x-margin-content)} * 2));
padding: 2px 5px; }
} }
.privacy-concerns-text { // Or if we are in the small view
margin: 0; @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 ] imports: [ NgIf ]
}) })
export class PrivacyConcernsComponent implements OnInit { 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 @Input() video: Video
@ -34,7 +34,7 @@ export class PrivacyConcernsComponent implements OnInit {
} }
acceptedPrivacyConcern () { acceptedPrivacyConcern () {
peertubeLocalStorage.setItem(PrivacyConcernsComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true') peertubeLocalStorage.setItem(PrivacyConcernsComponent.LS_PRIVACY_CONCERN_KEY, 'true')
this.display = false this.display = false
} }
@ -46,6 +46,6 @@ export class PrivacyConcernsComponent implements OnInit {
} }
private alreadyAccepted () { 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"> <div class="root" [ngClass]="{ 'theater-enabled': theaterEnabled }" [hidden]="!playlist && !video">
<!-- We need the video container for videojs so we just hide it --> <div class="margin-content player-margin-content">
<div id="video-wrapper"> <!-- We need the video container for videojs so we just hide it -->
<div *ngIf="remoteServerDown" class="remote-server-down"> <div id="video-wrapper">
<ng-container i18n>Sorry, but this video did not load because the remote instance did not respond.</ng-container> <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>
<div id="videojs-wrapper"> <my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>
<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>
<my-video-alert [video]="video" [user]="user" [noPlaylistVideoFound]="noPlaylistVideoFound"></my-video-alert>
<!-- Video information --> <!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom"> <div *ngIf="video" class="margin-content video-bottom">
<div class="video-info"> <div class="video-info">

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
<div class="sub-header-container"> <div class="sub-header-container">
<my-menu id="left-menu" role="navigation" aria-label="Main menu" i18n-ariaLabel></my-menu> <my-menu id="left-menu" role="navigation" aria-label="Main menu" i18n-ariaLabel></my-menu>
<main #mainContent tabindex="-1" id="content" class="main-col" [ngClass]="{ expanded: menu.isMenuCollapsed() }"> <main #mainContent tabindex="-1" id="content" class="main-col" [ngClass]="{ expanded: menu.isCollapsed() }">
<div class="main-row"> <div class="main-row">

View File

@ -22,17 +22,17 @@
} }
.main-row { .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 { .sub-header-container {
margin-top: $header-height; margin-top: pvar(--header-height);
background-color: pvar(--bg); background-color: pvar(--bg);
width: 100%; width: 100%;
} }
.root-header { .root-header {
height: $header-height; height: pvar(--header-height);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;

View File

@ -56,9 +56,9 @@ const routes: Routes = [
}, },
{ {
path: 'manage/update', path: 'manage/update/:channel',
pathMatch: 'prefix', pathMatch: 'full',
redirectTo: '/my-library/video-channels/update' 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_TOKEN_URL = environment.apiUrl + '/api/v1/users/token'
private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-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 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_ID: 'client_id',
CLIENT_SECRET: 'client_secret' CLIENT_SECRET: 'client_secret'
} }
@ -37,8 +37,8 @@ export class AuthService {
tokensRefreshed = new ReplaySubject<void>(1) tokensRefreshed = new ReplaySubject<void>(1)
loggedInHotkeys: Hotkey[] loggedInHotkeys: Hotkey[]
private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) private clientId: string = peertubeLocalStorage.getItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_ID)
private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LS_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
private loginChanged: Subject<AuthStatus> private loginChanged: Subject<AuthStatus>
private user: AuthUser = null private user: AuthUser = null
private refreshingTokenObservable: Observable<void> private refreshingTokenObservable: Observable<void>
@ -89,8 +89,8 @@ export class AuthService {
this.clientId = res.client_id this.clientId = res.client_id
this.clientSecret = res.client_secret this.clientSecret = res.client_secret
peertubeLocalStorage.setItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID, this.clientId) peertubeLocalStorage.setItem(AuthService.LS_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_SECRET, this.clientSecret)
logger.info('Client credentials loaded.') logger.info('Client credentials loaded.')
}, },

View File

@ -17,16 +17,12 @@ export class MenuService {
// Do not display menu on small or touch screens // Do not display menu on small or touch screens
if (this.screenService.isInSmallView() || this.screenService.isInTouchScreen()) { if (this.screenService.isInSmallView() || this.screenService.isInTouchScreen()) {
this.setMenuCollapsed(true) this.setMenuCollapsed(true)
} else {
this.setMenuCollapsed(this.localStorageService.getItem(MenuService.LS_MENU_COLLAPSED) === 'true')
this.menuChangedByUser = this.menuCollapsed
} }
this.handleWindowResize() this.handleWindowResize()
this.menuCollapsed = this.localStorageService.getItem(MenuService.LS_MENU_COLLAPSED) === 'true'
this.menuChangedByUser = this.menuCollapsed
}
isMenuCollapsed () {
return this.menuCollapsed
} }
toggleMenu () { toggleMenu () {
@ -43,21 +39,16 @@ export class MenuService {
setMenuCollapsed (collapsed: boolean) { setMenuCollapsed (collapsed: boolean) {
this.menuCollapsed = collapsed this.menuCollapsed = collapsed
if (!this.screenService.isInTouchScreen()) return if (this.menuCollapsed) {
document.body.classList.remove('menu-open')
// On touch screens, lock body scroll and display content overlay when memu is opened } else {
if (!this.menuCollapsed) {
document.body.classList.add('menu-open') document.body.classList.add('menu-open')
this.screenService.onFingerSwipe('left', () => this.setMenuCollapsed(true))
return
} }
document.body.classList.remove('menu-open')
} }
onResize () { onResize () {
if (this.screenService.isInSmallView() && !this.menuChangedByUser) { 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"> <div class="root" [hidden]="!loaded || (loggedIn && !user.account)">
<a class="peertube-title me-3" [routerLink]="getDefaultRoute()" [queryParams]="getDefaultRouteQuery()"> <a class="peertube-title" [routerLink]="getDefaultRoute()" [queryParams]="getDefaultRouteQuery()">
<span class="icon-logo"></span> <span class="icon-logo"></span>
<span class="instance-name">{{ instanceName }}</span> <span class="instance-name">{{ instanceName }}</span>
</a> </a>
<div class="d-flex align-items-center" [hidden]="!loaded || (loggedIn && !user.account)"> <my-search-typeahead></my-search-typeahead>
<my-search-typeahead class="w-100 me-5"></my-search-typeahead>
<div class="d-flex align-items-center">
@if (!loggedIn) { @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> <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
</a> </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 { } @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 <div
class="logged-in-container" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left auto" class="logged-in-container" ngbDropdown #dropdown="ngbDropdown" placement="bottom-left auto"
container="body" (openChange)="onDropdownOpenChange($event)" container="body"
> >
<button class="tertiary-button" ngbDropdownToggle> <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="display-name ellipsis">{{ user.account?.displayName }}</div>
<div class="username ellipsis fs-8">&#64;{{ user.username }}</div> <div class="username ellipsis fs-8">&#64;{{ user.username }}</div>
@ -37,8 +38,8 @@
<div ngbDropdownMenu> <div ngbDropdownMenu>
<a <a
*ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]" *ngIf="user.account" ngbDropdownItem class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
#profile (click)="onActiveLinkScrollToAnchor(profile)" #profile
> >
<my-global-icon iconName="user" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container> <my-global-icon iconName="user" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
</a> </a>
@ -46,8 +47,8 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a <a
*ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account" *ngIf="user.account" ngbDropdownItem class="dropdown-item" routerLink="/my-account"
#manageAccount (click)="onActiveLinkScrollToAnchor(manageAccount)" #manageAccount
> >
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> <ng-container i18n>Manage my account</ng-container> <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> <ng-container i18n>Manage my account</ng-container>
</a> </a>
@ -76,6 +77,8 @@
</div> </div>
} }
</div> </div>
<my-button theme="tertiary" rounded="true" class="menu-button margin-button" icon="menu" (click)="toggleMenu()"></my-button>
</div> </div>
<my-language-chooser #languageChooserModal></my-language-chooser> <my-language-chooser #languageChooserModal></my-language-chooser>

View File

@ -1,48 +1,72 @@
@use 'sass:math'; @use 'sass:math';
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
@use '_button-mixins' as *;
.root { .root {
--co-logo-size: 34px;
background-color: pvar(--bg); background-color: pvar(--bg);
padding: 1.5rem;
width: 100%;
display: flex;
align-items: center;
} }
.peertube-title { .peertube-title {
flex-shrink: 1;
min-width: var(--co-logo-size);
flex-grow: 1;
font-size: 24px; font-size: 24px;
font-weight: $font-bold; font-weight: $font-bold;
color: inherit !important; color: inherit !important;
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden;
@include padding-left(18px); @include padding-left(18px);
@include margin-right(0.5rem);
@include disable-default-a-behaviour; @include disable-default-a-behaviour;
}
.instance-name { .instance-name {
width: 100%; width: 100%;
@include ellipsis; @include ellipsis;
}
@media screen and (max-width: $mobile-view) { .icon-logo {
display: none; 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 { background-repeat: no-repeat;
display: inline-block; background-size: contain;
width: 34px;
height: 34px;
min-width: 34px;
max-width: 34px;
background-repeat: no-repeat; @include margin-left(18px);
@include margin-right(10px);
}
@include margin-left(18px); my-search-typeahead {
@include margin-right(10px); max-width: 270px;
}
@include margin-right(1.5rem);
}
.margin-button {
@include margin-right(0.75rem);
}
.menu-button {
display: none;
justify-self: end;
} }
.dropdown { .dropdown {
z-index: #{z('menu') + 1} !important; z-index: #{z('header') + 1} !important;
} }
.dropdown-item { .dropdown-item {
@ -64,11 +88,11 @@
} }
.logged-in-container { .logged-in-container {
flex: 1;
border-radius: 25px; border-radius: 25px;
transition: all .1s ease-in-out; transition: all .1s ease-in-out;
cursor: pointer; cursor: pointer;
max-width: 250px; max-width: 250px;
height: 100%;
.display-name { .display-name {
font-weight: $font-bold; 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 { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { Router, RouterLink } from '@angular/router' import { Router, RouterLink } from '@angular/router'
import { import {
@ -9,11 +9,9 @@ import {
MenuService, MenuService,
RedirectService, RedirectService,
ScreenService, ScreenService,
ServerService, ServerService
UserService
} from '@app/core' } from '@app/core'
import { NotificationDropdownComponent } from '@app/header/notification-dropdown.component' import { NotificationDropdownComponent } from '@app/header/notification-dropdown.component'
import { scrollToTop } from '@app/helpers'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component' import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
import { QuickSettingsModalComponent } from '@app/menu/quick-settings-modal.component' import { QuickSettingsModalComponent } from '@app/menu/quick-settings-modal.component'
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.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 private authSub: Subscription
constructor ( constructor (
private viewportScroller: ViewportScroller,
private authService: AuthService, private authService: AuthService,
private userService: UserService,
private serverService: ServerService, private serverService: ServerService,
private redirectService: RedirectService, private redirectService: RedirectService,
private hotkeysService: HotkeysService, private hotkeysService: HotkeysService,
private screenService: ScreenService, private screenService: ScreenService,
private menuService: MenuService,
private modalService: PeertubeModalService, private modalService: PeertubeModalService,
private router: Router private router: Router,
private menu: MenuService
) { } ) { }
get isInMobileView () {
return this.screenService.isInMobileView()
}
get language () { get language () {
return this.languageChooserModal.getCurrentLanguage() return this.languageChooserModal.getCurrentLanguage()
} }
@ -101,6 +93,14 @@ export class HeaderComponent implements OnInit, OnDestroy {
return this.serverConfig.instance.name return this.serverConfig.instance.name
} }
isInMobileView () {
return this.screenService.isInMobileView()
}
isInSmallView () {
return this.screenService.isInSmallView()
}
ngOnInit () { ngOnInit () {
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
@ -170,57 +170,14 @@ export class HeaderComponent implements OnInit, OnDestroy {
this.quickSettingsModal.show() 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 () { openHotkeysCheatSheet () {
this.hotkeysService.cheatSheetToggle.next(!this.hotkeysHelpVisible) this.hotkeysService.cheatSheetToggle.next(!this.hotkeysHelpVisible)
} }
toggleMenu () {
this.menu.toggleMenu()
}
private updateUserState () { private updateUserState () {
this.user = this.loggedIn this.user = this.loggedIn
? this.authService.getUser() ? this.authService.getUser()

View File

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

View File

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

View File

@ -5,7 +5,7 @@
#search-video { #search-video {
text-overflow: ellipsis; text-overflow: ellipsis;
@include peertube-input-text(270px); @include peertube-input-text(100%);
& { & {
padding-inline-start: 42px; // For the search icon padding-inline-start: 42px; // For the search icon
@ -15,14 +15,6 @@
&::placeholder { &::placeholder {
color: pvar(--input-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 { .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-wrapper">
<div class="main-menu-scrollbar"> <div class="main-menu-scrollbar">
<div class="main-menu"> <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"> <div class="toggle-menu-container">
@if (collapsed) { @if (collapsed) {
<button type="button" class="button-unstyle toggle-menu" i18n-title title="Display the lateral bar" (click)="toggleMenu()"> <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> <my-global-icon class="transform-rotate-180" iconName="chevron-left"></my-global-icon>
</button> </button>
} @else { } @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> <my-global-icon iconName="chevron-left"></my-global-icon>
</button> </button>
} }
</div> </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> <nav>
<ng-container *ngFor="let menuSection of menuSections" > <ng-container *ngFor="let menuSection of menuSections" >
<ul class="ul-unstyle" [ngClass]="[ menuSection.key, 'menu-block' ]"> <ul class="ul-unstyle" [ngClass]="[ menuSection.key, 'menu-block' ]">
@ -43,16 +65,10 @@
</ng-container> </ng-container>
</nav> </nav>
<div class="about"> <div *ngIf="loggedIn" class="about">
<div [ngClass]="{ 'visually-hidden': collapsed }" class="block-title">About {{ instanceName }}</div> <div [ngClass]="{ 'visually-hidden': collapsed }" class="block-title">{{ instanceName }}</div>
<div [ngClass]="{ 'visually-hidden': collapsed }" class="description">{{ shortDescription }}</div> <ng-container *ngTemplateOutlet="moreInfoButton"></ng-container>
<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>
</div> </div>
</div> </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> <a class="d-block fs-8" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer" i18n>Discover more platforms</a>
</div> </div>
</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); z-index: z(menu);
width: calc(#{$menu-width} - 2rem); width: calc(#{$menu-width} - 2rem);
position: fixed; 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 { &.collapsed {
--co-menu-x-padding: 0.25rem; --co-menu-x-padding: 0.25rem;
@ -20,14 +20,6 @@
@include margin-left(0); @include margin-left(0);
} }
@media screen and (max-width: $mobile-view) {
width: 100% !important;
.main-menu {
overflow-y: auto !important;
}
}
} }
.toggle-menu { .toggle-menu {
@ -102,8 +94,10 @@
@include padding-right(var(--co-menu-x-padding)); @include padding-right(var(--co-menu-x-padding));
} }
.menu-block, .menu-block:not(:last-child),
.collapsed .toggle-menu-container { .logged-in .menu-block,
.collapsed .toggle-menu-container,
.about-top {
&::after { &::after {
content: ''; content: '';
display: block; display: block;
@ -113,11 +107,14 @@
} }
} }
.collapsed .menu-block, .collapsed {
.collapsed .toggle-menu-container { .menu-block,
&::after { .toggle-menu-container,
width: 28px; .about {
margin: 1rem auto; &::after {
width: 28px;
margin: 1rem auto;
}
} }
} }
@ -148,6 +145,9 @@
color: pvar-fallback(--menu-fg, --fg-350); color: pvar-fallback(--menu-fg, --fg-350);
font-size: 14px; font-size: 14px;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
max-width: 180px;
@include ellipsis;
} }
.menu-link { .menu-link {
@ -201,3 +201,87 @@
my-button[theme=secondary] ::ng-deep my-global-icon { my-button[theme=secondary] ::ng-deep my-global-icon {
color: pvar(--secondary-icon-color) !important; 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, AuthUser,
HooksService, HooksService,
MenuService, MenuService,
RedirectService,
ServerService, ServerService,
UserService UserService
} from '@app/core' } from '@app/core'
@ -53,8 +54,8 @@ const debugLogger = debug('peertube:menu:MenuComponent')
}) })
export class MenuComponent implements OnInit, OnDestroy { export class MenuComponent implements OnInit, OnDestroy {
menuSections: MenuSection[] = [] menuSections: MenuSection[] = []
loggedIn: boolean
private isLoggedIn: boolean
private user: AuthUser private user: AuthUser
private canSeeVideoMakerBlock: boolean private canSeeVideoMakerBlock: boolean
@ -67,7 +68,8 @@ export class MenuComponent implements OnInit, OnDestroy {
private userService: UserService, private userService: UserService,
private serverService: ServerService, private serverService: ServerService,
private hooks: HooksService, private hooks: HooksService,
private menu: MenuService private menu: MenuService,
private redirectService: RedirectService
) { } ) { }
get shortDescription () { get shortDescription () {
@ -82,13 +84,17 @@ export class MenuComponent implements OnInit, OnDestroy {
return this.menu.isCollapsed() return this.menu.isCollapsed()
} }
get isOverlay () {
return this.menu.isCollapsed()
}
ngOnInit () { ngOnInit () {
this.isLoggedIn = this.authService.isLoggedIn() this.loggedIn = this.authService.isLoggedIn()
this.onUserStateChange() this.onUserStateChange()
this.authSub = this.authService.loginChangedSource.subscribe(status => { this.authSub = this.authService.loginChangedSource.subscribe(status => {
if (status === AuthStatus.LoggedIn) this.isLoggedIn = true if (status === AuthStatus.LoggedIn) this.loggedIn = true
else if (status === AuthStatus.LoggedOut) this.isLoggedIn = false else if (status === AuthStatus.LoggedOut) this.loggedIn = false
this.onUserStateChange() this.onUserStateChange()
}) })
@ -127,9 +133,14 @@ export class MenuComponent implements OnInit, OnDestroy {
title: $localize`Quick access`, title: $localize`Quick access`,
links: [ links: [
{ {
path: '/home', path: this.redirectService.getDefaultRoute(),
icon: 'home' as GlobalIconName, icon: 'home' as GlobalIconName,
label: $localize`Home` 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 { private buildLibraryLinks (): MenuSection {
let links: MenuLink[] = [] let links: MenuLink[] = []
if (this.isLoggedIn) { if (this.loggedIn) {
links = links.concat([ links = links.concat([
{ {
path: '/my-library/video-playlists', path: '/my-library/video-playlists',
icon: 'playlists' as GlobalIconName, icon: 'playlists' as GlobalIconName,
label: $localize`Playlists` label: $localize`Playlists`
}, },
{
path: '/videos/subscriptions',
icon: 'subscriptions' as GlobalIconName,
label: $localize`Subscriptions`
},
{ {
path: '/my-library/history/videos', path: '/my-library/history/videos',
icon: 'history' as GlobalIconName, icon: 'history' as GlobalIconName,
@ -168,7 +174,7 @@ export class MenuComponent implements OnInit, OnDestroy {
private buildVideoMakerLinks (): MenuSection { private buildVideoMakerLinks (): MenuSection {
let links: MenuLink[] = [] let links: MenuLink[] = []
if (this.isLoggedIn && this.canSeeVideoMakerBlock) { if (this.loggedIn && this.canSeeVideoMakerBlock) {
links = links.concat([ links = links.concat([
{ {
path: '/my-library/video-channels', path: '/my-library/video-channels',
@ -202,7 +208,7 @@ export class MenuComponent implements OnInit, OnDestroy {
private buildAdminLinks (): MenuSection { private buildAdminLinks (): MenuSection {
const links: MenuLink[] = [] const links: MenuLink[] = []
if (this.isLoggedIn) { if (this.loggedIn) {
if (this.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { if (this.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
links.push({ links.push({
path: '/admin/overview', path: '/admin/overview',
@ -238,7 +244,7 @@ export class MenuComponent implements OnInit, OnDestroy {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
private computeCanSeeVideoMakerBlock () { private computeCanSeeVideoMakerBlock () {
if (!this.isLoggedIn) return of(false) if (!this.loggedIn) return of(false)
if (!this.user.hasUploadDisabled()) return of(true) if (!this.user.hasUploadDisabled()) return of(true)
return this.authService.userInformationLoaded return this.authService.userInformationLoaded
@ -256,7 +262,7 @@ export class MenuComponent implements OnInit, OnDestroy {
} }
private onUserStateChange () { private onUserStateChange () {
this.user = this.isLoggedIn this.user = this.loggedIn
? this.authService.getUser() ? this.authService.getUser()
: undefined : undefined

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,13 @@
<table *ngIf="serverConfig"> <table *ngIf="serverConfig">
<caption i18n>Features found on this instance</caption> <caption i18n>Features found on this instance</caption>
<tr> <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> <td class="value">{{ getServerVersionAndCommit() }}</td>
</tr> </tr>
<tr> <tr>
<th class="label" scope="row"> <th class="t-label" scope="row">
<div i18n>Default NSFW/sensitive videos policy</div> <div i18n>Default NSFW/sensitive videos policy</div>
<span i18n class="fs-7 fw-normal fst-italic">can be redefined by the users</span> <span i18n class="fs-7 fw-normal fst-italic">can be redefined by the users</span>
</th> </th>
@ -18,31 +18,31 @@
</tr> </tr>
<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> <td class="value">{{ buildRegistrationLabel() }}</td>
</tr> </tr>
<tr> <tr>
<th i18n class="label" colspan="2">Video uploads</th> <th i18n class="t-label" colspan="2">Video uploads</th>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.transcoding.enabledResolutions.length !== 0"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.transcoding.enabledResolutions.length !== 0"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="sub-label" scope="row">Automatic transcription</th> <th i18n class="t-sub-label" scope="row">Automatic transcription</th>
<td> <td>
<my-feature-boolean [value]="serverConfig.videoTranscription.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.videoTranscription.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="sub-label" scope="row">Video uploads</th> <th i18n class="t-sub-label" scope="row">Video uploads</th>
<td> <td>
<span i18n *ngIf="serverConfig.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span> <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> <span i18n *ngIf="!serverConfig.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
@ -50,7 +50,7 @@
</tr> </tr>
<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"> <td class="value">
<ng-container *ngIf="initialUserVideoQuota !== -1"> <ng-container *ngIf="initialUserVideoQuota !== -1">
@ -70,90 +70,90 @@
</tr> </tr>
<tr> <tr>
<th i18n class="label" colspan="2">Live streaming</th> <th i18n class="t-label" colspan="2">Live streaming</th>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.live.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.live.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr *ngIf="serverConfig.live.enabled"> <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> <td>
<my-feature-boolean [value]="serverConfig.live.transcoding.enabled && serverConfig.live.transcoding.enabledResolutions.length > 1"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.live.transcoding.enabled && serverConfig.live.transcoding.enabledResolutions.length > 1"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr *ngIf="serverConfig.live.enabled"> <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> <td i18n>
{{ maxUserLives }} per user / {{ maxInstanceLives }} per instance {{ maxUserLives }} per user / {{ maxInstanceLives }} per instance
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="label" colspan="2">Video Import</th> <th i18n class="t-label" colspan="2">Video Import</th>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.import.videos.http.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.import.videos.http.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="sub-label" scope="row">Torrent import</th> <th i18n class="t-sub-label" scope="row">Torrent import</th>
<td> <td>
<my-feature-boolean [value]="serverConfig.import.videos.torrent.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.import.videos.torrent.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.import.videoChannelSynchronization.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.import.videoChannelSynchronization.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<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>
<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> <td>
<my-feature-boolean [value]="serverConfig.export.users.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.export.users.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.import.users.enabled"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.import.users.enabled"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="label" colspan="2">Search</th> <th i18n class="t-label" colspan="2">Search</th>
</tr> </tr>
<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> <td>
<my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean> <my-feature-boolean [value]="serverConfig.search.remoteUri.users"></my-feature-boolean>
</td> </td>
</tr> </tr>
<tr> <tr>
<th i18n class="label" colspan="2">Plugins & Themes</th> <th i18n class="t-label" colspan="2">Plugins & Themes</th>
</tr> </tr>
<tr> <tr>
<th i18n class="sub-label" scope="row">Available themes</th> <th i18n class="t-sub-label" scope="row">Available themes</th>
<td> <td>
<span class="theme" *ngFor="let theme of serverConfig.theme.registered"> <span class="theme" *ngFor="let theme of serverConfig.theme.registered">
{{ theme.name }} {{ theme.name }}
@ -162,7 +162,7 @@
</tr> </tr>
<tr> <tr>
<th i18n class="sub-label" scope="row">Plugins enabled</th> <th i18n class="t-sub-label" scope="row">Plugins enabled</th>
<td> <td>
<span class="plugin" *ngFor="let plugin of serverConfig.plugin.registered"> <span class="plugin" *ngFor="let plugin of serverConfig.plugin.registered">
{{ plugin.name }} {{ plugin.name }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core' 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 { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
import { logger } from '@root-helpers/logger' import { logger } from '@root-helpers/logger'
import { filter, Subscription } from 'rxjs' import { filter, Subscription } from 'rxjs'
@ -51,7 +51,7 @@ export class HorizontalMenuComponent implements OnInit, OnChanges, OnDestroy {
private routerSub: Subscription 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 this.activeParent = undefined
const currentUrl = window.location.pathname 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 => { const entry = this.menuEntries.find(parent => {
if (currentUrl.startsWith(parent.routerLink)) return true 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)) 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 (!entry) {
if (this.menuEntries.length !== 0) { 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 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"> <ul class="ul-unstyle d-flex">
@for (item of items; track item.label; let id = $index) { @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) {
@if (!item.isDisplayed || item.isDisplayed()) { @if (!item.isDisplayed || item.isDisplayed()) {
<li> <li class="d-inline-block" [id]="getId(id)" #itemsRendered>
<a [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()"> <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
{{ item.label }}
</a>
</li> </li>
} }
} }
</ul> </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; padding-top: 2px;
} }
.root {
overflow: hidden;
max-width: calc(100vw - 30px);
padding-bottom: 3px;
}
.list-overflow-parent { .list-overflow-parent {
display: flex; display: flex;
align-items: center; align-items: center;
// For the menu icon // For the menu icon
position: relative; position: relative;
max-width: calc(100vw - 30px);
&.has-border {
border-bottom: 1px solid pvar(--fg-200);
}
} }
.list-overflow-menu { .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 { import {
AfterViewInit, AfterViewInit,
booleanAttribute,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@ -46,6 +47,7 @@ export interface ListOverflowItem {
export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit { export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit {
@Input() items: T[] @Input() items: T[]
@Input() itemTemplate: TemplateRef<{ item: T }> @Input() itemTemplate: TemplateRef<{ item: T }>
@Input({ transform: booleanAttribute }) hasBorder = false
@ViewChild('modal', { static: true }) modal: ElementRef @ViewChild('modal', { static: true }) modal: ElementRef
@ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement> @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 { ServerService } from '@app/core'
import { PluginsManager } from '@root-helpers/plugins-manager' import { PluginsManager } from '@root-helpers/plugins-manager'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@ -7,11 +7,13 @@ import { LinkComponent } from '../common/link.component'
@Component({ @Component({
selector: 'my-login-link', selector: 'my-login-link',
templateUrl: './login-link.component.html', templateUrl: './login-link.component.html',
styleUrls: [ './login-link.component.scss' ],
standalone: true, standalone: true,
imports: [ LinkComponent ] imports: [ LinkComponent ]
}) })
export class LoginLinkComponent { export class LoginLinkComponent {
@Input() label = $localize`Login` @Input() label = $localize`Login`
@Input({ transform: booleanAttribute }) icon = false
@Input() className?: string @Input() className?: string

View File

@ -47,7 +47,11 @@ export class VideoThumbnailComponent {
} }
isLiveStreaming () { 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 () { isEndedLive () {

View File

@ -1,7 +1,16 @@
<div class="root" [formGroup]="form"> <div class="root" [formGroup]="form">
<div class="first-row"> <div class="scope-row" *ngIf="totalFollowing && !hideScope">
<div> @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"> <div class="d-flex flex-wrap me-2">
@for (quickFilter of quickFilters; track quickFilter) { @for (quickFilter of quickFilters; track quickFilter) {
<my-button <my-button
@ -37,25 +46,15 @@
</div> </div>
</div> </div>
<div class="d-flex flex-wrap align-items-center ms-3" *ngIf="!hideScope"> <div class="d-flex flex-wrap align-items-center ms-3" >
<label for="scope" i18n class="select-label">Display videos of:</label> <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> </div>
<div [ngbCollapse]="areFiltersCollapsed" [animation]="true"> <div [ngbCollapse]="areFiltersCollapsed" [animation]="true">
<div class="filters"> <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">
<div class="section-title"> <div class="section-title">
<ng-container i18n>Content preferences</ng-container> <ng-container i18n>Content preferences</ng-container>
@ -90,6 +89,24 @@
</div> </div>
</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">
<div class="section-title" i18n>Content type</div> <div class="section-title" i18n>Content type</div>

View File

@ -10,7 +10,13 @@ $filters-background: pvar(--bg-secondary-400);
margin-bottom: 45px; margin-bottom: 45px;
} }
.first-row { .scope-row {
font-size: 20px;
color: pvar(--fg-350);
margin-bottom: 0.5rem;
}
.filters-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
@ -128,7 +134,7 @@ my-select-categories {
} }
@media screen and (max-width: $small-view) { @media screen and (max-width: $small-view) {
.first-row { .filters-row {
flex-direction: column; 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 { ButtonComponent } from '../shared-main/buttons/button.component'
import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
import { VideoFilterActive, VideoFilters } from './video-filters.model' import { VideoFilterActive, VideoFilters } from './video-filters.model'
import { InstanceFollowService } from '../shared-instance/instance-follow.service'
const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent') const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent')
@ -45,7 +46,8 @@ type QuickFilter = {
PeertubeCheckboxComponent, PeertubeCheckboxComponent,
SelectOptionsComponent, SelectOptionsComponent,
ButtonComponent ButtonComponent
] ],
providers: [ InstanceFollowService ]
}) })
export class VideoFiltersHeaderComponent implements OnInit, OnDestroy { export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
@Input() filters: VideoFilters @Input() filters: VideoFilters
@ -63,6 +65,9 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
quickFilters: QuickFilter[] = [] quickFilters: QuickFilter[] = []
instanceName: string
totalFollowing: number
private videoCategories: VideoConstant<number>[] = [] private videoCategories: VideoConstant<number>[] = []
private videoLanguages: VideoConstant<string>[] = [] private videoLanguages: VideoConstant<string>[] = []
@ -74,11 +79,15 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
private fb: FormBuilder, private fb: FormBuilder,
private modalService: PeertubeModalService, private modalService: PeertubeModalService,
private redirectService: RedirectService, private redirectService: RedirectService,
private route: ActivatedRoute private route: ActivatedRoute,
private server: ServerService,
private followService: InstanceFollowService
) { ) {
} }
ngOnInit () { ngOnInit () {
this.instanceName = this.server.getHTMLConfig().instance.name
this.form = this.fb.group({ this.form = this.fb.group({
sort: [ '' ], sort: [ '' ],
nsfw: [ '' ], nsfw: [ '' ],
@ -113,9 +122,12 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
this.serverService.getVideoLanguages() this.serverService.getVideoLanguages()
.subscribe(languages => this.videoLanguages = languages) .subscribe(languages => this.videoLanguages = languages)
this.followService.getFollowing({ pagination: { count: 1, start: 0 }, state: 'accepted' })
.subscribe(({ total }) => this.totalFollowing = total)
this.availableScopes = [ this.availableScopes = [
{ id: 'local', label: $localize`This platform only` }, { id: 'local', label: $localize`Only videos from this platform` },
{ id: 'federated', label: $localize`All platforms` } { id: 'federated', label: $localize`Videos from all platforms` }
] ]
this.buildSortItems() 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 { 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 { import {
AuthService, AuthService,
ComponentPaginationLight, ComponentPaginationLight,
@ -11,7 +11,6 @@ import {
UserService UserService
} from '@app/core' } from '@app/core'
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component' 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 { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@peertube/peertube-core-utils'
import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models' import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger' 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 { concatMap, debounceTime, map, switchMap } from 'rxjs/operators'
import { ButtonComponent } from '../shared-main/buttons/button.component' import { ButtonComponent } from '../shared-main/buttons/button.component'
import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive' 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 { Syndication } from '../shared-main/feeds/syndication.model'
import { Video } from '../shared-main/video/video.model' import { Video } from '../shared-main/video/video.model'
import { VideoFiltersHeaderComponent } from './video-filters-header.component' import { VideoFiltersHeaderComponent } from './video-filters-header.component'
@ -52,14 +50,9 @@ enum GroupDate {
standalone: true, standalone: true,
imports: [ imports: [
NgIf, NgIf,
NgbTooltip,
NgClass, NgClass,
FeedComponent,
NgFor, NgFor,
RouterLinkActive,
RouterLink,
ButtonComponent, ButtonComponent,
NgTemplateOutlet,
ButtonComponent, ButtonComponent,
VideoFiltersHeaderComponent, VideoFiltersHeaderComponent,
InfiniteScrollerDirective, InfiniteScrollerDirective,

View File

@ -223,37 +223,10 @@ my-video-thumbnail,
} }
@media screen and (min-width: $small-view) { @media screen and (min-width: $small-view) {
:host-context(.expanded) { @include more-dropdown-control();
@include more-dropdown-control(); @include edit-button-control();
}
}
@media screen and (max-width: $small-view) {
:host-context(.expanded) {
@include edit-button-control();
}
} }
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {
:host-context(.expanded) { @include edit-button-in-mobile-view();
@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();
}
} }

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 './z-index';
@use './class-helpers/index.scss'; @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 */ /* clears the X from Chrome */
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-cancel-button,
@ -67,6 +74,14 @@ body {
--alert-primary-bg: #{pvar(--primary-200)}; --alert-primary-bg: #{pvar(--primary-200)};
--alert-primary-border-color: #{pvar(--primary-300)}; --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 // Light theme
&[data-pt-theme=peertube-core-light], &[data-pt-theme=peertube-core-light],
&[data-pt-theme=default] { &[data-pt-theme=default] {
@ -104,8 +119,8 @@ body {
--bg: hsl(0 14% 7%); --bg: hsl(0 14% 7%);
--bg-secondary: hsl(0 14% 22%); --bg-secondary: hsl(0 14% 22%);
--alert-primary-fg: #{pvar(--on-primary)}; --alert-primary-fg: #{pvar(--primary-650)};
--alert-primary-bg: #{pvar(--primary-200)}; --alert-primary-bg: #{pvar(--primary-100)};
--alert-primary-border-color: #{pvar(--primary-300)}; --alert-primary-border-color: #{pvar(--primary-300)};
} }
@ -126,7 +141,7 @@ body {
} }
::selection { ::selection {
color: pvar(--bg); color: pvar(--on-primary);
background-color: pvar(--primary-450); background-color: pvar(--primary-450);
} }
@ -201,17 +216,6 @@ code {
margin: 0 calc(#{pvar(--x-margin-content)} * -1); 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 { .skip-to-content-sub-menu {
display: block; display: block;
z-index: z(modal); z-index: z(modal);
@ -223,24 +227,21 @@ code {
// Override some properties if the main content is expanded (no menu on the left) // Override some properties if the main content is expanded (no menu on the left)
&.expanded { &.expanded {
--main-col-width: 100vw; @include main-col-expanded();
width: calc(100% - #{$collapsed-menu-width});
@include margin-left($collapsed-menu-width);
} }
&.lock-scroll .main-row > router-outlet + * { /* stylelint-disable-line selector-max-compound-selectors */ &.lock-scroll .main-row > router-outlet + * { /* stylelint-disable-line selector-max-compound-selectors */
// Lock and hide body scrollbars // Lock and hide body scrollbars
position: fixed; 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] { my-global-icon[iconName=external-link] {
margin: 0 0.3em; margin: 0 0.3em;
width: 0.9em; 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 */ /* 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,
.main-col.expanded { .main-col.expanded {
.sub-menu { .title-page {
padding: 0 50px; 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 { .main-col {
width: 100%; @include main-col-expanded();
} }
} }
@media screen and (max-width: $small-view) { @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,
.main-col.expanded { .main-col.expanded {
--x-margin-content: 15px; width: 100%;
@include margin-left(0); @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 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 // Nav
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -81,7 +81,7 @@
.anchor { .anchor {
position: relative; position: relative;
top: #{- ($header-height + 20px)}; top: -calc(#{pvar(--header-height)} + 20px);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -97,7 +97,12 @@
} }
.secondary-button { .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 { .banner {
@include block-ratio('img', $banner-inverted-ratio); @include block-ratio('img', $banner-inverted-ratio);
img {
border-radius: 15px;
}
} }
.revert-margin-content.banner { .revert-margin-content.banner {

View File

@ -47,3 +47,15 @@
.min-width-0 { .min-width-0 {
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 { &.badge-primary {
color: pvar(--bg); color: pvar(--on-primary);
background-color: pvar(--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-width: 0;
$input-btn-focus-color: inherit; $input-btn-focus-color: inherit;
$input-focus-border-color: pvar(--input-border-color); $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-color: pvar(--fg);
$input-group-addon-bg: pvar(--bg-secondary-500); $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-bg: pvar(--primary-50);
$accordion-button-active-color: pvar(--fg); $accordion-button-active-color: pvar(--fg);
$accordion-button-focus-border-color: pvar(--primary-100); $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); $accordion-border-color: pvar(--input-border-color);
$card-color: pvar(--fg); $card-color: pvar(--fg);

View File

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

View File

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

View File

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

View File

@ -21,9 +21,12 @@ $white: #fff;
$button-font-size: 1rem; $button-font-size: 1rem;
$header-height: 94px; $header-height: 94px;
$header-height-mobile-view: 144px;
$menu-width: 248px; $menu-width: 248px;
$collapsed-menu-width: 48px; $menu-collapsed-width: 50px;
$menu-margin-left: 2rem;
$menu-overlay-view: 1200px;
$sub-menu-height: 81px; $sub-menu-height: 81px;
@ -53,7 +56,8 @@ $player-portrait-bottom-space: 50px;
$sub-menu-margin-bottom: 30px; $sub-menu-margin-bottom: 30px;
$sub-menu-margin-bottom-small-view: 10px; $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; $form-input-font-size: 16px;
$video-watch-info-margin-left: 44px; $video-watch-info-margin-left: 44px;
@ -71,6 +75,7 @@ $variables: (
--x-margin-content: var(--x-margin-content), --x-margin-content: var(--x-margin-content),
--x-videos-margin-content: var(--x-videos-margin-content), --x-videos-margin-content: var(--x-videos-margin-content),
--main-col-width: var(--main-col-width), --main-col-width: var(--main-col-width),
--header-height: var(--header-height),
--fg: var(--fg), --fg: var(--fg),
--bg: var(--bg), --bg: var(--bg),
@ -121,6 +126,8 @@ $variables: (
--on-primary: var(--on-primary), --on-primary: var(--on-primary),
--primary: var(--primary), --primary: var(--primary),
--primary-700: var(--primary-700),
--primary-650: var(--primary-650),
--primary-600: var(--primary-600), --primary-600: var(--primary-600),
--primary-550: var(--primary-550), --primary-550: var(--primary-550),
--primary-500: var(--primary-500), --primary-500: var(--primary-500),
@ -140,6 +147,8 @@ $variables: (
--alert-primary-bg: var(--alert-primary-bg), --alert-primary-bg: var(--alert-primary-bg),
--alert-primary-border-color: var(--alert-primary-border-color), --alert-primary-border-color: var(--alert-primary-border-color),
--menu-margin-left: var(--menu-margin-left),
// Optional variables // Optional variables
--menu-fg: var(--menu-fg), --menu-fg: var(--menu-fg),
--menu-bg: var(--menu-bg) --menu-bg: var(--menu-bg)
@ -168,7 +177,6 @@ $variables: (
$zindex: ( $zindex: (
miniature : 10, miniature : 10,
sub-menu : 12500,
overlay : 12550, overlay : 12550,
menu : 12600, menu : 12600,
search-typeahead: 12650, search-typeahead: 12650,
@ -193,3 +201,4 @@ $zindex: (
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
$separator-border-color: pvar(--bg-secondary-400); $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 { body .p-paginator .p-paginator-last:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { body .p-paginator .p-paginator-current {
color: pvar(--fg); 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 { body .p-paginator .p-paginator-pages .p-paginator-page:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--primary-100); box-shadow: $focus-box-shadow-form;
} }
body .p-paginator .p-dropdown { body .p-paginator .p-dropdown {
@include margin-left(0.5em); @include margin-left(0.5em);
@ -157,7 +157,7 @@ body .p-dropdown:not(.p-disabled):hover {
body .p-dropdown:not(.p-disabled).p-focus { body .p-dropdown:not(.p-disabled).p-focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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); border-color: pvar(--input-border-color);
} }
body .p-dropdown.p-dropdown-clearable .p-dropdown-label { 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 { body .p-datepicker:not(.p-disabled) .p-datepicker-header .p-datepicker-next:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { body .p-datepicker .p-datepicker-header {
padding: 0.429em 0.857em 0.429em 0.857em; 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 { body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { body .p-datepicker .p-datepicker-header .p-datepicker-title .p-datepicker-month {
@include margin-right(0.5rem); @include margin-right(0.5rem);
@ -349,7 +349,7 @@ body .p-datepicker table td > a {
body .p-datepicker table td > a:focus { body .p-datepicker table td > a:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 > a,
body .p-datepicker table td.p-datepicker-today > span { 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 { .p-chips:not(.p-disabled).p-focus .p-chips-multiple-container {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { .p-chips .p-chips-multiple-container {
padding: pvar(--input-y-padding) pvar(--input-x-padding); 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 { .p-multiselect:not(.p-disabled).p-focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; outline-offset: 0;
box-shadow: 0 0 0 0.25rem pvar(--primary-300); box-shadow: $focus-box-shadow-form;
} }
.p-multiselect .p-multiselect-label { .p-multiselect .p-multiselect-label {
padding: pvar(--input-y-padding) pvar(--input-x-padding); 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; transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
} }
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:enabled:hover { .p-multiselect-panel .p-multiselect-header .p-multiselect-close:enabled:hover {
color: pvar(--primary); color: pvar(--fg-400);
border-color: transparent; border-color: transparent;
background: transparent; background: transparent;
} }
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:focus-visible { .p-multiselect-panel .p-multiselect-header .p-multiselect-close:focus-visible {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { .p-multiselect-panel .p-multiselect-items {
padding: 0; padding: 0;
@ -708,7 +708,7 @@ p-multiselect.ng-dirty.ng-invalid > .p-multiselect {
.p-inputtext:enabled:focus { .p-inputtext:enabled:focus {
outline: 0 none; outline: 0 none;
outline-offset: 0; 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 { .p-inputtext.ng-dirty.ng-invalid {
border-color: pvar(--red); border-color: pvar(--red);
@ -924,7 +924,7 @@ p-table {
} }
&.p-focus { &.p-focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100); box-shadow: $focus-box-shadow-form;
} }
.p-label { .p-label {
@ -962,7 +962,7 @@ p-table {
&.focus-within, &.focus-within,
&:focus { &:focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100); box-shadow: $focus-box-shadow-form;
} }
&.p-disabled:hover { &.p-disabled:hover {
@ -998,7 +998,7 @@ p-table {
&.focus-within, &.focus-within,
&:focus { &:focus {
box-shadow: #{$focus-box-shadow-form} pvar(--primary-100) !important; box-shadow: #{$focus-box-shadow-form} !important;
} }
&.p-highlight { &.p-highlight {

View File

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

View File

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