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

View File

@@ -1,5 +1,5 @@
import { BreakpointObserver, MediaMatcher } from '@angular/cdk/layout'; 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 { Subscription } from 'rxjs';
import { MatSidenav, MatSidenavContent } from '@angular/material/sidenav'; import { MatSidenav, MatSidenavContent } from '@angular/material/sidenav';
import { CoreService } from '../../services/core.service'; import { CoreService } from '../../services/core.service';
@@ -42,36 +42,36 @@ interface quicklinks {
} }
@Component({ @Component({
selector: 'app-full', selector: 'app-full',
imports: [ imports: [
RouterModule, RouterModule,
AppNavItemComponent, AppNavItemComponent,
MaterialModule, MaterialModule,
CommonModule, CommonModule,
SidebarComponent, SidebarComponent,
NgScrollbarModule, NgScrollbarModule,
TablerIconsModule, TablerIconsModule,
HeaderComponent, HeaderComponent,
AppHorizontalHeaderComponent, AppHorizontalHeaderComponent,
AppHorizontalSidebarComponent, AppHorizontalSidebarComponent,
AppBreadcrumbComponent, AppBreadcrumbComponent,
CustomizerComponent, CustomizerComponent,
], ],
templateUrl: './full.component.html', templateUrl: './full.component.html',
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class FullComponent implements OnInit { export class FullComponent implements OnInit {
navItems = navItems; navItems = navItems;
@ViewChild('leftsidenav') @ViewChild('leftsidenav')
public sidenav: MatSidenav; public sidenav: MatSidenav;
resView = false; resView = false;
@ViewChild('content', { static: true }) content!: MatSidenavContent; @ViewChild('content', { static: true }) content!: MatSidenavContent;
//get options from service //get options from service
options: AppSettings; // options: AppSettings;
private layoutChangesSubscription = Subscription.EMPTY; private layoutChangesSubscription = Subscription.EMPTY;
private isMobileScreen = false; private isMobileScreen = false;
private isContentWidthFixed = true; private isContentWidthFixed = true;
@@ -190,30 +190,34 @@ export class FullComponent implements OnInit {
]; ];
constructor( constructor(
private settings: CoreService, protected settings: CoreService,
private mediaMatcher: MediaMatcher, private mediaMatcher: MediaMatcher,
private router: Router, private router: Router,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private navService: NavService, private cdr: ChangeDetectorRef private navService: NavService,
private cdr: ChangeDetectorRef
) { ) {
this.htmlElement = document.querySelector('html')!; this.htmlElement = document.querySelector('html')!;
this.options = this.settings.getOptions(); // this.options = this.settings.getOptions();
this.layoutChangesSubscription = this.breakpointObserver this.layoutChangesSubscription = this.breakpointObserver
.observe([MOBILE_VIEW, TABLET_VIEW, MONITOR_VIEW, BELOWMONITOR]) .observe([MOBILE_VIEW, TABLET_VIEW, MONITOR_VIEW, BELOWMONITOR])
.subscribe((state) => { .subscribe((state) => {
// SidenavOpened must be reset true when layout changes // 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]; this.isMobileScreen = state.breakpoints[BELOWMONITOR];
if (this.options.sidenavCollapsed == false) { if (this.settings.getOptions().sidenavCollapsed == false) {
this.options.sidenavCollapsed = state.breakpoints[TABLET_VIEW]; this.settings.setOptions({ sidenavCollapsed: state.breakpoints[TABLET_VIEW] });
} }
this.isContentWidthFixed = state.breakpoints[MONITOR_VIEW]; this.isContentWidthFixed = state.breakpoints[MONITOR_VIEW];
this.resView = state.breakpoints[BELOWMONITOR]; this.resView = state.breakpoints[BELOWMONITOR];
}); });
// Initialize project theme with options effect(() => {
this.receiveOptions(this.options); const options = this.settings.getOptionsSignal()();
this.receiveOptions(options);
});
// This is for scroll to top // This is for scroll to top
this.router.events this.router.events
.pipe(filter((event) => event instanceof NavigationEnd)) .pipe(filter((event) => event instanceof NavigationEnd))
@@ -230,7 +234,7 @@ export class FullComponent implements OnInit {
this.cdr.detectChanges(); // Ensures Angular updates the view this.cdr.detectChanges(); // Ensures Angular updates the view
} }
ngOnInit(): void {} ngOnInit(): void { }
ngOnDestroy() { ngOnDestroy() {
this.layoutChangesSubscription.unsubscribe(); this.layoutChangesSubscription.unsubscribe();
@@ -238,12 +242,12 @@ export class FullComponent implements OnInit {
toggleCollapsed() { toggleCollapsed() {
this.isContentWidthFixed = false; this.isContentWidthFixed = false;
this.options.sidenavCollapsed = !this.options.sidenavCollapsed; this.settings.setOptions({ sidenavCollapsed: !this.settings.getOptions().sidenavCollapsed });
this.resetCollapsedState(); this.resetCollapsedState();
} }
resetCollapsedState(timer = 400) { resetCollapsedState(timer = 400) {
setTimeout(() => this.settings.setOptions(this.options), timer); setTimeout(() => this.settings.setOptions(this.settings.getOptions()), timer);
} }
onSidenavClosedStart() { onSidenavClosedStart() {
@@ -252,8 +256,7 @@ export class FullComponent implements OnInit {
onSidenavOpenedChange(isOpened: boolean) { onSidenavOpenedChange(isOpened: boolean) {
this.isCollapsedWidthFixed = !this.isOver; this.isCollapsedWidthFixed = !this.isOver;
this.options.sidenavOpened = isOpened; this.settings.setOptions({ sidenavOpened: isOpened });
this.settings.setOptions(this.options);
} }
receiveOptions(options: AppSettings): void { receiveOptions(options: AppSettings): void {

View File

@@ -101,7 +101,7 @@
} }
</mat-menu> </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')"> <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> <i-tabler class="d-flex icon-22" [name]="'moon'"></i-tabler>
</button> </button>

View File

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

View File

@@ -5,13 +5,57 @@ import { AppSettings, defaults } from '../config';
providedIn: 'root', providedIn: 'root',
}) })
export class CoreService { 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() { getOptions() {
return this.optionsSignal(); return this.optionsSignal();
} }
getOptionsSignal() {
return this.optionsSignal;
}
setOptions(options: Partial<AppSettings>) { 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) => ({ this.optionsSignal.update((current) => ({
...current, ...current,
...options, ...options,