[refactor] Introduce ThemingService (#2943)

* [refactor] Introduce ThemingService

* [refactor] Implement ThemingService for web

* [refactor] Implement ThemingService on browser

* [refactor] Implement ThemingService for desktop

* [refactor] Remove deprecated platformUtils.service theme methods

* [fix] Move ThemingService from libs/common to libs/angular

* [fix] Simplify ThemeBuilder's constructor

* [fix] Dont notify subscribers of null values from theme$

* [fix] Always notify PaymentComponent of theme changes
This commit is contained in:
Addison Beck 2022-06-23 04:36:05 -07:00 committed by GitHub
parent fd69e163ff
commit 57b8144013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 188 additions and 159 deletions

View File

@ -1,5 +1,6 @@
import { A11yModule } from "@angular/cdk/a11y";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { LayoutModule } from "@angular/cdk/layout";
import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CurrencyPipe, DatePipe, registerLocaleData } from "@angular/common";
@ -181,6 +182,7 @@ registerLocaleData(localeZhTw, "zh-TW");
DragDropModule,
FormsModule,
JslibModule,
LayoutModule,
OverlayModule,
ReactiveFormsModule,
ScrollingModule,

View File

@ -1,9 +1,9 @@
import { Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service";
@ -16,7 +16,8 @@ export class InitService {
private i18nService: I18nService,
private popupUtilsService: PopupUtilsService,
private stateService: StateServiceAbstraction,
private logService: LogServiceAbstraction
private logService: LogServiceAbstraction,
private themingService: AbstractThemingService
) {}
init() {
@ -32,15 +33,7 @@ export class InitService {
}
const htmlEl = window.document.documentElement;
const theme = await this.platformUtilsService.getEffectiveTheme();
htmlEl.classList.add("theme_" + theme);
this.platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => {
const bwTheme = await this.stateService.getTheme();
if (bwTheme == null || bwTheme === ThemeType.System) {
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + sysTheme);
}
});
await this.themingService.monitorThemeChanges();
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
// Workaround for slow performance on external monitors on Chrome + MacOS

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
@ -38,7 +39,8 @@ export class OptionsComponent implements OnInit {
private messagingService: MessagingService,
private stateService: StateService,
private totpService: TotpService,
i18nService: I18nService
i18nService: I18nService,
private themingService: AbstractThemingService
) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
@ -145,8 +147,7 @@ export class OptionsComponent implements OnInit {
}
async saveTheme() {
await this.stateService.setTheme(this.theme);
window.setTimeout(() => window.location.reload(), 200);
await this.themingService.updateConfiguredTheme(this.theme);
}
async saveDefaultUriMatch() {

View File

@ -2,7 +2,6 @@ import { MessagingService } from "@bitwarden/common/abstractions/messaging.servi
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ClientType } from "@bitwarden/common/enums/clientType";
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { BrowserApi } from "../browser/browserApi";
import { SafariApp } from "../browser/safariApp";
@ -17,7 +16,6 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
{ tryResolve: (canceled: boolean, password: string) => Promise<boolean>; date: Date }
>();
private deviceCache: DeviceType = null;
private prefersColorSchemeDark = window.matchMedia("(prefers-color-scheme: dark)");
constructor(
private messagingService: MessagingService,
@ -355,23 +353,4 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
supportsSecureStorage(): boolean {
return false;
}
getDefaultSystemTheme(): Promise<ThemeType.Light | ThemeType.Dark> {
return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light);
}
onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) {
this.prefersColorSchemeDark.addEventListener("change", ({ matches }) => {
callback(matches ? ThemeType.Dark : ThemeType.Light);
});
}
async getEffectiveTheme() {
const theme = (await this.stateService.getTheme()) as ThemeType;
if (theme == null || theme === ThemeType.System) {
return this.getDefaultSystemTheme();
} else {
return theme;
}
}
}

View File

