yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_drop_handler.cc
Go to the documentation of this file.
1// clang-format off
2#ifdef __EMSCRIPTEN__
3
5
6#include <emscripten.h>
7#include <emscripten/html5.h>
8#include <algorithm>
9#include <cctype>
10#include <memory>
11
12#include "absl/strings/str_format.h"
13
14namespace yaze {
15namespace platform {
16
17// Static member initialization
18std::unique_ptr<WasmDropHandler> WasmDropHandler::instance_ = nullptr;
19
20// JavaScript interop for drag and drop operations
21EM_JS(void, setupDropZone_impl, (const char* element_id), {
22 var targetElement = document.body;
23 if (element_id && UTF8ToString(element_id).length > 0) {
24 var el = document.getElementById(UTF8ToString(element_id));
25 if (el) {
26 targetElement = el;
27 }
28 }
29
30 // Remove existing event listeners if any
31 if (window.yazeDropListeners) {
32 window.yazeDropListeners.forEach(function(listener) {
33 document.removeEventListener(listener.event, listener.handler);
34 });
35 }
36 window.yazeDropListeners = [];
37
38 // Create drop zone overlay if it doesn't exist
39 var overlay = document.getElementById('yaze-drop-overlay');
40 if (!overlay) {
41 overlay = document.createElement('div');
42 overlay.id = 'yaze-drop-overlay';
43 overlay.className = 'yaze-drop-overlay';
44 overlay.innerHTML = '<div class="yaze-drop-content"><div class="yaze-drop-icon">📁</div><div class="yaze-drop-text">Drop file here</div><div class="yaze-drop-info">Supported: .sfc, .smc, .zip, .pal, .tpl</div></div>';
45 document.body.appendChild(overlay);
46 }
47
48 // Helper function to check if file is a ROM or supported asset
49 function isSupportedFile(filename) {
50 var ext = filename.toLowerCase().split('.').pop();
51 return ext === 'sfc' || ext === 'smc' || ext === 'zip' ||
52 ext === 'pal' || ext === 'tpl';
53 }
54
55 // Helper function to check if dragged items contain files
56 function containsFiles(e) {
57 if (e.dataTransfer.types) {
58 for (var i = 0; i < e.dataTransfer.types.length; i++) {
59 if (e.dataTransfer.types[i] === "Files") {
60 return true;
61 }
62 }
63 }
64 return false;
65 }
66
67 // Drag enter handler
68 function handleDragEnter(e) {
69 if (containsFiles(e)) {
70 e.preventDefault();
71 e.stopPropagation();
72 Module._yazeHandleDragEnter();
73 overlay.classList.add('yaze-drop-active');
74 }
75 }
76
77 // Drag over handler
78 function handleDragOver(e) {
79 if (containsFiles(e)) {
80 e.preventDefault();
81 e.stopPropagation();
82 e.dataTransfer.dropEffect = 'copy';
83 }
84 }
85
86 // Drag leave handler
87 function handleDragLeave(e) {
88 if (e.target === document || e.target === overlay) {
89 e.preventDefault();
90 e.stopPropagation();
91 Module._yazeHandleDragLeave();
92 overlay.classList.remove('yaze-drop-active');
93 }
94 }
95
96 // Drop handler
97 function handleDrop(e) {
98 e.preventDefault();
99 e.stopPropagation();
100
101 overlay.classList.remove('yaze-drop-active');
102
103 var files = e.dataTransfer.files;
104 if (!files || files.length === 0) {
105 var errPtr = allocateUTF8("No files dropped");
106 Module._yazeHandleDropError(errPtr);
107 _free(errPtr);
108 return;
109 }
110
111 var file = files[0]; // Only handle first file
112
113 if (!isSupportedFile(file.name)) {
114 var errPtr = allocateUTF8("Invalid file type. Please drop a ROM (.sfc) or Palette (.pal, .tpl)");
115 Module._yazeHandleDropError(errPtr);
116 _free(errPtr);
117 return;
118 }
119
120 // Show loading state in overlay
121 overlay.classList.add('yaze-drop-loading');
122 overlay.querySelector('.yaze-drop-text').textContent = 'Loading file...';
123 overlay.querySelector('.yaze-drop-info').textContent = file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
124
125 var reader = new FileReader();
126 reader.onload = function() {
127 var filename = file.name;
128 var filenamePtr = allocateUTF8(filename);
129 var data = new Uint8Array(reader.result);
130 var dataPtr = Module._malloc(data.length);
131 Module.HEAPU8.set(data, dataPtr);
132 Module._yazeHandleDroppedFile(filenamePtr, dataPtr, data.length);
133 Module._free(dataPtr);
134 _free(filenamePtr);
135
136 // Hide loading state
137 setTimeout(function() {
138 overlay.classList.remove('yaze-drop-loading');
139 overlay.querySelector('.yaze-drop-text').textContent = 'Drop file here';
140 overlay.querySelector('.yaze-drop-info').textContent = 'Supported: .sfc, .smc, .zip, .pal, .tpl';
141 }, 500);
142 };
143
144 reader.onerror = function() {
145 var errPtr = allocateUTF8("Failed to read file: " + file.name);
146 Module._yazeHandleDropError(errPtr);
147 _free(errPtr);
148 overlay.classList.remove('yaze-drop-loading');
149 };
150
151 reader.readAsArrayBuffer(file);
152 }
153
154 // Register event listeners
155 var dragEnterHandler = handleDragEnter;
156 var dragOverHandler = handleDragOver;
157 var dragLeaveHandler = handleDragLeave;
158 var dropHandler = handleDrop;
159
160 document.addEventListener('dragenter', dragEnterHandler, false);
161 document.addEventListener('dragover', dragOverHandler, false);
162 document.addEventListener('dragleave', dragLeaveHandler, false);
163 document.addEventListener('drop', dropHandler, false);
164
165 // Store listeners for cleanup
166 window.yazeDropListeners = [
167 { event: 'dragenter', handler: dragEnterHandler },
168 { event: 'dragover', handler: dragOverHandler },
169 { event: 'dragleave', handler: dragLeaveHandler },
170 { event: 'drop', handler: dropHandler }
171 ];
172});
173
174EM_JS(void, disableDropZone_impl, (), {
175 if (window.yazeDropListeners) {
176 window.yazeDropListeners.forEach(function(listener) {
177 document.removeEventListener(listener.event, listener.handler);
178 });
179 window.yazeDropListeners = [];
180 }
181 var overlay = document.getElementById('yaze-drop-overlay');
182 if (overlay) {
183 overlay.classList.remove('yaze-drop-active');
184 overlay.classList.remove('yaze-drop-loading');
185 }
186});
187
188EM_JS(void, setOverlayVisible_impl, (bool visible), {
189 var overlay = document.getElementById('yaze-drop-overlay');
190 if (overlay) {
191 overlay.style.display = visible ? 'flex' : 'none';
192 }
193});
194
195EM_JS(void, setOverlayText_impl, (const char* text), {
196 var overlay = document.getElementById('yaze-drop-overlay');
197 if (overlay) {
198 var textElement = overlay.querySelector('.yaze-drop-text');
199 if (textElement) {
200 textElement.textContent = UTF8ToString(text);
201 }
202 }
203});
204
205EM_JS(bool, isDragDropSupported, (), {
206 return (typeof FileReader !== 'undefined' && typeof DataTransfer !== 'undefined' && 'draggable' in document.createElement('div'));
207});
208
209EM_JS(void, injectDropZoneStyles, (), {
210 if (document.getElementById('yaze-drop-styles')) {
211 return; // Already injected
212 }
213
214 var style = document.createElement('style');
215 style.id = 'yaze-drop-styles';
216 style.textContent = `
217 .yaze-drop-overlay {
218 display: none;
219 position: fixed;
220 top: 0;
221 left: 0;
222 right: 0;
223 bottom: 0;
224 background: rgba(0, 0, 0, 0.85);
225 z-index: 10000;
226 align-items: center;
227 justify-content: center;
228 pointer-events: none;
229 transition: all 0.3s ease;
230 }
231
232 .yaze-drop-overlay.yaze-drop-active {
233 display: flex;
234 pointer-events: all;
235 background: rgba(0, 0, 0, 0.9);
236 }
237
238 .yaze-drop-overlay.yaze-drop-loading {
239 display: flex;
240 pointer-events: all;
241 }
242
243 .yaze-drop-content {
244 text-align: center;
245 color: white;
246 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
247 padding: 60px;
248 border-radius: 20px;
249 background: rgba(255, 255, 255, 0.1);
250 border: 3px dashed rgba(255, 255, 255, 0.5);
251 animation: pulse 2s infinite;
252 }
253
254 .yaze-drop-overlay.yaze-drop-active .yaze-drop-content {
255 border-color: #4CAF50;
256 background: rgba(76, 175, 80, 0.2);
257 animation: pulse-active 1s infinite;
258 }
259
260 .yaze-drop-overlay.yaze-drop-loading .yaze-drop-content {
261 border-color: #2196F3;
262 background: rgba(33, 150, 243, 0.2);
263 border-style: solid;
264 animation: none;
265 }
266
267 .yaze-drop-icon {
268 font-size: 72px;
269 margin-bottom: 20px;
270 filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
271 }
272
273 .yaze-drop-text {
274 font-size: 28px;
275 font-weight: 600;
276 margin-bottom: 10px;
277 text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
278 }
279
280 .yaze-drop-info {
281 font-size: 16px;
282 opacity: 0.8;
283 font-weight: 400;
284 }
285
286 @keyframes pulse {
287 0%, 100% { transform: scale(1); opacity: 1; }
288 50% { transform: scale(1.02); opacity: 0.9; }
289 }
290
291 @keyframes pulse-active {
292 0%, 100% { transform: scale(1); }
293 50% { transform: scale(1.05); }
294 }
295 `;
296 document.head.appendChild(style);
297});
298
299// WasmDropHandler implementation
300WasmDropHandler& WasmDropHandler::GetInstance() {
301 if (!instance_) {
302 instance_ = std::unique_ptr<WasmDropHandler>(new WasmDropHandler());
303 }
304 return *instance_;
305}
306
307WasmDropHandler::WasmDropHandler() = default;
308WasmDropHandler::~WasmDropHandler() {
309 if (initialized_ && enabled_) {
310 disableDropZone_impl();
311 }
312}
313
314absl::Status WasmDropHandler::Initialize(const std::string& element_id,
315 DropCallback on_drop,
316 ErrorCallback on_error) {
317 if (!IsSupported()) {
318 return absl::FailedPreconditionError(
319 "Drag and drop not supported in this browser");
320 }
321
322 // Inject CSS styles
323 injectDropZoneStyles();
324
325 // Set callbacks
326 if (on_drop) {
327 drop_callback_ = on_drop;
328 }
329 if (on_error) {
330 error_callback_ = on_error;
331 }
332
333 // Setup drop zone
334 element_id_ = element_id;
335 setupDropZone_impl(element_id.c_str());
336
337 initialized_ = true;
338 enabled_ = true;
339
340 return absl::OkStatus();
341}
342
343void WasmDropHandler::SetDropCallback(DropCallback on_drop) {
344 drop_callback_ = on_drop;
345}
346
347void WasmDropHandler::SetErrorCallback(ErrorCallback on_error) {
348 error_callback_ = on_error;
349}
350
351void WasmDropHandler::SetEnabled(bool enabled) {
352 if (enabled_ != enabled) {
353 enabled_ = enabled;
354 if (!enabled) {
355 disableDropZone_impl();
356 } else if (initialized_) {
357 setupDropZone_impl(element_id_.c_str());
358 }
359 }
360}
361
362void WasmDropHandler::SetOverlayVisible(bool visible) {
363 setOverlayVisible_impl(visible);
364}
365
366void WasmDropHandler::SetOverlayText(const std::string& text) {
367 setOverlayText_impl(text.c_str());
368}
369
370bool WasmDropHandler::IsSupported() {
371 return isDragDropSupported();
372}
373
374bool WasmDropHandler::IsValidRomFile(const std::string& filename) {
375 // Get file extension
376 size_t dot_pos = filename.find_last_of('.');
377 if (dot_pos == std::string::npos) {
378 return false;
379 }
380
381 std::string ext = filename.substr(dot_pos + 1);
382
383 // Convert to lowercase for comparison
384 std::transform(ext.begin(), ext.end(), ext.begin(),
385 [](unsigned char c) { return std::tolower(c); });
386
387 return ext == "sfc" || ext == "smc" || ext == "zip" ||
388 ext == "pal" || ext == "tpl";
389}
390
391void WasmDropHandler::HandleDroppedFile(const char* filename,
392 const uint8_t* data, size_t size) {
393 auto& instance = GetInstance();
394
395 // Validate file
396 if (!IsValidRomFile(filename)) {
397 HandleDropError("Invalid file format");
398 return;
399 }
400
401 // Call the drop callback
402 if (instance.drop_callback_) {
403 std::vector<uint8_t> file_data(data, data + size);
404 instance.drop_callback_(filename, file_data);
405 } else {
406 emscripten_log(EM_LOG_WARN, "No drop callback registered for file: %s",
407 filename);
408 }
409
410 // Reset drag counter
411 instance.drag_counter_ = 0;
412}
413
414void WasmDropHandler::HandleDropError(const char* error_message) {
415 auto& instance = GetInstance();
416
417 if (instance.error_callback_) {
418 instance.error_callback_(error_message);
419 } else {
420 emscripten_log(EM_LOG_ERROR, "Drop error: %s", error_message);
421 }
422
423 // Reset drag counter
424 instance.drag_counter_ = 0;
425}
426
427void WasmDropHandler::HandleDragEnter() {
428 auto& instance = GetInstance();
429 instance.drag_counter_++;
430}
431
432void WasmDropHandler::HandleDragLeave() {
433 auto& instance = GetInstance();
434 instance.drag_counter_--;
435
436 // Only truly left when counter reaches 0
437 if (instance.drag_counter_ <= 0) {
438 instance.drag_counter_ = 0;
439 }
440}
441
442} // namespace platform
443} // namespace yaze
444
445// C-style callbacks for JavaScript interop - must be extern "C" with EMSCRIPTEN_KEEPALIVE
446extern "C" {
447
448EMSCRIPTEN_KEEPALIVE
449void yazeHandleDroppedFile(const char* filename, const uint8_t* data,
450 size_t size) {
451 yaze::platform::WasmDropHandler::HandleDroppedFile(filename, data, size);
452}
453
454EMSCRIPTEN_KEEPALIVE
455void yazeHandleDropError(const char* error_message) {
456 yaze::platform::WasmDropHandler::HandleDropError(error_message);
457}
458
459EMSCRIPTEN_KEEPALIVE
460void yazeHandleDragEnter() {
461 yaze::platform::WasmDropHandler::HandleDragEnter();
462}
463
464EMSCRIPTEN_KEEPALIVE
465void yazeHandleDragLeave() {
466 yaze::platform::WasmDropHandler::HandleDragLeave();
467}
468
469} // extern "C"
470
471#endif // __EMSCRIPTEN__
472// clang-format on
EM_JS(void, CallJsAiDriver,(const char *history_json), { if(window.yaze &&window.yaze.ai &&window.yaze.ai.processAgentRequest) { window.yaze.ai.processAgentRequest(UTF8ToString(history_json));} else { console.error("AI Driver not found in window.yaze.ai.processAgentRequest");} })