mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-5539] Migrate ThemingService (#8219)
* Update ThemingService * Finish ThemingService * Lint * More Tests & Docs * Refactor to ThemeStateService * Rename File * Fix Import * Remove `type` added to imports * Update InitServices * Fix Test * Remove Unreferenced Code * Remove Unneeded Null Check * Add Ticket Link * Add Back THEMING_DISK * Fix Desktop * Create SYSTEM_THEME_OBSERVABLE * Fix Browser Injection * Update Desktop Manual Access * Fix Default Theme * Update Test
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { fromEvent, map, merge, Observable, of, Subscription, switchMap } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "../../../services/injection-tokens";
|
||||
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class AngularThemingService implements AbstractThemingService {
|
||||
/**
|
||||
* Creates a system theme observable based on watching the given window.
|
||||
* @param window The window that should be watched for system theme changes.
|
||||
* @returns An observable that will track the system theme.
|
||||
*/
|
||||
static createSystemThemeFromWindow(window: Window): Observable<ThemeType> {
|
||||
return merge(
|
||||
// This observable should always emit at least once, so go and get the current system theme designation
|
||||
of(
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light,
|
||||
),
|
||||
// Start listening to changes
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change",
|
||||
).pipe(map((event) => (event.matches ? ThemeType.Dark : ThemeType.Light))),
|
||||
);
|
||||
}
|
||||
|
||||
readonly theme$ = this.themeStateService.selectedTheme$.pipe(
|
||||
switchMap((configuredTheme) => {
|
||||
if (configuredTheme === ThemeType.System) {
|
||||
return this.systemTheme$;
|
||||
}
|
||||
|
||||
return of(configuredTheme);
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
) {}
|
||||
|
||||
applyThemeChangesTo(document: Document): Subscription {
|
||||
return this.theme$.subscribe((theme) => {
|
||||
document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark,
|
||||
);
|
||||
document.documentElement.classList.add("theme_" + theme);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export class ThemeBuilder implements Theme {
|
||||
get effectiveTheme(): ThemeType {
|
||||
return this.configuredTheme != ThemeType.System ? this.configuredTheme : this.systemTheme;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly configuredTheme: ThemeType,
|
||||
readonly systemTheme: ThemeType,
|
||||
) {}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(this.configuredTheme, systemTheme);
|
||||
}
|
||||
|
||||
updateConfiguredTheme(configuredTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(configuredTheme, this.systemTheme);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
export interface Theme {
|
||||
configuredTheme: ThemeType;
|
||||
effectiveTheme: ThemeType;
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
/**
|
||||
* A service for managing and observing the current application theme.
|
||||
*/
|
||||
// FIXME: Rename to ThemingService
|
||||
export abstract class AbstractThemingService {
|
||||
theme$: Observable<Theme>;
|
||||
monitorThemeChanges: () => Promise<void>;
|
||||
updateSystemTheme: (systemTheme: ThemeType) => void;
|
||||
updateConfiguredTheme: (theme: ThemeType) => Promise<void>;
|
||||
/**
|
||||
* The effective theme based on the user configured choice and the current system theme if
|
||||
* the configured choice is {@link ThemeType.System}.
|
||||
*/
|
||||
theme$: Observable<ThemeType>;
|
||||
/**
|
||||
* Listens for effective theme changes and applies changes to the provided document.
|
||||
* @param document The document that should have theme classes applied to it.
|
||||
*
|
||||
* @returns A subscription that can be unsubscribed from to cancel the application of theme classes.
|
||||
*/
|
||||
applyThemeChangesTo: (document: Document) => Subscription;
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
import { ThemeBuilder } from "./theme-builder";
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
|
||||
export class ThemingService implements AbstractThemingService {
|
||||
private _theme = new BehaviorSubject<ThemeBuilder | null>(null);
|
||||
theme$: Observable<Theme> = this._theme.pipe(filter((x) => x !== null));
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private window: Window,
|
||||
private document: Document,
|
||||
) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.monitorThemeChanges();
|
||||
}
|
||||
|
||||
async monitorThemeChanges(): Promise<void> {
|
||||
this._theme.next(
|
||||
new ThemeBuilder(await this.stateService.getTheme(), await this.getSystemTheme()),
|
||||
);
|
||||
this.monitorConfiguredThemeChanges();
|
||||
this.monitorSystemThemeChanges();
|
||||
}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): void {
|
||||
this._theme.next(this._theme.getValue().updateSystemTheme(systemTheme));
|
||||
}
|
||||
|
||||
async updateConfiguredTheme(theme: ThemeType): Promise<void> {
|
||||
await this.stateService.setTheme(theme);
|
||||
this._theme.next(this._theme.getValue().updateConfiguredTheme(theme));
|
||||
}
|
||||
|
||||
protected monitorConfiguredThemeChanges(): void {
|
||||
this.theme$.subscribe((theme: Theme) => {
|
||||
this.document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark,
|
||||
);
|
||||
this.document.documentElement.classList.add("theme_" + theme.effectiveTheme);
|
||||
});
|
||||
}
|
||||
|
||||
// We use a media match query for monitoring the system theme on web and browser, but this doesn't work for electron apps on Linux.
|
||||
// In desktop we override these methods to track systemTheme with the electron renderer instead, which works for all OSs.
|
||||
protected async getSystemTheme(): Promise<ThemeType> {
|
||||
return this.window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
}
|
||||
|
||||
protected monitorSystemThemeChanges(): void {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change",
|
||||
).subscribe((event) => {
|
||||
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user