From 179700ccda3d18f970d1eb4bbc513fc36ea720db Mon Sep 17 00:00:00 2001 From: Leandro Marques <16769222+leandromqrs@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:12:43 -0300 Subject: [PATCH] chore: update electron to v41.x (#7097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update electron to v41.1.1 * feat(start-app): clear GPU cache on Electron version change for Linux * fix(window-decorators): disable custom window title bar for GNOME This fixes some issues when moving and resizing the window * refactor: Migrate url.format() (DEP0116) to file:// path approach * refactor(start-app): remove deprecated protocol.registerFileProtocol in start-app.ts * feat(electron-builder): update gnome content snap to gnome-42-2204 for improved compatibility and update flatpak permissions * fix(window-decorators): improve handling of custom window title bar for GNOME * feat(start-app): implement fallback to X11 in Snap if gnome-42-2204 runtime is unavailable * chore: update electron to v41.1.1 * chore(package-lock): remove unused dependencies from package-lock.json * fix(electron): move snap ozone-platform switch before app ready event app.commandLine.appendSwitch() must be called before Chromium initializes — after the ready event fires the GPU backend is already running and the switch is a no-op. Move the Snap X11 fallback (defense- in-depth for missing gnome-42-2204 runtime) to run synchronously at startup, alongside the existing gtk-version and speech-dispatcher switches. * fix(electron): align IS_GNOME_DESKTOP detection with preload.ts logic The original implementation only matched Ubuntu's GNOME session (XDG_CURRENT_DESKTOP contains both 'gnome' AND 'ubuntu'), missing plain GNOME on Fedora, Arch, etc. The preload.ts isGnomeDesktop() already used the correct approach: check four environment variables with OR logic. Align common.const.ts to match, preventing a split-brain where the main process and renderer disagree on whether the user is on GNOME — which would produce no title bar at all on non-Ubuntu GNOME desktops. * fix(electron): restore v41 bump lost in merge and gate title-bar toggle - Re-apply electron 41.2.0 + minimatch 10.2.5 override (master's 0e9218bd reverted the dependabot bump back to 37.10.3 while this branch's merge-base still contained 41.2.0, so the pre-merge diff was empty). - Regenerate root package-lock.json accordingly. - Drop unrelated esbuild additions from plugin-dev sub-lockfiles. - misc-settings-form: gate isUseCustomWindowTitleBar on IS_ELECTRON && !IS_GNOME_DESKTOP so the toggle does not appear in the web/PWA build. --------- Co-authored-by: Johannes Millan --- electron-builder.yaml | 19 ++++- electron/common.const.ts | 11 +++ electron/electronAPI.d.ts | 2 + electron/full-screen-blocker.ts | 19 ++--- electron/main-window.ts | 20 ++--- electron/preload.ts | 20 +++++ electron/start-app.ts | 75 +++++++++++++++---- package-lock.json | 27 +++++-- package.json | 4 +- src/app/app.constants.ts | 1 + src/app/core/theme/global-theme.service.ts | 18 +++-- .../form-cfgs/misc-settings-form.const.ts | 4 +- 12 files changed, 169 insertions(+), 51 deletions(-) diff --git a/electron-builder.yaml b/electron-builder.yaml index 0da8aa36f7..fe9177c33f 100644 --- a/electron-builder.yaml +++ b/electron-builder.yaml @@ -92,9 +92,21 @@ snap: # Fix for issue #6031: Add filesystem access for local file sync # https://github.com/super-productivity/super-productivity/issues/6031 - removable-media # Allows sync to external drives/USB storage + # Override electron-builder's hardcoded gnome-3-28-1804 content snap (core20 era) + # with gnome-42-2204 (core22 era) for up-to-date Mesa drivers and GSettings schemas. + # The plug name must match the template key to replace it rather than duplicate it. + # The explicit `content` attribute tells snapd to match the gnome-42-2204 slot. + # Pattern from Tidal HiFi: https://github.com/Mastermindzh/tidal-hifi + # gnome-42-2204 has global auto-connect (granted June 2022). + # Ref: electron-builder#9452, electron-builder#8548 (closed, not planned) + - gnome-3-28-1804: + interface: content + content: gnome-42-2204 + target: $SNAP/gnome-platform + default-provider: gnome-42-2204 flatpak: - runtimeVersion: '23.08' + runtimeVersion: '25.08' finishArgs: - --share=network - --share=ipc @@ -104,8 +116,13 @@ flatpak: - --socket=pulseaudio - --device=dri - --filesystem=home + - --talk-name=org.freedesktop.Notifications + - --talk-name=org.kde.StatusNotifierWatcher - --talk-name=org.gnome.Mutter.IdleMonitor + - --talk-name=org.freedesktop.secrets - --system-talk-name=org.freedesktop.login1 + - --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons + - --env=ELECTRON_TRASH=gio mac: appId: com.super-productivity.app diff --git a/electron/common.const.ts b/electron/common.const.ts index df1ce6f653..9ed099ab72 100644 --- a/electron/common.const.ts +++ b/electron/common.const.ts @@ -1 +1,12 @@ export const IS_MAC = process.platform === 'darwin'; +export const IS_GNOME_DESKTOP = + process.platform === 'linux' && + [ + process.env.XDG_CURRENT_DESKTOP, + process.env.XDG_SESSION_DESKTOP, + process.env.DESKTOP_SESSION, + process.env.GNOME_SHELL_SESSION_MODE, + ] + .filter((v): v is string => !!v) + .map((v) => v.toLowerCase()) + .some((v) => v.includes('gnome') || v.includes('ubuntu')); diff --git a/electron/electronAPI.d.ts b/electron/electronAPI.d.ts index 7b7f906a42..e0df585db0 100644 --- a/electron/electronAPI.d.ts +++ b/electron/electronAPI.d.ts @@ -86,6 +86,8 @@ export interface ElectronAPI { isLinux(): boolean; + isGnomeDesktop(): boolean; + isMacOS(): boolean; isSnap(): boolean; diff --git a/electron/full-screen-blocker.ts b/electron/full-screen-blocker.ts index 56b6b7d203..5866862a46 100644 --- a/electron/full-screen-blocker.ts +++ b/electron/full-screen-blocker.ts @@ -1,7 +1,6 @@ import { BrowserWindow, ipcMain } from 'electron'; import { IPC } from './shared-with-frontend/ipc-events.const'; import { TakeABreakConfig } from '../src/app/features/config/global-config.model'; -import { format } from 'url'; import { join, normalize } from 'path'; export const initFullScreenBlocker = (IS_DEV: boolean): void => { @@ -35,18 +34,14 @@ export const initFullScreenBlocker = (IS_DEV: boolean): void => { win.setFullScreenable(false); isFullScreenWindowOpen = true; win.loadURL( - format({ - pathname: normalize( - join( - __dirname, - IS_DEV - ? '../src/static/break-reminder-overlay.html' - : '../dist/static/break-reminder-overlay.html', - ), + `file://${normalize( + join( + __dirname, + IS_DEV + ? '../src/static/break-reminder-overlay.html' + : '../dist/static/break-reminder-overlay.html', ), - protocol: 'file:', - slashes: true, - }) + + )}` + `#msg=${encodeURI(msg)}&img=${encodeURI(randomImgUrl)}&time=${ takeABreakCfg.timedFullScreenBlockerDuration }`, diff --git a/electron/main-window.ts b/electron/main-window.ts index a4f176cba1..2cbf1f4b21 100644 --- a/electron/main-window.ts +++ b/electron/main-window.ts @@ -12,11 +12,10 @@ import { import { errorHandlerWithFrontendInform } from './error-handler-with-frontend-inform'; import * as path from 'path'; import { join, normalize } from 'path'; -import { format } from 'url'; import { IPC } from './shared-with-frontend/ipc-events.const'; import { readFileSync, stat } from 'fs'; import { error, log } from 'electron-log/main'; -import { IS_MAC } from './common.const'; +import { IS_MAC, IS_GNOME_DESKTOP } from './common.const'; import { destroyTaskWidget, getIsTaskWidgetAlwaysShow, @@ -92,8 +91,15 @@ export const createWindow = async ({ simpleStore[SimpleStoreKey.IS_USE_CUSTOM_WINDOW_TITLE_BAR]; const legacyIsUseObsidianStyleHeader = simpleStore[SimpleStoreKey.LEGACY_IS_USE_OBSIDIAN_STYLE_HEADER]; - const isUseCustomWindowTitleBar = - persistedIsUseCustomWindowTitleBar ?? legacyIsUseObsidianStyleHeader ?? true; + const userPrefersCustomWindowTitleBar = + persistedIsUseCustomWindowTitleBar ?? + legacyIsUseObsidianStyleHeader ?? + !IS_GNOME_DESKTOP; + // GNOME + Wayland combinations can miss native controls when titleBarStyle is hidden. + // Force native decorations on GNOME to keep window controls available. + const isUseCustomWindowTitleBar = IS_GNOME_DESKTOP + ? false + : userPrefersCustomWindowTitleBar; const titleBarStyle: BrowserWindowConstructorOptions['titleBarStyle'] = isUseCustomWindowTitleBar || IS_MAC ? 'hidden' : 'default'; // Determine initial symbol color based on system theme preference @@ -208,11 +214,7 @@ export const createWindow = async ({ ? customUrl : IS_DEV ? 'http://localhost:4200' - : format({ - pathname: normalize(join(__dirname, '../.tmp/angular-dist/browser/index.html')), - protocol: 'file:', - slashes: true, - }); + : `file://${normalize(join(__dirname, '../.tmp/angular-dist/browser/index.html'))}`; mainWin.loadURL(url).then(() => { // Set window title for dev mode diff --git a/electron/preload.ts b/electron/preload.ts index 990dab9349..2e3fcd96b2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -25,6 +25,25 @@ const _invoke: (channel: IPCEventValue, ...args: unknown[]) => Promise ...args ) => ipcRenderer.invoke(channel, ...args); +const isGnomeDesktop = (): boolean => { + if (process.platform !== 'linux') { + return false; + } + + const desktopValues = [ + process.env.XDG_CURRENT_DESKTOP, + process.env.XDG_SESSION_DESKTOP, + process.env.DESKTOP_SESSION, + process.env.GNOME_SHELL_SESSION_MODE, + ] + .filter((value): value is string => !!value) + .map((value) => value.toLowerCase()); + + return desktopValues.some( + (value) => value.includes('gnome') || value.includes('ubuntu'), + ); +}; + const ea: ElectronAPI = { on: ( channel: string, @@ -68,6 +87,7 @@ const ea: ElectronAPI = { }, getZoomFactor: () => webFrame.getZoomFactor(), isLinux: () => process.platform === 'linux', + isGnomeDesktop, isMacOS: () => process.platform === 'darwin', isSnap: () => process && process.env && !!process.env.SNAP, isFlatpak: () => process && process.env && !!process.env.FLATPAK_ID, diff --git a/electron/start-app.ts b/electron/start-app.ts index d95c77f1c5..c19a891bf7 100644 --- a/electron/start-app.ts +++ b/electron/start-app.ts @@ -1,15 +1,7 @@ import { initIpcInterfaces } from './ipc-handler'; import { initPluginOAuth } from './plugin-oauth'; import electronLog, { info, log, warn } from 'electron-log/main'; -import { - App, - app, - BrowserWindow, - globalShortcut, - ipcMain, - powerMonitor, - protocol, -} from 'electron'; +import { App, app, BrowserWindow, globalShortcut, ipcMain, powerMonitor } from 'electron'; import { join } from 'path'; import { initDebug } from './debug'; import electronDl from 'electron-dl'; @@ -29,6 +21,7 @@ import { processPendingProtocolUrls, } from './protocol-handler'; import { getIsQuiting, setIsLocked } from './shared-state'; +import * as fs from 'fs'; const ICONS_FOLDER = __dirname + '/assets/icons/'; const IS_MAC = process.platform === 'darwin'; @@ -67,6 +60,33 @@ export const startApp = (): void => { // https://github.com/electron/electron/issues/46538#issuecomment-2808806722 app.commandLine.appendSwitch('gtk-version', '3'); + // Defense-in-depth: Force X11 in Snap if the gnome-42-2204 runtime is not + // available or Wayland init fails. The primary fix is the gnome-42-2204 + // plug override in electron-builder.yaml. This code catches edge cases where + // the content snap is not connected or the runtime is missing. + // IMPORTANT: Must run before app.whenReady() — ozone platform is set during + // Chromium initialization and cannot be changed after the ready event fires. + // Users can override with: superproductivity --ozone-platform=wayland + if ( + process.platform === 'linux' && + process.env.SNAP && + !process.argv.some((arg) => arg.includes('--ozone-platform=')) + ) { + const gnomePlatformPath = join(process.env.SNAP || '', 'gnome-platform'); + try { + if ( + !fs.existsSync(gnomePlatformPath) || + fs.readdirSync(gnomePlatformPath).length === 0 + ) { + app.commandLine.appendSwitch('ozone-platform', 'x11'); + log('Snap: gnome-42-2204 runtime not found, forcing X11'); + } + } catch { + app.commandLine.appendSwitch('ozone-platform', 'x11'); + log('Snap: Could not check gnome runtime, forcing X11'); + } + } + // NOTE: needs to be executed before everything else process.argv.forEach((val) => { if (val && val.includes('--disable-tray')) { @@ -147,6 +167,38 @@ export const startApp = (): void => { // APP EVENT LISTENERS // ------------------- + appIN.on('ready', () => { + // Clear GPU cache when Electron version changes to prevent blank/black screens. + // Stale GPU shader caches from old Electron versions cause rendering failures. + // Pattern used by Obsidian's Flatpak wrapper. + if (process.platform === 'linux') { + const userDataPath = app.getPath('userData'); + const versionFile = join(userDataPath, '.electron-version'); + const currentVersion = process.versions.electron; + try { + let lastVersion = ''; + try { + lastVersion = fs.readFileSync(versionFile, 'utf8').trim(); + } catch { + // File doesn't exist on first run + } + if (lastVersion !== currentVersion) { + const gpuCachePath = join(userDataPath, 'GPUCache'); + if (fs.existsSync(gpuCachePath)) { + fs.rmSync(gpuCachePath, { recursive: true, force: true }); + log( + `Cleared GPUCache after Electron upgrade (${lastVersion} -> ${currentVersion})`, + ); + } + fs.mkdirSync(userDataPath, { recursive: true }); + fs.writeFileSync(versionFile, currentVersion); + } + } catch (e) { + log('Failed to check/clear GPU cache:', e); + } + } + }); + appIN.on('ready', () => createMainWin()); appIN.on('ready', () => initBackupAdapter()); appIN.on('ready', () => initLocalFileSyncAdapter()); @@ -289,11 +341,6 @@ export const startApp = (): void => { showOrFocus(mainWin); } }); - - protocol.registerFileProtocol('file', (request, callback) => { - const pathname = decodeURI(request.url.replace('file:///', '')); - callback(pathname); - }); }); appIN.on('will-quit', () => { diff --git a/package-lock.json b/package-lock.json index 7b5ba66be4..bd2521235c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,7 +98,7 @@ "cross-env": "^7.0.3", "detect-it": "^4.0.1", "dotenv": "^17.3.1", - "electron": "37.10.3", + "electron": "41.2.0", "electron-builder": "^26.7.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -15004,15 +15004,15 @@ } }, "node_modules/electron": { - "version": "37.10.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.10.3.tgz", - "integrity": "sha512-3IjCGSjQmH50IbW2PFveaTzK+KwcFX9PEhE7KXb9v5IT8cLAiryAN7qezm/XzODhDRlLu0xKG1j8xWBtZ/bx/g==", + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz", + "integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", + "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -15332,6 +15332,23 @@ "node": ">= 4.0.0" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/elementtree": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", diff --git a/package.json b/package.json index 4cfe9b87f2..075111dd22 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "cross-env": "^7.0.3", "detect-it": "^4.0.1", "dotenv": "^17.3.1", - "electron": "37.10.3", + "electron": "41.2.0", "electron-builder": "^26.7.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -282,7 +282,7 @@ }, "overrides": { "app-builder-lib": { - "minimatch": "10.1.1" + "minimatch": "10.2.5" } }, "optionalDependencies": { diff --git a/src/app/app.constants.ts b/src/app/app.constants.ts index 98138ce6c2..8c3bd870a9 100644 --- a/src/app/app.constants.ts +++ b/src/app/app.constants.ts @@ -3,6 +3,7 @@ import { IS_ANDROID_WEB_VIEW } from './util/is-android-web-view'; export const IS_ELECTRON = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1; // effectively IS_BROWSER export const IS_WEB_BROWSER = !IS_ELECTRON && !IS_ANDROID_WEB_VIEW; +export const IS_GNOME_DESKTOP = IS_ELECTRON && window.ea.isGnomeDesktop(); export const TRACKING_INTERVAL = 1000; diff --git a/src/app/core/theme/global-theme.service.ts b/src/app/core/theme/global-theme.service.ts index 41ba7517d5..0239b8dcbd 100644 --- a/src/app/core/theme/global-theme.service.ts +++ b/src/app/core/theme/global-theme.service.ts @@ -9,7 +9,7 @@ import { untracked, } from '@angular/core'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; -import { BodyClass, IS_ELECTRON } from '../../app.constants'; +import { BodyClass, IS_ELECTRON, IS_GNOME_DESKTOP } from '../../app.constants'; import { IS_MAC } from '../../util/is-mac'; import { distinctUntilChanged, map, startWith, switchMap, take } from 'rxjs/operators'; import { IS_TOUCH_ONLY } from '../../util/is-touch-only'; @@ -79,6 +79,11 @@ export class GlobalThemeService { private _keyboardListenerHandles: PluginListenerHandle[] = []; private _focusinListener: ((event: FocusEvent) => void) | null = null; + private _isCustomWindowTitleBarEnabled(): boolean { + const misc = this._globalConfigService.misc(); + return misc?.isUseCustomWindowTitleBar ?? !IS_GNOME_DESKTOP; + } + darkMode = signal( (localStorage.getItem(LS.DARK_MODE) as DarkModeCfg) || 'system', ); @@ -269,10 +274,12 @@ export class GlobalThemeService { if (IS_ELECTRON && !IS_MAC) { effect(() => { const isDark = this.isDarkTheme(); - // Use untracked to prevent reading misc from creating a dependency - const misc = untracked(() => this._globalConfigService.misc()); + // Use untracked to prevent creating additional dependencies in this effect + const isCustomWindowTitleBarEnabled = untracked(() => + this._isCustomWindowTitleBarEnabled(), + ); // Only update if custom window title bar is enabled - if (misc?.isUseCustomWindowTitleBar !== false) { + if (isCustomWindowTitleBarEnabled) { window.ea.updateTitleBarDarkMode(isDark); } }); @@ -359,8 +366,7 @@ export class GlobalThemeService { }); effect(() => { - const misc = this._globalConfigService.misc(); - if (misc?.isUseCustomWindowTitleBar !== false) { + if (this._isCustomWindowTitleBarEnabled()) { this.document.body.classList.add(BodyClass.isObsidianStyleHeader); } else { this.document.body.classList.remove(BodyClass.isObsidianStyleHeader); diff --git a/src/app/features/config/form-cfgs/misc-settings-form.const.ts b/src/app/features/config/form-cfgs/misc-settings-form.const.ts index 091e1b394f..f4b4b5505c 100644 --- a/src/app/features/config/form-cfgs/misc-settings-form.const.ts +++ b/src/app/features/config/form-cfgs/misc-settings-form.const.ts @@ -4,7 +4,7 @@ import { MiscConfig, } from '../global-config.model'; import { T } from '../../../t.const'; -import { IS_ELECTRON } from '../../../app.constants'; +import { IS_ELECTRON, IS_GNOME_DESKTOP } from '../../../app.constants'; export const MISC_SETTINGS_FORM_CFG: ConfigFormSection = { title: T.GCF.MISC.TITLE, @@ -90,7 +90,7 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection = { label: T.GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN, }, }, - ...((IS_ELECTRON + ...((IS_ELECTRON && !IS_GNOME_DESKTOP ? [ { key: 'isUseCustomWindowTitleBar',