Skip to content

Commit 323a0c7

Browse files
committed
Add support for mod metadata handling and display in the mod dashboard
1 parent 0b2d726 commit 323a0c7

File tree

6 files changed

+226
-71
lines changed

6 files changed

+226
-71
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"test": "node ."
1212
},
1313
"dependencies": {
14+
"@iarna/toml": "^2.2.5",
1415
"adm-zip": "^0.5.16",
1516
"axios": "^1.9.0",
1617
"cli-progress": "^3.12.0",

ui/chrome/js/moddashboard.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const mods = await window.mcAPI.getModFiles()
12
document.getElementById("search").addEventListener("click", async function() {
23
let search = document.getElementById("searchText").value;
34
let searchUrl = "https://mc-backend-six.vercel.app/api/search?q=" + encodeURIComponent(search)
@@ -21,4 +22,19 @@ document.getElementById("search").addEventListener("click", async function() {
2122
`
2223
resultDiv.appendChild(projectDiv)
2324
}
24-
});
25+
});
26+
27+
const modsDiv = document.getElementById("installedMods")
28+
for (var i = 0; i < mods.length; i++) {
29+
let mod = mods[i]
30+
let modDiv = document.createElement("div")
31+
modDiv.className = "mod"
32+
modDiv.innerHTML = `
33+
<h3>${mod.metadata[0].name}</h3>
34+
<p>${mod.metadata[0].description}</p>
35+
<p>Version: ${mod.metadata[0].version}</p>
36+
<p>Path: ${mod.path}</p>
37+
<button class="uninstall" onclick="uninstallMod('${mod.path}')">Uninstall</button>
38+
`
39+
modsDiv.appendChild(modDiv)
40+
}

ui/chrome/moddashboard/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
const mainMod = await downloadMod(modId)
3636
loopThroughDependencies(mainMod)
3737
}
38+
39+
async function uninstallMod(path) {
40+
window.mcAPI.deleteFromModsFolder(path)
41+
}
3842
</script>
3943
</head>
4044

@@ -51,6 +55,12 @@ <h1>Mod Dashboard</h1>
5155
<div id="result">
5256

5357
</div>
58+
<div id="modList">
59+
<h2>Installed Mods</h2>
60+
<ul id="installedMods">
61+
<!-- Installed mods will be listed here -->
62+
</ul>
63+
</div>
5464
</main>
5565
<script src="../js/moddashboard.js" defer type="module"></script>
5666

ui/externalScripts/main_preload.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ contextBridge.exposeInMainWorld('mcAPI', {
77
downloadToModsFolder: (url) => {
88
ipcRenderer.send('downloadToModsFolder', url);
99
},
10-
getModFiles: (url) => {
11-
ipcRenderer.send('getModFiles', url);
12-
}
10+
getModFiles: async () => {
11+
return await ipcRenderer.invoke('getModsWithMetadata');
12+
},
13+
deleteFromModsFolder: (path) => {
14+
ipcRenderer.send('deleteFromModsFolder', path);
15+
},
1316
})

ui_index.cjs

Lines changed: 185 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,199 @@
1+
// main.js
2+
13
const { app, BrowserWindow, ipcMain } = require('electron');
24
const path = require('path');
35
const fs = require('fs');
46
const { spawn } = require('child_process');
5-
const mainIndex = path.join(__dirname, 'index.js')
6-
7-
const createWindow = () => {
8-
const win = new BrowserWindow({
9-
width: 800,
10-
height: 600,
11-
webPreferences: {
12-
preload: path.join(__dirname, 'ui', 'externalScripts', 'main_preload.js')
13-
}
14-
});
157

16-
win.loadFile(path.join(__dirname, 'ui', 'chrome', 'index.html'));
8+
// New dependencies for reading mod metadata
9+
const AdmZip = require('adm-zip');
10+
const toml = require('@iarna/toml');
11+
12+
// Path to your non-UI entry point
13+
const mainIndex = path.join(__dirname, 'index.js');
14+
15+
function createWindow() {
16+
const win = new BrowserWindow({
17+
width: 800,
18+
height: 600,
19+
webPreferences: {
20+
preload: path.join(__dirname, 'ui', 'externalScripts', 'main_preload.js'),
21+
},
22+
});
23+
24+
win.loadFile(path.join(__dirname, 'ui', 'chrome', 'index.html'));
25+
}
26+
27+
function createConsole() {
28+
const consoleWin = new BrowserWindow({
29+
width: 800,
30+
height: 600,
31+
webPreferences: {
32+
preload: path.join(__dirname, 'ui', 'externalScripts', 'console_preload.js'),
33+
},
34+
});
35+
36+
consoleWin.loadFile(path.join(__dirname, 'ui', 'chrome', 'console.html'));
37+
return consoleWin;
1738
}
1839

19-
const createConsole = () => {
20-
const consoleWin = new BrowserWindow({
21-
width: 800,
22-
height: 600,
23-
webPreferences: {
24-
preload: path.join(__dirname, 'ui', 'externalScripts', 'console_preload.js')
25-
}
40+
/**
41+
* Extracts NeoForge/Fabric mod metadata from a .jar/.zip
42+
* Priorities:
43+
* 1. META-INF/neoforge.mods.toml
44+
* 2. META-INF/mods.toml (legacy)
45+
* 3. fabric.mod.json
46+
* 4. META-INF/MANIFEST.MF
47+
*/
48+
function getModMetadata(jarPath) {
49+
const zip = new AdmZip(jarPath);
50+
51+
// 1. NeoForge TOML
52+
let entry = zip.getEntry('META-INF/neoforge.mods.toml');
53+
if (entry) {
54+
const raw = entry.getData().toString('utf8');
55+
const parsed = toml.parse(raw);
56+
return parsed.mods.map(mod => ({
57+
id: mod.modId,
58+
version: mod.version,
59+
name: mod.displayName,
60+
description: mod.description,
61+
authors: mod.authors,
62+
dependencies: mod.dependencies || {},
63+
}));
64+
}
65+
66+
// 2. Legacy mods.toml
67+
entry = zip.getEntry('META-INF/mods.toml');
68+
if (entry) {
69+
const raw = entry.getData().toString('utf8');
70+
const parsed = toml.parse(raw);
71+
return parsed.mods.map(mod => ({
72+
id: mod.modId,
73+
version: mod.version,
74+
name: mod.displayName,
75+
description: mod.description,
76+
authors: mod.authors,
77+
dependencies: mod.dependencies || {},
78+
}));
79+
}
80+
81+
// 3. Fabric JSON
82+
entry = zip.getEntry('fabric.mod.json');
83+
if (entry) {
84+
const json = JSON.parse(entry.getData().toString('utf8'));
85+
return [{
86+
id: json.id,
87+
version: json.version,
88+
name: (json.metadata && json.metadata.name) || json.id,
89+
description: json.metadata && json.metadata.description,
90+
authors: (json.metadata && json.metadata.contributors || []).map(c => c.name),
91+
dependencies: json.depends || {},
92+
}];
93+
}
94+
95+
// 4. Manifest fallback
96+
entry = zip.getEntry('META-INF/MANIFEST.MF');
97+
if (entry) {
98+
const text = entry.getData().toString('utf8');
99+
const props = {};
100+
text.split(/\r?\n/).forEach(line => {
101+
const [k, v] = line.split(': ');
102+
if (k && v) props[k.trim()] = v.trim();
26103
});
104+
return [{
105+
id: props['Implementation-Title'] || path.basename(jarPath),
106+
version: props['Implementation-Version'] || 'unknown',
107+
}];
108+
}
27109

28-
consoleWin.loadFile(path.join(__dirname, 'ui', 'chrome', 'console.html'));
29-
return consoleWin
110+
// No metadata found
111+
return [];
30112
}
31113

32114
app.whenReady().then(() => {
33-
ipcMain.on('launch', (event, arg) => {
34-
let consoleWin = createConsole();
35-
const process = spawn('node', [mainIndex, "--ui"]);
36-
37-
process.stdout.on('data', (data) => {
38-
consoleWin.webContents.send('msg', String(data)); // Send to renderer
39-
console.log(`stdout: ${data}`);
40-
});
41-
42-
process.stderr.on('data', (data) => {
43-
consoleWin.webContents.send('error', String(data)); // Send to renderer
44-
console.error(`stderr: ${data}`);
45-
});
46-
47-
process.on('close', (code) => {
48-
console.log(`child process exited with code ${code}`);
49-
});
50-
51-
process.on('error', (error) => {
52-
console.error(`Error: ${error}`);
53-
});
54-
55-
process.on('exit', (code) => {
56-
console.log(`Process exited with code: ${code}`);
57-
event.reply('process-exit', code);
58-
consoleWin.close();
59-
consoleWin = null;
60-
});
115+
// Launch handler (existing)
116+
ipcMain.on('launch', (event, arg) => {
117+
let consoleWin = createConsole();
118+
const proc = spawn('node', [mainIndex, '--ui']);
119+
120+
proc.stdout.on('data', data => {
121+
consoleWin.webContents.send('msg', data.toString());
122+
console.log(`stdout: ${data}`);
61123
});
62-
ipcMain.on("downloadToModsFolder", (event, url) => {
63-
fetch(url)
64-
.then(response => {
65-
if (!response.ok) {
66-
throw new Error('Network response was not ok');
67-
}
68-
return response.arrayBuffer();
69-
})
70-
.then(buffer => {
71-
const modsFolder = path.join(__dirname, ".minecraft", 'mods');
72-
if (!fs.existsSync(modsFolder)) {
73-
fs.mkdirSync(modsFolder);
74-
}
75-
const filePath = path.join(modsFolder, decodeURIComponent(path.basename(url)));
76-
fs.writeFileSync(filePath, Buffer.from(buffer));
77-
event.reply('download-complete', filePath);
78-
})
124+
125+
proc.stderr.on('data', data => {
126+
consoleWin.webContents.send('error', data.toString());
127+
console.error(`stderr: ${data}`);
79128
});
80-
createWindow();
81-
});
129+
130+
proc.on('close', code => {
131+
console.log(`child process exited with code ${code}`);
132+
});
133+
134+
proc.on('error', error => {
135+
console.error(`Error: ${error}`);
136+
});
137+
138+
proc.on('exit', code => {
139+
console.log(`Process exited with code: ${code}`);
140+
event.reply('process-exit', code);
141+
consoleWin.close();
142+
consoleWin = null;
143+
});
144+
});
145+
146+
// Download-to-mods-folder handler (existing)
147+
ipcMain.on('downloadToModsFolder', (event, url) => {
148+
fetch(url)
149+
.then(response => {
150+
if (!response.ok) throw new Error('Network response was not ok');
151+
return response.arrayBuffer();
152+
})
153+
.then(buffer => {
154+
const modsFolder = path.join(__dirname, '.minecraft', 'mods');
155+
if (!fs.existsSync(modsFolder)) fs.mkdirSync(modsFolder);
156+
const filePath = path.join(modsFolder, decodeURIComponent(path.basename(url)));
157+
fs.writeFileSync(filePath, Buffer.from(buffer));
158+
event.reply('download-complete', filePath);
159+
})
160+
.catch(err => {
161+
event.reply('download-error', err.message);
162+
});
163+
});
164+
165+
// List raw mod files (existing)
166+
ipcMain.handle('getInstalledMods', () => {
167+
const modsFolder = path.join(__dirname, '.minecraft', 'mods');
168+
if (!fs.existsSync(modsFolder)) fs.mkdirSync(modsFolder);
169+
return fs.readdirSync(modsFolder)
170+
.filter(f => f.endsWith('.jar') || f.endsWith('.zip'))
171+
.map(f => path.join(modsFolder, f));
172+
});
173+
174+
// New: List mods with parsed metadata
175+
ipcMain.handle('getModsWithMetadata', () => {
176+
const modsFolder = path.join(__dirname, '.minecraft', 'mods');
177+
if (!fs.existsSync(modsFolder)) fs.mkdirSync(modsFolder);
178+
179+
return fs.readdirSync(modsFolder)
180+
.filter(f => f.endsWith('.jar') || f.endsWith('.zip'))
181+
.map(filename => {
182+
const fullPath = path.join(modsFolder, filename);
183+
const metadata = getModMetadata(fullPath);
184+
return { path: fullPath, metadata };
185+
});
186+
});
187+
188+
ipcMain.on('deleteFromModsFolder', (event, path) => {
189+
fs.rmSync(path, { force: true });
190+
});
191+
192+
// Open main window
193+
createWindow();
194+
});
195+
196+
// Quit on all windows closed (optional, standard behavior)
197+
app.on('window-all-closed', () => {
198+
if (process.platform !== 'darwin') app.quit();
199+
});

0 commit comments

Comments
 (0)