|
736 | 736 | </label> |
737 | 737 | </div> |
738 | 738 | </div> |
| 739 | +
|
| 740 | + <div class="setting-row" style="margin-top: 12px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 12px;"> |
| 741 | + <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> |
| 742 | + </div> |
| 743 | + </div> |
| 744 | +
|
| 745 | + <!-- Upload ROM Modal --> |
| 746 | + <div id="uploadModal" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.85);z-index:100;padding:20px;overflow-y:auto;"> |
| 747 | + <div style="max-width:320px;margin:0 auto;"> |
| 748 | + <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;"> |
| 749 | + <h3 style="font-size:16px;font-weight:600;color:#fff;margin:0;">Upload ROM</h3> |
| 750 | + <button id="uploadModalClose" style="background:none;border:none;color:#888;font-size:20px;cursor:pointer;padding:4px 8px;">✕</button> |
| 751 | + </div> |
| 752 | + <div style="margin-bottom:12px;"> |
| 753 | + <label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">System</label> |
| 754 | + <select id="uploadSystem" style="width:100%;padding:8px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);border-radius:6px;color:#fff;font-size:14px;"></select> |
| 755 | + </div> |
| 756 | + <div style="margin-bottom:12px;"> |
| 757 | + <label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">Players</label> |
| 758 | + <div id="uploadPlayers" style="display:flex;gap:8px;flex-wrap:wrap;"></div> |
| 759 | + </div> |
| 760 | + <div style="margin-bottom:16px;"> |
| 761 | + <label style="font-size:12px;color:#888;display:block;margin-bottom:4px;">ROM File</label> |
| 762 | + <div id="uploadDropZone" style="border:2px dashed rgba(255,255,255,0.15);border-radius:8px;padding:24px;text-align:center;cursor:pointer;transition:all 0.15s;"> |
| 763 | + <div id="uploadFileName" style="color:#888;font-size:13px;">Tap to select or drop file</div> |
| 764 | + </div> |
| 765 | + <input type="file" id="uploadFileInput" accept=".zip,.7z,.nes,.smc,.sfc,.gb,.gba,.gbc,.n64,.z64,.v64,.bin,.cue,.iso,.md,.gen,.smd,.nds" style="display:none;"> |
| 766 | + </div> |
| 767 | + <button id="uploadSubmit" disabled style="width:100%;padding:12px;background:#1AAFFF;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer;opacity:0.4;transition:all 0.15s;">Upload</button> |
| 768 | + <div id="uploadProgress" style="display:none;margin-top:12px;text-align:center;color:#888;font-size:12px;"></div> |
| 769 | + </div> |
739 | 770 | </div> |
740 | 771 |
|
741 | 772 | <div class="loading-overlay" id="loadingOverlay"> |
|
1212 | 1243 | if (this._showingSettings) this.updateResolutionOptions(); |
1213 | 1244 | }; |
1214 | 1245 |
|
| 1246 | + // ── Upload ROM Modal ────────────────────────────────────── |
| 1247 | + const uploadModal = this.shadowRoot.getElementById('uploadModal'); |
| 1248 | + const uploadSystem = this.shadowRoot.getElementById('uploadSystem'); |
| 1249 | + const uploadPlayers = this.shadowRoot.getElementById('uploadPlayers'); |
| 1250 | + const uploadFileInput = this.shadowRoot.getElementById('uploadFileInput'); |
| 1251 | + const uploadDropZone = this.shadowRoot.getElementById('uploadDropZone'); |
| 1252 | + const uploadFileName = this.shadowRoot.getElementById('uploadFileName'); |
| 1253 | + const uploadSubmit = this.shadowRoot.getElementById('uploadSubmit'); |
| 1254 | + const uploadProgress = this.shadowRoot.getElementById('uploadProgress'); |
| 1255 | + let uploadSelectedFile = null; |
| 1256 | + let uploadSelectedPlayers = '1p'; |
| 1257 | + |
| 1258 | + // Open modal |
| 1259 | + this.shadowRoot.getElementById('uploadRomBtn').onclick = () => { |
| 1260 | + uploadModal.style.display = 'block'; |
| 1261 | + uploadSelectedFile = null; |
| 1262 | + uploadFileName.textContent = 'Tap to select or drop file'; |
| 1263 | + uploadSubmit.disabled = true; |
| 1264 | + uploadSubmit.style.opacity = '0.4'; |
| 1265 | + uploadProgress.style.display = 'none'; |
| 1266 | + // Populate systems |
| 1267 | + fetch('/api/systems').then(r => r.json()).then(systems => { |
| 1268 | + uploadSystem.innerHTML = systems.map(s => |
| 1269 | + '<option value="' + s + '">' + (this.coreNames[s] || s) + '</option>' |
| 1270 | + ).join(''); |
| 1271 | + }); |
| 1272 | + renderPlayerBtns(); |
| 1273 | + }; |
| 1274 | + |
| 1275 | + // Close modal |
| 1276 | + this.shadowRoot.getElementById('uploadModalClose').onclick = () => { |
| 1277 | + uploadModal.style.display = 'none'; |
| 1278 | + }; |
| 1279 | + |
| 1280 | + // Player buttons |
| 1281 | + const renderPlayerBtns = () => { |
| 1282 | + let html = ''; |
| 1283 | + for (let i = 1; i <= 4; i++) { |
| 1284 | + const val = i + 'p'; |
| 1285 | + const active = uploadSelectedPlayers === val; |
| 1286 | + html += '<button data-p="' + val + '" style="flex:1;padding:8px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;' + |
| 1287 | + (active ? 'background:#1AAFFF;color:#fff;border:1px solid #1AAFFF;' : 'background:rgba(255,255,255,0.08);color:#888;border:1px solid rgba(255,255,255,0.12);') + |
| 1288 | + '">' + i + 'P</button>'; |
| 1289 | + } |
| 1290 | + uploadPlayers.innerHTML = html; |
| 1291 | + uploadPlayers.querySelectorAll('button').forEach(btn => { |
| 1292 | + btn.onclick = () => { uploadSelectedPlayers = btn.dataset.p; renderPlayerBtns(); }; |
| 1293 | + }); |
| 1294 | + }; |
| 1295 | + |
| 1296 | + // File selection |
| 1297 | + uploadDropZone.onclick = () => uploadFileInput.click(); |
| 1298 | + uploadDropZone.ondragover = (e) => { e.preventDefault(); uploadDropZone.style.borderColor = '#1AAFFF'; }; |
| 1299 | + uploadDropZone.ondragleave = () => { uploadDropZone.style.borderColor = 'rgba(255,255,255,0.15)'; }; |
| 1300 | + uploadDropZone.ondrop = (e) => { |
| 1301 | + e.preventDefault(); |
| 1302 | + uploadDropZone.style.borderColor = 'rgba(255,255,255,0.15)'; |
| 1303 | + if (e.dataTransfer.files.length) { uploadSelectedFile = e.dataTransfer.files[0]; uploadFileName.textContent = uploadSelectedFile.name; uploadSubmit.disabled = false; uploadSubmit.style.opacity = '1'; } |
| 1304 | + }; |
| 1305 | + uploadFileInput.onchange = () => { |
| 1306 | + if (uploadFileInput.files.length) { uploadSelectedFile = uploadFileInput.files[0]; uploadFileName.textContent = uploadSelectedFile.name; uploadSubmit.disabled = false; uploadSubmit.style.opacity = '1'; } |
| 1307 | + }; |
| 1308 | + |
| 1309 | + // Submit upload |
| 1310 | + uploadSubmit.onclick = async () => { |
| 1311 | + if (!uploadSelectedFile) return; |
| 1312 | + uploadSubmit.disabled = true; |
| 1313 | + uploadSubmit.textContent = 'Uploading...'; |
| 1314 | + uploadProgress.style.display = 'block'; |
| 1315 | + uploadProgress.textContent = 'Uploading ' + uploadSelectedFile.name + '...'; |
| 1316 | + try { |
| 1317 | + const form = new FormData(); |
| 1318 | + form.append('rom', uploadSelectedFile); |
| 1319 | + form.append('system', uploadSystem.value); |
| 1320 | + form.append('players', uploadSelectedPlayers); |
| 1321 | + const resp = await fetch('/api/upload-rom', { method: 'POST', body: form }); |
| 1322 | + const data = await resp.json(); |
| 1323 | + if (data.ok) { |
| 1324 | + uploadProgress.textContent = '✓ Uploaded to ' + data.path; |
| 1325 | + uploadProgress.style.color = '#4CAF50'; |
| 1326 | + setTimeout(() => { uploadModal.style.display = 'none'; this.loadPresets(); }, 1500); |
| 1327 | + } else { |
| 1328 | + uploadProgress.textContent = '✕ ' + (data.error || 'Upload failed'); |
| 1329 | + uploadProgress.style.color = '#f44'; |
| 1330 | + uploadSubmit.disabled = false; |
| 1331 | + } |
| 1332 | + } catch (e) { |
| 1333 | + uploadProgress.textContent = '✕ ' + e.message; |
| 1334 | + uploadProgress.style.color = '#f44'; |
| 1335 | + uploadSubmit.disabled = false; |
| 1336 | + } |
| 1337 | + uploadSubmit.textContent = 'Upload'; |
| 1338 | + }; |
| 1339 | + |
1215 | 1340 | // Game height slider |
1216 | 1341 | this.gameHeightSlider.oninput = (e) => { |
1217 | 1342 | const value = parseInt(e.target.value); |
|
0 commit comments