@ -3,6 +3,7 @@ import { FormControl } from "@angular/forms";
import { debounceTime } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@ -74,7 +75,8 @@ export class SettingsComponent implements OnInit {
private stateService: StateService,
private messagingService: MessagingService,
private cryptoService: CryptoService,
private modalService: ModalService
private modalService: ModalService,
private themingService: AbstractThemingService
) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
@ -342,8 +344,7 @@ export class SettingsComponent implements OnInit {
}
async saveTheme() {
await this.stateService.setTheme(this.theme);
window.setTimeout(() => window.location.reload(), 200);
await this.themingService.updateConfiguredTheme(this.theme);
}
async saveMinOnCopyToClipboard() {

View File

@ -0,0 +1,18 @@
import { Injectable } from "@angular/core";
import { ipcRenderer } from "electron";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
@Injectable()
export class DesktopThemingService extends ThemingService {
protected async getSystemTheme(): Promise<ThemeType> {
return await ipcRenderer.invoke("systemTheme");
}
protected monitorSystemThemeChanges(): void {
ipcRenderer.on("systemThemeUpdated", (_event, theme: ThemeType) =>
this.updateSystemTheme(theme)
);
}
}

View File

@ -1,6 +1,7 @@
import { Inject, Injectable } from "@angular/core";
import { WINDOW } from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "@bitwarden/common/abstractions/event.service";
@ -11,7 +12,6 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstr
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/abstractions/twoFactor.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { ContainerService } from "@bitwarden/common/services/container.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vaultTimeout.service";
@ -33,7 +33,8 @@ export class InitService {
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,
private cryptoService: CryptoServiceAbstraction,
private nativeMessagingService: NativeMessagingService
private nativeMessagingService: NativeMessagingService,
private themingService: AbstractThemingService
) {}
init() {
@ -50,17 +51,7 @@ export class InitService {
setTimeout(() => this.notificationsService.init(), 3000);
const htmlEl = this.win.document.documentElement;
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
const theme = await this.platformUtilsService.getEffectiveTheme();
htmlEl.classList.add("theme_" + theme);
this.platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => {
const bwTheme = await this.stateService.getTheme();
if (bwTheme == null || bwTheme === ThemeType.System) {
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + sysTheme);
}
});
await this.themingService.monitorThemeChanges();
let installAction = null;
const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();

View File

@ -9,6 +9,7 @@ import {
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
} from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
@ -43,6 +44,7 @@ import { StateService } from "../../services/state.service";
import { LoginGuard } from "../guards/login.guard";
import { SearchBarService } from "../layout/search/search-bar.service";
import { DesktopThemingService } from "./desktop-theming.service";
import { InitService } from "./init.service";
const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
@ -129,6 +131,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
STATE_SERVICE_USE_CACHE,
],
},
{
provide: AbstractThemingService,
useClass: DesktopThemingService,
},
],
})
export class ServicesModule {}

View File

@ -1,4 +1,5 @@
import { DragDropModule } from "@angular/cdk/drag-drop";
import { LayoutModule } from "@angular/cdk/layout";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
@ -18,6 +19,7 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
ServicesModule,
InfiniteScrollModule,
DragDropModule,
LayoutModule,
OssRoutingModule,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
],

View File

