diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index 093cb3b..eeb0d9a 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -1042,6 +1042,11 @@ body, button { margin-top: 5%; } +/* Add spacing to the bottom of each settings tab. */ +.settingsTab > *:last-child { + margin-bottom: 20%; +} + /* Tab header shared styles. */ .settingsTabHeader { display: flex; @@ -1364,7 +1369,8 @@ input:checked + .toggleSwitchSlider:before { * * */ #settingsReqModsContent, -#settingsOptModsContent { +#settingsOptModsContent, +#settingsDropinModsContent { font-size: 12px; background: rgba(0, 0, 0, 0.25); border-radius: 3px; @@ -1382,11 +1388,13 @@ input:checked + .toggleSwitchSlider:before { } #settingsReqModsContainer, -#settingsOptModsContainer { +#settingsOptModsContainer, +#settingsDropinModsContainer { padding-bottom: 25px; } -.settingsMod { +.settingsMod, +.settingsDropinMod { padding: 10px; } @@ -1432,13 +1440,11 @@ input:checked + .toggleSwitchSlider:before { pointer-events: none; } -.settingsMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus, -.settingsSubMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus { +.settingsBaseMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus { background-color: rgb(165, 195, 37); } -.settingsMod:not([enabled]) > .settingsSubModContainer .settingsModContent, -.settingsSubMod:not([enabled]) > .settingsSubModContainer .settingsModContent { +.settingsBaseMod:not([enabled]) > .settingsSubModContainer .settingsModContent { opacity: 0.5; } @@ -1508,6 +1514,50 @@ settingsSubModContainer > .settingsSubMod:only-child { opacity: 1; } +.settingsDropinRemoveButton { + background: none; + border: none; + font-size: 10px; + text-align: left; + padding: 0px; + color: #c32625; + font-weight: bold; + cursor: pointer; + outline: none; + transition: 0.25s ease; +} +.settingsDropinRemoveButton:hover, +.settingsDropinRemoveButton:focus { + text-shadow: 0px 0px 20px #c32625, 0px 0px 20px #c32625, 0px 0px 20px #c32625; +} +.settingsDropinRemoveButton:active { + color: #9b1f1f; + text-shadow: 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f; +} + +#settingsDropinFileSystemButton { + background: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(126, 126, 126, 0.57); + border-radius: 3px; + height: 50px; + width: 100%; + text-align: left; + padding: 0px 50px; + cursor: pointer; + outline: none; + transition: 0.25s ease; + margin-bottom: 10px; +} +#settingsDropinFileSystemButton:hover, +#settingsDropinFileSystemButton:focus { + background: rgba(54, 54, 54, 0.25); + text-shadow: 0px 0px 20px white; +} + +#settingsDropinRefreshNote { + font-size: 10px; +} + /* * * * Settings View (Java Tab) * * */ diff --git a/app/assets/js/dropinmodutil.js b/app/assets/js/dropinmodutil.js new file mode 100644 index 0000000..d2ab1f2 --- /dev/null +++ b/app/assets/js/dropinmodutil.js @@ -0,0 +1,109 @@ +const fs = require('fs') +const path = require('path') +const { shell } = require('electron') + +// Group #1: File Name (without .disabled, if any) +// Group #2: File Extension (jar, zip, or litemod) +// Group #3: If it is disabled (if string 'disabled' is present) +const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ +const DISABLED_EXT = '.disabled' + +/** + * Scan for drop-in mods in both the mods folder and version + * safe mods folder. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} version The minecraft version of the server configuration. + * + * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} + * An array of objects storing metadata about each discovered mod. + */ +exports.scanForDropinMods = function(modsDir, version) { + const modsDiscovered = [] + if(fs.existsSync(modsDir)){ + let modCandidates = fs.readdirSync(modsDir) + let verCandidates = [] + const versionDir = path.join(modsDir, version) + if(fs.existsSync(versionDir)){ + verCandidates = fs.readdirSync(versionDir) + } + for(file of modCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: match[0], + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + for(file of verCandidates){ + const match = MOD_REGEX.exec(file) + if(match != null){ + modsDiscovered.push({ + fullName: path.join(version, match[0]), + name: match[1], + ext: match[2], + disabled: match[3] != null + }) + } + } + } + return modsDiscovered +} + +/** + * Delete a drop-in mod from the file system. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to delete. + * + * @returns {boolean} True if the mod was deleted, otherwise false. + */ +exports.deleteDropinMod = function(modsDir, fullName){ + /*return new Promise((resolve, reject) => { + fs.unlink(path.join(modsDir, fullName), (err) => { + if(err){ + reject(err) + } else { + resolve() + } + }) + })*/ + const res = shell.moveItemToTrash(path.join(modsDir, fullName)) + if(!res){ + shell.beep() + } + return res +} + +/** + * Toggle a discovered mod on or off. This is achieved by either + * adding or disabling the .disabled extension to the local file. + * + * @param {string} modsDir The path to the mods directory. + * @param {string} fullName The fullName of the discovered mod to toggle. + * @param {boolean} enable Whether to toggle on or off the mod. + * + * @returns {Promise.} A promise which resolves when the mod has + * been toggled. If an IO error occurs the promise will be rejected. + */ +exports.toggleDropinMod = function(modsDir, fullName, enable){ + return new Promise((resolve, reject) => { + const oldPath = path.join(modsDir, fullName) + const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) + + fs.rename(oldPath, newPath, (err) => { + if(err){ + reject(err) + } else { + resolve() + } + }) + }) +} + +exports.isDropinModEnabled = function(fullName){ + return !fullName.endsWith(DISABLED_EXT) +} \ No newline at end of file diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js index 11b2da6..8702872 100644 --- a/app/assets/js/scripts/overlay.js +++ b/app/assets/js/scripts/overlay.js @@ -4,6 +4,15 @@ /* Overlay Wrapper Functions */ +/** + * Check to see if the overlay is visible. + * + * @returns {boolean} Whether or not the overlay is visible. + */ +function isOverlayVisible(){ + return document.getElementById('main').hasAttribute('overlay'); +} + /** * Toggle the visibility of the overlay. * diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js index 4fef65e..7c4ed91 100644 --- a/app/assets/js/scripts/settings.js +++ b/app/assets/js/scripts/settings.js @@ -3,6 +3,7 @@ const os = require('os') const semver = require('semver') const { AssetGuard } = require('./assets/js/assetguard') +const DropinModUtil = require('./assets/js/dropinmodutil') const settingsState = { invalid: new Set() @@ -233,6 +234,7 @@ settingsNavDone.onclick = () => { saveSettingsValues() saveModConfiguration() ConfigManager.save() + saveDropinModConfiguration() switchView(getCurrentView(), VIEWS.landing) } @@ -450,7 +452,7 @@ function parseModulesForUI(mdls, submodules, servConf){ if(mdl.getRequired().isRequired()){ - reqMods += `
+ reqMods += `
@@ -474,7 +476,7 @@ function parseModulesForUI(mdls, submodules, servConf){ const conf = servConf[mdl.getVersionlessID()] const val = typeof conf === 'object' ? conf.value : conf - optMods += `
+ optMods += `
@@ -542,14 +544,16 @@ function saveModConfiguration(){ function _saveModConfiguration(modConf){ for(m of Object.entries(modConf)){ const tSwitch = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`) - if(typeof m[1] === 'boolean'){ - modConf[m[0]] = tSwitch[0].checked - } else { - if(m[1] != null){ - if(tSwitch.length > 0){ - modConf[m[0]].value = tSwitch[0].checked + if(!tSwitch[0].hasAttribute('dropin')){ + if(typeof m[1] === 'boolean'){ + modConf[m[0]] = tSwitch[0].checked + } else { + if(m[1] != null){ + if(tSwitch.length > 0){ + modConf[m[0]].value = tSwitch[0].checked + } + modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) } - modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) } } } @@ -557,7 +561,6 @@ function _saveModConfiguration(modConf){ } function loadSelectedServerOnModsTab(){ - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) document.getElementById('settingsSelServContent').innerHTML = ` @@ -583,13 +586,110 @@ function loadSelectedServerOnModsTab(){ ` } -document.getElementById("settingsSwitchServerButton").addEventListener('click', (e) => { +let CACHE_SETTINGS_MODS_DIR +let CACHE_DROPIN_MODS + +function resolveDropinModsForUI(){ + const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.getID(), 'mods') + CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.getMinecraftVersion()) + + let dropinMods = '' + + for(dropin of CACHE_DROPIN_MODS){ + dropinMods += `
+
+
+
+
+ ${dropin.name} +
+ +
+
+
+ +
+
` + } + + document.getElementById('settingsDropinModsContent').innerHTML = dropinMods +} + +function bindDropinModsRemoveButton(){ + const sEls = settingsModsContainer.querySelectorAll('[remmod]') + Array.from(sEls).map((v, index, arr) => { + v.onclick = () => { + const fullName = v.getAttribute('remmod') + const res = DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName) + if(res){ + document.getElementById(fullName).remove() + } else { + setOverlayContent( + `Failed to Delete
Drop-in Mod ${fullName}`, + 'Make sure the file is not in use and try again.', + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + } + } + }) +} + +function bindDropinModFileSystemButton(){ + const fsBtn = document.getElementById('settingsDropinFileSystemButton') + fsBtn.onclick = () => { + shell.openItem(CACHE_SETTINGS_MODS_DIR) + } +} + +function saveDropinModConfiguration(){ + for(dropin of CACHE_DROPIN_MODS){ + const dropinUI = document.getElementById(dropin.fullName) + if(dropinUI != null){ + const dropinUIEnabled = dropinUI.hasAttribute('enabled') + if(DropinModUtil.isDropinModEnabled(dropin.fullName) != dropinUIEnabled){ + DropinModUtil.toggleDropinMod(CACHE_SETTINGS_MODS_DIR, dropin.fullName, dropinUIEnabled).catch(err => { + if(!isOverlayVisible()){ + setOverlayContent( + 'Failed to Toggle
One or More Drop-in Mods', + err.message, + 'Okay' + ) + setOverlayHandler(null) + toggleOverlay(true) + } + }) + } + } + } +} + +document.getElementById('settingsSwitchServerButton').addEventListener('click', (e) => { e.target.blur() toggleServerSelection(true) }) +document.addEventListener('keydown', (e) => { + if(getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods'){ + if(e.key === 'F5'){ + resolveDropinModsForUI() + bindDropinModsRemoveButton() + bindDropinModFileSystemButton() + bindModsToggleSwitch() + } + } +}) + function animateModsTabRefresh(){ $('#settingsTabMods').fadeOut(500, () => { + saveModConfiguration() + ConfigManager.save() + saveDropinModConfiguration() prepareModsTab() $('#settingsTabMods').fadeIn(500) }) @@ -600,6 +700,9 @@ function animateModsTabRefresh(){ */ function prepareModsTab(first){ resolveModsForUI() + resolveDropinModsForUI() + bindDropinModsRemoveButton() + bindDropinModFileSystemButton() bindModsToggleSwitch() loadSelectedServerOnModsTab() } diff --git a/app/settings.ejs b/app/settings.ejs index 5b273f9..cc0d1dd 100644 --- a/app/settings.ejs +++ b/app/settings.ejs @@ -117,6 +117,13 @@
Optional Mods
+
+
+
+
Drop-in Mods
+ +
+