This commit introduces native operating system file dialogs in the desktop application replacing the existing web-based dialogs. It lays the foundation for future enhancements such as: - Providing error messages when saving or executing files, addressing #264. - Creating system restore points, addressing #50. Documentation updates: - Update `desktop-vs-web-features.md` with added functionality. - Update `README.md` with security feature highlights. - Update home page documentation to emphasize security features. Other supporting changes include: - Integrate IPC communication channels for secure Electron dialog API interactions. - Refactor `IpcRegistration` for more type-safety and simplicity. - Introduce a Vue hook to encapsulate dialog functionality. - Improve errors during IPC registration for easier troubleshooting. - Move `ClientLoggerFactory` for consistency in hooks organization and remove `LoggerFactory` interface for simplicity. - Add tests for the save file dialog in the browser context. - Add `Blob` polyfill in tests to compensate for the missing `blob.text()` function in `jsdom` (see jsdom/jsdom#2555). Improve environment detection logic: - Treat test environment as browser environments to correctly activate features based on the environment. This resolves issues where the environment is misidentified as desktop, but Electron preloader APIs are missing. - Rename `isDesktop` environment identification variable to `isRunningAsDesktopApplication` for better clarity and to avoid confusion with desktop environments in web/browser/test environments. - Simplify `BrowserRuntimeEnvironment` to consistently detect non-desktop application environments. - Improve environment detection for Electron main process (electron/electron#2288).
This commit is contained in:
@@ -84,4 +84,23 @@ export const BrowserConditions: readonly BrowserCondition[] = [
|
||||
notExistingPartsInUserAgent: ['like Mac OS X'], // Eliminate iOS and iPadOS for Safari
|
||||
touchSupport: TouchSupportExpectation.MustNotExist, // Distinguish from iPadOS for Safari
|
||||
},
|
||||
...generateJsdomBrowserConditions(),
|
||||
] as const;
|
||||
|
||||
function generateJsdomBrowserConditions(): readonly BrowserCondition[] {
|
||||
// jsdom user agent format: `Mozilla/5.0 (${process.platform || "unknown OS"}) ...` (https://archive.ph/2023.02.14-193200/https://github.com/jsdom/jsdom#advanced-configuration)
|
||||
const operatingSystemPlatformMap: Partial<Record<
|
||||
OperatingSystem,
|
||||
NodeJS.Platform> // Enforce right platform constants at compile time
|
||||
> = {
|
||||
[OperatingSystem.Linux]: 'linux',
|
||||
[OperatingSystem.Windows]: 'win32',
|
||||
[OperatingSystem.macOS]: 'darwin',
|
||||
} as const;
|
||||
return Object
|
||||
.entries(operatingSystemPlatformMap)
|
||||
.map(([operatingSystemKey, platformString]): BrowserCondition => ({
|
||||
operatingSystem: Number(operatingSystemKey),
|
||||
existingPartsInSameUserAgent: ['jsdom', platformString],
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { RuntimeEnvironment } from '../RuntimeEnvironment';
|
||||
@@ -8,7 +7,7 @@ import { BrowserEnvironment, BrowserOsDetector } from './BrowserOs/BrowserOsDete
|
||||
import { isTouchEnabledDevice } from './TouchSupportDetection';
|
||||
|
||||
export class BrowserRuntimeEnvironment implements RuntimeEnvironment {
|
||||
public readonly isDesktop: boolean;
|
||||
public readonly isRunningAsDesktopApplication: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
@@ -18,31 +17,34 @@ export class BrowserRuntimeEnvironment implements RuntimeEnvironment {
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: BrowserOsDetector = new ConditionBasedOsDetector(),
|
||||
touchDetector = isTouchEnabledDevice,
|
||||
touchDetector: TouchDetector = isTouchEnabledDevice,
|
||||
) {
|
||||
if (!window) { throw new Error('missing window'); } // do not trust strictNullChecks for global objects
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isDesktop = isDesktop(window);
|
||||
if (this.isDesktop) {
|
||||
this.os = window?.os;
|
||||
} else {
|
||||
this.os = undefined;
|
||||
const userAgent = getUserAgent(window);
|
||||
if (userAgent) {
|
||||
const browserEnvironment: BrowserEnvironment = {
|
||||
userAgent,
|
||||
isTouchSupported: touchDetector(),
|
||||
};
|
||||
this.os = browserOsDetector.detect(browserEnvironment);
|
||||
}
|
||||
}
|
||||
this.isRunningAsDesktopApplication = isElectronRendererProcess(window);
|
||||
this.os = determineOperatingSystem(window, touchDetector, browserOsDetector);
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(window: Partial<Window>): string | undefined {
|
||||
return window?.navigator?.userAgent;
|
||||
function isElectronRendererProcess(globalWindow: Partial<Window>): boolean {
|
||||
return globalWindow.isRunningAsDesktopApplication === true; // Preloader injects this
|
||||
// We could also do `globalWindow?.navigator?.userAgent?.includes('Electron') === true;`
|
||||
}
|
||||
|
||||
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||
return window?.isDesktop === true;
|
||||
function determineOperatingSystem(
|
||||
globalWindow: Partial<Window>,
|
||||
touchDetector: TouchDetector,
|
||||
browserOsDetector: BrowserOsDetector,
|
||||
): OperatingSystem | undefined {
|
||||
const userAgent = globalWindow?.navigator?.userAgent;
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
const browserEnvironment: BrowserEnvironment = {
|
||||
userAgent,
|
||||
isTouchSupported: touchDetector(),
|
||||
};
|
||||
return browserOsDetector.detect(browserEnvironment);
|
||||
}
|
||||
|
||||
type TouchDetector = () => boolean;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RuntimeEnvironment } from '../RuntimeEnvironment';
|
||||
import { convertPlatformToOs } from './NodeOsMapper';
|
||||
|
||||
export class NodeRuntimeEnvironment implements RuntimeEnvironment {
|
||||
public readonly isDesktop: boolean;
|
||||
public readonly isRunningAsDesktopApplication: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
@@ -14,7 +14,7 @@ export class NodeRuntimeEnvironment implements RuntimeEnvironment {
|
||||
convertToOs: PlatformToOperatingSystemConverter = convertPlatformToOs,
|
||||
) {
|
||||
if (!nodeProcess) { throw new Error('missing process'); } // do not trust strictNullChecks for global objects
|
||||
this.isDesktop = true;
|
||||
this.isRunningAsDesktopApplication = true;
|
||||
this.os = convertToOs(nodeProcess.platform);
|
||||
this.isNonProduction = nodeProcess.env.NODE_ENV !== 'production'; // populated by Vite
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface RuntimeEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly isRunningAsDesktopApplication: boolean;
|
||||
readonly os: OperatingSystem | undefined;
|
||||
readonly isNonProduction: boolean;
|
||||
}
|
||||
|
||||
@@ -3,32 +3,47 @@ import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
|
||||
import { RuntimeEnvironment } from './RuntimeEnvironment';
|
||||
|
||||
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment({
|
||||
getGlobalWindow: () => globalThis.window,
|
||||
getGlobalProcess: () => globalThis.process,
|
||||
window: globalThis.window,
|
||||
process: globalThis.process,
|
||||
});
|
||||
|
||||
export function determineAndCreateRuntimeEnvironment(
|
||||
globalAccessor: GlobalAccessor,
|
||||
globalAccessor: GlobalPropertiesAccessor,
|
||||
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
|
||||
window,
|
||||
) => new BrowserRuntimeEnvironment(window),
|
||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = () => new NodeRuntimeEnvironment(),
|
||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = (
|
||||
process: NodeJS.Process,
|
||||
) => new NodeRuntimeEnvironment(process),
|
||||
): RuntimeEnvironment {
|
||||
if (globalAccessor.getGlobalProcess()) {
|
||||
return nodeEnvironmentFactory();
|
||||
if (isElectronMainProcess(globalAccessor.process)) {
|
||||
return nodeEnvironmentFactory(globalAccessor.process);
|
||||
}
|
||||
const window = globalAccessor.getGlobalWindow();
|
||||
if (window) {
|
||||
return browserEnvironmentFactory(window);
|
||||
const { window } = globalAccessor;
|
||||
if (!window) {
|
||||
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
|
||||
}
|
||||
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a Node.js environment.');
|
||||
return browserEnvironmentFactory(window);
|
||||
}
|
||||
|
||||
function isElectronMainProcess(
|
||||
nodeProcess: NodeJS.Process | undefined,
|
||||
): nodeProcess is NodeJS.Process {
|
||||
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
|
||||
if (!nodeProcess) {
|
||||
return false;
|
||||
}
|
||||
if (nodeProcess.versions.electron) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
|
||||
|
||||
export type NodeRuntimeEnvironmentFactory = () => NodeRuntimeEnvironment;
|
||||
export type NodeRuntimeEnvironmentFactory = (process: NodeJS.Process) => NodeRuntimeEnvironment;
|
||||
|
||||
export interface GlobalAccessor {
|
||||
getGlobalWindow(): Window | undefined;
|
||||
getGlobalProcess(): NodeJS.Process | undefined;
|
||||
export interface GlobalPropertiesAccessor {
|
||||
readonly window: Window | undefined;
|
||||
readonly process: NodeJS.Process | undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user