|
603 | 603 | } |
604 | 604 | .perf-profile-add:hover { border-color: #4a9eff; color: #4a9eff; } |
605 | 605 |
|
| 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 | + } |
606 | 679 | /* Save files section */ |
607 | 680 | .saves-list { |
608 | 681 | display: flex; |
|
1026 | 1099 | <div class="settings-tabs"> |
1027 | 1100 | <button class="settings-tab active" data-pane="options">⚙️ Options</button> |
1028 | 1101 | <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> |
1030 | 1103 | </div> |
1031 | 1104 |
|
1032 | 1105 | <div class="settings-pane active" id="paneOptions"> |
|
1079 | 1152 | </div> |
1080 | 1153 | </div> |
1081 | 1154 |
|
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> |
1085 | 1155 | </div> |
1086 | 1156 |
|
1087 | 1157 | <div class="settings-pane" id="panePerf"> |
|
1168 | 1238 | </div><!-- /perfSubNative --> |
1169 | 1239 | </div> |
1170 | 1240 |
|
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> |
1172 | 1249 | <div class="saves-list" id="savesList"> |
1173 | 1250 | <div class="saves-empty">Loading...</div> |
1174 | 1251 | </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> |
1175 | 1258 | </div> |
1176 | 1259 | </div> |
1177 | 1260 |
|
@@ -1695,9 +1778,9 @@ <h3 style="font-size:16px;font-weight:600;color:#fff;margin:0;">Upload ROM</h3> |
1695 | 1778 | this.shadowRoot.querySelectorAll('.settings-pane').forEach(p => p.classList.toggle('active', p.id === 'pane' + pane.charAt(0).toUpperCase() + pane.slice(1))); |
1696 | 1779 | // Update mobile header title |
1697 | 1780 | 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(); } |
1701 | 1784 | if (pane === 'perf') { |
1702 | 1785 | this._renderPerfSettings(); |
1703 | 1786 | this._initPerfSubtabs(); |
@@ -2120,6 +2203,92 @@ <h3 style="font-size:16px;font-weight:600;color:#fff;margin:0;">Upload ROM</h3> |
2120 | 2203 | } |
2121 | 2204 | } |
2122 | 2205 |
|
| 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 | + |
2123 | 2292 | // ── Perf Settings ───────────────────────────────────────────── |
2124 | 2293 | _perfOptionDefs() { |
2125 | 2294 | return { |
|
0 commit comments