feat: Refactor settings handling in full layout and header components for improved theme management

This commit is contained in:
Marek Lesko
2025-10-30 16:22:30 +00:00
parent d5619e42da
commit b7cb864982
5 changed files with 101 additions and 65 deletions

View File

@@ -1,17 +1,17 @@
<span [dir]="options.dir!">
<span [dir]="settings.getOptions().dir">
<mat-sidenav-container class="mainWrapper" autosize autoFocus [ngClass]="{
'sidebarNav-mini':
options.sidenavCollapsed && options.navPos !== 'top' && !resView,
'sidebarNav-horizontal': options.horizontal,
cardBorder: options.cardBorder
settings.getOptions().sidenavCollapsed && settings.getOptions().navPos !== 'top' && !resView,
'sidebarNav-horizontal': settings.getOptions().horizontal,
cardBorder: settings.getOptions().cardBorder
}">
<!-- ============================================================== -->
<!-- Vertical Sidebar -->
<!-- ============================================================== -->
@if (!options.horizontal) {
@if (!settings.getOptions().horizontal) {
<mat-sidenav #leftsidenav [mode]="isOver ? 'over' : 'side'" [opened]="
options.navPos === 'side' &&
options.sidenavOpened &&
settings.getOptions().navPos === 'side' &&
settings.getOptions().sidenavOpened &&
!isOver &&
!resView
" (openedChange)="onSidenavOpenedChange($event)" (closedStart)="onSidenavClosedStart()" class="sidebarNav">
@@ -47,7 +47,7 @@
<!-- horizontal Sidebar -->
<!-- ============================================================== -->
@if (resView) {
<mat-sidenav #leftsidenav [mode]="'over'" [opened]="options.sidenavOpened && !isTablet"
<mat-sidenav #leftsidenav [mode]="'over'" [opened]="settings.getOptions().sidenavOpened && !isTablet"
(openedChange)="onSidenavOpenedChange($event)" (closedStart)="onSidenavClosedStart()" class="sidebarNav">
<app-sidebar> </app-sidebar>
<ng-scrollbar class="position-relative" style="height: 100%">
@@ -82,19 +82,19 @@
<!-- ============================================================== -->
<!-- VerticalHeader -->
<!-- ============================================================== -->
@if (!options.horizontal) {
@if (!settings.getOptions().horizontal) {
<app-header [showToggle]="!isOver" (toggleCollapsed)="toggleCollapsed()" (toggleMobileNav)="sidenav.toggle()"
(toggleMobileFilterNav)="toggleFilterNav()" (optionsChange)="receiveOptions($event)"></app-header>
} @else {
<!-- horizontal header -->
<app-horizontal-header (toggleMobileNav)="sidenav.toggle()" (toggleMobileFilterNav)="toggleFilterNav()"
(optionsChange)="receiveOptions($event)"></app-horizontal-header>
} @if(options.horizontal) {
></app-horizontal-header>
} @if(settings.getOptions().horizontal) {
<app-horizontal-sidebar></app-horizontal-sidebar>
}
<main class="pageWrapper" [ngClass]="{
maxWidth: options.boxed
maxWidth: settings.getOptions().boxed
}">
<app-breadcrumb></app-breadcrumb>
<!-- ============================================================== -->
@@ -115,7 +115,7 @@
<div>
<div class="d-flex justify-content-between align-items-center">
<div class="branding">
@if(options.theme === 'light') {
@if(settings.getOptions().theme === 'light') {
<a href="/">
<img src="./assets/images/logos/dark-logo.svg" class="align-middle m-2" alt="logo" />
</a>

View File

@@ -1,5 +1,5 @@
import { BreakpointObserver, MediaMatcher } from '@angular/cdk/layout';
import { ChangeDetectorRef, Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ChangeDetectorRef, Component, effect, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Subscription } from 'rxjs';
import { MatSidenav, MatSidenavContent } from '@angular/material/sidenav';
import { CoreService } from '../../services/core.service';
@@ -71,7 +71,7 @@ export class FullComponent implements OnInit {
resView = false;
@ViewChild('content', { static: true }) content!: MatSidenavContent;
//get options from service
options: AppSettings;
// options: AppSettings;
private layoutChangesSubscription = Subscription.EMPTY;
private isMobileScreen = false;
private isContentWidthFixed = true;
@@ -190,29 +190,33 @@ export class FullComponent implements OnInit {
];
constructor(
private settings: CoreService,
protected settings: CoreService,
private mediaMatcher: MediaMatcher,
private router: Router,
private breakpointObserver: BreakpointObserver,
private navService: NavService, private cdr: ChangeDetectorRef
private navService: NavService,
private cdr: ChangeDetectorRef
) {
this.htmlElement = document.querySelector('html')!;
this.options = this.settings.getOptions();
// this.options = this.settings.getOptions();
this.layoutChangesSubscription = this.breakpointObserver
.observe([MOBILE_VIEW, TABLET_VIEW, MONITOR_VIEW, BELOWMONITOR])
.subscribe((state) => {
// SidenavOpened must be reset true when layout changes
this.options.sidenavOpened = true;
this.settings.setOptions({ sidenavOpened: true });
// this.options.sidenavOpened = true;
this.isMobileScreen = state.breakpoints[BELOWMONITOR];
if (this.options.sidenavCollapsed == false) {
this.options.sidenavCollapsed = state.breakpoints[TABLET_VIEW];
if (this.settings.getOptions().sidenavCollapsed == false) {
this.settings.setOptions({ sidenavCollapsed: state.breakpoints[TABLET_VIEW] });
}
this.isContentWidthFixed = state.breakpoints[MONITOR_VIEW];
this.resView = state.breakpoints[BELOWMONITOR];
});
// Initialize project theme with options
this.receiveOptions(this.options);
effect(() => {
const options = this.settings.getOptionsSignal()();
this.receiveOptions(options);
});
// This is for scroll to top
this.router.events
@@ -238,12 +242,12 @@ export class FullComponent implements OnInit {
toggleCollapsed() {
this.isContentWidthFixed = false;
this.options.sidenavCollapsed = !this.options.sidenavCollapsed;
this.settings.setOptions({ sidenavCollapsed: !this.settings.getOptions().sidenavCollapsed });
this.resetCollapsedState();
}
resetCollapsedState(timer = 400) {
setTimeout(() => this.settings.setOptions(this.options), timer);
setTimeout(() => this.settings.setOptions(this.settings.getOptions()), timer);
}
onSidenavClosedStart() {
@@ -252,8 +256,7 @@ export class FullComponent implements OnInit {
onSidenavOpenedChange(isOpened: boolean) {
this.isCollapsedWidthFixed = !this.isOver;
this.options.sidenavOpened = isOpened;
this.settings.setOptions(this.options);
this.settings.setOptions({ sidenavOpened: isOpened });
}
receiveOptions(options: AppSettings): void {

View File

@@ -101,7 +101,7 @@
}
</mat-menu>
@if(options.theme=='light'){
@if(settings.getOptions().theme=='light'){
<button mat-icon-button aria-label="lightdark" class="d-flex justify-content-center" (click)="setlightDark('dark')">
<i-tabler class="d-flex icon-22" [name]="'moon'"></i-tabler>
</button>

View File

@@ -8,7 +8,6 @@ import { TablerIconsModule } from 'angular-tabler-icons';
import { MaterialModule } from '../../../../material.module';
import { BrandingComponent } from '../../vertical/sidebar/branding.component';
import { FormsModule } from '@angular/forms';
import { AppSettings } from '../../../../config';
import { AuthenticationService } from '../../../../services/authentication.service';
interface notifications {
@@ -74,19 +73,14 @@ export class AppHorizontalHeaderComponent {
},
];
@Output() optionsChange = new EventEmitter<AppSettings>();
constructor(
private settings: CoreService,
protected settings: CoreService,
private vsidenav: CoreService,
public dialog: MatDialog,
private translate: TranslateService,
public auth: AuthenticationService
) {
// translate.setDefaultLang('en');
this.options = this.settings.getOptions();
}
options: AppSettings;
) { }
openDialog() {
const dialogRef = this.dialog.open(AppHorizontalSearchDialogComponent);
@@ -96,13 +90,8 @@ export class AppHorizontalHeaderComponent {
});
}
private emitOptions() {
this.optionsChange.emit(this.options);
}
setlightDark(theme: string) {
this.options.theme = theme;
this.emitOptions();
this.settings.setOptions({ theme: theme });
}
changeLanguage(lang: any): void {

View File

@@ -5,13 +5,57 @@ import { AppSettings, defaults } from '../config';
providedIn: 'root',
})
export class CoreService {
private optionsSignal = signal<AppSettings>(defaults);
// detect user's preferred color scheme (safe for SSR)
private static detectTheme(): 'dark' | 'light' {
// detect theme if set manually
const theme = localStorage.getItem('theme') as 'dark' | 'light' | null;
if (theme)
return theme;
else
try {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
} catch {
// ignore and fall through
}
return defaults.theme === 'dark' ? 'dark' : 'light';
}
// merge detected theme into initial defaults
private initialOptions = { ...defaults, theme: CoreService.detectTheme() as string } as AppSettings;
private optionsSignal = signal<AppSettings>(this.initialOptions);
constructor() {
// listen for changes to the OS/browser color-scheme and update settings
if (typeof window !== 'undefined' && window.matchMedia) {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
this.setOptions({ theme: e.matches ? 'dark' : 'light' });
};
if (typeof mq.addEventListener === 'function') {
mq.addEventListener('change', handler);
} else if (typeof (mq as any).addListener === 'function') {
// fallback for older browsers
(mq as any).addListener(handler);
}
}
}
getOptions() {
return this.optionsSignal();
}
getOptionsSignal() {
return this.optionsSignal;
}
setOptions(options: Partial<AppSettings>) {
// if theme is specified, persist to localStorage
if (options.theme !== null && options.theme !== undefined)
localStorage.setItem('theme', options.theme);
this.optionsSignal.update((current) => ({
...current,
...options,