diff --git a/src/main/electron-app/features/get-electron-theme.injectable.ts b/src/main/electron-app/features/get-electron-theme.injectable.ts new file mode 100644 index 0000000000..220c047aa6 --- /dev/null +++ b/src/main/electron-app/features/get-electron-theme.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import nativeThemeInjectable from "./native-theme.injectable"; + +const getElectronThemeInjectable = getInjectable({ + id: "get-electron-theme", + + instantiate: (di) => { + const nativeTheme = di.inject(nativeThemeInjectable); + + return () => nativeTheme.shouldUseDarkColors ? "dark" : "light"; + }, +}); + +export default getElectronThemeInjectable; diff --git a/src/main/electron-app/features/native-theme.injectable.ts b/src/main/electron-app/features/native-theme.injectable.ts new file mode 100644 index 0000000000..a2d29d8835 --- /dev/null +++ b/src/main/electron-app/features/native-theme.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { nativeTheme } from "electron"; + +const nativeThemeInjectable = getInjectable({ + id: "native-theme", + instantiate: () => nativeTheme, + causesSideEffects: true, +}); + +export default nativeThemeInjectable; diff --git a/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts b/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..83548fd3f0 --- /dev/null +++ b/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import currentOperatingSystemThemeStateInjectable from "../../theme/current-operating-system-theme-state.injectable"; +import nativeThemeInjectable from "./native-theme.injectable"; +import getElectronThemeInjectable from "./get-electron-theme.injectable"; + +const syncThemeFromOperatingSystemInjectable = getInjectable({ + id: "sync-theme-from-operating-system", + + instantiate: (di) => { + const currentThemeState = di.inject(currentOperatingSystemThemeStateInjectable); + const nativeTheme = di.inject(nativeThemeInjectable); + const getElectronTheme = di.inject(getElectronThemeInjectable); + + return getStartableStoppable("sync-theme-from-operating-system", () => { + const updateThemeState = () => { + const newTheme = getElectronTheme(); + + currentThemeState.set(newTheme); + }; + + nativeTheme.on("updated", updateThemeState); + + return () => { + nativeTheme.off("updated", updateThemeState); + }; + }); + }, +}); + +export default syncThemeFromOperatingSystemInjectable; diff --git a/src/main/electron-app/runnables/setup-os-theme-updates.injectable.ts b/src/main/electron-app/runnables/setup-os-theme-updates.injectable.ts deleted file mode 100644 index 01f74bbf69..0000000000 --- a/src/main/electron-app/runnables/setup-os-theme-updates.injectable.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { broadcastNativeThemeOnUpdate } from "../../native-theme"; -import { whenApplicationIsLoadingInjectionToken } from "../../start-main-application/runnable-tokens/when-application-is-loading-injection-token"; - -const setupOsThemeUpdatesInjectable = getInjectable({ - id: "setup-os-theme-updates", - - instantiate: () => ({ - run: () => { - broadcastNativeThemeOnUpdate(); - }, - }), - - // Todo: remove explicit usage of IPC to get rid of this. - causesSideEffects: true, - - injectionToken: whenApplicationIsLoadingInjectionToken, -}); - -export default setupOsThemeUpdatesInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 05dc73e055..9f78c9c9e7 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -47,7 +47,6 @@ import catalogCategoryRegistryInjectable from "../common/catalog/catalog-categor import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable"; import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable"; import setupRunnablesForAfterRootFrameIsReadyInjectable from "./start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable"; -import setupOsThemeUpdatesInjectable from "./electron-app/runnables/setup-os-theme-updates.injectable"; import setupSentryInjectable from "./start-main-application/runnables/setup-sentry.injectable"; import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable"; import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable"; @@ -79,6 +78,8 @@ import createElectronWindowForInjectable from "./start-main-application/lens-win import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable"; import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; +import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable"; +import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, @@ -136,8 +137,8 @@ export const getDiForUnitTesting = ( di.override(registerChannelInjectable, () => () => undefined); di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory"); - di.override(broadcastMessageInjectable, () => { - throw new Error("Tried to broadcast message over IPC without explicit override."); + di.override(broadcastMessageInjectable, () => (channel) => { + throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`); }); di.override(spawnInjectable, () => () => { @@ -184,7 +185,6 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => { setupIpcMainHandlersInjectable, setupLensProxyInjectable, setupRunnablesForAfterRootFrameIsReadyInjectable, - setupOsThemeUpdatesInjectable, setupSentryInjectable, setupShellInjectable, setupSyncingOfWeblinksInjectable, @@ -224,6 +224,8 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(showMessagePopupInjectable, () => () => {}); di.override(waitForElectronToBeReadyInjectable, () => () => Promise.resolve()); di.override(ipcMainInjectable, () => ({})); + di.override(getElectronThemeInjectable, () => () => "dark"); + di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} })); di.override(createElectronWindowForInjectable, () => () => async () => ({ show: () => {}, diff --git a/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts new file mode 100644 index 0000000000..6d039ead92 --- /dev/null +++ b/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction } from "mobx"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { setNativeThemeChannel } from "../../../common/ipc/native-theme"; +import currentOperatingSystemThemeInjectable from "../current-operating-system-theme.injectable"; +import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; + +const broadcastThemeChangeInjectable = getInjectable({ + id: "broadcast-theme-change", + + instantiate: (di) => { + const currentTheme = di.inject(currentOperatingSystemThemeInjectable); + const broadcastMessage = di.inject(broadcastMessageInjectable); + + return getStartableStoppable("broadcast-theme-change", () => + reaction(() => currentTheme.get(), (theme) => { + broadcastMessage(setNativeThemeChannel, theme); + }), + ); + }, +}); + +export default broadcastThemeChangeInjectable; diff --git a/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts new file mode 100644 index 0000000000..02d44cb72b --- /dev/null +++ b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { whenApplicationIsLoadingInjectionToken } from "../../start-main-application/runnable-tokens/when-application-is-loading-injection-token"; +import broadcastThemeChangeInjectable from "./broadcast-theme-change.injectable"; + +const startBroadcastingThemeChangeInjectable = getInjectable({ + id: "start-broadcasting-theme-change", + + instantiate: (di) => { + const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); + + return { + run: async () => { + await broadcastThemeChange.start(); + }, + }; + }, + + injectionToken: whenApplicationIsLoadingInjectionToken, +}); + +export default startBroadcastingThemeChangeInjectable; diff --git a/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts new file mode 100644 index 0000000000..a5f922af13 --- /dev/null +++ b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import broadcastThemeChangeInjectable from "./broadcast-theme-change.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopBroadcastingThemeChangeInjectable = getInjectable({ + id: "stop-broadcasting-theme-change", + + instantiate: (di) => { + const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); + + return { + run: async () => { + await broadcastThemeChange.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopBroadcastingThemeChangeInjectable; diff --git a/src/main/theme/current-operating-system-theme-state.injectable.ts b/src/main/theme/current-operating-system-theme-state.injectable.ts new file mode 100644 index 0000000000..1bfa532ce8 --- /dev/null +++ b/src/main/theme/current-operating-system-theme-state.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import getElectronThemeInjectable from "../electron-app/features/get-electron-theme.injectable"; + +export type Theme = "dark" | "light"; + +const currentOperatingSystemThemeStateInjectable = getInjectable({ + id: "current-operating-system-theme-state", + + instantiate: (di) => { + const getElectronTheme = di.inject(getElectronThemeInjectable); + const defaultTheme = getElectronTheme(); + + return observable.box( + defaultTheme, + ); + }, +}); + +export default currentOperatingSystemThemeStateInjectable; diff --git a/src/main/theme/current-operating-system-theme.injectable.ts b/src/main/theme/current-operating-system-theme.injectable.ts new file mode 100644 index 0000000000..64efa58e67 --- /dev/null +++ b/src/main/theme/current-operating-system-theme.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import currentOperatingSystemThemeStateInjectable from "./current-operating-system-theme-state.injectable"; + +const currentOperatingSystemThemeInjectable = getInjectable({ + id: "current-operating-system-theme", + + instantiate: (di) => { + const currentThemeState = di.inject(currentOperatingSystemThemeStateInjectable); + + return computed(() => currentThemeState.get()); + }, +}); + +export default currentOperatingSystemThemeInjectable; diff --git a/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..344e3e40da --- /dev/null +++ b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import syncThemeFromOperatingSystemInjectable from "../../electron-app/features/sync-theme-from-operating-system.injectable"; +import { whenApplicationIsLoadingInjectionToken } from "../../start-main-application/runnable-tokens/when-application-is-loading-injection-token"; + +const startSyncingThemeFromOperatingSystemInjectable = getInjectable({ + id: "start-syncing-theme-from-operating-system", + + instantiate: (di) => { + const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable); + + return { + run: async () => { + await syncTheme.start(); + }, + }; + }, + + injectionToken: whenApplicationIsLoadingInjectionToken, +}); + +export default startSyncingThemeFromOperatingSystemInjectable; diff --git a/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..08657281c2 --- /dev/null +++ b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import syncThemeFromOperatingSystemInjectable from "../../electron-app/features/sync-theme-from-operating-system.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopSyncingThemeFromOperatingSystemInjectable = getInjectable({ + id: "stop-syncing-theme-from-operating-system", + + instantiate: (di) => { + const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable); + + return { + run: async () => { + await syncTheme.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopSyncingThemeFromOperatingSystemInjectable;