diff --git a/bun.lock b/bun.lock index 97014f7ba..b1c882684 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ "@wasm-fmt/clang-format": "^23.1.1", "autoprefixer": "^10.6.1", "cmake-js": "^8.0.0", - "electron": "^44.7.2", + "electron": "^44.8.0", "electron-builder": "^26.15.3", "electron-conf": "^1.3.0", "electron-updater": "^6.8.9", @@ -561,7 +561,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@44.7.2", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-u3Pbz0Hi1sSSwBMz2v8LiagsfUG4QURPUe/qphM+g4THKP8o4BYdnuboyhlLO8HIC/KfV5rvltyacbP4jEq9yw=="], + "electron": ["electron@44.8.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-Yn2tJ335l8MGRJj86vaAYq1MOidtiw9eXdmHX6p3ecHQWwucsMMBxDzqUrYnXEHyqdc/IhOh2KGHzmizJvzd3Q=="], "electron-builder": ["electron-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.3", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA=="], @@ -571,7 +571,7 @@ "electron-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="], - "electron-to-chromium": ["electron-to-chromium@1.5.456", "", {}, "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.458", "", {}, "sha512-/hIH2gRSmxrf4/X4Nw/t/Wc8PVS22l8/3HnWjWTWsTOM5dsnMUPZF/+u+yr3LqweNuTBJE1rq6qAQzKkZVwMfA=="], "electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="], @@ -993,7 +993,7 @@ "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], - "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -1201,8 +1201,6 @@ "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1231,8 +1229,6 @@ "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "electron-updater/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], diff --git a/package.json b/package.json index 141d44756..62363890c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "1.20261016.3", + "version": "1.20261016.4", "engines": { "bun": ">=1.2.0" }, @@ -77,7 +77,7 @@ "@wasm-fmt/clang-format": "^23.1.1", "autoprefixer": "^10.6.1", "cmake-js": "^8.0.0", - "electron": "^44.7.2", + "electron": "^44.8.0", "electron-builder": "^26.15.3", "electron-conf": "^1.3.0", "electron-updater": "^6.8.9", diff --git a/src/main/MainWindow.ts b/src/main/MainWindow.ts index c850c0358..64e31e5b3 100644 --- a/src/main/MainWindow.ts +++ b/src/main/MainWindow.ts @@ -3,6 +3,7 @@ import { autoUpdater, type ProgressInfo, type UpdateCheckResult } from 'electron import fs from 'node:fs'; import path from 'node:path'; +import type { OpenCORTheme } from '../renderer/index'; import { formatError, isDataUrlFileName, type ISettings, isUrl } from '../renderer/src/common/common'; import { FULL_URI_SCHEME, LONG_DELAY } from '../renderer/src/common/constants'; import { appImagePath, isMacOs, isPackaged } from '../renderer/src/common/electron'; @@ -153,6 +154,13 @@ export const saveSettings = (settings: ISettings): void => { electronConf.set('settings', settings); }; +export const applyTheme = (theme: OpenCORTheme = electronConf.get('settings.general.theme')): void => { + // Apply the given theme (or the one in our settings if none is given) to OpenCOR, so that it is used by our renderer + // and by Electron (e.g., for its dialogs). + + electron.nativeTheme.themeSource = theme === 'light' || theme === 'dark' ? theme : 'system'; +}; + let _resetAll = false; export const resetAll = (): void => { diff --git a/src/main/index.ts b/src/main/index.ts index 99a2ea929..bf2c32d65 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,6 +7,7 @@ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; +import type { OpenCORTheme } from '../renderer/index'; import { formatError, type ISettings, isObject } from '../renderer/src/common/common'; import { SHORT_DELAY, URI_SCHEME } from '../renderer/src/common/constants'; import { appImagePath, isLinux, isPackaged, isWindows } from '../renderer/src/common/electron'; @@ -22,6 +23,7 @@ import { startRendererServer, stopRendererServer } from '../renderer/src/common/ import { enableDisableFileCloseAndCloseAllMenuItems, enableDisableMainMenu } from './MainMenu'; import { + applyTheme, checkForUpdates, downloadAndInstallUpdate, fileClosed, @@ -370,7 +372,8 @@ electron.app }, settings: { general: { - checkForUpdatesAtStartup: true + checkForUpdatesAtStartup: true, + theme: 'system' } } }); @@ -399,6 +402,10 @@ electron.app }); } + // Apply our theme, so that our splash screen window and main window use it from the start. + + applyTheme(); + // Create our splash window. const splashScreenWindow = new SplashScreenWindow(); @@ -448,6 +455,9 @@ electron.app // Handle some requests from our renderer process. + electron.ipcMain.handle('apply-theme', (_event, theme: OpenCORTheme) => { + applyTheme(theme); + }); electron.ipcMain.handle('check-for-updates', (_event, atStartup: boolean) => { checkForUpdates(atStartup); }); diff --git a/src/preload/index.ts b/src/preload/index.ts index fcd6832f0..2127288d6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs'; import loc from '../../dist/libOpenCOR/Release/libOpenCOR.node'; +import type { OpenCORTheme } from '../renderer/index'; import type { ISettings } from '../renderer/src/common/common'; // A helper function to listen to a channel from the main process. @@ -84,6 +85,7 @@ electron.contextBridge.exposeInMainWorld('electronApi', { // Renderer process asking the main process to do something for it. + applyTheme: (theme: OpenCORTheme) => electron.ipcRenderer.invoke('apply-theme', theme), checkForUpdates: (atStartup: boolean) => electron.ipcRenderer.invoke('check-for-updates', atStartup), clearGitHubCache: (): Promise => electron.ipcRenderer.invoke('clear-github-cache'), deleteGitHubAccessToken: (): Promise => electron.ipcRenderer.invoke('delete-github-access-token'), diff --git a/src/renderer/index.html b/src/renderer/index.html index 26ef446b8..d5c52108b 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -16,11 +16,15 @@ overflow: hidden; } @media (prefers-color-scheme: dark) { - body { + :root:not(.opencor-light-mode) body { background-color: #18181b; /* Dark version of --p-content-background. */ color: #ffffff; /* Dark version of --p-content-color. */ } } + :root.opencor-dark-mode body { + background-color: #18181b; /* Dark version of --p-content-background. */ + color: #ffffff; /* Dark version of --p-content-color. */ + } diff --git a/src/renderer/package.json b/src/renderer/package.json index 5e6b20f66..86302c6b7 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "1.20261016.3", + "version": "1.20261016.4", "libopencorVersion": "1.20261016.1", "scripts": { "build": "vite build && bun scripts/generate.version.ts", diff --git a/src/renderer/src/assets/app.css b/src/renderer/src/assets/app.css index da3172b3c..14128285b 100644 --- a/src/renderer/src/assets/app.css +++ b/src/renderer/src/assets/app.css @@ -15,6 +15,10 @@ /* biome-ignore lint/correctness/noInvalidPositionAtImportRule: ignored using `--suppress` */ @import "primeicons/primeicons.css"; +/* Make Tailwind CSS's dark variant follow our theme rather than (necessarily) the system's theme. */ + +@custom-variant dark (&:where(.opencor-dark-mode, .opencor-dark-mode *)); + body { *, ::before, diff --git a/src/renderer/src/common/common.ts b/src/renderer/src/common/common.ts index df0a6ceda..a61a19e2b 100644 --- a/src/renderer/src/common/common.ts +++ b/src/renderer/src/common/common.ts @@ -1,4 +1,4 @@ -import type { IOpenCORSimulationDataValue, OpenCORSimulationData } from '../../index'; +import type { IOpenCORSimulationDataValue, OpenCORSimulationData, OpenCORTheme } from '../../index'; import * as dependencies from './dependencies'; import { electronApi } from './electronApi'; @@ -7,6 +7,7 @@ import { electronApi } from './electronApi'; export interface ISettingsGeneral { checkForUpdatesAtStartup: boolean; + theme: OpenCORTheme; } export interface ISettings { diff --git a/src/renderer/src/common/electronApi.ts b/src/renderer/src/common/electronApi.ts index 4088c252a..512ba37e4 100644 --- a/src/renderer/src/common/electronApi.ts +++ b/src/renderer/src/common/electronApi.ts @@ -1,3 +1,5 @@ +import type { OpenCORTheme } from '../../index'; + import type { ISettings } from './common'; interface IElectronApi { @@ -9,6 +11,7 @@ interface IElectronApi { // Renderer process asking the main process to do something for it. + applyTheme: (theme: OpenCORTheme) => void; checkForUpdates: (atStartup: boolean) => void; clearGitHubCache: () => Promise; deleteGitHubAccessToken: () => Promise; diff --git a/src/renderer/src/common/settings.ts b/src/renderer/src/common/settings.ts index ba6bbb4ef..822dc7b0b 100644 --- a/src/renderer/src/common/settings.ts +++ b/src/renderer/src/common/settings.ts @@ -6,7 +6,8 @@ import { electronApi } from './electronApi'; const defaultSettings = (): ISettings => { return { general: { - checkForUpdatesAtStartup: true + checkForUpdatesAtStartup: true, + theme: 'system' } }; }; diff --git a/src/renderer/src/common/vueCommon.ts b/src/renderer/src/common/vueCommon.ts index 21f41ae65..6735984d1 100644 --- a/src/renderer/src/common/vueCommon.ts +++ b/src/renderer/src/common/vueCommon.ts @@ -7,6 +7,7 @@ import type { OpenCORTheme } from '../../index'; import * as locSedApi from '../libopencor/locSedApi'; import { MEDIUM_DELAY, VERY_SHORT_DELAY } from './constants'; +import { electronApi } from './electronApi'; // A constant to know the UID of the active instance of OpenCOR. @@ -26,6 +27,7 @@ export const useTheme = vueusecore.createGlobalState(() => { }; const updateDocumentClasses = () => { + document.documentElement.classList.toggle('opencor-light-mode', isLightMode.value); document.documentElement.classList.toggle('opencor-dark-mode', isDarkMode.value); }; @@ -41,7 +43,10 @@ export const useTheme = vueusecore.createGlobalState(() => { }; const setTheme = (newTheme: OpenCORTheme | undefined) => { - _theme.value = newTheme ?? 'system'; + // Note: the given theme may come from our settings, which may have been edited by hand, so we fall back on the + // system theme for anything that is not a valid theme. + + _theme.value = newTheme === 'light' || newTheme === 'dark' ? newTheme : 'system'; if (_theme.value === 'light') { isLightMode.value = true; @@ -56,6 +61,15 @@ export const useTheme = vueusecore.createGlobalState(() => { updateDocumentClasses(); }; + const applyTheme = (newTheme: OpenCORTheme) => { + // Set the given theme and let Electron know about it (e.g., for its dialogs), if we are running in Electron. + // Note: unlike setTheme(), this is to be used when the user changes our theme, not when we initialise it. + + setTheme(newTheme); + + electronApi?.applyTheme(_theme.value); + }; + const useLightMode = (): boolean => { return isLightMode.value; }; @@ -67,6 +81,7 @@ export const useTheme = vueusecore.createGlobalState(() => { return { theme, setTheme, + applyTheme, useLightMode, useDarkMode }; diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index 5bdafd87c..11401d651 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -742,9 +742,7 @@ if (common.isDesktop()) { color: var(--p-red-50); } -@media (prefers-color-scheme: dark) { - .remove-button:hover { - background-color: var(--p-red-400); - } +.opencor-dark-mode .remove-button:hover { + background-color: var(--p-red-400); } diff --git a/src/renderer/src/components/MainMenu.vue b/src/renderer/src/components/MainMenu.vue index 8b6b0b55e..b26e21e60 100644 --- a/src/renderer/src/components/MainMenu.vue +++ b/src/renderer/src/components/MainMenu.vue @@ -21,12 +21,13 @@ @@ -37,14 +38,19 @@ import * as vueusecore from '@vueuse/core'; import type Menubar from 'primevue/menubar'; import * as vue from 'vue'; +import type { OpenCORTheme } from '../../index'; + import * as common from '../common/common'; +import { settings } from '../common/settings'; import * as version from '../common/version'; +import * as vueCommon from '../common/vueCommon'; const props = defineProps<{ id?: string; isActiveApp: boolean; hasFiles: boolean; uiEnabled: boolean; + themeCanBeChanged: boolean; }>(); const emit = defineEmits<{ @@ -62,6 +68,22 @@ const isWindowsOrLinux = common.isWindows() || common.isLinux(); const isMacOs = common.isMacOs(); const updateAvailable = version.updateAvailable; +// Theme button, which allows the user to cycle through the system, light, and dark themes. + +const theme = vueCommon.useTheme(); + +const onThemeSwitch = (newTheme: OpenCORTheme) => { + // Switch to the next theme, apply it, and save it in our settings. + + theme.applyTheme(newTheme); + + settings.general.theme = newTheme; + + settings.save(); +}; + +// Menu items, which are computed so that they can be reactive to changes in our props. + const items = vue.computed(() => [ { label: 'File', @@ -232,6 +254,7 @@ if (common.isDesktop()) {