From de15440e6d90bd412b432ff0ccf607af7eaf425a Mon Sep 17 00:00:00 2001 From: Daniel Scalzi Date: Fri, 4 Jan 2019 14:25:27 -0500 Subject: [PATCH] Added option to change data folder location. (#17) Removed commonDirectory. Removed instanceDirectory. Added dataDirectory. The common and instance directories are now resolved from this. The config.json and distribution.json are now stored in Electron's data folder (app.getPath('userData')). Users can edit the dataDirectory under launcher settings. --- app/assets/css/launcher.css | 188 ++++++++++++++++-------------- app/assets/js/configmanager.js | 85 +++++++++----- app/assets/js/scripts/landing.js | 12 +- app/assets/js/scripts/settings.js | 28 +++-- app/settings.ejs | 47 +++++--- package-lock.json | 2 +- package.json | 4 +- 7 files changed, 223 insertions(+), 143 deletions(-) diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index 29a12b1..e203abf 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -1092,8 +1092,9 @@ body, button { display: flex; align-items: center; justify-content: space-between; - margin: 20px 0px; + padding: 20px 0px; width: 75%; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); } .settingsFieldLeft { display: flex; @@ -1107,6 +1108,7 @@ body, button { .settingsFieldDesc { font-size: 12px; color: rgba(255, 255, 255, .95); + margin-top: 5px; } .settingsDivider { height: 1px; @@ -1187,6 +1189,93 @@ input:checked + .toggleSwitchSlider:before { cursor: ew-resize; } +/* File selectors */ + +/* Main container for File selectors. */ +.settingsFileSelContainer { + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); + margin-bottom: 20px; + margin-top: 20px; + width: 75%; +} + +/* File selector title. */ +.settingsFileSelTitle { + margin-bottom: 10px; +} + +/* Wrapper container for the actionable elements. */ +.settingsFileSelActions { + display: flex; + width: 90%; +} + +/* File selector icon settings. */ +.settingsFileSelIcon { + display: flex; + align-items: center; + background: rgba(126, 126, 126, 0.57); + border-radius: 3px 0px 0px 3px; + padding: 5px; + transition: 0.25s ease; +} +.settingsFileSelSVG { + width: 20px; + height: 20px; + fill: white; +} + +/* Disabled text field which stores the selected file path. */ +.settingsFileSelVal { + border-radius: 0px !important; + width: 100%; + padding: 5px 10px; + font-size: 12px; + height: 30px; +} + +/* File input for file selection. */ +.settingsFileSelSel { + width: 0px; + height: 0px; + opacity: 0; +} +.settingsFileSelSel::-webkit-file-upload-button { + display: none; +} + +/* Wrapper label to add a custom style to the file input. */ +.settingsFileSelLabel { + border-left: 0px; + border-radius: 0px 3px 3px 0px; + font-size: 12px; + padding: 0px 5px; + cursor: pointer; + display: flex; + align-items: center; + background: rgba(126, 126, 126, 0.57); + transition: 0.25s ease; + white-space: nowrap; +} +.settingsFileSelLabel:hover, +.settingsFileSelLabel:focus, +.settingsFileSelSel:focus ~ #settingsJavaExecLabel { + text-shadow: 0px 0px 20px white; +} + +/* Description for the file selector. */ +.settingsFileSelDesc { + font-size: 10px; + margin: 20px 0px; + color: lightgrey; + width: 89%; +} +.settingsFileSelDesc strong { + font-family: 'Avenir Medium'; +} + /* * * * Settings View (Account Tab) * * */ @@ -1347,7 +1436,9 @@ input:checked + .toggleSwitchSlider:before { #settingsGameResolutionContainer { display: flex; flex-direction: column; - margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.50); + width: 75%; } #settingsGameResolutionContent { display: flex; @@ -1805,20 +1896,6 @@ input:checked + .toggleSwitchSlider:before { font-weight: bold; } -/* Main container for the Java executable setting. */ -#settingsJavaExecContainer { - display: flex; - flex-direction: column; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - margin-bottom: 20px; - width: 75%; -} - -/* Java executable setting title. */ -#settingsJavaExecTitle { - margin-bottom: 10px; -} - /* Status text which displays details on the selected executable. */ #settingsJavaExecDetails { font-weight: bold; @@ -1826,76 +1903,6 @@ input:checked + .toggleSwitchSlider:before { font-size: 12px; } -/* Wrapper container for the actionable elements. */ -#settingsJavaExecActions { - display: flex; - width: 90%; -} - -/* Java icon settings. */ -.settingsJavaIcon { - display: flex; - align-items: center; - background: rgba(126, 126, 126, 0.57); - border-radius: 3px 0px 0px 3px; - padding: 5px; - transition: 0.25s ease; -} -.settingsJavaIconSVG { - width: 20px; - height: 20px; - fill: white; -} - -/* Disabled text field which stores the selected executable path. */ -#settingsJavaExecVal { - border-radius: 0px !important; - width: 100%; - padding: 5px 10px; - font-size: 12px; -} - -/* File input for executable selection. */ -#settingsJavaExecSel { - width: 0px; - height: 0px; - opacity: 0; -} -#settingsJavaExecSel::-webkit-file-upload-button { - display: none; -} - -/* Wrapper label to add a custom style to the file input. */ -#settingsJavaExecLabel { - border-left: 0px; - border-radius: 0px 3px 3px 0px; - font-size: 12px; - padding: 0px 5px; - cursor: pointer; - display: flex; - align-items: center; - background: rgba(126, 126, 126, 0.57); - transition: 0.25s ease; - white-space: nowrap; -} -#settingsJavaExecLabel:hover, -#settingsJavaExecLabel:focus, -#settingsJavaExecSel:focus ~ #settingsJavaExecLabel { - text-shadow: 0px 0px 20px white; -} - -/* Description for the Java executable setting. */ -#settingsJavaExecDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - font-weight: bold; - width: 89%; -} -#settingsJavaExecDesc strong { - font-family: 'Avenir Medium'; -} - /* Main container for the JVM options setting. */ #settingsJVMOptsContainer { width: 75%; @@ -1932,6 +1939,15 @@ input:checked + .toggleSwitchSlider:before { width: 89%; } +/* * * +* Settings View (Launcher Tab) +* * */ + +/* Tailored style for the data directory header. */ +#settingsDataDirTitle { + margin-bottom: 10px; +} + /* * * * Settings View (About Tab) * * */ diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index 3061d71..ef908c7 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -1,13 +1,46 @@ -const fs = require('fs-extra') -const os = require('os') -const path = require('path') +const fs = require('fs-extra') +const os = require('os') +const path = require('path') const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) const dataPath = path.join(sysRoot, '.westeroscraft') -const firstLaunch = !fs.existsSync(dataPath) +// Forked processes do not have access to electron, so we have this workaround. +const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData') + +/** + * Retrieve the absolute path of the launcher directory. + * + * @returns {string} The absolute path of the launcher directory. + */ +exports.getLauncherDirectory = function(){ + return launcherDir +} + +/** + * Get the launcher's data directory. This is where all files related + * to game launch are installed (common, instances, java, etc). + * + * @returns {string} The absolute path of the launcher's data directory. + */ +exports.getDataDirectory = function(def = false){ + return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory +} + +/** + * Set the new data directory. + * + * @param {string} dataDirectory The new data directory. + */ +exports.setDataDirectory = function(dataDirectory){ + config.settings.launcher.dataDirectory = dataDirectory +} + +const configPath = path.join(exports.getLauncherDirectory(), 'config.json') +const configPathLEGACY = path.join(dataPath, 'config.json') +const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) exports.getAbsoluteMinRAM = function(){ const mem = os.totalmem() @@ -56,7 +89,8 @@ const DEFAULT_CONFIG = { launchDetached: true }, launcher: { - allowPrerelease: false + allowPrerelease: false, + dataDirectory: dataPath } }, newsCache: { @@ -64,8 +98,6 @@ const DEFAULT_CONFIG = { content: null, dismissed: false }, - commonDirectory: path.join(dataPath, 'common'), - instanceDirectory: path.join(dataPath, 'instances'), clientToken: null, selectedServer: null, // Resolved selectedAccount: null, @@ -81,8 +113,7 @@ let config = null * Save the current configuration to a file. */ exports.save = function(){ - const filePath = path.join(dataPath, 'config.json') - fs.writeFileSync(filePath, JSON.stringify(config, null, 4), 'UTF-8') + fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') } /** @@ -92,24 +123,29 @@ exports.save = function(){ * need to be externally assigned. */ exports.load = function(){ - // Determine the effective configuration. - const filePath = path.join(dataPath, 'config.json') + let doLoad = true - if(!fs.existsSync(filePath)){ + if(!fs.existsSync(configPath)){ // Create all parent directories. - fs.ensureDirSync(path.join(filePath, '..')) - config = DEFAULT_CONFIG - exports.save() - } else { + fs.ensureDirSync(path.join(configPath, '..')) + if(fs.existsSync(configPathLEGACY)){ + fs.moveSync(configPathLEGACY, configPath) + } else { + doLoad = false + config = DEFAULT_CONFIG + exports.save() + } + } + if(doLoad){ let doValidate = false try { - config = JSON.parse(fs.readFileSync(filePath, 'UTF-8')) + config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) doValidate = true } catch (err){ logger.error(err) logger.log('Configuration file contains malformed JSON or is corrupt.') logger.log('Generating a new configuration file.') - fs.ensureDirSync(path.join(filePath, '..')) + fs.ensureDirSync(path.join(configPath, '..')) config = DEFAULT_CONFIG exports.save() } @@ -152,15 +188,6 @@ function validateKeySet(srcObj, destObj){ return destObj } -/** - * Retrieve the absolute path of the launcher directory. - * - * @returns {string} The absolute path of the launcher directory. - */ -exports.getLauncherDirectory = function(){ - return dataPath -} - /** * Check to see if this is the first time the user has launched the * application. This is determined by the existance of the data path. @@ -218,7 +245,7 @@ exports.setNewsCacheDismissed = function(dismissed){ * @returns {string} The launcher's common directory. */ exports.getCommonDirectory = function(){ - return config.commonDirectory + return path.join(exports.getDataDirectory(), 'common') } /** @@ -228,7 +255,7 @@ exports.getCommonDirectory = function(){ * @returns {string} The launcher's instance directory. */ exports.getInstanceDirectory = function(){ - return config.instanceDirectory + return path.join(exports.getDataDirectory(), 'instances') } /** diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js index e7b7384..31a6f92 100644 --- a/app/assets/js/scripts/landing.js +++ b/app/assets/js/scripts/landing.js @@ -275,11 +275,15 @@ function asyncSystemScan(mcVersion, launchAfter = true){ const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold') + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + // Fork a process to run validations. sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ ConfigManager.getCommonDirectory(), ConfigManager.getJavaExecutable() ], { + env: forkEnv, stdio: 'pipe' }) // Stdout @@ -307,7 +311,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){ ) setOverlayHandler(() => { setLaunchDetails('Preparing Java Download..') - sysAEx.send({task: 'execute', function: '_enqueueOracleJRE', argsArr: [ConfigManager.getLauncherDirectory()]}) + sysAEx.send({task: 'execute', function: '_enqueueOracleJRE', argsArr: [ConfigManager.getDataDirectory()]}) toggleOverlay(false) }) setDismissHandler(() => { @@ -431,7 +435,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){ // Begin system Java scan. setLaunchDetails('Checking system info..') - sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory(), mcVersion]}) + sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory(), mcVersion]}) } @@ -470,11 +474,15 @@ function dlAsync(login = true){ const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold') const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold') + const forkEnv = JSON.parse(JSON.stringify(process.env)) + forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + // Start AssetExec to run validations and downloads in a forked process. aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ ConfigManager.getCommonDirectory(), ConfigManager.getJavaExecutable() ], { + env: forkEnv, stdio: 'pipe' }) // Stdout diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js index 52445cf..8e8c54b 100644 --- a/app/assets/js/scripts/settings.js +++ b/app/assets/js/scripts/settings.js @@ -41,6 +41,24 @@ document.addEventListener('click', closeSettingsSelect) bindSettingsSelect() +function bindFileSelectors(){ + for(let ele of document.getElementsByClassName('settingsFileSelSel')){ + if(ele.id === 'settingsJavaExecSel'){ + ele.onchange = (e) => { + ele.previousElementSibling.value = ele.files[0].path + populateJavaExecDetails(ele.previousElementSibling.value) + } + } else { + ele.onchange = (e) => { + ele.previousElementSibling.value = ele.files[0].path + } + } + } +} + +bindFileSelectors() + + /** * General Settings Functions */ @@ -97,6 +115,8 @@ function initSettingsValues(){ if(cVal === 'JavaExecutable'){ populateJavaExecDetails(v.value) v.value = gFn() + } else if (cVal === 'DataDirectory'){ + v.value = gFn() } else if(cVal === 'JVMOptions'){ v.value = gFn().join(' ') } else { @@ -905,8 +925,6 @@ const settingsMinRAMLabel = document.getElementById('settingsMinRAMLabel') const settingsMemoryTotal = document.getElementById('settingsMemoryTotal') const settingsMemoryAvail = document.getElementById('settingsMemoryAvail') const settingsJavaExecDetails = document.getElementById('settingsJavaExecDetails') -const settingsJavaExecVal = document.getElementById('settingsJavaExecVal') -const settingsJavaExecSel = document.getElementById('settingsJavaExecSel') // Store maximum memory values. const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM() @@ -1092,12 +1110,6 @@ function populateMemoryStatus(){ settingsMemoryAvail.innerHTML = Number(os.freemem()/1000000000).toFixed(1) + 'G' } -// Bind the executable file input to the display text input. -settingsJavaExecSel.onchange = (e) => { - settingsJavaExecVal.value = settingsJavaExecSel.files[0].path - populateJavaExecDetails(settingsJavaExecVal.value) -} - /** * Validate the provided executable path and display the data on * the UI. diff --git a/app/settings.ejs b/app/settings.ejs index b1ed183..caa0f53 100644 --- a/app/settings.ejs +++ b/app/settings.ejs @@ -53,7 +53,6 @@ -
Launch in fullscreen. @@ -65,7 +64,6 @@
-
Automatically connect to the server on launch. @@ -77,7 +75,6 @@
-
Launch game process detached from launcher. @@ -186,13 +183,13 @@
-
-
Java Executable
-
+
+
Java Executable
+
Selected: Java 8 Update 172 (x64)
-
-
- +
+
+ @@ -205,18 +202,18 @@
- - > - + + > +
-
The Java executable is validated before game launch. Requires Java 8 x64.
The path should end with bin<%= process.platform === 'win32' ? '\\javaw.exe' : '/java' %>.
+
The Java executable is validated before game launch. Requires Java 8 x64.
The path should end with bin<%= process.platform === 'win32' ? '\\javaw.exe' : '/java' %>.
Additional JVM Options
-
- +
+ @@ -251,6 +248,26 @@
+
+
+
Data Directory
+
+
+ + + + + + + +
+ + + +
+
+
All game files and local Java installations will be stored in the data directory.
Screenshots and world saves are stored in the instance folder for the corresponding server configuration.
+