diff --git a/src-electron/main-process/ipcListeners.js b/src-electron/main-process/ipcListeners.js index bc511bbd2..bb577f92b 100644 --- a/src-electron/main-process/ipcListeners.js +++ b/src-electron/main-process/ipcListeners.js @@ -45,6 +45,11 @@ ipcMain.on('get-is-portable', ()=>{ browserWindow.webContents.send('receive-is-portable', process.execPath.startsWith(os.tmpdir())); }) +ipcMain.on('restart', ()=>{ + app.relaunch(); + app.exit(); +}) + ipcMain.on('get-assets-path', ()=>{ if (process.env.PROD) { browserWindow.webContents.send('receive-assets-path', global.__statics); diff --git a/src/components/settings-components/SettingsView.vue b/src/components/settings-components/SettingsView.vue index 37c169374..bdf02a8b5 100644 --- a/src/components/settings-components/SettingsView.vue +++ b/src/components/settings-components/SettingsView.vue @@ -123,13 +123,23 @@ new SettingsRow( 'Locations', 'Browse profile folder', - 'Open the directory where mods are stored for the current profile.', + 'Change the directory where mods and profiles are stored.', () => { return Profile.getActiveProfile().getPathOfProfile(); }, 'fa-door-open', () => this.emitInvoke('BrowseProfileFolder') ), + new SettingsRow( + 'Locations', + 'Change data folder directory', + 'Open the directory where mods are stored for the current profile. The folder will not be deleted, and existing profiles will not carry across.', + () => { + return PathResolver.ROOT; + }, + 'fa-folder-open', + () => this.emitInvoke('ChangeDataFolder') + ), new SettingsRow( 'Debugging', 'Copy LogOutput contents to clipboard', diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue index 85fca8262..912924786 100644 --- a/src/pages/Manager.vue +++ b/src/pages/Manager.vue @@ -1045,6 +1045,28 @@ this.view = 'installed'; } + changeDataFolder() { + const dir: string = PathResolver.ROOT; + ipcRenderer.once('receive-selection', (_sender: any, files: string[] | null) => { + if (files !== null && files.length === 1) { + const filesInDirectory = fs.readdirSync(files[0]); + if (filesInDirectory.length > 0 && files[0] !== PathResolver.APPDATA_DIR) { + this.showError(new R2Error("Selected directory is not empty", `Directory is not empty: ${files[0]}. Contains ${filesInDirectory.length} files.`, "Select an empty directory or create a new one.")); + return; + } else { + ManagerSettings.getSingleton().setDataDirectory(files[0]); + ipcRenderer.send('restart'); + } + } + }); + ipcRenderer.send('open-dialog', { + title: 'Select a new folder to store r2modman data', + defaultPath: dir, + properties: ['openDirectory'], + buttonLabel: 'Select Data Folder' + }); + } + handleSettingsCallbacks(invokedSetting: any) { switch(invokedSetting) { case "BrowseDataFolder": @@ -1107,6 +1129,9 @@ case "ShowDependencyStrings": this.showDependencyStrings = true; break; + case "ChangeDataFolder": + this.changeDataFolder(); + break; } } diff --git a/src/pages/Splash.vue b/src/pages/Splash.vue index 262f42c54..69d6a6e8a 100644 --- a/src/pages/Splash.vue +++ b/src/pages/Splash.vue @@ -244,8 +244,8 @@ export default class Splash extends Vue { async created() { ipcRenderer.once('receive-appData-directory', (_sender: any, appData: string) => { - PathResolver.ROOT = path.join(appData, 'r2modmanPlus-local'); - fs.ensureDirSync(PathResolver.ROOT); + PathResolver.APPDATA_DIR = path.join(appData, 'r2modmanPlus-local'); + fs.ensureDirSync(PathResolver.APPDATA_DIR); ThemeManager.apply(); Logger.Log(LogSeverity.INFO, `Starting manager on version ${ManagerInformation.VERSION.toString()}`); ipcRenderer.once('receive-is-portable', (_sender: any, isPortable: boolean) => { diff --git a/src/r2mm/manager/ManagerSettings.ts b/src/r2mm/manager/ManagerSettings.ts index 85cc1d1c6..f23d250c0 100644 --- a/src/r2mm/manager/ManagerSettings.ts +++ b/src/r2mm/manager/ManagerSettings.ts @@ -32,9 +32,10 @@ export default class ManagerSettings { public darkTheme: boolean = false; public launchParameters: string = ''; public ignoreCache: boolean = false; + public dataDirectory: string = PathResolver.APPDATA_DIR; public load(): R2Error | void { - configPath = path.join(PathResolver.ROOT, 'config'); + configPath = path.join(PathResolver.APPDATA_DIR, 'config'); configFile = path.join(configPath, 'conf.yml'); fs.ensureDirSync(configPath); if (fs.existsSync(configFile)) { @@ -49,6 +50,7 @@ export default class ManagerSettings { this.darkTheme = parsedYaml.darkTheme; this.launchParameters = parsedYaml.launchParameters || ''; this.ignoreCache = parsedYaml.ignoreCache || false; + this.dataDirectory = parsedYaml.dataDirectory || PathResolver.APPDATA_DIR; } catch(e) { const err: Error = e; return new YamlParseError( @@ -139,4 +141,9 @@ export default class ManagerSettings { this.ignoreCache = ignore; return this.save(); } + + public setDataDirectory(dataDirectory: string): R2Error | void { + this.dataDirectory = dataDirectory; + return this.save(); + } } diff --git a/src/r2mm/manager/PathResolver.ts b/src/r2mm/manager/PathResolver.ts index 8cb1982fa..a2b240651 100644 --- a/src/r2mm/manager/PathResolver.ts +++ b/src/r2mm/manager/PathResolver.ts @@ -1,20 +1,30 @@ import * as path from 'path'; +import * as fs from 'fs-extra'; +import ManagerSettings from './ManagerSettings'; export default class PathResolver { + private static _APPDATA_DIR: string = ''; private static _ROOT: string = ''; private static _MOD_ROOT: string = ''; - static get ROOT(): string { - return PathResolver._ROOT; + static set APPDATA_DIR(appDataDir: string) { + PathResolver._APPDATA_DIR = appDataDir; + ManagerSettings.getSingleton().load(); + PathResolver._ROOT = ManagerSettings.getSingleton().dataDirectory; + fs.ensureDirSync(PathResolver._ROOT); + PathResolver._MOD_ROOT = path.join(PathResolver._ROOT, 'mods'); } - static set ROOT(root: string) { - PathResolver._ROOT = root; - PathResolver._MOD_ROOT = path.join(root, 'mods'); + static get ROOT(): string { + return PathResolver._ROOT; } static get MOD_ROOT(): string { return PathResolver._MOD_ROOT; } + + static get APPDATA_DIR(): string { + return PathResolver._APPDATA_DIR; + } } diff --git a/tests/test-setup.test.ts b/tests/test-setup.test.ts index e181f2bdd..36f4c34af 100644 --- a/tests/test-setup.test.ts +++ b/tests/test-setup.test.ts @@ -6,13 +6,13 @@ import Profile from '../src/model/Profile'; export default class TestSetup { public static setUp() { - PathResolver.ROOT = '__test_data__'; + PathResolver.APPDATA_DIR = '__test_data__'; new Profile('Default'); } public static tearDown() { - fs.emptyDirSync(PathResolver.ROOT); - fs.removeSync(PathResolver.ROOT); + fs.emptyDirSync(PathResolver.APPDATA_DIR); + fs.removeSync(PathResolver.APPDATA_DIR); } } diff --git a/tests/unit/manager/ManagerSettings.test.ts b/tests/unit/manager/ManagerSettings.test.ts index e62af4153..aacd12709 100644 --- a/tests/unit/manager/ManagerSettings.test.ts +++ b/tests/unit/manager/ManagerSettings.test.ts @@ -23,12 +23,6 @@ describe('ManagerSettings', () => { }); }); - it('Ensure file is created on construction', () => { - expect(fs.existsSync(path.join(PathResolver.ROOT, 'config', 'conf.yml'))).equals(false); - ManagerSettings.getSingleton().load(); - expect(fs.existsSync(path.join(PathResolver.ROOT, 'config', 'conf.yml'))).equals(true); - }); - context('Ensure values saved', () => { it('Expand/collapse cards', () => { booleanSettingTestHelper(