[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:
parent
531ae3184f
commit
e6fe0d1d13
|
@ -18,7 +18,6 @@ apps/desktop/src/auth/scripts/duo.js
|
||||||
|
|
||||||
apps/web/config.js
|
apps/web/config.js
|
||||||
apps/web/scripts/*.js
|
apps/web/scripts/*.js
|
||||||
apps/web/src/theme.js
|
|
||||||
apps/web/tailwind.config.js
|
apps/web/tailwind.config.js
|
||||||
|
|
||||||
apps/cli/config/config.js
|
apps/cli/config/config.js
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai
|
||||||
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
@ -51,6 +52,7 @@ describe("NotificationBackground", () => {
|
||||||
const domainSettingsService = mock<DomainSettingsService>();
|
const domainSettingsService = mock<DomainSettingsService>();
|
||||||
const environmentService = mock<EnvironmentService>();
|
const environmentService = mock<EnvironmentService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
|
const themeStateService = mock<ThemeStateService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
notificationBackground = new NotificationBackground(
|
notificationBackground = new NotificationBackground(
|
||||||
|
@ -64,6 +66,7 @@ describe("NotificationBackground", () => {
|
||||||
domainSettingsService,
|
domainSettingsService,
|
||||||
environmentService,
|
environmentService,
|
||||||
logService,
|
logService,
|
||||||
|
themeStateService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
@ -77,6 +78,7 @@ export default class NotificationBackground {
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private themeStateService: ThemeStateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
@ -165,7 +167,7 @@ export default class NotificationBackground {
|
||||||
const notificationType = notificationQueueMessage.type;
|
const notificationType = notificationQueueMessage.type;
|
||||||
const typeData: Record<string, any> = {
|
const typeData: Record<string, any> = {
|
||||||
isVaultLocked: notificationQueueMessage.wasVaultLocked,
|
isVaultLocked: notificationQueueMessage.wasVaultLocked,
|
||||||
theme: await this.stateService.getTheme(),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (notificationType) {
|
switch (notificationType) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { mock, mockReset } from "jest-mock-extended";
|
import { mock, mockReset } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
|
@ -10,6 +11,7 @@ import { AutofillSettingsService } from "@bitwarden/common/autofill/services/aut
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
@ -53,6 +55,7 @@ describe("OverlayBackground", () => {
|
||||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
const platformUtilsService = mock<BrowserPlatformUtilsService>();
|
const platformUtilsService = mock<BrowserPlatformUtilsService>();
|
||||||
|
const themeStateService = mock<ThemeStateService>();
|
||||||
const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => {
|
const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => {
|
||||||
const { initList, initButton } = options;
|
const { initList, initButton } = options;
|
||||||
if (initButton) {
|
if (initButton) {
|
||||||
|
@ -79,12 +82,15 @@ describe("OverlayBackground", () => {
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
i18nService,
|
i18nService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
|
themeStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(overlayBackground as any, "getOverlayVisibility")
|
.spyOn(overlayBackground as any, "getOverlayVisibility")
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
|
||||||
|
themeStateService.selectedTheme$ = of(ThemeType.Light);
|
||||||
|
|
||||||
void overlayBackground.init();
|
void overlayBackground.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -993,7 +999,7 @@ describe("OverlayBackground", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets the system theme", async () => {
|
it("gets the system theme", async () => {
|
||||||
jest.spyOn(overlayBackground["stateService"], "getTheme").mockResolvedValue(ThemeType.System);
|
themeStateService.selectedTheme$ = of(ThemeType.System);
|
||||||
|
|
||||||
await initOverlayElementPorts({ initList: true, initButton: false });
|
await initOverlayElementPorts({ initList: true, initButton: false });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||||
|
@ -96,6 +97,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private themeStateService: ThemeStateService,
|
||||||
) {
|
) {
|
||||||
this.iconsServerUrl = this.environmentService.getIconsUrl();
|
this.iconsServerUrl = this.environmentService.getIconsUrl();
|
||||||
}
|
}
|
||||||
|
@ -695,7 +697,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||||
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
|
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
|
||||||
authStatus: await this.getAuthStatus(),
|
authStatus: await this.getAuthStatus(),
|
||||||
styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
|
styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
|
||||||
theme: await this.stateService.getTheme(),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
translations: this.getTranslations(),
|
translations: this.getTranslations(),
|
||||||
ciphers: isOverlayListPort ? this.getOverlayCipherData() : null,
|
ciphers: isOverlayListPort ? this.getOverlayCipherData() : null,
|
||||||
});
|
});
|
||||||
|
|
|
@ -116,6 +116,7 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
|
||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
|
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||||
|
@ -450,6 +451,9 @@ export default class MainBackground {
|
||||||
async () => this.biometricUnlock(),
|
async () => this.biometricUnlock(),
|
||||||
self,
|
self,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
|
||||||
|
|
||||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||||
this.cryptoService = new BrowserCryptoService(
|
this.cryptoService = new BrowserCryptoService(
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
|
@ -858,6 +862,7 @@ export default class MainBackground {
|
||||||
this.domainSettingsService,
|
this.domainSettingsService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
themeStateService,
|
||||||
);
|
);
|
||||||
this.overlayBackground = new OverlayBackground(
|
this.overlayBackground = new OverlayBackground(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
|
@ -869,6 +874,7 @@ export default class MainBackground {
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
themeStateService,
|
||||||
);
|
);
|
||||||
this.filelessImporterBackground = new FilelessImporterBackground(
|
this.filelessImporterBackground = new FilelessImporterBackground(
|
||||||
this.configService,
|
this.configService,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from "@angular/core";
|
import { DOCUMENT } from "@angular/common";
|
||||||
|
import { Inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -19,6 +20,7 @@ export class InitService {
|
||||||
private logService: LogServiceAbstraction,
|
private logService: LogServiceAbstraction,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -34,7 +36,7 @@ export class InitService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlEl = window.document.documentElement;
|
const htmlEl = window.document.documentElement;
|
||||||
await this.themingService.monitorThemeChanges();
|
this.themingService.applyThemeChangesTo(this.document);
|
||||||
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
||||||
|
|
||||||
// Workaround for slow performance on external monitors on Chrome + MacOS
|
// Workaround for slow performance on external monitors on Chrome + MacOS
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { DomSanitizer } from "@angular/platform-browser";
|
||||||
import { ToastrService } from "ngx-toastr";
|
import { ToastrService } from "ngx-toastr";
|
||||||
|
|
||||||
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
|
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
|
||||||
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
|
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
|
||||||
import {
|
import {
|
||||||
MEMORY_STORAGE,
|
MEMORY_STORAGE,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
OBSERVABLE_DISK_STORAGE,
|
OBSERVABLE_DISK_STORAGE,
|
||||||
OBSERVABLE_MEMORY_STORAGE,
|
OBSERVABLE_MEMORY_STORAGE,
|
||||||
|
SYSTEM_THEME_OBSERVABLE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import {
|
import {
|
||||||
|
@ -488,11 +488,8 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AbstractThemingService,
|
provide: SYSTEM_THEME_OBSERVABLE,
|
||||||
useFactory: (
|
useFactory: (platformUtilsService: PlatformUtilsService) => {
|
||||||
stateService: StateServiceAbstraction,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
) => {
|
|
||||||
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
|
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
|
||||||
// In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
|
// In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
|
||||||
let windowContext = window;
|
let windowContext = window;
|
||||||
|
@ -501,9 +498,9 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||||
windowContext = backgroundWindow;
|
windowContext = backgroundWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ThemingService(stateService, windowContext, document);
|
return AngularThemingService.createSystemThemeFromWindow(windowContext);
|
||||||
},
|
},
|
||||||
deps: [StateServiceAbstraction, PlatformUtilsService],
|
deps: [PlatformUtilsService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ConfigService,
|
provide: ConfigService,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
@ -16,6 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../platform/flags";
|
||||||
|
@ -57,7 +57,7 @@ export class OptionsComponent implements OnInit {
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
private themingService: AbstractThemingService,
|
private themeStateService: ThemeStateService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
) {
|
) {
|
||||||
|
@ -125,7 +125,7 @@ export class OptionsComponent implements OnInit {
|
||||||
|
|
||||||
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
||||||
|
|
||||||
this.theme = await this.stateService.getTheme();
|
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||||
|
|
||||||
const defaultUriMatch = await firstValueFrom(
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.domainSettingsService.defaultUriMatchStrategy$,
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
|
@ -186,7 +186,7 @@ export class OptionsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTheme() {
|
async saveTheme() {
|
||||||
await this.themingService.updateConfiguredTheme(this.theme);
|
await this.themeStateService.setSelectedTheme(this.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveClearClipboard() {
|
async saveClearClipboard() {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { FormBuilder } from "@angular/forms";
|
||||||
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
|
||||||
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
@ -21,6 +20,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||||
|
@ -116,7 +116,7 @@ export class SettingsComponent implements OnInit {
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private themingService: AbstractThemingService,
|
private themeStateService: ThemeStateService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationServiceAbstraction,
|
private userVerificationService: UserVerificationServiceAbstraction,
|
||||||
|
@ -263,7 +263,7 @@ export class SettingsComponent implements OnInit {
|
||||||
await this.stateService.getEnableBrowserIntegrationFingerprint(),
|
await this.stateService.getEnableBrowserIntegrationFingerprint(),
|
||||||
enableDuckDuckGoBrowserIntegration:
|
enableDuckDuckGoBrowserIntegration:
|
||||||
await this.stateService.getEnableDuckDuckGoBrowserIntegration(),
|
await this.stateService.getEnableDuckDuckGoBrowserIntegration(),
|
||||||
theme: await this.stateService.getTheme(),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
locale: await firstValueFrom(this.i18nService.locale$),
|
locale: await firstValueFrom(this.i18nService.locale$),
|
||||||
};
|
};
|
||||||
this.form.setValue(initialValues, { emitEvent: false });
|
this.form.setValue(initialValues, { emitEvent: false });
|
||||||
|
@ -557,7 +557,7 @@ export class SettingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTheme() {
|
async saveTheme() {
|
||||||
await this.themingService.updateConfiguredTheme(this.form.value.theme);
|
await this.themeStateService.setSelectedTheme(this.form.value.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveMinOnCopyToClipboard() {
|
async saveMinOnCopyToClipboard() {
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
|
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
|
||||||
|
|
||||||
export class DesktopThemingService extends ThemingService {
|
|
||||||
protected async getSystemTheme(): Promise<ThemeType> {
|
|
||||||
return await ipc.platform.getSystemTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected monitorSystemThemeChanges(): void {
|
|
||||||
ipc.platform.onSystemThemeUpdated((theme: ThemeType) => this.updateSystemTheme(theme));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DOCUMENT } from "@angular/common";
|
||||||
import { Inject, Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||||
|
@ -38,6 +39,7 @@ export class InitService {
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -58,7 +60,7 @@ export class InitService {
|
||||||
setTimeout(() => this.notificationsService.init(), 3000);
|
setTimeout(() => this.notificationsService.init(), 3000);
|
||||||
const htmlEl = this.win.document.documentElement;
|
const htmlEl = this.win.document.documentElement;
|
||||||
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
|
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
|
||||||
await this.themingService.monitorThemeChanges();
|
this.themingService.applyThemeChangesTo(this.document);
|
||||||
let installAction = null;
|
let installAction = null;
|
||||||
const installedVersion = await this.stateService.getInstalledVersion();
|
const installedVersion = await this.stateService.getInstalledVersion();
|
||||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import { DOCUMENT } from "@angular/common";
|
|
||||||
import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core";
|
import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
|
||||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
|
||||||
import {
|
import {
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
|
@ -13,7 +10,7 @@ import {
|
||||||
OBSERVABLE_MEMORY_STORAGE,
|
OBSERVABLE_MEMORY_STORAGE,
|
||||||
OBSERVABLE_DISK_STORAGE,
|
OBSERVABLE_DISK_STORAGE,
|
||||||
WINDOW,
|
WINDOW,
|
||||||
SafeInjectionToken,
|
SYSTEM_THEME_OBSERVABLE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
@ -63,13 +60,13 @@ import { ElectronRendererSecureStorageService } from "../../platform/services/el
|
||||||
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
||||||
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
|
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
||||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||||
import { SearchBarService } from "../layout/search/search-bar.service";
|
import { SearchBarService } from "../layout/search/search-bar.service";
|
||||||
|
|
||||||
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
||||||
import { DesktopThemingService } from "./desktop-theming.service";
|
|
||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
||||||
|
|
||||||
|
@ -151,11 +148,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
||||||
provide: FileDownloadService,
|
provide: FileDownloadService,
|
||||||
useClass: DesktopFileDownloadService,
|
useClass: DesktopFileDownloadService,
|
||||||
},
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: AbstractThemingService,
|
provide: SYSTEM_THEME_OBSERVABLE,
|
||||||
useClass: DesktopThemingService,
|
useFactory: () => fromIpcSystemTheme(),
|
||||||
deps: [StateServiceAbstraction, WINDOW, DOCUMENT as SafeInjectionToken<Document>],
|
},
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
provide: EncryptedMessageHandlerService,
|
provide: EncryptedMessageHandlerService,
|
||||||
deps: [
|
deps: [
|
||||||
|
|
|
@ -246,8 +246,7 @@ export class WindowMain {
|
||||||
// Retrieve the background color
|
// Retrieve the background color
|
||||||
// Resolves background color missmatch when starting the application.
|
// Resolves background color missmatch when starting the application.
|
||||||
async getBackgroundColor(): Promise<string> {
|
async getBackgroundColor(): Promise<string> {
|
||||||
const data: { theme?: string } = await this.storageService.get("global");
|
let theme = await this.storageService.get("global_theming_selection");
|
||||||
let theme = data?.theme;
|
|
||||||
|
|
||||||
if (theme == null || theme === "system") {
|
if (theme == null || theme === "system") {
|
||||||
theme = nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
theme = nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { defer, fromEventPattern, merge } from "rxjs";
|
||||||
|
|
||||||
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns An observable watching the system theme via IPC channels
|
||||||
|
*/
|
||||||
|
export const fromIpcSystemTheme = () => {
|
||||||
|
return merge(
|
||||||
|
defer(() => ipc.platform.getSystemTheme()),
|
||||||
|
fromEventPattern<ThemeType>((handler) =>
|
||||||
|
ipc.platform.onSystemThemeUpdated((theme) => handler(theme)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -23,6 +23,7 @@ import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/comm
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
|
@ -32,6 +33,10 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
|
||||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
|
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
|
||||||
|
import {
|
||||||
|
DefaultThemeStateService,
|
||||||
|
ThemeStateService,
|
||||||
|
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
|
@ -133,6 +138,13 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ThemeStateService,
|
||||||
|
useFactory: (globalStateProvider: GlobalStateProvider) =>
|
||||||
|
// Web chooses to have Light as the default theme
|
||||||
|
new DefaultThemeStateService(globalStateProvider, ThemeType.Light),
|
||||||
|
deps: [GlobalStateProvider],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DOCUMENT } from "@angular/common";
|
||||||
import { Inject, Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||||
|
@ -35,6 +36,7 @@ export class InitService {
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -55,7 +57,7 @@ export class InitService {
|
||||||
this.twoFactorService.init();
|
this.twoFactorService.init();
|
||||||
const htmlEl = this.win.document.documentElement;
|
const htmlEl = this.win.document.documentElement;
|
||||||
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
||||||
await this.themingService.monitorThemeChanges();
|
this.themingService.applyThemeChangesTo(this.document);
|
||||||
const containerService = new ContainerService(this.cryptoService, this.encryptService);
|
const containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||||
containerService.attachToGlobal(this.win);
|
containerService.attachToGlobal(this.win);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
|
||||||
import { GlobalState as BaseGlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState as BaseGlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
|
|
||||||
export class GlobalState extends BaseGlobalState {
|
export class GlobalState extends BaseGlobalState {
|
||||||
theme?: ThemeType = ThemeType.Light;
|
|
||||||
rememberEmail = true;
|
rememberEmail = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs";
|
import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
@ -14,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -35,7 +35,6 @@ export class PreferencesComponent implements OnInit {
|
||||||
themeOptions: any[];
|
themeOptions: any[];
|
||||||
|
|
||||||
private startingLocale: string;
|
private startingLocale: string;
|
||||||
private startingTheme: ThemeType;
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
form = this.formBuilder.group({
|
form = this.formBuilder.group({
|
||||||
|
@ -54,7 +53,7 @@ export class PreferencesComponent implements OnInit {
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private themingService: AbstractThemingService,
|
private themeStateService: ThemeStateService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
) {
|
) {
|
||||||
|
@ -141,11 +140,10 @@ export class PreferencesComponent implements OnInit {
|
||||||
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
|
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
|
||||||
),
|
),
|
||||||
enableFavicons: !(await this.settingsService.getDisableFavicon()),
|
enableFavicons: !(await this.settingsService.getDisableFavicon()),
|
||||||
theme: await this.stateService.getTheme(),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
locale: (await firstValueFrom(this.i18nService.locale$)) ?? null,
|
locale: (await firstValueFrom(this.i18nService.locale$)) ?? null,
|
||||||
};
|
};
|
||||||
this.startingLocale = initialFormValues.locale;
|
this.startingLocale = initialFormValues.locale;
|
||||||
this.startingTheme = initialFormValues.theme;
|
|
||||||
this.form.setValue(initialFormValues, { emitEvent: false });
|
this.form.setValue(initialFormValues, { emitEvent: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,10 +163,7 @@ export class PreferencesComponent implements OnInit {
|
||||||
values.vaultTimeoutAction,
|
values.vaultTimeoutAction,
|
||||||
);
|
);
|
||||||
await this.settingsService.setDisableFavicon(!values.enableFavicons);
|
await this.settingsService.setDisableFavicon(!values.enableFavicons);
|
||||||
if (values.theme !== this.startingTheme) {
|
await this.themeStateService.setSelectedTheme(values.theme);
|
||||||
await this.themingService.updateConfiguredTheme(values.theme);
|
|
||||||
this.startingTheme = values.theme;
|
|
||||||
}
|
|
||||||
await this.i18nService.setLocale(values.locale);
|
await this.i18nService.setLocale(values.locale);
|
||||||
if (values.locale !== this.startingLocale) {
|
if (values.locale !== this.startingLocale) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
// Set theme on page load
|
|
||||||
// This is done outside the Angular app to avoid a flash of unthemed content before it loads
|
|
||||||
// The defaultTheme is also set in the html itself to make sure that some theming is always applied
|
|
||||||
(function () {
|
|
||||||
const defaultTheme = "light";
|
|
||||||
const htmlEl = document.documentElement;
|
|
||||||
let theme = defaultTheme;
|
|
||||||
|
|
||||||
const globalState = window.localStorage.getItem("global");
|
|
||||||
if (globalState != null) {
|
|
||||||
const globalStateJson = JSON.parse(globalState);
|
|
||||||
if (globalStateJson != null && globalStateJson.theme != null) {
|
|
||||||
if (globalStateJson.theme.indexOf("system") > -1) {
|
|
||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
||||||
} else if (globalStateJson.theme.indexOf("dark") > -1) {
|
|
||||||
theme = "dark";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!htmlEl.classList.contains("theme_" + theme)) {
|
|
||||||
htmlEl.classList.remove("theme_" + defaultTheme);
|
|
||||||
htmlEl.classList.add("theme_" + theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Set theme on page load
|
||||||
|
// This is done outside the Angular app to avoid a flash of unthemed content before it loads
|
||||||
|
const setTheme = () => {
|
||||||
|
const getLegacyTheme = (): string | null => {
|
||||||
|
// MANUAL-STATE-ACCESS: Calling global to get setting before migration
|
||||||
|
// has had a chance to run, can be remove in the future.
|
||||||
|
// Tracking Issue: https://bitwarden.atlassian.net/browse/PM-6676
|
||||||
|
const globalState = window.localStorage.getItem("global");
|
||||||
|
|
||||||
|
const parsedGlobalState = JSON.parse(globalState) as { theme?: string } | null;
|
||||||
|
return parsedGlobalState ? parsedGlobalState.theme : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTheme = "light";
|
||||||
|
const htmlEl = document.documentElement;
|
||||||
|
let theme = defaultTheme;
|
||||||
|
|
||||||
|
// First try the new state providers location, then the legacy location
|
||||||
|
const themeFromState =
|
||||||
|
window.localStorage.getItem("global_theming_selection") ?? getLegacyTheme();
|
||||||
|
|
||||||
|
if (themeFromState) {
|
||||||
|
if (themeFromState.indexOf("system") > -1) {
|
||||||
|
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
} else if (themeFromState.indexOf("dark") > -1) {
|
||||||
|
theme = "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!htmlEl.classList.contains("theme_" + theme)) {
|
||||||
|
// The defaultTheme is also set in the html itself to make sure that some theming is always applied
|
||||||
|
htmlEl.classList.remove("theme_" + defaultTheme);
|
||||||
|
htmlEl.classList.add("theme_" + theme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme();
|
|
@ -26,7 +26,12 @@
|
||||||
"strictTemplates": true,
|
"strictTemplates": true,
|
||||||
"preserveWhitespaces": true
|
"preserveWhitespaces": true
|
||||||
},
|
},
|
||||||
"files": ["src/polyfills.ts", "src/main.ts", "../../bitwarden_license/bit-web/src/main.ts"],
|
"files": [
|
||||||
|
"src/polyfills.ts",
|
||||||
|
"src/main.ts",
|
||||||
|
"../../bitwarden_license/bit-web/src/main.ts",
|
||||||
|
"src/theme.ts"
|
||||||
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"src/connectors/*.ts",
|
"src/connectors/*.ts",
|
||||||
"src/**/*.stories.ts",
|
"src/**/*.stories.ts",
|
||||||
|
|
|
@ -323,7 +323,7 @@ const webpackConfig = {
|
||||||
"connectors/sso": "./src/connectors/sso.ts",
|
"connectors/sso": "./src/connectors/sso.ts",
|
||||||
"connectors/captcha": "./src/connectors/captcha.ts",
|
"connectors/captcha": "./src/connectors/captcha.ts",
|
||||||
"connectors/duo-redirect": "./src/connectors/duo-redirect.ts",
|
"connectors/duo-redirect": "./src/connectors/duo-redirect.ts",
|
||||||
theme_head: "./src/theme.js",
|
theme_head: "./src/theme.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
|
|
|
@ -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 { 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 {
|
export abstract class AbstractThemingService {
|
||||||
theme$: Observable<Theme>;
|
/**
|
||||||
monitorThemeChanges: () => Promise<void>;
|
* The effective theme based on the user configured choice and the current system theme if
|
||||||
updateSystemTheme: (systemTheme: ThemeType) => void;
|
* the configured choice is {@link ThemeType.System}.
|
||||||
updateConfiguredTheme: (theme: ThemeType) => Promise<void>;
|
*/
|
||||||
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { InjectionToken } from "@angular/core";
|
import { InjectionToken } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
|
|
||||||
declare const tag: unique symbol;
|
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 LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
|
||||||
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
|
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
|
||||||
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");
|
export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURES");
|
||||||
|
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
|
||||||
|
"SYSTEM_THEME_OBSERVABLE",
|
||||||
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { DOCUMENT } from "@angular/common";
|
|
||||||
import { LOCALE_ID, NgModule } from "@angular/core";
|
import { LOCALE_ID, NgModule } from "@angular/core";
|
||||||
import { UnwrapOpaque } from "type-fest";
|
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 { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* 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 { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/services/audit.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 { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||||
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
||||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.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 { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
|
||||||
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
||||||
|
|
||||||
|
@ -248,6 +251,7 @@ import {
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
SYSTEM_LANGUAGE,
|
SYSTEM_LANGUAGE,
|
||||||
|
SYSTEM_THEME_OBSERVABLE,
|
||||||
WINDOW,
|
WINDOW,
|
||||||
} from "./injection-tokens";
|
} from "./injection-tokens";
|
||||||
import { ModalService } from "./modal.service";
|
import { ModalService } from "./modal.service";
|
||||||
|
@ -300,6 +304,21 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||||
provide: LOG_MAC_FAILURES,
|
provide: LOG_MAC_FAILURES,
|
||||||
useValue: true,
|
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({
|
safeProvider({
|
||||||
provide: AppIdServiceAbstraction,
|
provide: AppIdServiceAbstraction,
|
||||||
useClass: AppIdService,
|
useClass: AppIdService,
|
||||||
|
@ -772,11 +791,6 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||||
useClass: TwoFactorService,
|
useClass: TwoFactorService,
|
||||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: AbstractThemingService,
|
|
||||||
useClass: ThemingService,
|
|
||||||
deps: [StateServiceAbstraction, WINDOW, DOCUMENT as SafeInjectionToken<Document>],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: FormValidationErrorsServiceAbstraction,
|
provide: FormValidationErrorsServiceAbstraction,
|
||||||
useClass: FormValidationErrorsService,
|
useClass: FormValidationErrorsService,
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||||
import { KdfType, ThemeType } from "../enums";
|
import { KdfType } from "../enums";
|
||||||
import { ServerConfigData } from "../models/data/server-config.data";
|
import { ServerConfigData } from "../models/data/server-config.data";
|
||||||
import { Account, AccountDecryptionOptions } from "../models/domain/account";
|
import { Account, AccountDecryptionOptions } from "../models/domain/account";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
|
@ -342,8 +342,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
|
||||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
|
||||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||||
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
|
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
} from "../abstractions/storage.service";
|
} from "../abstractions/storage.service";
|
||||||
import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType } from "../enums";
|
import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
|
||||||
import { StateFactory } from "../factories/state-factory";
|
import { StateFactory } from "../factories/state-factory";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
import { ServerConfigData } from "../models/data/server-config.data";
|
import { ServerConfigData } from "../models/data/server-config.data";
|
||||||
|
@ -1754,23 +1754,6 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(options?: StorageOptions): Promise<ThemeType> {
|
|
||||||
return (
|
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setTheme(value: ThemeType, options?: StorageOptions): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
globals.theme = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTwoFactorToken(options?: StorageOptions): Promise<string> {
|
async getTwoFactorToken(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
|
|
@ -63,6 +63,7 @@ export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
|
export const THEMING_DISK = new StateDefinition("theming", "disk");
|
||||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
||||||
|
|
||||||
// Secrets Manager
|
// Secrets Manager
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
|
import { ThemeType } from "../enums";
|
||||||
|
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "../state";
|
||||||
|
|
||||||
|
export abstract class ThemeStateService {
|
||||||
|
/**
|
||||||
|
* The users selected theme.
|
||||||
|
*/
|
||||||
|
selectedTheme$: Observable<ThemeType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method for updating the current users configured theme.
|
||||||
|
* @param theme The chosen user theme.
|
||||||
|
*/
|
||||||
|
setSelectedTheme: (theme: ThemeType) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const THEME_SELECTION = new KeyDefinition<ThemeType>(THEMING_DISK, "selection", {
|
||||||
|
deserializer: (s) => s,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class DefaultThemeStateService implements ThemeStateService {
|
||||||
|
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
|
||||||
|
|
||||||
|
selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private globalStateProvider: GlobalStateProvider,
|
||||||
|
private defaultTheme: ThemeType = ThemeType.System,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async setSelectedTheme(theme: ThemeType): Promise<void> {
|
||||||
|
await this.selectedThemeState.update(() => theme, {
|
||||||
|
shouldUpdate: (currentTheme) => currentTheme !== theme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-m
|
||||||
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
|
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
|
||||||
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
|
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
|
||||||
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
|
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
|
||||||
|
import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-state-providers";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
|
@ -39,7 +40,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 2;
|
export const MIN_VERSION = 2;
|
||||||
export const CURRENT_VERSION = 34;
|
export const CURRENT_VERSION = 35;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
|
@ -76,7 +77,8 @@ export function createMigrationBuilder() {
|
||||||
.with(EnableContextMenuMigrator, 30, 31)
|
.with(EnableContextMenuMigrator, 30, 31)
|
||||||
.with(PreferredLanguageMigrator, 31, 32)
|
.with(PreferredLanguageMigrator, 31, 32)
|
||||||
.with(AppIdMigrator, 32, 33)
|
.with(AppIdMigrator, 32, 33)
|
||||||
.with(DomainSettingsMigrator, 33, CURRENT_VERSION);
|
.with(DomainSettingsMigrator, 33, 34)
|
||||||
|
.with(MoveThemeToStateProviderMigrator, 34, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|
|
@ -286,7 +286,11 @@ function expectInjectedData(
|
||||||
export async function runMigrator<
|
export async function runMigrator<
|
||||||
TMigrator extends Migrator<number, number>,
|
TMigrator extends Migrator<number, number>,
|
||||||
TUsers extends readonly string[] = string[],
|
TUsers extends readonly string[] = string[],
|
||||||
>(migrator: TMigrator, initalData?: InitialDataHint<TUsers>): Promise<Record<string, unknown>> {
|
>(
|
||||||
|
migrator: TMigrator,
|
||||||
|
initalData?: InitialDataHint<TUsers>,
|
||||||
|
direction: "migrate" | "rollback" = "migrate",
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
// Inject fake data at every level of the object
|
// Inject fake data at every level of the object
|
||||||
const allInjectedData = injectData(initalData, []);
|
const allInjectedData = injectData(initalData, []);
|
||||||
|
|
||||||
|
@ -294,7 +298,11 @@ export async function runMigrator<
|
||||||
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
|
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
|
||||||
|
|
||||||
// Run their migrations
|
// Run their migrations
|
||||||
await migrator.migrate(helper);
|
if (direction === "rollback") {
|
||||||
|
await migrator.rollback(helper);
|
||||||
|
} else {
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
}
|
||||||
const [data, leftoverInjectedData] = expectInjectedData(
|
const [data, leftoverInjectedData] = expectInjectedData(
|
||||||
fakeStorageService.internalStore,
|
fakeStorageService.internalStore,
|
||||||
allInjectedData,
|
allInjectedData,
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { runMigrator } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { MoveThemeToStateProviderMigrator } from "./35-move-theme-to-state-providers";
|
||||||
|
|
||||||
|
describe("MoveThemeToStateProviders", () => {
|
||||||
|
const sut = new MoveThemeToStateProviderMigrator(34, 35);
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
it("migrates global theme and deletes it", async () => {
|
||||||
|
const output = await runMigrator(sut, {
|
||||||
|
global: {
|
||||||
|
theme: "dark",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
global_theming_selection: "dark",
|
||||||
|
global: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([{}, null])(
|
||||||
|
"doesn't touch it if global state looks like: '%s'",
|
||||||
|
async (globalState) => {
|
||||||
|
const output = await runMigrator(sut, {
|
||||||
|
global: globalState,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
global: globalState,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
it("migrates state provider theme back to original location when no global", async () => {
|
||||||
|
const output = await runMigrator(
|
||||||
|
sut,
|
||||||
|
{
|
||||||
|
global_theming_selection: "disk",
|
||||||
|
},
|
||||||
|
"rollback",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
global: {
|
||||||
|
theme: "disk",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates state provider theme back to legacy location when there is an existing global object", async () => {
|
||||||
|
const output = await runMigrator(
|
||||||
|
sut,
|
||||||
|
{
|
||||||
|
global_theming_selection: "disk",
|
||||||
|
global: {
|
||||||
|
other: "stuff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rollback",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
global: {
|
||||||
|
theme: "disk",
|
||||||
|
other: "stuff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing if no theme in state provider location", async () => {
|
||||||
|
const output = await runMigrator(sut, {}, "rollback");
|
||||||
|
expect(output).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedGlobal = { theme?: string };
|
||||||
|
|
||||||
|
const THEME_SELECTION: KeyDefinitionLike = {
|
||||||
|
key: "selection",
|
||||||
|
stateDefinition: { name: "theming" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MoveThemeToStateProviderMigrator extends Migrator<34, 35> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const legacyGlobalState = await helper.get<ExpectedGlobal>("global");
|
||||||
|
const theme = legacyGlobalState?.theme;
|
||||||
|
if (theme != null) {
|
||||||
|
await helper.setToGlobal(THEME_SELECTION, theme);
|
||||||
|
delete legacyGlobalState.theme;
|
||||||
|
await helper.set("global", legacyGlobalState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const theme = await helper.getFromGlobal<string>(THEME_SELECTION);
|
||||||
|
if (theme != null) {
|
||||||
|
const legacyGlobal = (await helper.get<ExpectedGlobal>("global")) ?? {};
|
||||||
|
legacyGlobal.theme = theme;
|
||||||
|
await helper.set("global", legacyGlobal);
|
||||||
|
await helper.removeFromGlobal(THEME_SELECTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue