diff --git a/CMake/platforms/emscripten.cmake b/CMake/platforms/emscripten.cmake index e43f9f162..0e2c1378b 100644 --- a/CMake/platforms/emscripten.cmake +++ b/CMake/platforms/emscripten.cmake @@ -12,3 +12,4 @@ set(NOEXIT ON) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/file-manager.js" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/Packaging/emscripten/file-manager.js b/Packaging/emscripten/file-manager.js new file mode 100644 index 000000000..c8ab78d20 --- /dev/null +++ b/Packaging/emscripten/file-manager.js @@ -0,0 +1,232 @@ +// File Manager functionality +(function() { + const modal = document.getElementById('fileManagerModal'); + const fileManagerBtn = document.getElementById('fileManagerBtn'); + const closeModalBtn = document.getElementById('closeModal'); + const dropZone = document.getElementById('dropZone'); + const fileInput = document.getElementById('fileInput'); + const browseBtn = document.getElementById('browseBtn'); + const resetSettingsBtn = document.getElementById('resetSettingsBtn'); + const mpqFilesList = document.getElementById('mpqFilesList'); + + // Open/close modal + fileManagerBtn.addEventListener('click', () => { + modal.classList.add('show'); + refreshFileList(); + }); + + closeModalBtn.addEventListener('click', () => { + modal.classList.remove('show'); + }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.classList.remove('show'); + } + }); + + // Browse button + browseBtn.addEventListener('click', () => { + fileInput.click(); + }); + + // Drag and drop + dropZone.addEventListener('click', () => { + fileInput.click(); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('dragover'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('dragover'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('dragover'); + handleFiles(e.dataTransfer.files); + }); + + fileInput.addEventListener('change', (e) => { + handleFiles(e.target.files); + }); + + // Handle file upload + function handleFiles(files) { + if (!files || files.length === 0) return; + + // Wait for Module and FS to be ready + if (typeof Module === 'undefined' || typeof FS === 'undefined') { + alert('Game is still loading. Please wait and try again.'); + return; + } + + const mpqFiles = Array.from(files).filter(f => + f.name.toLowerCase().endsWith('.mpq') + ); + + if (mpqFiles.length === 0) { + alert('Please select MPQ files only.'); + return; + } + + let processed = 0; + mpqFiles.forEach(file => { + const reader = new FileReader(); + reader.onload = function(e) { + try { + const data = new Uint8Array(e.target.result); + // Upload to the devilution subdirectory where the game searches + const path = '/libsdl/diasurgical/devilution/' + file.name; // Might want to make this dynamic later, since source mods might rename the paths + + // Create directory if it doesn't exist + try { + FS.mkdir('/libsdl/diasurgical/devilution'); + } catch (e) { + // Directory might already exist, ignore + } + + // Write file to IDBFS-backed directory + FS.writeFile(path, data); + console.log('Uploaded:', file.name, '(' + formatBytes(file.size) + ')'); + + processed++; + if (processed === mpqFiles.length) { + // Sync to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing files:', err); + alert('Error saving files. Check console.'); + } else { + alert('Files uploaded successfully! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } + } catch (err) { + console.error('Error writing file:', err); + alert('Error uploading file: ' + file.name); + } + }; + reader.readAsArrayBuffer(file); + }); + } + + // Refresh file list + function refreshFileList() { + if (typeof Module === 'undefined' || typeof FS === 'undefined') { + mpqFilesList.innerHTML = '

Game is loading...

'; + return; + } + + try { + // Check if devilution directory exists + try { + FS.stat('/libsdl/diasurgical/devilution'); + } catch (e) { + // Directory doesn't exist yet + mpqFilesList.innerHTML = '

No MPQ files found.

'; + return; + } + + const files = FS.readdir('/libsdl/diasurgical/devilution'); + const mpqFiles = files.filter(f => + f.toLowerCase().endsWith('.mpq') && f !== '.' && f !== '..' + ); + + if (mpqFiles.length === 0) { + mpqFilesList.innerHTML = '

