From 57b814401309f524140a0c79c6a282fbfc43f355 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 23 Jun 2022 04:36:05 -0700 Subject: [PATCH] [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 --- apps/browser/src/popup/app.module.ts | 2 + .../src/popup/services/init.service.ts | 15 ++-- .../src/popup/settings/options.component.ts | 7 +- .../services/browserPlatformUtils.service.ts | 21 ------ .../src/app/accounts/settings.component.ts | 7 +- .../app/services/desktop-theming.service.ts | 18 +++++ apps/desktop/src/app/services/init.service.ts | 17 ++--- .../src/app/services/services.module.ts | 6 ++ apps/web/src/app/app.module.ts | 2 + apps/web/src/app/services/init.service.ts | 19 ++--- .../web/src/app/settings/payment.component.ts | 36 ++++++---- .../src/app/settings/preferences.component.ts | 10 ++- .../src/services/webPlatformUtils.service.ts | 34 +-------- .../src/services/jslib-services.module.ts | 6 ++ libs/angular/src/services/theming/theme.ts | 6 ++ .../src/services/theming/themeBuilder.ts | 19 +++++ .../theming/theming.service.abstraction.ts | 12 ++++ .../src/services/theming/theming.service.ts | 71 +++++++++++++++++++ .../src/abstractions/platformUtils.service.ts | 6 -- .../services/electronPlatformUtils.service.ts | 20 ------ .../cli/services/cliPlatformUtils.service.ts | 13 ---- 21 files changed, 188 insertions(+), 159 deletions(-) create mode 100644 apps/desktop/src/app/services/desktop-theming.service.ts create mode 100644 libs/angular/src/services/theming/theme.ts create mode 100644 libs/angular/src/services/theming/themeBuilder.ts create mode 100644 libs/angular/src/services/theming/theming.service.abstraction.ts create mode 100644 libs/angular/src/services/theming/theming.service.ts diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 343ad24a23..ae8bef7264 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -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, diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index e95a2e154c..a73792cc10 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -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 diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index c85bcdc377..e29d224f63 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -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() { diff --git a/apps/browser/src/services/browserPlatformUtils.service.ts b/apps/browser/src/services/browserPlatformUtils.service.ts index 415a6a5ccf..42784af40e 100644 --- a/apps/browser/src/services/browserPlatformUtils.service.ts +++ b/apps/browser/src/services/browserPlatformUtils.service.ts @@ -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; 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 { - 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; - } - } } diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 79396679c7..d8ef021155 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -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() { diff --git a/apps/desktop/src/app/services/desktop-theming.service.ts b/apps/desktop/src/app/services/desktop-theming.service.ts new file mode 100644 index 0000000000..812b8935bb --- /dev/null +++ b/apps/desktop/src/app/services/desktop-theming.service.ts @@ -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 { + return await ipcRenderer.invoke("systemTheme"); + } + + protected monitorSystemThemeChanges(): void { + ipcRenderer.on("systemThemeUpdated", (_event, theme: ThemeType) => + this.updateSystemTheme(theme) + ); + } +} diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 5aaacabd19..e99c47823d 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -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(); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 78e9efbdf2..2079fc64c9 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -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 {} diff --git a/apps/web/src/app/app.module.ts b/apps/web/src/app/app.module.ts index 15d5a1900f..b23c4d182e 100644 --- a/apps/web/src/app/app.module.ts +++ b/apps/web/src/app/app.module.ts @@ -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 ], diff --git a/apps/web/src/app/services/init.service.ts b/apps/web/src/app/services/init.service.ts index 1a9178b28e..047a28da18 100644 --- a/apps/web/src/app/services/init.service.ts +++ b/apps/web/src/app/services/init.service.ts @@ -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); }; diff --git a/apps/web/src/app/settings/payment.component.ts b/apps/web/src/app/settings/payment.component.ts index 075463357a..479671e561 100644 --- a/apps/web/src/app/settings/payment.component.ts +++ b/apps/web/src/app/settings/payment.component.ts @@ -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 = new Subject(); + 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; + } + }); } } diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index f02ed55fdb..53c68fa12e 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -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) { diff --git a/apps/web/src/services/webPlatformUtils.service.ts b/apps/web/src/services/webPlatformUtils.service.ts index 07f424b676..40591841ee 100644 --- a/apps/web/src/services/webPlatformUtils.service.ts +++ b/apps/web/src/services/webPlatformUtils.service.ts @@ -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 { - return Promise.resolve(this.prefersColorSchemeDark.matches ? ThemeType.Dark : ThemeType.Light); - } - - async getEffectiveTheme(): Promise { - 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); - }); - } - } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9062d86c04..fba3678ff6 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -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("SYSTEM_LANGUAGE"); useClass: TwoFactorService, deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }, + { + provide: AbstractThemingService, + useClass: ThemingService, + }, ], }) export class JslibServicesModule {} diff --git a/libs/angular/src/services/theming/theme.ts b/libs/angular/src/services/theming/theme.ts new file mode 100644 index 0000000000..91311ac3b3 --- /dev/null +++ b/libs/angular/src/services/theming/theme.ts @@ -0,0 +1,6 @@ +import { ThemeType } from "@bitwarden/common/enums/themeType"; + +export interface Theme { + configuredTheme: ThemeType; + effectiveTheme: ThemeType; +} diff --git a/libs/angular/src/services/theming/themeBuilder.ts b/libs/angular/src/services/theming/themeBuilder.ts new file mode 100644 index 0000000000..87b294032d --- /dev/null +++ b/libs/angular/src/services/theming/themeBuilder.ts @@ -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); + } +} diff --git a/libs/angular/src/services/theming/theming.service.abstraction.ts b/libs/angular/src/services/theming/theming.service.abstraction.ts new file mode 100644 index 0000000000..cacb3a10f2 --- /dev/null +++ b/libs/angular/src/services/theming/theming.service.abstraction.ts @@ -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; + monitorThemeChanges: () => Promise; + updateSystemTheme: (systemTheme: ThemeType) => void; + updateConfiguredTheme: (theme: ThemeType) => Promise; +} diff --git a/libs/angular/src/services/theming/theming.service.ts b/libs/angular/src/services/theming/theming.service.ts new file mode 100644 index 0000000000..7d67503694 --- /dev/null +++ b/libs/angular/src/services/theming/theming.service.ts @@ -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(null); + theme$: Observable = this._theme.pipe(filter((x) => x !== null)); + + constructor( + private stateService: StateService, + private mediaMatcher: MediaMatcher, + @Inject(DOCUMENT) private document: Document + ) { + this.monitorThemeChanges(); + } + + async monitorThemeChanges(): Promise { + 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 { + 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 { + return this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)").matches + ? ThemeType.Dark + : ThemeType.Light; + } + + protected monitorSystemThemeChanges(): void { + fromEvent( + this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)"), + "change" + ).subscribe((event) => { + this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light); + }); + } +} diff --git a/libs/common/src/abstractions/platformUtils.service.ts b/libs/common/src/abstractions/platformUtils.service.ts index 4a014868d5..f27be9664a 100644 --- a/libs/common/src/abstractions/platformUtils.service.ts +++ b/libs/common/src/abstractions/platformUtils.service.ts @@ -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; supportsBiometric: () => Promise; authenticateBiometric: () => Promise; - getDefaultSystemTheme: () => Promise; - onDefaultSystemThemeChange: ( - callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown - ) => unknown; - getEffectiveTheme: () => Promise; supportsSecureStorage: () => boolean; } diff --git a/libs/electron/src/services/electronPlatformUtils.service.ts b/libs/electron/src/services/electronPlatformUtils.service.ts index a07e354aed..8a9ddbf5e3 100644 --- a/libs/electron/src/services/electronPlatformUtils.service.ts +++ b/libs/electron/src/services/electronPlatformUtils.service.ts @@ -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; } diff --git a/libs/node/src/cli/services/cliPlatformUtils.service.ts b/libs/node/src/cli/services/cliPlatformUtils.service.ts index 936e62d87e..c72cf6c08f 100644 --- a/libs/node/src/cli/services/cliPlatformUtils.service.ts +++ b/libs/node/src/cli/services/cliPlatformUtils.service.ts @@ -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; }