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:
@@ -19,10 +19,14 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState);
|
||||
const { os, isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
||||
|
||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||
const canRun = computed<boolean>(() => getCanRunState(
|
||||
currentState.value.os,
|
||||
isRunningAsDesktopApplication,
|
||||
os,
|
||||
));
|
||||
|
||||
async function executeCode() {
|
||||
if (!codeRunner) { throw new Error('missing code runner'); }
|
||||
@@ -33,7 +37,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktopVersion: isDesktop,
|
||||
canRun,
|
||||
executeCode,
|
||||
};
|
||||
@@ -42,10 +45,10 @@ export default defineComponent({
|
||||
|
||||
function getCanRunState(
|
||||
selectedOs: OperatingSystem,
|
||||
isDesktopVersion: boolean,
|
||||
isRunningAsDesktopApplication: boolean,
|
||||
hostOs: OperatingSystem | undefined,
|
||||
): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<IconButton
|
||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
||||
:text="isRunningAsDesktopApplication ? 'Save' : 'Download'"
|
||||
:icon-name="isRunningAsDesktopApplication ? 'floppy-disk' : 'file-arrow-down'"
|
||||
@click="saveCode"
|
||||
/>
|
||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||
@@ -16,12 +16,11 @@ import {
|
||||
defineComponent, ref, computed,
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptFileName } from '@/application/CodeRunner/ScriptFileName';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import InstructionList from './Instructions/InstructionList.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
@@ -35,7 +34,8 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
@@ -44,13 +44,17 @@ export default defineComponent({
|
||||
fileName.value,
|
||||
));
|
||||
|
||||
function saveCode() {
|
||||
saveCodeToDisk(fileName.value, currentState.value);
|
||||
async function saveCode() {
|
||||
await dialog.saveFile(
|
||||
currentState.value.code.current,
|
||||
fileName.value,
|
||||
getType(currentState.value.collection.scripting.language),
|
||||
);
|
||||
areInstructionsVisible.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktopVersion: isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
instructions,
|
||||
fileName,
|
||||
areInstructionsVisible,
|
||||
@@ -59,12 +63,6 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
SaveFileDialog.saveFile(content, fileName, type);
|
||||
}
|
||||
|
||||
function getType(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile:
|
||||
|
||||
@@ -188,6 +188,7 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
||||
.appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
|
||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||
.appendCommentLine(' ✔️ Committed to your safety with strong security measures.')
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog';
|
||||
|
||||
export function determineDialogBasedOnEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog,
|
||||
browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(),
|
||||
): Dialog {
|
||||
if (!environment.isRunningAsDesktopApplication) {
|
||||
return browserDialogFactory();
|
||||
}
|
||||
const dialog = windowInjectedDialogFactory();
|
||||
if (!dialog) {
|
||||
throw new Error([
|
||||
'The Dialog API could not be retrieved from the window object.',
|
||||
'This may indicate that the Dialog API is either not implemented or not correctly exposed in the current desktop environment.',
|
||||
].join('\n'));
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
export type WindowDialogCreationFunction = () => Dialog | undefined;
|
||||
|
||||
export type BrowserDialogCreationFunction = () => Dialog;
|
||||
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { determineDialogBasedOnEnvironment } from './ClientDialogFactory';
|
||||
|
||||
export function useDialog(
|
||||
factory: DialogFactory = () => determineDialogBasedOnEnvironment(CurrentEnvironment),
|
||||
) {
|
||||
const dialog = factory();
|
||||
return {
|
||||
dialog,
|
||||
};
|
||||
}
|
||||
|
||||
export type DialogFactory = () => Dialog;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
export class ClientLoggerFactory implements LoggerFactory {
|
||||
public static readonly Current: LoggerFactory = new ClientLoggerFactory();
|
||||
|
||||
public readonly logger: Logger;
|
||||
|
||||
protected constructor(
|
||||
environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
windowAccessor: WindowAccessor = () => globalThis.window,
|
||||
noopLoggerFactory: LoggerCreationFunction = () => new NoopLogger(),
|
||||
windowInjectedLoggerFactory: LoggerCreationFunction = () => new WindowInjectedLogger(),
|
||||
consoleLoggerFactory: LoggerCreationFunction = () => new ConsoleLogger(),
|
||||
) {
|
||||
if (isUnitOrIntegrationTests(environment, windowAccessor)) {
|
||||
this.logger = noopLoggerFactory(); // keep the test outputs clean
|
||||
return;
|
||||
}
|
||||
if (environment.isRunningAsDesktopApplication) {
|
||||
this.logger = windowInjectedLoggerFactory();
|
||||
return;
|
||||
}
|
||||
if (environment.isNonProduction) {
|
||||
this.logger = consoleLoggerFactory();
|
||||
return;
|
||||
}
|
||||
this.logger = noopLoggerFactory();
|
||||
}
|
||||
}
|
||||
|
||||
export type WindowAccessor = () => OptionalWindow;
|
||||
|
||||
export type LoggerCreationFunction = () => Logger;
|
||||
|
||||
type OptionalWindow = Window | undefined | null;
|
||||
|
||||
function isUnitOrIntegrationTests(
|
||||
environment: RuntimeEnvironment,
|
||||
windowAccessor: WindowAccessor,
|
||||
): boolean {
|
||||
/*
|
||||
In a desktop application context, Electron preloader process inject a logger into
|
||||
the global window object. If we're in a desktop (Node) environment and the logger isn't
|
||||
injected, it indicates a testing environment.
|
||||
*/
|
||||
return environment.isRunningAsDesktopApplication && !windowAccessor()?.log;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
|
||||
export interface LoggerFactory {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ClientLoggerFactory } from './ClientLoggerFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="privacy-policy">
|
||||
<div v-if="!isDesktop" class="line">
|
||||
<div v-if="!isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
🚫🍪
|
||||
</div>
|
||||
<div>No cookies!</div>
|
||||
</div>
|
||||
<div v-if="isDesktop" class="line">
|
||||
<div v-if="isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
🚫🌐
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
of the <a :href="repositoryUrl" target="_blank" rel="noopener noreferrer">source code</a> with no changes.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isDesktop" class="line">
|
||||
<div v-if="!isRunningAsDesktopApplication" class="line">
|
||||
<div class="line__emoji">
|
||||
📈
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ import { injectKey } from '@/presentation/injectionSymbols';
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { info } = injectKey((keys) => keys.useApplication);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
|
||||
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||
@@ -68,7 +68,7 @@ export default defineComponent({
|
||||
return {
|
||||
repositoryUrl,
|
||||
feedbackUrl,
|
||||
isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="footer">
|
||||
<div class="footer__section">
|
||||
<span v-if="isDesktop" class="footer__section__item">
|
||||
<span v-if="isRunningAsDesktopApplication" class="footer__section__item">
|
||||
<AppIcon class="icon" icon="globe" />
|
||||
<span>
|
||||
Online version at <a :href="homepageUrl" target="_blank" rel="noopener noreferrer">{{ homepageUrl }}</a>
|
||||
@@ -68,7 +68,7 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { info } = injectKey((keys) => keys.useApplication);
|
||||
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
|
||||
const isPrivacyDialogVisible = ref(false);
|
||||
|
||||
@@ -87,7 +87,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
isDesktop,
|
||||
isRunningAsDesktopApplication,
|
||||
isPrivacyDialogVisible,
|
||||
showPrivacyDialog,
|
||||
version,
|
||||
|
||||
Reference in New Issue
Block a user