1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 22:33: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:
Justin Baur
2024-03-13 10:25:39 -05:00
committed by GitHub
parent 531ae3184f
commit e6fe0d1d13
38 changed files with 396 additions and 222 deletions

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}

View File

@@ -1,6 +0,0 @@
import { ThemeType } from "@bitwarden/common/platform/enums";
export interface Theme {
configuredTheme: ThemeType;
effectiveTheme: ThemeType;
}

View File

@@ -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;
}

View File

@@ -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);
});
}
}

View File

@@ -1,10 +1,12 @@
import { InjectionToken } from "@angular/core";
import { Observable } from "rxjs";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
declare const tag: unique symbol;
@@ -43,3 +45,6 @@ export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promi
export const LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
"SYSTEM_THEME_OBSERVABLE",
);

View File

@@ -1,4 +1,3 @@
import { DOCUMENT } from "@angular/common";
import { LOCALE_ID, NgModule } from "@angular/core";
import { UnwrapOpaque } from "type-fest";
@@ -160,6 +159,10 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
/* eslint-enable import/no-restricted-paths */
import {
DefaultThemeStateService,
ThemeStateService,
} from "@bitwarden/common/platform/theming/theme-state.service";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -231,7 +234,7 @@ import { UnauthGuard } from "../auth/guards/unauth.guard";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { BroadcasterService } from "../platform/services/broadcaster.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { ThemingService } from "../platform/services/theming/theming.service";
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
@@ -248,6 +251,7 @@ import {
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
SYSTEM_LANGUAGE,
SYSTEM_THEME_OBSERVABLE,
WINDOW,
} from "./injection-tokens";
import { ModalService } from "./modal.service";
@@ -300,6 +304,21 @@ const typesafeProviders: Array<SafeProvider> = [
provide: LOG_MAC_FAILURES,
useValue: true,
}),
safeProvider({
provide: SYSTEM_THEME_OBSERVABLE,
useFactory: (window: Window) => AngularThemingService.createSystemThemeFromWindow(window),
deps: [WINDOW],
}),
safeProvider({
provide: ThemeStateService,
useClass: DefaultThemeStateService,
deps: [GlobalStateProvider],
}),
safeProvider({
provide: AbstractThemingService,
useClass: AngularThemingService,
deps: [ThemeStateService, SYSTEM_THEME_OBSERVABLE],
}),
safeProvider({
provide: AppIdServiceAbstraction,
useClass: AppIdService,
@@ -772,11 +791,6 @@ const typesafeProviders: Array<SafeProvider> = [
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}),
safeProvider({
provide: AbstractThemingService,
useClass: ThemingService,
deps: [StateServiceAbstraction, WINDOW, DOCUMENT as SafeInjectionToken<Document>],
}),
safeProvider({
provide: FormValidationErrorsServiceAbstraction,
useClass: FormValidationErrorsService,