89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
}
|