No MPQ files found.

'; + return; + } + + mpqFilesList.innerHTML = mpqFiles.map(filename => { + const path = '/libsdl/diasurgical/devilution/' + filename; + const stat = FS.stat(path); + return ` +
+ ${filename} + ${formatBytes(stat.size)} + +
+ `; + }).join(''); + } catch (err) { + console.error('Error reading files:', err); + mpqFilesList.innerHTML = '

Error reading files.

'; + } + } + + // Delete file + window.deleteFile = function(filename) { + if (!confirm('Delete ' + filename + '? This will reload the game.')) { + return; + } + + try { + const path = '/libsdl/diasurgical/devilution/' + filename; + FS.unlink(path); + + // Sync deletion to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing deletion:', err); + alert('Error deleting file. Check console.'); + } else { + alert('File deleted! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } catch (err) { + console.error('Error deleting file:', err); + alert('Error deleting file: ' + filename); + } + }; + + // Reset settings + resetSettingsBtn.addEventListener('click', () => { + if (!confirm('Reset game settings? This will delete diablo.ini but keep your saves. The game will reload.')) { + return; + } + + try { + const iniPath = '/libsdl/diasurgical/devilution/diablo.ini'; + + // Check if file exists + try { + FS.stat(iniPath); + // File exists, delete it + FS.unlink(iniPath); + console.log('Deleted diablo.ini'); + } catch (e) { + // File doesn't exist, that's fine + console.log('diablo.ini not found (already reset)'); + } + + // Sync to IndexedDB + FS.syncfs(false, function(err) { + if (err) { + console.error('Error syncing settings reset:', err); + alert('Error resetting settings. Check console.'); + } else { + alert('Settings reset! Reloading game...'); + setTimeout(() => location.reload(), 500); + } + }); + } catch (err) { + console.error('Error resetting settings:', err); + alert('Error resetting settings.'); + } + }); + + // Helper function + function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } +})(); diff --git a/Packaging/emscripten/index.html b/Packaging/emscripten/index.html index 397cbe77b..30e117ce2 100644 --- a/Packaging/emscripten/index.html +++ b/Packaging/emscripten/index.html @@ -33,6 +33,11 @@ background-color: black; width: 640px; height: 480px; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + -moz-user-select: none; + -ms-user-select: none; } #emscripten_logo { @@ -140,10 +145,148 @@ font-family: 'Lucida Console', Monaco, monospace; outline: none; } + + /* File Manager Styles */ + #fileManagerBtn { + position: fixed; + top: 20px; + right: 20px; + cursor: pointer; + z-index: 1000; + } + + #fileManagerModal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 2000; + justify-content: center; + align-items: center; + } + + #fileManagerModal.show { + display: flex; + } + + .modal-content { + background: white; + border: 1px solid black; + padding: 20px; + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .modal-header h2 { + margin: 0; + } + + .close-btn { + border: 1px solid black; + cursor: pointer; + padding: 5px 10px; + } + + .drop-zone { + border: 1px solid black; + padding: 20px; + text-align: center; + margin: 20px 0; + cursor: pointer; + } + + .drop-zone p { + margin: 10px 0; + } + + .file-list { + margin: 20px 0; + } + + .file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + margin: 5px 0; + border: 1px solid black; + } + + .file-item-name { + flex: 1; + } + + .file-item-size { + margin: 0 15px; + } + + .btn { + padding: 8px 16px; + border: 1px solid black; + cursor: pointer; + } + + .section { + margin: 20px 0; + } + + #fileInput { + display: none; + } + +
+ +
+
Downloading...
@@ -184,6 +327,9 @@ // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 canvas.addEventListener("webglcontextlost", function (e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); + // Prevent canvas from being dragged + canvas.addEventListener("dragstart", function (e) { e.preventDefault(); }, false); + return canvas; })(), setStatus: function (text) { @@ -224,6 +370,7 @@ }; }; +