chore: update electron to v41.x (#7097)

* 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 <johannes.millan@gmail.com>
This commit is contained in:
Leandro Marques 2026-04-17 13:12:43 -03:00 committed by GitHub
parent 5787437a9a
commit 179700ccda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 169 additions and 51 deletions

View file

@ -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

View file

@ -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'));

View file

@ -86,6 +86,8 @@ export interface ElectronAPI {
isLinux(): boolean;
isGnomeDesktop(): boolean;
isMacOS(): boolean;
isSnap(): boolean;

View file

@ -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
}`,

View file

@ -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

View file

@ -25,6 +25,25 @@ const _invoke: (channel: IPCEventValue, ...args: unknown[]) => Promise<unknown>
...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,

View file

@ -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', () => {

27
package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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;

View file

@ -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<DarkModeCfg>(
(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);

View file

@ -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<MiscConfig> = {
title: T.GCF.MISC.TITLE,
@ -90,7 +90,7 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection<MiscConfig> = {
label: T.GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN,
},
},
...((IS_ELECTRON
...((IS_ELECTRON && !IS_GNOME_DESKTOP
? [
{
key: 'isUseCustomWindowTitleBar',