diff --git a/README.md b/README.md index da10bef4..12ce51ea 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ For a detailed comparison of features between the desktop and web versions of pr - **Transparent**. Have full visibility into what the tweaks do as you enable them. - **Reversible**. Revert if something feels wrong. - **Accessible**. No need to run any compiled software on your computer with web version. +- **Secure**: Security is a top priority at privacy.sexy with comprehensive safeguards in place. [Learn more](./SECURITY.md). - **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere). - **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features. - **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md). diff --git a/docs/desktop-vs-web-features.md b/docs/desktop-vs-web-features.md index a9f3f8f4..b8698adb 100644 --- a/docs/desktop-vs-web-features.md +++ b/docs/desktop-vs-web-features.md @@ -53,9 +53,16 @@ Log file locations vary by operating system: The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience. This direct execution capability isn't available in the web version due to inherent browser restrictions. +**Logging and storage:** + For enhanced auditability and easier troubleshooting, the desktop version keeps a record of executed scripts in designated directories. These locations vary based on the operating system: - macOS: `$HOME/Library/Application Support/privacy.sexy/runs` - Linux: `$HOME/.config/privacy.sexy/runs` - Windows: `%APPDATA%\privacy.sexy\runs` + +**Native file system dialogs:** + +The desktop version uses native system file save dialogs, offering more features and reliability compared to the browser's file system dialogs. +These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities. diff --git a/src/infrastructure/Dialog/Browser/BrowserDialog.ts b/src/infrastructure/Dialog/Browser/BrowserDialog.ts new file mode 100644 index 00000000..10cb28d0 --- /dev/null +++ b/src/infrastructure/Dialog/Browser/BrowserDialog.ts @@ -0,0 +1,19 @@ +import { Dialog, FileType } from '@/presentation/common/Dialog'; +import { FileSaverDialog } from './FileSaverDialog'; +import { BrowserSaveFileDialog } from './BrowserSaveFileDialog'; + +export class BrowserDialog implements Dialog { + constructor(private readonly saveFileDialog: BrowserSaveFileDialog = new FileSaverDialog()) { + + } + + public saveFile( + fileContents: string, + fileName: string, + type: FileType, + ): Promise { + return Promise.resolve( + this.saveFileDialog.saveFile(fileContents, fileName, type), + ); + } +} diff --git a/src/infrastructure/Dialog/Browser/BrowserSaveFileDialog.ts b/src/infrastructure/Dialog/Browser/BrowserSaveFileDialog.ts new file mode 100644 index 00000000..c64f2da2 --- /dev/null +++ b/src/infrastructure/Dialog/Browser/BrowserSaveFileDialog.ts @@ -0,0 +1,9 @@ +import { FileType } from '@/presentation/common/Dialog'; + +export interface BrowserSaveFileDialog { + saveFile( + fileContents: string, + fileName: string, + fileType: FileType, + ): void; +} diff --git a/src/infrastructure/Dialog/Browser/FileSaverDialog.ts b/src/infrastructure/Dialog/Browser/FileSaverDialog.ts new file mode 100644 index 00000000..12639f04 --- /dev/null +++ b/src/infrastructure/Dialog/Browser/FileSaverDialog.ts @@ -0,0 +1,39 @@ +import fileSaver from 'file-saver'; +import { FileType } from '@/presentation/common/Dialog'; +import { BrowserSaveFileDialog } from './BrowserSaveFileDialog'; + +export type SaveAsFunction = (data: Blob, filename?: string) => void; + +export type WindowOpenFunction = (url: string, target: string, features: string) => void; + +export class FileSaverDialog implements BrowserSaveFileDialog { + constructor( + private readonly fileSaverSaveAs: SaveAsFunction = fileSaver.saveAs, + private readonly windowOpen: WindowOpenFunction = window.open.bind(window), + ) { } + + public saveFile( + fileContents: string, + fileName: string, + fileType: FileType, + ): void { + const mimeType = MimeTypes[fileType]; + this.saveBlob(fileContents, mimeType, fileName); + } + + private saveBlob(file: BlobPart, mimeType: string, fileName: string): void { + try { + const blob = new Blob([file], { type: mimeType }); + this.fileSaverSaveAs(blob, fileName); + } catch (e) { + this.windowOpen(`data:${mimeType},${encodeURIComponent(file.toString())}`, '_blank', ''); + } + } +} + +const MimeTypes: Record = { + // Some browsers (including firefox + IE) require right mime type + // otherwise they ignore extension and save the file as text. + [FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file + [FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ +} as const; diff --git a/src/infrastructure/Dialog/Electron/ElectronDialog.ts b/src/infrastructure/Dialog/Electron/ElectronDialog.ts new file mode 100644 index 00000000..cbcde8c3 --- /dev/null +++ b/src/infrastructure/Dialog/Electron/ElectronDialog.ts @@ -0,0 +1,17 @@ +import { Dialog, FileType } from '@/presentation/common/Dialog'; +import { NodeElectronSaveFileDialog } from './NodeElectronSaveFileDialog'; +import { ElectronSaveFileDialog } from './ElectronSaveFileDialog'; + +export class ElectronDialog implements Dialog { + constructor( + private readonly fileSaveDialog: ElectronSaveFileDialog = new NodeElectronSaveFileDialog(), + ) { } + + public async saveFile( + fileContents: string, + fileName: string, + type: FileType, + ): Promise { + await this.fileSaveDialog.saveFile(fileContents, fileName, type); + } +} diff --git a/src/infrastructure/Dialog/Electron/ElectronSaveFileDialog.ts b/src/infrastructure/Dialog/Electron/ElectronSaveFileDialog.ts new file mode 100644 index 00000000..6e35fd52 --- /dev/null +++ b/src/infrastructure/Dialog/Electron/ElectronSaveFileDialog.ts @@ -0,0 +1,9 @@ +import { FileType } from '@/presentation/common/Dialog'; + +export interface ElectronSaveFileDialog { + saveFile( + fileContents: string, + fileName: string, + type: FileType, + ): Promise; +} diff --git a/src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts b/src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts new file mode 100644 index 00000000..2753caac --- /dev/null +++ b/src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts @@ -0,0 +1,98 @@ +import { join } from 'node:path'; +import { writeFile } from 'node:fs/promises'; +import { app, dialog } from 'electron/main'; +import { Logger } from '@/application/Common/Log/Logger'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { FileType } from '@/presentation/common/Dialog'; +import { ElectronSaveFileDialog } from './ElectronSaveFileDialog'; + +export interface ElectronFileDialogOperations { + getUserDownloadsPath(): string; + showSaveDialog(options: Electron.SaveDialogOptions): Promise; +} + +export interface NodeFileOperations { + readonly join: typeof join; + writeFile(file: string, data: string): Promise; +} + +export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog { + constructor( + private readonly logger: Logger = ElectronLogger, + private readonly electron: ElectronFileDialogOperations = { + getUserDownloadsPath: () => app.getPath('downloads'), + showSaveDialog: dialog.showSaveDialog.bind(dialog), + }, + private readonly node: NodeFileOperations = { + join, + writeFile, + }, + ) { } + + public async saveFile( + fileContents: string, + fileName: string, + type: FileType, + ): Promise { + const userSelectedFilePath = await this.showSaveFileDialog(fileName, type); + if (!userSelectedFilePath) { + this.logger.info(`File save cancelled by user: ${fileName}`); + return; + } + await this.writeFile(userSelectedFilePath, fileContents); + } + + private async writeFile(filePath: string, fileContents: string): Promise { + try { + this.logger.info(`Saving file: ${filePath}`); + await this.node.writeFile(filePath, fileContents); + this.logger.info(`File saved: ${filePath}`); + } catch (error) { + this.logger.error(`Error saving file: ${error.message}`); + } + } + + private async showSaveFileDialog(fileName: string, type: FileType): Promise { + const downloadsFolder = this.electron.getUserDownloadsPath(); + const defaultFilePath = this.node.join(downloadsFolder, fileName); + const dialogResult = await this.electron.showSaveDialog({ + title: fileName, + defaultPath: defaultFilePath, + filters: getDialogFileFilters(type), + properties: [ + 'createDirectory', // Enables directory creation on macOS. + 'showOverwriteConfirmation', // Shows overwrite confirmation on Linux. + ], + }); + if (dialogResult.canceled) { + return undefined; + } + return dialogResult.filePath; + } +} + +function getDialogFileFilters(fileType: FileType): Electron.FileFilter[] { + const filters = FileTypeSpecificFilters[fileType]; + return [ + ...filters, + { + name: 'All Files', + extensions: ['*'], + }, + ]; +} + +const FileTypeSpecificFilters: Record = { + [FileType.BatchFile]: [ + { + name: 'Batch Files', + extensions: ['bat', 'cmd'], + }, + ], + [FileType.ShellScript]: [ + { + name: 'Shell Scripts', + extensions: ['sh', 'bash', 'zsh'], + }, + ], +}; diff --git a/src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserConditions.ts b/src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserConditions.ts index 02608cd7..68276b72 100644 --- a/src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserConditions.ts +++ b/src/infrastructure/RuntimeEnvironment/Browser/BrowserOs/BrowserConditions.ts @@ -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 // 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], + })); +} diff --git a/src/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.ts index ddffec95..ba8c28f9 100644 --- a/src/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.ts +++ b/src/infrastructure/RuntimeEnvironment/Browser/BrowserRuntimeEnvironment.ts @@ -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, 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): string | undefined { - return window?.navigator?.userAgent; +function isElectronRendererProcess(globalWindow: Partial): boolean { + return globalWindow.isRunningAsDesktopApplication === true; // Preloader injects this + // We could also do `globalWindow?.navigator?.userAgent?.includes('Electron') === true;` } -function isDesktop(window: Partial): boolean { - return window?.isDesktop === true; +function determineOperatingSystem( + globalWindow: Partial, + 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; diff --git a/src/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.ts index ff3561b3..fc7742b2 100644 --- a/src/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.ts +++ b/src/infrastructure/RuntimeEnvironment/Node/NodeRuntimeEnvironment.ts @@ -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 } diff --git a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts index 009a9513..083525a4 100644 --- a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts +++ b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts @@ -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; } diff --git a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.ts b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.ts index b3328810..73bda1fc 100644 --- a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.ts +++ b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory.ts @@ -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; } diff --git a/src/infrastructure/SaveFileDialog.ts b/src/infrastructure/SaveFileDialog.ts deleted file mode 100644 index c00bf599..00000000 --- a/src/infrastructure/SaveFileDialog.ts +++ /dev/null @@ -1,33 +0,0 @@ -import fileSaver from 'file-saver'; - -export enum FileType { - BatchFile, - ShellScript, -} - -export class SaveFileDialog { - public static saveFile( - text: string, - fileName: string, - type: FileType, - ): void { - const mimeType = this.mimeTypes[type]; - this.saveBlob(text, mimeType, fileName); - } - - private static readonly mimeTypes: Record = { - // Some browsers (including firefox + IE) require right mime type - // otherwise they ignore extension and save the file as text. - [FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file - [FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ - }; - - private static saveBlob(file: BlobPart, fileType: string, fileName: string): void { - try { - const blob = new Blob([file], { type: fileType }); - fileSaver.saveAs(blob, fileName); - } catch (e) { - window.open(`data:${fileType},${encodeURIComponent(file.toString())}`, '_blank', ''); - } - } -} diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts index 74e4286b..0912f5ba 100644 --- a/src/infrastructure/WindowVariables/WindowVariables.ts +++ b/src/infrastructure/WindowVariables/WindowVariables.ts @@ -1,11 +1,13 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; import { Logger } from '@/application/Common/Log/Logger'; import { CodeRunner } from '@/application/CodeRunner/CodeRunner'; +import { Dialog } from '@/presentation/common/Dialog'; /* Primary entry point for platform-specific injections */ export interface WindowVariables { - readonly isDesktop: boolean; + readonly isRunningAsDesktopApplication?: true; readonly codeRunner?: CodeRunner; readonly os?: OperatingSystem; - readonly log: Logger; + readonly log?: Logger; + readonly dialog?: Dialog; } diff --git a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts index a197fbff..751809e3 100644 --- a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts +++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts @@ -18,13 +18,14 @@ export function validateWindowVariables(variables: Partial) { } function* testEveryProperty(variables: Partial): Iterable { - const tests: { - [K in PropertyKeys>]: boolean; - } = { + const tests: Record>, boolean> = { os: testOperatingSystem(variables.os), - isDesktop: testIsDesktop(variables.isDesktop), + isRunningAsDesktopApplication: testIsRunningAsDesktopApplication( + variables.isRunningAsDesktopApplication, + ), codeRunner: testCodeRunner(variables), log: testLogger(variables), + dialog: testDialog(variables), }; for (const [propertyName, testResult] of Object.entries(tests)) { @@ -48,23 +49,30 @@ function testOperatingSystem(os: unknown): boolean { } function testLogger(variables: Partial): boolean { - if (!variables.isDesktop) { + if (!variables.isRunningAsDesktopApplication) { return true; } return isPlainObject(variables.log); } function testCodeRunner(variables: Partial): boolean { - if (!variables.isDesktop) { + if (!variables.isRunningAsDesktopApplication) { return true; } return isPlainObject(variables.codeRunner) && isFunction(variables.codeRunner.runCode); } -function testIsDesktop(isDesktop: unknown): boolean { - if (isDesktop === undefined) { +function testIsRunningAsDesktopApplication(isRunningAsDesktopApplication: unknown): boolean { + if (isRunningAsDesktopApplication === undefined) { return true; } - return isBoolean(isDesktop); + return isBoolean(isRunningAsDesktopApplication); +} + +function testDialog(variables: Partial): boolean { + if (!variables.isRunningAsDesktopApplication) { + return true; + } + return isPlainObject(variables.dialog); } diff --git a/src/presentation/bootstrapping/DependencyProvider.ts b/src/presentation/bootstrapping/DependencyProvider.ts index 1e85ab31..0d78387d 100644 --- a/src/presentation/bootstrapping/DependencyProvider.ts +++ b/src/presentation/bootstrapping/DependencyProvider.ts @@ -11,9 +11,10 @@ import { } from '@/presentation/injectionSymbols'; import { PropertyKeys } from '@/TypeHelpers'; import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState'; -import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger'; +import { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger'; import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner'; import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; +import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog'; export function provideDependencies( context: IApplicationContext, @@ -67,6 +68,10 @@ export function provideDependencies( InjectionKeys.useCodeRunner, useCodeRunner, ), + useDialog: (di) => di.provide( + InjectionKeys.useDialog, + useDialog, + ), }; registerAll(Object.values(resolvers), api); } diff --git a/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts b/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts index a0280916..b58debb0 100644 --- a/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts +++ b/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts @@ -1,6 +1,6 @@ import { Logger } from '@/application/Common/Log/Logger'; +import { ClientLoggerFactory } from '@/presentation/components/Shared/Hooks/Log/ClientLoggerFactory'; import { Bootstrapper } from '../Bootstrapper'; -import { ClientLoggerFactory } from '../ClientLoggerFactory'; export class AppInitializationLogger implements Bootstrapper { constructor( diff --git a/src/presentation/common/Dialog.ts b/src/presentation/common/Dialog.ts new file mode 100644 index 00000000..adb39c02 --- /dev/null +++ b/src/presentation/common/Dialog.ts @@ -0,0 +1,8 @@ +export interface Dialog { + saveFile(fileContents: string, fileName: string, type: FileType): Promise; +} + +export enum FileType { + BatchFile, + ShellScript, +} diff --git a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue index bb919373..65c89594 100644 --- a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue +++ b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue @@ -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(() => getCanRunState(currentState.value.os, isDesktop, os)); + const canRun = computed(() => 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; } diff --git a/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue b/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue index a9d6e183..1d35451a 100644 --- a/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue +++ b/src/presentation/components/Code/CodeButtons/Save/CodeSaveButton.vue @@ -1,8 +1,8 @@