|
180 | 180 | </div> |
181 | 181 | <div class="controllers" id="controllers"></div> |
182 | 182 | <div class="bt-controllers" id="btControllers"></div> |
183 | | - <div class="status connecting" id="status">Connecting...</div> |
| 183 | + <div class="status connecting hidden" id="status">Connecting...</div> |
184 | 184 | <div id="pauseOverlay"></div> |
185 | 185 | <game-menu id="gameMenu" mode="screen"></game-menu> |
186 | 186 |
|
|
206 | 206 | let isPaused = false; |
207 | 207 | let gameMenu = null; |
208 | 208 |
|
| 209 | + // Detect if this is the local kiosk screen vs a remote browser |
| 210 | + const isKiosk = ['localhost', '127.0.0.1'].includes(window.location.hostname) || window.location.protocol === 'file:'; |
| 211 | + |
209 | 212 | let wsManager = null; |
210 | 213 | let screenId = null; |
211 | 214 | let loaderScript = null; // Track the loader script element |
|
310 | 313 | } |
311 | 314 | } |
312 | 315 |
|
313 | | - function showStatus() { document.getElementById('status').classList.remove('hidden'); } |
| 316 | + function showStatus() { if (isKiosk) document.getElementById('status').classList.remove('hidden'); } |
314 | 317 | function hideStatus() { document.getElementById('status').classList.add('hidden'); } |
315 | 318 |
|
316 | 319 | // ============================================ |
|
336 | 339 | // WEBSOCKET |
337 | 340 | // ============================================ |
338 | 341 | function updateStatus(status, text) { |
| 342 | + if (!isKiosk) return; |
339 | 343 | const el = document.getElementById('status'); |
340 | 344 | el.className = 'status ' + status; |
341 | 345 | el.textContent = text; |
342 | 346 | } |
343 | 347 |
|
344 | 348 | // Network status: poll connection info and display WiFi SSID / Ethernet / Disconnected |
| 349 | + // Only on kiosk — remote screens don't need Pi's WiFi status |
345 | 350 | let _networkPollInterval = null; |
346 | 351 | function updateNetworkStatus() { |
| 352 | + if (!isKiosk) return; |
347 | 353 | fetch('/api/network-info').then(r => r.json()).then(info => { |
348 | 354 | const conn = info.connection; |
349 | 355 | if (conn?.type === 'wifi') updateStatus('connected', '📶 ' + conn.name); |
350 | 356 | else if (conn?.type === 'ethernet') updateStatus('connected', '🔌 Ethernet'); |
351 | 357 | else updateStatus('disconnected', '⚠ No Internet'); |
352 | 358 | }).catch(() => updateStatus('disconnected', '⚠ No Internet')); |
353 | 359 | } |
354 | | - // Poll network status every 10s to catch changes |
355 | | - _networkPollInterval = setInterval(updateNetworkStatus, 10000); |
356 | | - |
357 | | - // Click status label → open WiFi Manager |
358 | | - document.getElementById('status').addEventListener('click', () => { |
359 | | - window.location.href = 'http://' + window.location.hostname + ':3457'; |
360 | | - }); |
| 360 | + if (isKiosk) { |
| 361 | + _networkPollInterval = setInterval(updateNetworkStatus, 10000); |
| 362 | + document.getElementById('status').addEventListener('click', () => { |
| 363 | + window.location.href = 'http://' + window.location.hostname + ':3457'; |
| 364 | + }); |
| 365 | + } else { |
| 366 | + document.getElementById('status').classList.add('hidden'); |
| 367 | + } |
361 | 368 |
|
362 | 369 | function connectWebSocket() { |
363 | 370 | updateStatus('connecting', 'Connecting...'); |
|
406 | 413 | break; |
407 | 414 |
|
408 | 415 | case 'btControllersUpdate': |
409 | | - renderBtControllers(msg.controllers || []); |
| 416 | + if (isKiosk) renderBtControllers(msg.controllers || []); |
410 | 417 | break; |
411 | 418 |
|
412 | 419 | case 'startGame': |
|
959 | 966 | resizeTimeout = setTimeout(() => qrCodeGenerated && generateQRCode(true), 150); |
960 | 967 | }); |
961 | 968 |
|
962 | | - function showQRCode() { document.getElementById('qrContainer').classList.remove('hidden'); } |
| 969 | + // Flush in-game saves on page unload (refresh/navigation) |
| 970 | + window.addEventListener('beforeunload', () => { |
| 971 | + try { |
| 972 | + const gm = window.EJS_emulator?.gameManager; |
| 973 | + if (!gm) return; |
| 974 | + gm.saveSaveFiles?.(); |
| 975 | + // Upload save via sendBeacon (works in beforeunload unlike fetch) |
| 976 | + const savePath = gm.getSaveFilePath?.(); |
| 977 | + if (savePath && gm.FS?.analyzePath?.(savePath)?.exists) { |
| 978 | + const data = gm.FS.readFile(savePath); |
| 979 | + if (data?.length) { |
| 980 | + const parts = savePath.split('/'); |
| 981 | + const coreName = parts[parts.length - 2]; |
| 982 | + const fileName = parts[parts.length - 1].replace(/\.[^.]+$/, ''); |
| 983 | + navigator.sendBeacon('/api/saves/' + encodeURIComponent(coreName) + '/' + encodeURIComponent(fileName), new Blob([data])); |
| 984 | + } |
| 985 | + } |
| 986 | + } catch {} |
| 987 | + }); |
| 988 | + |
| 989 | + function showQRCode() { if (isKiosk) document.getElementById('qrContainer').classList.remove('hidden'); } |
963 | 990 | function hideQRCode() { document.getElementById('qrContainer').classList.add('hidden'); } |
964 | 991 |
|
965 | 992 | // Z-index management for QR and game menu overlap on mobile |
|
1023 | 1050 | initializeEmulator(); |
1024 | 1051 | } |
1025 | 1052 |
|
1026 | | - function resetToGameMenu() { |
1027 | | - saveState(0); |
| 1053 | + async function uploadSaveToServer(gm) { |
| 1054 | + try { |
| 1055 | + const savePath = gm?.getSaveFilePath?.(); |
| 1056 | + if (!savePath || !gm.FS?.analyzePath?.(savePath)?.exists) return; |
| 1057 | + const data = gm.FS.readFile(savePath); |
| 1058 | + if (!data || data.length === 0) return; |
| 1059 | + const parts = savePath.split('/'); |
| 1060 | + const coreName = parts[parts.length - 2]; |
| 1061 | + const fileName = parts[parts.length - 1].replace(/\.[^.]+$/, ''); |
| 1062 | + const resp = await fetch('/api/saves/' + encodeURIComponent(coreName) + '/' + encodeURIComponent(fileName), { |
| 1063 | + method: 'PUT', body: data, |
| 1064 | + headers: { 'Content-Type': 'application/octet-stream' } |
| 1065 | + }); |
| 1066 | + if (resp.ok) console.log('[save] Uploaded to server:', data.length, 'bytes'); |
| 1067 | + else console.warn('[save] Server upload failed:', resp.status); |
| 1068 | + } catch (e) { console.warn('[save] Upload error:', e); } |
| 1069 | + } |
| 1070 | + |
| 1071 | + async function resetToGameMenu() { |
| 1072 | + // Stop server save interval |
| 1073 | + if (window._serverSaveInterval) { clearInterval(window._serverSaveInterval); window._serverSaveInterval = null; } |
| 1074 | + |
| 1075 | + // Flush in-game saves to server |
| 1076 | + try { |
| 1077 | + const gm = window.EJS_emulator?.gameManager; |
| 1078 | + if (gm) { |
| 1079 | + gm.saveSaveFiles?.(); |
| 1080 | + await uploadSaveToServer(gm); |
| 1081 | + } |
| 1082 | + } catch (e) { console.warn('[save] flush error:', e); } |
1028 | 1083 |
|
1029 | | - // Wait for save to complete before cleanup |
| 1084 | + // Save emulator state |
| 1085 | + await saveState(0); |
| 1086 | + |
| 1087 | + // syncfs complete — now safe to clean up |
1030 | 1088 | setTimeout(() => { |
1031 | 1089 | // Clean up emulator if it exists |
1032 | 1090 | try { |
|
1179 | 1237 | window.EJS_backgroundBlur = false; |
1180 | 1238 | window.EJS_backgroundColor = '#1a1a1a'; |
1181 | 1239 | window.EJS_defaultOptions = { "save-state-location": "browser", "fps": "hide" }; |
| 1240 | + window.EJS_fixedSaveInterval = 15000; // Auto-save SRAM to IndexedDB every 15s |
1182 | 1241 |
|
1183 | 1242 | // N64 performance optimizations (mupen64plus_next) |
1184 | 1243 | if (pendingROM?.core === 'n64' || pendingROM?.core === 'mupen64plus_next') { |
|
1339 | 1398 | else showAudioIndicator(); |
1340 | 1399 | } |
1341 | 1400 | }, 500); |
| 1401 | + |
| 1402 | + // Periodic server save sync (every 30s) |
| 1403 | + if (window._serverSaveInterval) clearInterval(window._serverSaveInterval); |
| 1404 | + window._serverSaveInterval = setInterval(() => { |
| 1405 | + const gm = window.EJS_emulator?.gameManager; |
| 1406 | + if (gm && currentState === State.PLAYING) { |
| 1407 | + gm.saveSaveFiles?.(); |
| 1408 | + uploadSaveToServer(gm); |
| 1409 | + } |
| 1410 | + }, 30000); |
| 1411 | + |
| 1412 | + // Load server-side save file into emulator |
| 1413 | + (async () => { |
| 1414 | + try { |
| 1415 | + const gm = window.EJS_emulator?.gameManager; |
| 1416 | + if (!gm) return; |
| 1417 | + const savePath = gm.getSaveFilePath?.(); |
| 1418 | + if (!savePath) return; |
| 1419 | + // Extract core and game name from path: /data/saves/CoreName/GameName.srm |
| 1420 | + const parts = savePath.split('/'); |
| 1421 | + const coreName = parts[parts.length - 2]; |
| 1422 | + const fileName = parts[parts.length - 1].replace(/\.[^.]+$/, ''); |
| 1423 | + console.log('[save] Checking server for save:', coreName, fileName); |
| 1424 | + const resp = await fetch('/api/saves/' + encodeURIComponent(coreName) + '/' + encodeURIComponent(fileName)); |
| 1425 | + if (resp.ok) { |
| 1426 | + const data = new Uint8Array(await resp.arrayBuffer()); |
| 1427 | + console.log('[save] Loaded server save:', data.length, 'bytes'); |
| 1428 | + // Write to WASM FS and tell core to reload |
| 1429 | + const dir = savePath.substring(0, savePath.lastIndexOf('/')); |
| 1430 | + try { gm.FS.mkdirTree(dir); } catch {} |
| 1431 | + gm.FS.writeFile(savePath, data); |
| 1432 | + gm.loadSaveFiles?.(); |
| 1433 | + console.log('[save] Injected save into emulator'); |
| 1434 | + } else { |
| 1435 | + console.log('[save] No server save found (new game)'); |
| 1436 | + } |
| 1437 | + } catch (e) { console.warn('[save] Server load error:', e); } |
| 1438 | + })(); |
| 1439 | + |
1342 | 1440 | // Auto-load previous save state if "Load on start" setting is enabled |
1343 | 1441 | if (localStorage.getItem('retrobox_loadOnStart') === 'true') { |
1344 | 1442 | setTimeout(() => { |
|
0 commit comments