瀏覽代碼

Added support for drop-in mods on the UI.

Daniel Scalzi 7 年之前
父節點
當前提交
556199aa55
共有 5 個文件被更改,包括 296 次插入18 次删除
  1. 57 7
      app/assets/css/launcher.css
  2. 109 0
      app/assets/js/dropinmodutil.js
  3. 9 0
      app/assets/js/scripts/overlay.js
  4. 114 11
      app/assets/js/scripts/settings.js
  5. 7 0
      app/settings.ejs

+ 57 - 7
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)
 * * */

+ 109 - 0
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.<void>} 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)
+}

+ 9 - 0
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.
  * 

+ 114 - 11
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 += `<div id="${mdl.getVersionlessID()}" class="settings${submodules ? 'Sub' : ''}Mod" enabled>
+                reqMods += `<div id="${mdl.getVersionlessID()}" class="settingsBaseMod settings${submodules ? 'Sub' : ''}Mod" enabled>
                     <div class="settingsModContent">
                         <div class="settingsModMainWrapper">
                             <div class="settingsModStatus"></div>
@@ -474,7 +476,7 @@ function parseModulesForUI(mdls, submodules, servConf){
                 const conf = servConf[mdl.getVersionlessID()]
                 const val = typeof conf === 'object' ? conf.value : conf
 
-                optMods += `<div id="${mdl.getVersionlessID()}" class="settings${submodules ? 'Sub' : ''}Mod" ${val ? 'enabled' : ''}>
+                optMods += `<div id="${mdl.getVersionlessID()}" class="settingsBaseMod settings${submodules ? 'Sub' : ''}Mod" ${val ? 'enabled' : ''}>
                     <div class="settingsModContent">
                         <div class="settingsModMainWrapper">
                             <div class="settingsModStatus"></div>
@@ -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 += `<div id="${dropin.fullName}" class="settingsBaseMod settingsDropinMod" ${!dropin.disabled ? 'enabled' : ''}>
+                    <div class="settingsModContent">
+                        <div class="settingsModMainWrapper">
+                            <div class="settingsModStatus"></div>
+                            <div class="settingsModDetails">
+                                <span class="settingsModName">${dropin.name}</span>
+                                <div class="settingsDropinRemoveWrapper">
+                                    <button class="settingsDropinRemoveButton" remmod="${dropin.fullName}">Remove</button>
+                                </div>
+                            </div>
+                        </div>
+                        <label class="toggleSwitch">
+                            <input type="checkbox" formod="${dropin.fullName}" dropin ${!dropin.disabled ? 'checked' : ''}>
+                            <span class="toggleSwitchSlider"></span>
+                        </label>
+                    </div>
+                </div>`
+    }
+
+    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<br>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<br>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()
 }

+ 7 - 0
app/settings.ejs

@@ -117,6 +117,13 @@
                     <div class="settingsModsHeader">Optional Mods</div>
                     <div id="settingsOptModsContent">
                         
+                    </div>
+                </div>
+                <div id="settingsDropinModsContainer">
+                    <div class="settingsModsHeader">Drop-in Mods</div>
+                    <button id="settingsDropinFileSystemButton">+ Add Mods <span id="settingsDropinRefreshNote">(F5 to Refresh)</span></button>
+                    <div id="settingsDropinModsContent">
+                        
                     </div>
                 </div>
             </div>