From b7cb86498224438b6e6a5ddc0a8ceba7b73e85d5 Mon Sep 17 00:00:00 2001 From: Marek Lesko Date: Thu, 30 Oct 2025 16:22:30 +0000 Subject: [PATCH] feat: Refactor settings handling in full layout and header components for improved theme management --- Web/src/app/layouts/full/full.component.html | 26 +++---- Web/src/app/layouts/full/full.component.ts | 73 ++++++++++--------- .../horizontal/header/header.component.html | 2 +- .../horizontal/header/header.component.ts | 19 +---- Web/src/app/services/core.service.ts | 46 +++++++++++- 5 files changed, 101 insertions(+), 65 deletions(-) diff --git a/Web/src/app/layouts/full/full.component.html b/Web/src/app/layouts/full/full.component.html index 7b10d45..2532b84 100755 --- a/Web/src/app/layouts/full/full.component.html +++ b/Web/src/app/layouts/full/full.component.html @@ -1,17 +1,17 @@ - + - @if (!options.horizontal) { + @if (!settings.getOptions().horizontal) { @@ -47,7 +47,7 @@ @if (resView) { - @@ -82,19 +82,19 @@ - @if (!options.horizontal) { + @if (!settings.getOptions().horizontal) { } @else { - } @if(options.horizontal) { + > + } @if(settings.getOptions().horizontal) { }
@@ -115,7 +115,7 @@
- @if(options.theme === 'light') { + @if(settings.getOptions().theme === 'light') { logo diff --git a/Web/src/app/layouts/full/full.component.ts b/Web/src/app/layouts/full/full.component.ts index 3a21320..b3cb3f0 100755 --- a/Web/src/app/layouts/full/full.component.ts +++ b/Web/src/app/layouts/full/full.component.ts @@ -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'; @@ -42,36 +42,36 @@ interface quicklinks { } @Component({ - selector: 'app-full', - imports: [ - RouterModule, - AppNavItemComponent, - MaterialModule, - CommonModule, - SidebarComponent, - NgScrollbarModule, - TablerIconsModule, - HeaderComponent, - AppHorizontalHeaderComponent, - AppHorizontalSidebarComponent, - AppBreadcrumbComponent, - CustomizerComponent, - ], - templateUrl: './full.component.html', - - encapsulation: ViewEncapsulation.None + selector: 'app-full', + imports: [ + RouterModule, + AppNavItemComponent, + MaterialModule, + CommonModule, + SidebarComponent, + NgScrollbarModule, + TablerIconsModule, + HeaderComponent, + AppHorizontalHeaderComponent, + AppHorizontalSidebarComponent, + AppBreadcrumbComponent, + CustomizerComponent, + ], + templateUrl: './full.component.html', + + encapsulation: ViewEncapsulation.None }) export class FullComponent implements OnInit { navItems = navItems; - + @ViewChild('leftsidenav') public sidenav: MatSidenav; 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,30 +190,34 @@ 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 .pipe(filter((event) => event instanceof NavigationEnd)) @@ -230,7 +234,7 @@ export class FullComponent implements OnInit { this.cdr.detectChanges(); // Ensures Angular updates the view } - ngOnInit(): void {} + ngOnInit(): void { } ngOnDestroy() { this.layoutChangesSubscription.unsubscribe(); @@ -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 { diff --git a/Web/src/app/layouts/full/horizontal/header/header.component.html b/Web/src/app/layouts/full/horizontal/header/header.component.html index 1d89af8..5480acb 100755 --- a/Web/src/app/layouts/full/horizontal/header/header.component.html +++ b/Web/src/app/layouts/full/horizontal/header/header.component.html @@ -101,7 +101,7 @@ } - @if(options.theme=='light'){ + @if(settings.getOptions().theme=='light'){ diff --git a/Web/src/app/layouts/full/horizontal/header/header.component.ts b/Web/src/app/layouts/full/horizontal/header/header.component.ts index c98b1ea..dfbb318 100755 --- a/Web/src/app/layouts/full/horizontal/header/header.component.ts +++ b/Web/src/app/layouts/full/horizontal/header/header.component.ts @@ -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(); - 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 { diff --git a/Web/src/app/services/core.service.ts b/Web/src/app/services/core.service.ts index 25dc14e..36cf7b3 100755 --- a/Web/src/app/services/core.service.ts +++ b/Web/src/app/services/core.service.ts @@ -5,13 +5,57 @@ import { AppSettings, defaults } from '../config'; providedIn: 'root', }) export class CoreService { - private optionsSignal = signal(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(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) { + // 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,