diff --git a/docs/DEBUGGING-GUIDE.md b/docs/DEBUGGING-GUIDE.md index e6b52fcf2..26ef62ced 100644 --- a/docs/DEBUGGING-GUIDE.md +++ b/docs/DEBUGGING-GUIDE.md @@ -225,7 +225,6 @@ log("error", "download failed", { **During Development:** - Press **F12** or **Ctrl+Shift+I** in the running Vortex window -- Or add to your code: `require('@electron/remote').getCurrentWindow().webContents.openDevTools()` **From Command Line:** @@ -287,9 +286,6 @@ Vortex uses Electron, which provides Chrome DevTools for debugging. // In main process import { BrowserWindow } from "electron"; BrowserWindow.getFocusedWindow()?.webContents.openDevTools(); - -// In renderer process -require("@electron/remote").getCurrentWindow().webContents.openDevTools(); ``` ### Network Tab diff --git a/extensions/gamebryo-plugin-management/src/util/UserlistPersistor.ts b/extensions/gamebryo-plugin-management/src/util/UserlistPersistor.ts index dee58459d..723e88599 100644 --- a/extensions/gamebryo-plugin-management/src/util/UserlistPersistor.ts +++ b/extensions/gamebryo-plugin-management/src/util/UserlistPersistor.ts @@ -2,17 +2,12 @@ import { ILOOTList, ILOOTPlugin } from "../types/ILOOTList"; import { gameSupported } from "./gameSupport"; -import * as RemoteT from "@electron/remote"; -import Promise from "bluebird"; -import { dialog as dialogIn } from "electron"; -import { dump, load } from "js-yaml"; -import * as _ from "lodash"; -import * as path from "path"; -import { fs, types, util } from "vortex-api"; - -const remote = util.lazyRequire(() => - require("@electron/remote"), -); +import Promise from 'bluebird'; +import { dialog as dialogIn } from 'electron'; +import { dump, load } from 'js-yaml'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { fs, types, util } from 'vortex-api'; /** * persistor syncing to and from the loot userlist.yaml file @@ -179,32 +174,41 @@ class UserlistPersistor implements types.IPersistor { }); } - private handleInvalidList() { - const dialog = process.type === "renderer" ? remote.dialog : dialogIn; + private async handleInvalidList() { + const showMessageBox = async (options: Electron.MessageBoxOptions) => { + if (process.type === 'renderer') { + const result = await (window as any).api.dialog.showMessageBox(options); + return result.response; + } else { + const result = await dialogIn.showMessageBox(null, options); + return result.response; + } + }; let res = 0; - if (this.mMode === "masterlist") { - res = dialog.showMessageBoxSync(null, { - title: "Masterlist invalid", - message: - `The masterlist "${this.mUserlistPath}" can\'t be read. ` + - "\n\n" + - "If you continue now, the masterlist will be reset.", - - buttons: ["Reset Masterlist", "Quit Vortex"], + if (this.mMode === 'masterlist') { + res = await showMessageBox({ + title: 'Masterlist invalid', + message: `The masterlist "${this.mUserlistPath}" can\'t be read. ` + + '\n\n' + + 'If you continue now, the masterlist will be reset.', + + buttons: [ + 'Reset Masterlist', + 'Quit Vortex' + ], }); } else { - res = dialog.showMessageBoxSync(null, { - title: "Userlist invalid", - message: - `The LOOT userlist "${this.mUserlistPath}" can\'t be read. ` + - "\n\n" + - "You should quit Vortex now and repair the file.\n" + - "If (and only if!) you're certain you didn't modify the file yourself, " + - "please send in a bug report with that file attached." + - "\n\n" + - "If you continue now, the userlist will be reset and all your plugin " + - "rules and group assignments will be lost.", + res = await showMessageBox({ + title: 'Userlist invalid', + message: `The LOOT userlist "${this.mUserlistPath}" can\'t be read. ` + + '\n\n' + + 'You should quit Vortex now and repair the file.\n' + + 'If (and only if!) you\'re certain you didn\'t modify the file yourself, ' + + 'please send in a bug report with that file attached.' + + '\n\n' + + 'If you continue now, the userlist will be reset and all your plugin ' + + 'rules and group assignments will be lost.', noLink: true, defaultId: 1, buttons: ["Reset Userlist", "Quit Vortex"], @@ -250,26 +254,25 @@ class UserlistPersistor implements types.IPersistor { let empty: boolean = false; - return fs - .readFileAsync(this.mUserlistPath) - .then((data: Buffer) => { - if (data.byteLength <= 5) { - // the smallest non-empty file is actually around 20 bytes long and - // the smallest useful file probably 30. This is really to catch - // cases where the file is not parseable because it's completely empty - // or contains only "null" or something silly like that - empty = true; - } + return fs.readFileAsync(this.mUserlistPath) + .then(async (data: Buffer) => { + if (data.byteLength <= 5) { + // the smallest non-empty file is actually around 20 bytes long and + // the smallest useful file probably 30. This is really to catch + // cases where the file is not parseable because it's completely empty + // or contains only "null" or something silly like that + empty = true; + } - let newList: Partial = {}; - try { - newList = load(data.toString(), { json: true }) as any; - } catch (err) { - this.handleInvalidList(); - } - if (typeof newList !== "object") { - this.handleInvalidList(); - } + let newList: Partial = {}; + try { + newList = load(data.toString(), { json: true }) as any; + } catch (err) { + await this.handleInvalidList(); + } + if (typeof (newList) !== 'object') { + await this.handleInvalidList(); + } ["globals", "plugins", "groups"].forEach((key) => { if ([null, undefined].indexOf(newList[key]) !== -1) { diff --git a/extensions/theme-switcher/src/util.ts b/extensions/theme-switcher/src/util.ts index 8bba27ced..e21ab8f9d 100644 --- a/extensions/theme-switcher/src/util.ts +++ b/extensions/theme-switcher/src/util.ts @@ -9,27 +9,17 @@ interface IFont { family: string; } -const getAvailableFontImpl = () => { +// Get available system fonts - runs directly in renderer process +export function getAvailableFonts(): Promise { // eslint-disable-next-line @typescript-eslint/no-var-requires - const fontScanner = require("font-scanner"); - return fontScanner - .getAvailableFonts() - .then((fonts: IFont[]) => - Array.from( - new Set([ - "Inter", - "Roboto", - "Montserrat", - "BebasNeue", - ...(fonts || []).map((font) => font.family).sort(), - ]), - ), - ); -}; - -const getAvailableFonts: () => Promise = util.makeRemoteCall( - "get-available-fonts", - getAvailableFontImpl, -); - -export { getAvailableFonts }; + const fontScanner = require('font-scanner'); + return fontScanner.getAvailableFonts() + .then((fonts: IFont[]) => Array.from(new Set( + [ + 'Inter', + 'Roboto', + 'Montserrat', + 'BebasNeue', + ...(fonts || []).map(font => font.family).sort(), + ]))); +} diff --git a/playwright/src/nexusmods-auth-helpers.ts b/playwright/src/nexusmods-auth-helpers.ts index 57025d62b..5d330fa94 100644 --- a/playwright/src/nexusmods-auth-helpers.ts +++ b/playwright/src/nexusmods-auth-helpers.ts @@ -110,15 +110,10 @@ export async function blockExternalBrowserLaunch( /** * Extracts the OAuth URL from Vortex's Redux store */ -export async function extractOAuthUrl( - mainWindow: Page, -): Promise { - return await mainWindow.evaluate(() => { +export async function extractOAuthUrl(mainWindow: Page): Promise { + return await mainWindow.evaluate(async () => { try { - const remote = (window as any).require("@electron/remote"); - const getReduxState = remote.getGlobal("getReduxState"); - const stateJson = getReduxState(); - const state = JSON.parse(stateJson); + const state = await (window as any).api.redux.getState() as any; const oauthUrl = state?.session?.nexus?.oauthPending; if (oauthUrl) { diff --git a/playwright/tests/manage-fake-stardew-valley.spec.ts b/playwright/tests/manage-fake-stardew-valley.spec.ts index e888d9751..0597f3a1f 100644 --- a/playwright/tests/manage-fake-stardew-valley.spec.ts +++ b/playwright/tests/manage-fake-stardew-valley.spec.ts @@ -141,10 +141,9 @@ test("manage fake Stardew Valley", async ({ await mainWindow.waitForTimeout(3000); // Verify game was discovered - console.log("7. Verifying..."); - const isDiscovered = await mainWindow.evaluate(() => { - const remote = (window as any).require("@electron/remote"); - const state = JSON.parse(remote.getGlobal("getReduxState")()); + console.log('7. Verifying...'); + const isDiscovered = await mainWindow.evaluate(async () => { + const state = await (window as any).api.redux.getState() as any; return !!state?.settings?.gameMode?.discovered?.stardewvalley; }); @@ -168,9 +167,8 @@ test("manage fake Stardew Valley", async ({ console.log(` Download Popup URL: ${modUrl}`); // Check Vortex download state before download - const downloadsBefore = await mainWindow.evaluate(() => { - const remote = (window as any).require("@electron/remote"); - const state = JSON.parse(remote.getGlobal("getReduxState")()); + const downloadsBefore = await mainWindow.evaluate(async () => { + const state = await (window as any).api.redux.getState() as any; return { downloadCount: Object.keys(state?.persistent?.downloads?.files || {}) .length, @@ -199,9 +197,8 @@ test("manage fake Stardew Valley", async ({ await mainWindow.waitForTimeout(2000); // Check Vortex download state after download - const downloadsAfter = await mainWindow.evaluate(() => { - const remote = (window as any).require("@electron/remote"); - const state = JSON.parse(remote.getGlobal("getReduxState")()); + const downloadsAfter = await mainWindow.evaluate(async () => { + const state = await (window as any).api.redux.getState() as any; const downloads = state?.persistent?.downloads?.files || {}; return { downloadCount: Object.keys(downloads).length, diff --git a/src/extensions/dashboard/views/Dashboard.tsx b/src/extensions/dashboard/views/Dashboard.tsx index 884ce8138..1c208c992 100644 --- a/src/extensions/dashboard/views/Dashboard.tsx +++ b/src/extensions/dashboard/views/Dashboard.tsx @@ -9,7 +9,6 @@ import { translate, } from "../../../renderer/controls/ComponentEx"; import Debouncer from "../../../util/Debouncer"; -import lazyRequire from "../../../util/lazyRequire"; import { getSafe } from "../../../util/storeHelper"; import MainPage from "../../../renderer/views/MainPage"; @@ -26,15 +25,12 @@ import PackeryGrid from "./PackeryGrid"; import type { IPackeryItemProps } from "./PackeryItem"; import PackeryItem from "./PackeryItem"; -import type * as remoteT from "@electron/remote"; import * as _ from "lodash"; import * as React from "react"; import { Button, MenuItem } from "react-bootstrap"; import type * as Redux from "redux"; import type { ThunkDispatch } from "redux-thunk"; -const remote: typeof remoteT = lazyRequire(() => require("@electron/remote")); - const UPDATE_FREQUENCY_MS = 1000; interface IBaseProps { @@ -78,6 +74,8 @@ class Dashboard extends ComponentEx { private mUpdateTimer: NodeJS.Timeout; private mLayoutDebouncer: Debouncer; private mWindowFocused: boolean = true; + private mUnsubscribeFocus: (() => void) | undefined; + private mUnsubscribeBlur: (() => void) | undefined; constructor(props: IProps) { super(props); @@ -93,27 +91,24 @@ class Dashboard extends ComponentEx { } return null; }, 500); - // assuming this doesn't change? - const window = remote.getCurrentWindow(); - this.mWindowFocused = window.isFocused(); } public componentDidMount() { this.startUpdateCycle(); - const win = remote.getCurrentWindow(); - win.on("focus", this.onFocus); - win.on("blur", this.onBlur); - window.addEventListener("beforeunload", () => { - win.removeListener("focus", this.onFocus); - win.removeListener("blur", this.onBlur); + // Check initial focus state + window.api.window.isFocused(window.windowId).then((focused) => { + this.mWindowFocused = focused; }); + // Subscribe to focus/blur events via preload API + this.mUnsubscribeFocus = window.api.window.onFocus(this.onFocus); + this.mUnsubscribeBlur = window.api.window.onBlur(this.onBlur); } public componentWillUnmount() { clearTimeout(this.mUpdateTimer); - const win = remote.getCurrentWindow(); - win.removeListener("focus", this.onFocus); - win.removeListener("blur", this.onBlur); + // Unsubscribe from focus/blur events + this.mUnsubscribeFocus?.(); + this.mUnsubscribeBlur?.(); } public UNSAFE_componentWillReceiveProps(nextProps: IProps) { diff --git a/src/extensions/diagnostics_files/views/DiagnosticsFilesDialog.tsx b/src/extensions/diagnostics_files/views/DiagnosticsFilesDialog.tsx index 794889cf0..bd4e33f44 100644 --- a/src/extensions/diagnostics_files/views/DiagnosticsFilesDialog.tsx +++ b/src/extensions/diagnostics_files/views/DiagnosticsFilesDialog.tsx @@ -15,7 +15,6 @@ import { showError } from "../../../util/message"; import type { ILog, ISession } from "../types/ISession"; import { loadVortexLogs } from "../util/loadVortexLogs"; -import type * as RemoteT from "@electron/remote"; import PromiseBB from "bluebird"; import update from "immutability-helper"; import * as os from "os"; @@ -31,11 +30,8 @@ import { } from "react-bootstrap"; import type * as Redux from "redux"; import type { ThunkDispatch } from "redux-thunk"; -import lazyRequire from "../../../util/lazyRequire"; import { util } from "../../.."; -const remote = lazyRequire(() => require("@electron/remote")); - export interface IBaseProps { visible: boolean; onHide: () => void; @@ -313,7 +309,7 @@ class DiagnosticsFilesDialog extends ComponentEx { .filter((line) => enabledLevels.has(line.type)) .map((line) => `${line.time} - ${line.type}: ${line.text}`) .join(os.EOL); - remote.clipboard.writeText(filteredLog); + window.api.clipboard.writeText(filteredLog); }; private openLogFolder = () => { diff --git a/src/extensions/download_management/DownloadManager.ts b/src/extensions/download_management/DownloadManager.ts index c0b7f3d09..af5f1e8ab 100644 --- a/src/extensions/download_management/DownloadManager.ts +++ b/src/extensions/download_management/DownloadManager.ts @@ -4,7 +4,6 @@ import { StalledError, UserCanceled, } from "../../util/CustomErrors"; -import makeRemoteCall from "../../util/electronRemote"; import * as fs from "../../util/fs"; import { log } from "../../util/log"; import { delayed, INVALID_FILENAME_RE, truthy } from "../../util/util"; @@ -38,13 +37,13 @@ import type { IExtensionApi } from "../../types/api"; import { simulateHttpError } from "./debug/simulateHttpError"; import { getErrorMessageOrDefault, unknownToError } from "../../shared/errors"; +import { getPreloadApi } from "../../util/preloadAccess"; -const getCookies = makeRemoteCall( - "get-cookies", - (electron, webContents, filter: Electron.CookiesGetFilter) => { - return webContents.session.cookies.get(filter); - }, -); +function getCookies( + filter: Electron.CookiesGetFilter, +): Promise { + return getPreloadApi().session.getCookies(filter); +} // assume urls are valid for at least 5 minutes const URL_RESOLVE_EXPIRE_MS = 1000 * 60 * 5; @@ -1647,7 +1646,9 @@ class DownloadManager { new ProcessCanceled("Failed to resolve download URL"), ); } - return resolved.urls[0]; + // Ensure URL is a string, not a URL object (URL objects don't serialize properly through IPC) + const url = resolved.urls[0]; + return typeof url === "string" ? url : String(url); }), confirmedOffset: 0, confirmedSize: this.mMinChunkSize, @@ -2330,7 +2331,9 @@ class DownloadManager { new ProcessCanceled("Failed to resolve download URL"), ); } - return resolved.urls[0]; + // Ensure URL is a string, not a URL object + const url = resolved.urls[0]; + return typeof url === "string" ? url : String(url); }), }); offset += chunkSize; @@ -2410,7 +2413,9 @@ class DownloadManager { new ProcessCanceled("Failed to resolve download URL"), ); } - return resolved.urls[0]; + // Ensure URL is a string, not a URL object + const url = resolved.urls[0]; + return typeof url === "string" ? url : String(url); }), // Immutable confirmed fields confirmedOffset, diff --git a/src/extensions/download_management/FileAssembler.ts b/src/extensions/download_management/FileAssembler.ts index c0fd800be..09fa44189 100644 --- a/src/extensions/download_management/FileAssembler.ts +++ b/src/extensions/download_management/FileAssembler.ts @@ -3,17 +3,23 @@ import { getVisibleWindow } from "../../util/errorHandling"; import * as fs from "../../util/fs"; import { log } from "../../util/log"; import { makeQueue } from "../../util/util"; +import { getPreloadApi } from "../../util/preloadAccess"; import PromiseBB from "bluebird"; import { dialog as dialogIn } from "electron"; import * as fsFast from "fs-extra"; import * as path from "path"; -const dialog = - process.type === "renderer" - ? // tslint:disable-next-line:no-var-requires - require("@electron/remote").dialog - : dialogIn; +const showMessageBox = async ( + options: Electron.MessageBoxOptions, +): Promise => { + if (process.type === "renderer") { + return getPreloadApi().dialog.showMessageBox(options); + } else { + const win = getVisibleWindow(); + return dialogIn.showMessageBox(win, options); + } +}; /** * assembles a file received in chunks. @@ -157,7 +163,7 @@ class FileAssembler { : PromiseBB.resolve(synced), ) .catch({ code: "ENOSPC" }, () => { - dialog.showMessageBoxSync(getVisibleWindow(), { + return showMessageBox({ type: "warning", title: "Disk is full", message: @@ -166,9 +172,11 @@ class FileAssembler { buttons: ["Cancel", "Retry"], defaultId: 1, noLink: true, - }) === 1 - ? this.addChunk(offset, data) - : PromiseBB.reject(new UserCanceled()); + }).then((result) => + result.response === 1 + ? this.addChunk(offset, data) + : PromiseBB.reject(new UserCanceled()), + ); }), false, ); diff --git a/src/extensions/download_management/index.ts b/src/extensions/download_management/index.ts index 63f5468be..7ea405644 100644 --- a/src/extensions/download_management/index.ts +++ b/src/extensions/download_management/index.ts @@ -56,7 +56,6 @@ import type DownloadManager from "./DownloadManager"; import type { DownloadObserver } from "./DownloadObserver"; import type observe from "./DownloadObserver"; -import type * as RemoteT from "@electron/remote"; import PromiseBB from "bluebird"; import * as _ from "lodash"; import Zip from "node-7z"; @@ -65,7 +64,6 @@ import type * as Redux from "redux"; import { generate as shortid } from "shortid"; import { fileMD5 } from "vortexmt"; import winapi from "winapi-bindings"; -import lazyRequire from "../../util/lazyRequire"; import setDownloadGames from "./util/setDownloadGames"; import { ensureLoggedIn } from "../nexus_integration/util"; import NXMUrl from "../nexus_integration/NXMUrl"; @@ -77,8 +75,6 @@ import { import extendAPI from "./util/extendApi"; import { unknownToError } from "../../shared/errors"; -const remote = lazyRequire(() => require("@electron/remote")); - let observer: DownloadObserver; let manager: DownloadManager; let updateDebouncer: Debouncer; @@ -1449,12 +1445,13 @@ function init(context: IExtensionContextExt): boolean { { let powerTimer: NodeJS.Timeout; let powerBlockerId: number; - const stopTimer = () => { - if ( - powerBlockerId !== undefined && - remote.powerSaveBlocker.isStarted(powerBlockerId) - ) { - remote.powerSaveBlocker.stop(powerBlockerId); + const stopTimer = async () => { + if (powerBlockerId !== undefined) { + const isStarted = + await window.api.powerSaveBlocker.isStarted(powerBlockerId); + if (isStarted) { + await window.api.powerSaveBlocker.stop(powerBlockerId); + } } powerBlockerId = undefined; powerTimer = undefined; @@ -1514,9 +1511,12 @@ function init(context: IExtensionContextExt): boolean { clearTimeout(powerTimer); } if (powerBlockerId === undefined) { - powerBlockerId = remote.powerSaveBlocker.start( - "prevent-app-suspension", - ); + // Start power save blocker asynchronously + window.api.powerSaveBlocker + .start("prevent-app-suspension") + .then((id) => { + powerBlockerId = id; + }); } powerTimer = setTimeout(stopTimer, 60000); } diff --git a/src/extensions/extension_manager/index.ts b/src/extensions/extension_manager/index.ts index d0d2f676d..3d275c9c9 100644 --- a/src/extensions/extension_manager/index.ts +++ b/src/extensions/extension_manager/index.ts @@ -6,7 +6,7 @@ import type { NotificationDismiss } from "../../types/INotification"; import type { IExtensionLoadFailure, IState } from "../../types/IState"; import { relaunch } from "../../util/commandLine"; import { DataInvalid, ProcessCanceled } from "../../util/CustomErrors"; -import { isExtSame } from "../../util/ExtensionManager"; +import { isExtSame } from "../../util/extensionUtil"; import { log } from "../../util/log"; import makeReactive from "../../util/makeReactive"; diff --git a/src/extensions/gamemode_management/util/ProcessMonitor.ts b/src/extensions/gamemode_management/util/ProcessMonitor.ts index 88dabc020..992954ab5 100644 --- a/src/extensions/gamemode_management/util/ProcessMonitor.ts +++ b/src/extensions/gamemode_management/util/ProcessMonitor.ts @@ -8,7 +8,6 @@ import { currentGame, currentGameDiscovery } from "../../../util/selectors"; import { getSafe } from "../../../util/storeHelper"; import { setdefault } from "../../../util/util"; -import type { BrowserWindow } from "electron"; import * as path from "path"; import type * as Redux from "redux"; import type { IProcessInfo, IProcessProvider } from "./processProvider"; @@ -87,7 +86,9 @@ import { defaultProcessProvider } from "./processProvider"; class ProcessMonitor { private mTimer: NodeJS.Timeout; private mStore: Redux.Store; - private mWindow: BrowserWindow; + private mIsFocused: boolean = true; + private mUnsubscribeFocus: (() => void) | null = null; + private mUnsubscribeBlur: (() => void) | null = null; private mActive: boolean = false; private mProcessProvider: IProcessProvider; @@ -126,8 +127,15 @@ class ProcessMonitor { clearTimeout(this.mTimer); } - if (process.type === "renderer") { - this.mWindow = require("@electron/remote").getCurrentWindow(); + // Track focus state via preload events in renderer process + if (process.type === "renderer" && window?.api?.window) { + this.mIsFocused = true; // Assume focused initially + this.mUnsubscribeFocus = window.api.window.onFocus(() => { + this.mIsFocused = true; + }); + this.mUnsubscribeBlur = window.api.window.onBlur(() => { + this.mIsFocused = false; + }); } log("debug", "start process monitor"); @@ -147,8 +155,7 @@ class ProcessMonitor { return; } - const isFocused = this.mWindow === undefined || this.mWindow.isFocused(); - const delay = isFocused ? 2000 : 5000; + const delay = this.mIsFocused ? 2000 : 5000; const startedAt = Date.now(); void this.doCheck() @@ -468,6 +475,13 @@ class ProcessMonitor { clearTimeout(this.mTimer); this.mTimer = undefined; this.mActive = false; + + // Unsubscribe from focus events + this.mUnsubscribeFocus?.(); + this.mUnsubscribeBlur?.(); + this.mUnsubscribeFocus = null; + this.mUnsubscribeBlur = null; + log("debug", "stop process monitor"); } } diff --git a/src/extensions/mod_management/views/Settings.tsx b/src/extensions/mod_management/views/Settings.tsx index c625e7ddf..47ac36c15 100644 --- a/src/extensions/mod_management/views/Settings.tsx +++ b/src/extensions/mod_management/views/Settings.tsx @@ -72,7 +72,6 @@ import { modPathsForGame } from "../selectors"; import { STAGING_DIR_TAG } from "../stagingDirectory"; import getText from "../texts"; -import * as remote from "@electron/remote"; import PromiseBB from "bluebird"; import * as path from "path"; import * as React from "react"; @@ -140,6 +139,7 @@ interface IComponentState { supportedActivators: IDeploymentMethod[]; currentActivator: string; changingActivator: boolean; + appPath: string; } type IProps = IBaseProps & IActionProps & IConnectedProps; @@ -158,6 +158,7 @@ class Settings extends ComponentEx { currentActivator: props.currentActivator, installPath: props.installPath, changingActivator: false, + appPath: undefined, }); } @@ -174,6 +175,16 @@ class Settings extends ComponentEx { this.nextState.currentActivator = activators.length > 0 ? activators[0].id : undefined; } + + // Load the app path for validation + window.api.app.getAppPath().then((appPath) => { + // In asar builds getAppPath returns the path of the asar so need to go up 2 levels + // (resources/app.asar) + if (path.basename(appPath) === "app.asar") { + appPath = path.dirname(path.dirname(appPath)); + } + this.nextState.appPath = appPath; + }); } public UNSAFE_componentWillReceiveProps(newProps: IProps) { @@ -921,12 +932,8 @@ class Settings extends ComponentEx { reason?: string; } { const { downloadsPath } = this.props; - let vortexPath = remote.app.getAppPath(); - if (path.basename(vortexPath) === "app.asar") { - // in asar builds getAppPath returns the path of the asar so need to go up 2 levels - // (resources/app.asar) - vortexPath = path.dirname(path.dirname(vortexPath)); - } + const { appPath } = this.state; + if (downloadsPath !== undefined) { const downPath = path.dirname(downloadsPath); const normalizedDownloadsPath = path.normalize(downPath.toLowerCase()); @@ -950,7 +957,7 @@ class Settings extends ComponentEx { }; } - if (isChildPath(input, vortexPath)) { + if (appPath !== undefined && isChildPath(input, appPath)) { return { state: "error", reason: @@ -1083,7 +1090,9 @@ class Settings extends ComponentEx { const { modPaths, onShowError, suggestInstallPathDirectory } = this.props; PromiseBB.join( fs.statAsync(modPaths[""]), - fs.statAsync(remote.app.getPath("userData")), + window.api.app + .getPath("userData") + .then((userDataPath) => fs.statAsync(userDataPath)), ) .then((stats) => { let suggestion: string; diff --git a/src/extensions/nexus_integration/util.ts b/src/extensions/nexus_integration/util.ts index bf374bae7..c5874bffa 100644 --- a/src/extensions/nexus_integration/util.ts +++ b/src/extensions/nexus_integration/util.ts @@ -1,4 +1,3 @@ -import type * as RemoteT from "@electron/remote"; import type { EndorsedStatus, ICollectionQuery, @@ -47,8 +46,8 @@ import { import { contextify, setApiKey, setOauthToken } from "../../util/errorHandling"; import * as fs from "../../util/fs"; import getVortexPath from "../../util/getVortexPath"; +import { getPreloadApi, getWindowId } from "../../util/preloadAccess"; import { RateLimitExceeded } from "../../util/github"; -import lazyRequire from "../../util/lazyRequire"; import { log } from "../../util/log"; import { calcDuration, showError } from "../../util/message"; import { jsonRequest } from "../../util/network"; @@ -94,8 +93,6 @@ import type { IValidateKeyDataV2 } from "./types/IValidateKeyData"; import { IAccountStatus } from "./types/IValidateKeyData"; import { getErrorMessageOrDefault, unknownToError } from "../../shared/errors"; -const remote = lazyRequire(() => require("@electron/remote")); - const UPDATE_CHECK_DELAY = 60 * 60 * 1000; const GAMES_JSON_URL = "https://data.nexusmods.com/file/nexus-data/games.json"; @@ -149,7 +146,7 @@ export function onCancelLoginImpl(api: IExtensionApi) { api.events.emit("did-login", new UserCanceled()); } -export function bringToFront() { +export async function bringToFront() { // if window is snapped in windows (aero snap), bringing the window to front // will unsnap it and it will be moved/resized to where it was before snapping. // This is quite irritating so this will store the (snapped) window position @@ -157,17 +154,18 @@ export function bringToFront() { // This will cause a short "flicker" if the window was snapped and it will // still unsnap the window as far as windows is concerned. - const window = remote.getCurrentWindow(); - const [x, y] = window.getPosition(); - const [w, h] = window.getSize(); + const windowId = getWindowId(); + const api = getPreloadApi(); + const [x, y] = await api.window.getPosition(windowId); + const [w, h] = await api.window.getSize(windowId); - window.setAlwaysOnTop(true); - window.show(); - window.setAlwaysOnTop(false); + await api.window.setAlwaysOnTop(windowId, true); + await api.window.show(windowId); + await api.window.setAlwaysOnTop(windowId, false); setTimeout(() => { - window.setPosition(x, y); - window.setSize(w, h); + void api.window.setPosition(windowId, x, y); + void api.window.setSize(windowId, w, h); }, 100); } @@ -317,7 +315,7 @@ export function requestLogin( async (err: Error, token: ITokenReply) => { // received reply from site for this state - bringToFront(); + void bringToFront(); api.store.dispatch(setLoginId(undefined)); // set state to undefined so that we can close the modal? api.store.dispatch(setDialogVisible(undefined)); diff --git a/src/extensions/profile_management/index.ts b/src/extensions/profile_management/index.ts index a5f458742..3737353c9 100644 --- a/src/extensions/profile_management/index.ts +++ b/src/extensions/profile_management/index.ts @@ -476,8 +476,8 @@ function genOnProfileChange( }; // changes to profile files are only saved back to the profile at this point - queue = queue.then(() => refreshProfile(store, oldProfile, "import")); const oldProfile = state.persistent.profiles[prev]; + queue = queue.then(() => refreshProfile(store, oldProfile, "import")); api.events.emit("profile-will-change", current, enqueue); diff --git a/src/extensions/settings_interface/SettingsInterface.tsx b/src/extensions/settings_interface/SettingsInterface.tsx index 740f3331b..9bdad7acf 100644 --- a/src/extensions/settings_interface/SettingsInterface.tsx +++ b/src/extensions/settings_interface/SettingsInterface.tsx @@ -1,9 +1,19 @@ -import { showDialog } from "../../actions/notifications"; -import { resetSuppression } from "../../actions/notificationSettings"; -import { setCustomTitlebar } from "../../actions/window"; +import type * as Redux from "redux"; +import type { ThunkDispatch } from "redux-thunk"; + +import PromiseBB from "bluebird"; +import * as path from "path"; +import * as React from "react"; +import { + Alert, + Button, + ControlLabel, + FormControl, + FormGroup, + HelpBlock, +} from "react-bootstrap"; +import { useSelector } from "react-redux"; -import More from "../../renderer/controls/More"; -import Toggle from "../../renderer/controls/Toggle"; import type { DialogActions, DialogType, @@ -12,25 +22,28 @@ import type { } from "../../types/IDialog"; import type { IState } from "../../types/IState"; import type { IParameters } from "../../util/commandLine"; -import { relaunch } from "../../util/commandLine"; +import type { + IAvailableExtension, + IExtensionDownloadInfo, +} from "../extension_manager/types"; + +import { showDialog } from "../../actions/notifications"; +import { resetSuppression } from "../../actions/notificationSettings"; +import { setCustomTitlebar } from "../../actions/window"; import { ComponentEx, connect, translate, } from "../../renderer/controls/ComponentEx"; +import More from "../../renderer/controls/More"; +import Toggle from "../../renderer/controls/Toggle"; +import { relaunch } from "../../util/commandLine"; import getVortexPath from "../../util/getVortexPath"; -import lazyRequire from "../../util/lazyRequire"; import { log } from "../../util/log"; import { truthy } from "../../util/util"; - -import type { - IAvailableExtension, - IExtensionDownloadInfo, -} from "../extension_manager/types"; import { readExtensibleDir } from "../extension_manager/util"; import getTextModManagement from "../mod_management/texts"; import getTextProfiles from "../profile_management/texts"; - import { setAutoDeployment, setAutoEnable, @@ -50,25 +63,6 @@ import { import { nativeCountryName, nativeLanguageName } from "./languagemap"; import getText from "./texts"; -import type * as remoteT from "@electron/remote"; -import PromiseBB from "bluebird"; -import { app } from "electron"; -import * as path from "path"; -import * as React from "react"; -import { - Alert, - Button, - ControlLabel, - FormControl, - FormGroup, - HelpBlock, -} from "react-bootstrap"; -import { useSelector } from "react-redux"; -import type * as Redux from "redux"; -import type { ThunkDispatch } from "redux-thunk"; - -const remote: typeof remoteT = lazyRequire(() => require("@electron/remote")); - interface ILanguage { key: string; language: string; @@ -180,7 +174,8 @@ class SettingsInterfaceImpl extends ComponentEx { {t("You need to restart Vortex to activate this change")} - @@ -195,10 +190,11 @@ class SettingsInterfaceImpl extends ComponentEx {
{t("Language")} + {languages.reduce((prev, language) => { if (language.ext.length < 2) { @@ -211,14 +207,17 @@ class SettingsInterfaceImpl extends ComponentEx { return prev; }, [])} + {t( "When you select a language for the first time you may have to restart Vortex.", )} + {t("Customisation")} +
{ {t("Custom Window Title Bar")}
+
{ {t("Enable Desktop Notifications")}
+
{t("Hide Top-Level Category")} + {
+
{
+
{t( @@ -267,8 +271,10 @@ class SettingsInterfaceImpl extends ComponentEx {
+ {t("Advanced")} +
{/*
@@ -286,6 +292,7 @@ class SettingsInterfaceImpl extends ComponentEx {
{t("Enable Profile Management")} + {
+
{ > {t("Enable GPU Acceleration")} + {startup.disableGPU === true ? ( @@ -315,32 +324,41 @@ class SettingsInterfaceImpl extends ComponentEx {
+ {t("Automation")} +
{t("Deploy Mods when Enabled")} + {getTextModManagement("deployment", t)} + {t("Install Mods when downloaded")} + {t("Enable Mods when installed (in current profile)")} + {t("Run Vortex when my computer starts")} + {startMinimizedToggle}
+ {t("Notifications")} +
+ {restartNotification} ); @@ -408,11 +427,12 @@ class SettingsInterfaceImpl extends ComponentEx { } return (