From 79133f90462748d6e11e86faeb1d02fbe973e96a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 19 May 2026 15:28:06 +0200 Subject: [PATCH] feat(electron): suffix version string per distribution target Append a short channel marker to the (display-only) version string so bug reports and the config footer reveal which build a user runs, e.g.: win nsis W portable P MS Store MS mac dmg D App Store MAS linux AppImage AI snap SN flatpak FP deb/rpm L android Play A F-Droid AF ios I web WB Desktop channels are detected in the Electron process (process.platform, process.mas/windowsStore, APPIMAGE/SNAP/FLATPAK_ID) via a shared helper reused by the tray-GUID logic and exposed sync over the preload bridge (no IPC, getAppVersionStr stays synchronous). Mobile/web are detected in the frontend. deb vs rpm is not distinguishable at runtime, so both report L. No caller semver-compares this string. --- electron/electronAPI.d.ts | 5 ++ electron/indicator.ts | 6 +- electron/preload.ts | 8 ++ .../shared-with-frontend/get-dist-channel.ts | 51 ++++++++++++ src/app/util/get-app-version-str.spec.ts | 39 +++++++++ src/app/util/get-app-version-str.ts | 82 +++++++++++++++++-- 6 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 electron/shared-with-frontend/get-dist-channel.ts create mode 100644 src/app/util/get-app-version-str.spec.ts diff --git a/electron/electronAPI.d.ts b/electron/electronAPI.d.ts index b230fee2aa..bda757ddb8 100644 --- a/electron/electronAPI.d.ts +++ b/electron/electronAPI.d.ts @@ -19,6 +19,7 @@ import { LocalRestApiRequestPayload, LocalRestApiResponsePayload, } from './shared-with-frontend/local-rest-api.model'; +import { ElectronDistChannel } from './shared-with-frontend/get-dist-channel'; export interface ElectronAPI { on( @@ -26,6 +27,10 @@ export interface ElectronAPI { listener: (event: IpcRendererEvent, ...args: unknown[]) => void, ): void; + // SYNC + // ---- + getDistChannel(): ElectronDistChannel | null; + // INVOKE // ------ getUserDataPath(): Promise; diff --git a/electron/indicator.ts b/electron/indicator.ts index 5c46a8dfff..efdd2c040c 100644 --- a/electron/indicator.ts +++ b/electron/indicator.ts @@ -9,6 +9,7 @@ import { } from 'electron'; import { log } from 'electron-log/main'; import { IPC } from './shared-with-frontend/ipc-events.const'; +import { getDistChannel } from './shared-with-frontend/get-dist-channel'; import { getIsTrayShowCurrentTask, getIsTrayShowCurrentCountdown } from './shared-state'; import { TaskCopy } from '../src/app/features/tasks/task.model'; import { release } from 'os'; @@ -76,10 +77,11 @@ const WINDOWS_TRAY_GUIDS = { } as const; const getWindowsTrayGuid = (): string => { - if (process.env.PORTABLE_EXECUTABLE_DIR) { + const channel = getDistChannel(); + if (channel === 'win-portable') { return WINDOWS_TRAY_GUIDS.portable; } - if ((process as NodeJS.Process & { windowsStore?: boolean }).windowsStore) { + if (channel === 'win-store') { return WINDOWS_TRAY_GUIDS.store; } return WINDOWS_TRAY_GUIDS.nsis; diff --git a/electron/preload.ts b/electron/preload.ts index 4b62a95238..f7ba75d213 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -7,6 +7,10 @@ import { } from 'electron'; import { ElectronAPI } from './electronAPI.d'; import { IPC, IPCEventValue } from './shared-with-frontend/ipc-events.const'; +import { + getDistChannel, + ElectronDistChannel, +} from './shared-with-frontend/get-dist-channel'; import { LocalBackupMeta } from '../src/app/imex/local-backup/local-backup.model'; import { PluginManifest, @@ -52,6 +56,10 @@ const ea: ElectronAPI = { // NOTE: there is no proper way to unsubscribe apart from unsubscribing all ipcRenderer.on(channel, listener); }, + // SYNC + // ---- + getDistChannel: (): ElectronDistChannel | null => getDistChannel(), + // INVOKE // ------ getUserDataPath: () => _invoke('GET_PATH', 'userData') as Promise, diff --git a/electron/shared-with-frontend/get-dist-channel.ts b/electron/shared-with-frontend/get-dist-channel.ts new file mode 100644 index 0000000000..77e1805cbb --- /dev/null +++ b/electron/shared-with-frontend/get-dist-channel.ts @@ -0,0 +1,51 @@ +/** + * Distribution channels detectable from the Electron process. + * Mobile (android/ios) and plain web are determined in the frontend instead. + */ +export type ElectronDistChannel = + | 'win-nsis' + | 'win-portable' + | 'win-store' + | 'mac-dmg' + | 'mac-store' + | 'linux-appimage' + | 'linux-snap' + | 'linux-flatpak' + | 'linux-native'; + +/** + * Detects which distribution channel the running Electron app belongs to. + * Returns `null` on unknown platforms. + * + * Shared between the Electron main process (tray GUID selection) and the + * preload bridge (channel suffix appended to the diagnostic version string). + * Safe to call from preload: only reads `process` / `process.env`. + * + * Note: deb vs rpm cannot be told apart at runtime (electron-builder sets no + * env var for native packages), so both report `linux-native`. + */ +export const getDistChannel = (): ElectronDistChannel | null => { + const p = process as NodeJS.Process & { windowsStore?: boolean; mas?: boolean }; + switch (process.platform) { + case 'win32': + if (process.env.PORTABLE_EXECUTABLE_DIR) { + return 'win-portable'; + } + return p.windowsStore ? 'win-store' : 'win-nsis'; + case 'darwin': + return p.mas ? 'mac-store' : 'mac-dmg'; + case 'linux': + if (process.env.APPIMAGE) { + return 'linux-appimage'; + } + if (process.env.SNAP) { + return 'linux-snap'; + } + if (process.env.FLATPAK_ID) { + return 'linux-flatpak'; + } + return 'linux-native'; + default: + return null; + } +}; diff --git a/src/app/util/get-app-version-str.spec.ts b/src/app/util/get-app-version-str.spec.ts new file mode 100644 index 0000000000..521f8e30c3 --- /dev/null +++ b/src/app/util/get-app-version-str.spec.ts @@ -0,0 +1,39 @@ +import { DistChannel, distChannelSuffix, getAppVersionStr } from './get-app-version-str'; +import { environment } from '../../environments/environment'; + +describe('distChannelSuffix', () => { + const cases: [DistChannel, string][] = [ + ['win-nsis', 'W'], + ['win-portable', 'P'], + ['win-store', 'MS'], + ['mac-dmg', 'D'], + ['mac-store', 'MAS'], + ['linux-appimage', 'AI'], + ['linux-snap', 'SN'], + ['linux-flatpak', 'FP'], + ['linux-native', 'L'], + ['android-play', 'A'], + ['android-fdroid', 'AF'], + ['ios', 'I'], + ['web', 'WB'], + ]; + + cases.forEach(([channel, suffix]) => { + it(`maps ${channel} -> "${suffix}"`, () => { + expect(distChannelSuffix(channel)).toBe(suffix); + }); + }); + + it('maps null/undefined -> "" (no suffix)', () => { + expect(distChannelSuffix(null)).toBe(''); + expect(distChannelSuffix(undefined)).toBe(''); + }); +}); + +describe('getAppVersionStr', () => { + // In the Karma/browser env IS_ELECTRON, IS_IOS and IS_ANDROID_WEB_VIEW are + // all false, so the channel resolves to web -> "WB". + it('appends the web suffix in a browser context', () => { + expect(getAppVersionStr()).toBe(`${environment.version}WB`); + }); +}); diff --git a/src/app/util/get-app-version-str.ts b/src/app/util/get-app-version-str.ts index eafbd001cb..fd2089e8fb 100644 --- a/src/app/util/get-app-version-str.ts +++ b/src/app/util/get-app-version-str.ts @@ -1,9 +1,81 @@ -import { IS_ANDROID_WEB_VIEW } from './is-android-web-view'; +import { IS_ANDROID_WEB_VIEW, IS_F_DROID_APP } from './is-android-web-view'; +import { IS_IOS } from './is-ios'; +import { IS_ELECTRON } from '../app.constants'; import { androidInterface } from '../features/android/android-interface'; import { environment } from '../../environments/environment'; -export const getAppVersionStr = (): string => { - const b = - (IS_ANDROID_WEB_VIEW && androidInterface?.getVersion?.()) || environment.version; - return IS_ANDROID_WEB_VIEW ? `${b}A` : b; +/** + * Every distribution target the app ships to. Mobile/web are detected in the + * frontend; desktop ones come from the Electron `getDistChannel()` bridge. + */ +export type DistChannel = + | 'win-nsis' + | 'win-portable' + | 'win-store' + | 'mac-dmg' + | 'mac-store' + | 'linux-appimage' + | 'linux-snap' + | 'linux-flatpak' + | 'linux-native' + | 'android-play' + | 'android-fdroid' + | 'ios' + | 'web'; + +/** + * Channel marker appended to the (display-only) version string so bug reports + * and the config footer reveal which build a user runs, e.g. `18.6.0AI` for + * the Linux AppImage. Display-only: no caller does a semver compare on this. + */ +export const distChannelSuffix = (channel: DistChannel | null | undefined): string => { + switch (channel) { + case 'win-nsis': + return 'W'; + case 'win-portable': + return 'P'; + case 'win-store': + return 'MS'; + case 'mac-dmg': + return 'D'; + case 'mac-store': + return 'MAS'; + case 'linux-appimage': + return 'AI'; + case 'linux-snap': + return 'SN'; + case 'linux-flatpak': + return 'FP'; + case 'linux-native': + return 'L'; + case 'android-play': + return 'A'; + case 'android-fdroid': + return 'AF'; + case 'ios': + return 'I'; + case 'web': + return 'WB'; + default: + return ''; + } +}; + +const detectChannel = (): DistChannel => { + if (IS_IOS) { + return 'ios'; + } + if (IS_ANDROID_WEB_VIEW) { + return IS_F_DROID_APP ? 'android-fdroid' : 'android-play'; + } + if (IS_ELECTRON && typeof window !== 'undefined') { + return window.ea?.getDistChannel?.() ?? 'web'; + } + return 'web'; +}; + +export const getAppVersionStr = (): string => { + const base = + (IS_ANDROID_WEB_VIEW && androidInterface?.getVersion?.()) || environment.version; + return `${base}${distChannelSuffix(detectChannel())}`; };