/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ApplicationRef, Injectable, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ThemingService { private ref = inject(ApplicationRef); private darkModeOn = false; themes = [ 'dark-theme', 'light-theme' ]; // <- list all themes in this array theme = new BehaviorSubject('light-theme'); // <- initial theme constructor() { // The user's own choice, kept in localStorage by the theme toggle, is the only thing that // decides the theme. // // The OS preference used to decide it instead, and a media-query listener kept it in step with // Windows. That is what turned a page black after printing: Chrome re-evaluates // prefers-color-scheme around the print dialog, the listener fired on the way back, and the app // flipped to dark against the user's saved choice — on every screen, and only after a print, // which is exactly how it was reported. A member of staff who set the app light does not expect // printing a report to change it, and the toggle they used had no way to win against the OS. // // Nothing here reads the OS any more, so there is no listener to fire and nothing to fight the // toggle. isDarkMode() still reports the OS preference for the one caller that offers it as a // default on first use. this.darkModeOn = !!this.storedPreference(); this.setDarkMode(this.darkModeOn); } /** The saved choice, or null when the user has never set one. */ private storedPreference(): boolean | null { try { const raw = localStorage.getItem('mifosXThemeDarkEnabled'); return raw === null ? null : JSON.parse(raw); } catch { // A blocked or corrupt localStorage must not stop the app from rendering; light is the // default the rest of the stylesheet already assumes. return null; } } /** * Whether the app is currently in dark mode. * * Reports the app's own state, not the OS preference. It used to read the media query and assign * the result to darkModeOn on the way past — so merely asking the question overwrote the user's * saved choice, and setInitialDarkMode() then applied whatever the OS happened to say. */ isDarkMode(): boolean { return this.darkModeOn; } /** The OS preference, offered as a starting point when the user has never chosen for themselves. */ isSystemDarkMode(): boolean { return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); } setDarkMode(isDarkMode: boolean) { this.darkModeOn = isDarkMode; if (isDarkMode) { document.body.classList.add('dark-theme'); document.body.classList.remove('light-theme'); this.theme.next('dark-theme'); } else { document.body.classList.add('light-theme'); document.body.classList.remove('dark-theme'); this.theme.next('light-theme'); } } setInitialDarkMode(): void { this.setDarkMode(this.darkModeOn); } }