@ -1,6 +1,7 @@
import { Inject, Injectable } from "@angular/core";
import { WINDOW } from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import {
EnvironmentService as EnvironmentServiceAbstraction,
@ -9,11 +10,9 @@ import {
import { EventService as EventLoggingServiceAbstraction } from "@bitwarden/common/abstractions/event.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/abstractions/twoFactor.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { ContainerService } from "@bitwarden/common/services/container.service";
import { EventService as EventLoggingService } from "@bitwarden/common/services/event.service";
import { VaultTimeoutService as VaultTimeoutService } from "@bitwarden/common/services/vaultTimeout.service";
@ -31,8 +30,8 @@ export class InitService {
private eventLoggingService: EventLoggingServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private stateService: StateServiceAbstraction,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private cryptoService: CryptoServiceAbstraction
private cryptoService: CryptoServiceAbstraction,
private themingService: AbstractThemingService
) {}
init() {
@ -44,7 +43,6 @@ export class InitService {
this.environmentService.setUrls(urls);
setTimeout(() => this.notificationsService.init(), 3000);
(this.vaultTimeoutService as VaultTimeoutService).init(true);
const locale = await this.stateService.getLocale();
await (this.i18nService as I18nService).init(locale);
@ -52,16 +50,7 @@ export class InitService {
this.twoFactorService.init();
const htmlEl = this.win.document.documentElement;
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
// Initial theme is set in index.html which must be updated if there are any changes to theming logic
this.platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => {
const bwTheme = await this.stateService.getTheme();
if (bwTheme === ThemeType.System) {
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + sysTheme);
}
});
await this.themingService.monitorThemeChanges();
const containerService = new ContainerService(this.cryptoService);
containerService.attachToWindow(this.win);
};

View File

@ -1,8 +1,9 @@
import { Component, Input, OnInit } from "@angular/core";
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
@ -17,7 +18,7 @@ const darkInputPlaceholderColor = ThemeVariables.darkInputPlaceholderColor;
selector: "app-payment",
templateUrl: "payment.component.html",
})
export class PaymentComponent implements OnInit {
export class PaymentComponent implements OnInit, OnDestroy {
@Input() showMethods = true;
@Input() showOptions = true;
@Input() method = PaymentMethodType.Card;
@ -25,6 +26,8 @@ export class PaymentComponent implements OnInit {
@Input() hidePaypal = false;
@Input() hideCredit = false;
private destroy$: Subject<void> = new Subject<void>();
bank: any = {
routing_number: null,
account_number: null,
@ -48,9 +51,9 @@ export class PaymentComponent implements OnInit {
private StripeElementClasses: any;
constructor(
private platformUtilsService: PlatformUtilsService,
private apiService: ApiService,
private logService: LogService
private logService: LogService,
private themingService: AbstractThemingService
) {
this.stripeScript = window.document.createElement("script");
this.stripeScript.src = "https://js.stripe.com/v3/";
@ -100,6 +103,8 @@ export class PaymentComponent implements OnInit {
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
window.document.head.removeChild(this.stripeScript);
window.setTimeout(() => {
Array.from(window.document.querySelectorAll("iframe")).forEach((el) => {
@ -275,15 +280,16 @@ export class PaymentComponent implements OnInit {
}
private async setTheme() {
const theme = await this.platformUtilsService.getEffectiveTheme();
if (theme === ThemeType.Dark) {
this.StripeElementStyle.base.color = darkInputColor;
this.StripeElementStyle.base["::placeholder"].color = darkInputPlaceholderColor;
this.StripeElementStyle.invalid.color = darkInputColor;
} else {
this.StripeElementStyle.base.color = lightInputColor;
this.StripeElementStyle.base["::placeholder"].color = lightInputPlaceholderColor;
this.StripeElementStyle.invalid.color = lightInputColor;
}
this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe((theme) => {
if (theme.effectiveTheme === ThemeType.Dark) {
this.StripeElementStyle.base.color = darkInputColor;
this.StripeElementStyle.base["::placeholder"].color = darkInputPlaceholderColor;
this.StripeElementStyle.invalid.color = darkInputColor;
} else {
this.StripeElementStyle.base.color = lightInputColor;
this.StripeElementStyle.base["::placeholder"].color = lightInputPlaceholderColor;
this.StripeElementStyle.invalid.color = lightInputColor;
}
});
}
}

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@ -34,7 +35,8 @@ export class PreferencesComponent implements OnInit {
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService
private messagingService: MessagingService,
private themingService: AbstractThemingService
) {
this.vaultTimeouts = [
{ name: i18nService.t("oneMinute"), value: 1 },
@ -100,12 +102,8 @@ export class PreferencesComponent implements OnInit {
await this.stateService.setEnableFullWidth(this.enableFullWidth);
this.messagingService.send("setFullWidth");
if (this.theme !== this.startingTheme) {
await this.stateService.setTheme(this.theme);
await this.themingService.updateConfiguredTheme(this.theme);
this.startingTheme = this.theme;
const effectiveTheme = await this.platformUtilsService.getEffectiveTheme();
const htmlEl = window.document.documentElement;
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + effectiveTheme);
}
await this.stateService.setLocale(this.locale);
if (this.locale !== this.startingLocale) {

View File

@ -5,21 +5,17 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ClientType } from "@bitwarden/common/enums/clientType";
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
@Injectable()
export class WebPlatformUtilsService implements PlatformUtilsService {
private browserCache: DeviceType = null;
private prefersColorSchemeDark = window.matchMedia("(prefers-color-scheme: dark)");
constructor(
private i18nService: I18nService,
private messagingService: MessagingService,
private logService: LogService,
private stateService: StateService
private logService: LogService
) {}
getDevice(): DeviceType {
@ -303,32 +299,4 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
supportsSecureStorage() {
return false;
}
getDefaultSystemTheme(): Promise<ThemeType.Light | ThemeType.Dark> {
return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light);
}
async getEffectiveTheme(): Promise<ThemeType.Light | ThemeType.Dark> {
const theme = await this.stateService.getTheme();
if (theme === ThemeType.Dark) {
return ThemeType.Dark;
} else if (theme === ThemeType.System) {
return this.getDefaultSystemTheme();
} else {
return ThemeType.Light;
}
}
onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) {
try {
this.prefersColorSchemeDark.addEventListener("change", ({ matches }) => {
callback(matches ? ThemeType.Dark : ThemeType.Light);
});
} catch (e) {
// Safari older than v14
this.prefersColorSchemeDark.addListener((ev) => {
callback(ev.matches ? ThemeType.Dark : ThemeType.Light);
});
}
}
}

View File

@ -1,5 +1,7 @@
import { InjectionToken, Injector, LOCALE_ID, NgModule } from "@angular/core";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -423,6 +425,10 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
},
{
provide: AbstractThemingService,
useClass: ThemingService,
},
],
})
export class JslibServicesModule {}

View File

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

View File

@ -0,0 +1,19 @@
import { ThemeType } from "@bitwarden/common/enums/themeType";
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

@ -0,0 +1,12 @@
import { Observable } from "rxjs";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { Theme } from "./theme";
export abstract class AbstractThemingService {
theme$: Observable<Theme>;
monitorThemeChanges: () => Promise<void>;
updateSystemTheme: (systemTheme: ThemeType) => void;
updateConfiguredTheme: (theme: ThemeType) => Promise<void>;
}

View File

@ -0,0 +1,71 @@
import { MediaMatcher } from "@angular/cdk/layout";
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { Theme } from "./theme";
import { ThemeBuilder } from "./themeBuilder";
import { AbstractThemingService } from "./theming.service.abstraction";
@Injectable()
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 mediaMatcher: MediaMatcher,
@Inject(DOCUMENT) private document: Document
) {
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.mediaMatcher.matchMedia("(prefers-color-scheme: dark)").matches
? ThemeType.Dark
: ThemeType.Light;
}
protected monitorSystemThemeChanges(): void {
fromEvent<MediaQueryListEvent>(
this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)"),
"change"
).subscribe((event) => {
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
});
}
}

View File

@ -1,6 +1,5 @@
import { ClientType } from "../enums/clientType";
import { DeviceType } from "../enums/deviceType";
import { ThemeType } from "../enums/themeType";
interface ToastOptions {
timeout?: number;
@ -43,10 +42,5 @@ export abstract class PlatformUtilsService {
readFromClipboard: (options?: any) => Promise<string>;
supportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
getDefaultSystemTheme: () => Promise<ThemeType.Light | ThemeType.Dark>;
onDefaultSystemThemeChange: (
callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown
) => unknown;
getEffectiveTheme: () => Promise<ThemeType>;
supportsSecureStorage: () => boolean;
}

View File

@ -6,7 +6,6 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ClientType } from "@bitwarden/common/enums/clientType";
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import { isDev, isMacAppStore } from "../utils";
@ -189,25 +188,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
});
}
getDefaultSystemTheme() {
return ipcRenderer.invoke("systemTheme");
}
onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) {
ipcRenderer.on("systemThemeUpdated", (event, theme: ThemeType.Light | ThemeType.Dark) =>
callback(theme)
);
}
async getEffectiveTheme() {
const theme = await this.stateService.getTheme();
if (theme == null || theme === ThemeType.System) {
return this.getDefaultSystemTheme();
} else {
return theme;
}
}
supportsSecureStorage(): boolean {
return true;
}

View File

@ -3,7 +3,6 @@ import * as child_process from "child_process";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ClientType } from "@bitwarden/common/enums/clientType";
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
// eslint-disable-next-line
const open = require("open");
@ -148,18 +147,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(false);
}
getDefaultSystemTheme() {
return Promise.resolve(ThemeType.Light as ThemeType.Light | ThemeType.Dark);
}
onDefaultSystemThemeChange() {
/* noop */
}
getEffectiveTheme() {
return Promise.resolve(ThemeType.Light);
}
supportsSecureStorage(): boolean {
return false;
}