Skip to content

Commit ecce9f9

Browse files
author
Kiosk
committed
feat: ROMs tab with library browser, upload, download, delete
- Renamed Saves tab to ROMs with upload button at top - ROM library with collapsible system accordions - ROM download and delete with path-traversal protection - Saves section preserved above ROM library
1 parent 9e956f1 commit ecce9f9

2 files changed

Lines changed: 199 additions & 8 deletions

File tree

game-menu.html

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,79 @@
603603
}
604604
.perf-profile-add:hover { border-color: #4a9eff; color: #4a9eff; }
605605
606+
/* Section labels */
607+
.roms-section-label {
608+
font-size: 10px;
609+
font-weight: 700;
610+
color: rgba(255, 255, 255, 0.25);
611+
text-transform: uppercase;
612+
letter-spacing: 1px;
613+
padding: 0 0 4px;
614+
}
615+
/* ROM list */
616+
.roms-list {
617+
display: flex;
618+
flex-direction: column;
619+
gap: 4px;
620+
}
621+
.rom-system-header {
622+
display: flex;
623+
align-items: center;
624+
gap: 8px;
625+
padding: 8px;
626+
background: rgba(255, 255, 255, 0.04);
627+
border-radius: 6px;
628+
cursor: pointer;
629+
user-select: none;
630+
transition: background 0.15s;
631+
}
632+
.rom-system-header:hover { background: rgba(255, 255, 255, 0.08); }
633+
.rom-system-chevron {
634+
font-size: 10px;
635+
color: rgba(255, 255, 255, 0.3);
636+
transition: transform 0.2s;
637+
flex-shrink: 0;
638+
}
639+
.rom-system-header.expanded .rom-system-chevron { transform: rotate(90deg); }
640+
.rom-system-name {
641+
flex: 1;
642+
font-size: 12px;
643+
font-weight: 600;
644+
color: rgba(255, 255, 255, 0.6);
645+
}
646+
.rom-system-count {
647+
font-size: 10px;
648+
color: rgba(255, 255, 255, 0.25);
649+
}
650+
.rom-system-items {
651+
display: none;
652+
flex-direction: column;
653+
gap: 2px;
654+
padding: 2px 0 2px 18px;
655+
}
656+
.rom-system-header.expanded + .rom-system-items { display: flex; }
657+
.rom-item {
658+
display: flex;
659+
align-items: center;
660+
gap: 8px;
661+
padding: 6px 8px;
662+
background: rgba(255, 255, 255, 0.04);
663+
border-radius: 4px;
664+
font-size: 12px;
665+
color: rgba(255, 255, 255, 0.7);
666+
}
667+
.rom-item:hover { background: rgba(255, 255, 255, 0.08); }
668+
.rom-name {
669+
flex: 1;
670+
overflow: hidden;
671+
text-overflow: ellipsis;
672+
white-space: nowrap;
673+
}
674+
.rom-players {
675+
font-size: 10px;
676+
color: rgba(255, 255, 255, 0.3);
677+
white-space: nowrap;
678+
}
606679
/* Save files section */
607680
.saves-list {
608681
display: flex;
@@ -1026,7 +1099,7 @@
10261099
<div class="settings-tabs">
10271100
<button class="settings-tab active" data-pane="options">⚙️ Options</button>
10281101
<button class="settings-tab" data-pane="perf">⚡ Perf</button>
1029-
<button class="settings-tab" data-pane="saves">💾 Saves</button>
1102+
<button class="settings-tab" data-pane="roms">📂 ROMs</button>
10301103
</div>
10311104
10321105
<div class="settings-pane active" id="paneOptions">
@@ -1079,9 +1152,6 @@
10791152
</div>
10801153
</div>
10811154
1082-
<div class="setting-row" style="margin-top: 12px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 12px;">
1083-
<button id="uploadRomBtn" style="width:100%;padding:10px;background:rgba(26,175,255,0.15);border:1px solid rgba(26,175,255,0.3);border-radius:8px;color:#1AAFFF;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;">📁 Upload ROM to Library</button>
1084-
</div>
10851155
</div>
10861156
10871157
<div class="settings-pane" id="panePerf">
@@ -1168,10 +1238,23 @@
11681238
</div><!-- /perfSubNative -->
11691239
</div>
11701240
1171-
<div class="settings-pane" id="paneSaves">
1241+
<div class="settings-pane" id="paneRoms">
1242+
<!-- Upload button (top) -->
1243+
<div style="margin-bottom:14px;">
1244+
<button id="uploadRomBtn" style="width:100%;padding:10px;background:rgba(26,175,255,0.15);border:1px solid rgba(26,175,255,0.3);border-radius:8px;color:#1AAFFF;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;">📁 Upload ROM</button>
1245+
</div>
1246+
1247+
<!-- Saves section -->
1248+
<div class="roms-section-label">SAVE FILES</div>
11721249
<div class="saves-list" id="savesList">
11731250
<div class="saves-empty">Loading...</div>
11741251
</div>
1252+
1253+
<!-- ROMs section -->
1254+
<div class="roms-section-label" style="margin-top:14px;">ROM LIBRARY</div>
1255+
<div class="roms-list" id="romsList">
1256+
<div class="saves-empty">Loading...</div>
1257+
</div>
11751258
</div>
11761259
</div>
11771260
@@ -1695,9 +1778,9 @@ <h3 style="font-size:16px;font-weight:600;color:#fff;margin:0;">Upload ROM</h3>
16951778
this.shadowRoot.querySelectorAll('.settings-pane').forEach(p => p.classList.toggle('active', p.id === 'pane' + pane.charAt(0).toUpperCase() + pane.slice(1)));
16961779
// Update mobile header title
16971780
const titleEl = this.shadowRoot.getElementById('settingsModalTitle');
1698-
if (titleEl) titleEl.textContent = pane === 'saves' ? 'Save Files' : pane === 'perf' ? 'Performance' : 'Options';
1699-
// Load saves when switching to saves tab
1700-
if (pane === 'saves') this._loadSaves();
1781+
if (titleEl) titleEl.textContent = pane === 'roms' ? 'ROMs' : pane === 'perf' ? 'Performance' : 'Options';
1782+
// Load saves + roms when switching to roms tab
1783+
if (pane === 'roms') { this._loadSaves(); this._loadRoms(); }
17011784
if (pane === 'perf') {
17021785
this._renderPerfSettings();
17031786
this._initPerfSubtabs();
@@ -2120,6 +2203,92 @@ <h3 style="font-size:16px;font-weight:600;color:#fff;margin:0;">Upload ROM</h3>
21202203
}
21212204
}
21222205

2206+
async _loadRoms() {
2207+
const list = this.shadowRoot.getElementById('romsList');
2208+
if (!list) return;
2209+
list.innerHTML = '<div class="saves-empty">Loading...</div>';
2210+
try {
2211+
const resp = await fetch('/api/presets?t=' + Date.now());
2212+
const presets = await resp.json();
2213+
const systems = Object.keys(presets);
2214+
if (!systems.length) {
2215+
list.innerHTML = '<div class="saves-empty">No ROMs found</div>';
2216+
return;
2217+
}
2218+
let html = '';
2219+
let totalRoms = 0;
2220+
for (const system of systems.sort()) {
2221+
const playerGroups = presets[system];
2222+
const roms = [];
2223+
for (const [pc, games] of Object.entries(playerGroups)) {
2224+
for (const g of games) {
2225+
roms.push({ name: g.name, players: pc, path: g.rom });
2226+
}
2227+
}
2228+
if (!roms.length) continue;
2229+
totalRoms += roms.length;
2230+
const displayName = this.coreNames[system] || system;
2231+
const sysId = 'romsys_' + system;
2232+
html += '<div class="rom-system-header" data-sys="' + sysId + '">';
2233+
html += '<span class="rom-system-chevron">▶</span>';
2234+
html += '<span class="rom-system-name">' + this._escHtml(displayName) + '</span>';
2235+
html += '<span class="rom-system-count">' + roms.length + '</span>';
2236+
html += '</div>';
2237+
html += '<div class="rom-system-items" id="' + sysId + '">';
2238+
for (const r of roms.sort((a, b) => a.name.localeCompare(b.name))) {
2239+
html += '<div class="rom-item">';
2240+
html += '<span class="rom-name">' + this._escHtml(r.name) + '</span>';
2241+
html += '<span class="rom-players">' + r.players + '</span>';
2242+
html += '<button class="save-btn download" data-path="' + this._escHtml(r.path) + '" data-name="' + this._escHtml(r.name) + '" title="Download ROM">📥</button>';
2243+
html += '<button class="save-btn delete" data-path="' + this._escHtml(r.path) + '" data-name="' + this._escHtml(r.name) + '" title="Delete ROM">✕</button>';
2244+
html += '</div>';
2245+
}
2246+
html += '</div>';
2247+
}
2248+
if (!totalRoms) {
2249+
list.innerHTML = '<div class="saves-empty">No ROMs found</div>';
2250+
return;
2251+
}
2252+
list.innerHTML = html;
2253+
// Attach expand/collapse handlers
2254+
list.querySelectorAll('.rom-system-header').forEach(hdr => {
2255+
hdr.onclick = () => hdr.classList.toggle('expanded');
2256+
});
2257+
// Attach download handlers
2258+
list.querySelectorAll('.save-btn.download').forEach(btn => {
2259+
btn.onclick = (e) => {
2260+
e.stopPropagation();
2261+
const a = document.createElement('a');
2262+
a.href = '/' + btn.dataset.path;
2263+
a.download = btn.dataset.path.split('/').pop();
2264+
a.click();
2265+
};
2266+
});
2267+
// Attach delete handlers
2268+
list.querySelectorAll('.save-btn.delete').forEach(btn => {
2269+
btn.onclick = async (e) => {
2270+
e.stopPropagation();
2271+
const name = btn.dataset.name;
2272+
if (!confirm('Delete ROM?\\n\\n' + name + '\\n\\nThis cannot be undone.')) return;
2273+
try {
2274+
const resp = await fetch('/api/delete-rom', {
2275+
method: 'POST',
2276+
headers: { 'Content-Type': 'application/json' },
2277+
body: JSON.stringify({ path: btn.dataset.path })
2278+
});
2279+
if (resp.ok) {
2280+
this._loadRoms();
2281+
} else {
2282+
alert('Failed to delete ROM');
2283+
}
2284+
} catch { alert('Failed to delete ROM'); }
2285+
};
2286+
});
2287+
} catch (e) {
2288+
list.innerHTML = '<div class="saves-empty">Failed to load ROMs</div>';
2289+
}
2290+
}
2291+
21232292
// ── Perf Settings ─────────────────────────────────────────────
21242293
_perfOptionDefs() {
21252294
return {

server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,28 @@ const serverConfig = {
557557
}
558558
}
559559

560+
// Delete a ROM file
561+
if (pathname === "/api/delete-rom" && req.method === "POST") {
562+
try {
563+
const { path: romPath } = await req.json() as { path: string };
564+
if (!romPath || !romPath.startsWith("presets/")) {
565+
return new Response(JSON.stringify({ ok: false, error: "Invalid path" }), { status: 400, headers: { "Content-Type": "application/json", ...getHeaders(req) } });
566+
}
567+
// Prevent path traversal
568+
const resolved = join(ROOT_DIR, romPath);
569+
if (!resolved.startsWith(join(ROOT_DIR, "presets"))) {
570+
return new Response(JSON.stringify({ ok: false, error: "Invalid path" }), { status: 400, headers: { "Content-Type": "application/json", ...getHeaders(req) } });
571+
}
572+
if (!existsSync(resolved)) {
573+
return new Response(JSON.stringify({ ok: false, error: "Not found" }), { status: 404, headers: { "Content-Type": "application/json", ...getHeaders(req) } });
574+
}
575+
unlinkSync(resolved);
576+
return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json", ...getHeaders(req) } });
577+
} catch (e: any) {
578+
return new Response(JSON.stringify({ ok: false, error: e.message }), { status: 500, headers: { "Content-Type": "application/json", ...getHeaders(req) } });
579+
}
580+
}
581+
560582
if (pathname === "/api/systems" && req.method === "GET") {
561583
const presetsDir = join(ROOT_DIR, "presets");
562584
try {

0 commit comments

Comments
 (0)