extends RefCounted var _javascript_callback: Variant var _result_callback: Callable var _callback_name: String = "" func open(accept: String, max_bytes: int, result_callback: Callable) -> bool: if OS.get_name() != "Web" or not Engine.has_singleton("JavaScriptBridge"): return false if _result_callback.is_valid(): return false var javascript_bridge: Object = Engine.get_singleton("JavaScriptBridge") _callback_name = "__whaletownFilePicker%d" % get_instance_id() _result_callback = result_callback _javascript_callback = javascript_bridge.create_callback(_on_javascript_result) var window: Variant = javascript_bridge.get_interface("window") if window == null: _reset() return false window[_callback_name] = _javascript_callback var script := """ (() => { const callbackName = %s; const callback = window[callbackName]; if (typeof callback !== "function") return false; const previous = document.getElementById("whaletown-web-file-picker"); if (previous) previous.remove(); const input = document.createElement("input"); input.id = "whaletown-web-file-picker"; input.type = "file"; input.accept = %s; input.style.display = "none"; let finished = false; const finish = (status, name, mime, payload) => { if (finished) return; finished = true; callback(status, name || "", mime || "", payload || ""); input.remove(); delete window[callbackName]; }; input.addEventListener("cancel", () => finish("cancel", "", "", "")); input.addEventListener("change", () => { const file = input.files && input.files[0]; if (!file) { finish("cancel", "", "", ""); return; } if (file.size > %d) { finish("too_large", file.name, file.type, ""); return; } const reader = new FileReader(); reader.onerror = () => finish("read_error", file.name, file.type, ""); reader.onload = () => { const bytes = new Uint8Array(reader.result); const chunks = []; const chunkSize = 0x8000; for (let offset = 0; offset < bytes.length; offset += chunkSize) { chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize))); } finish("ok", file.name, file.type, btoa(chunks.join(""))); }; reader.readAsArrayBuffer(file); }); document.body.appendChild(input); input.click(); return true; })() """ % [JSON.stringify(_callback_name), JSON.stringify(accept), max_bytes] var opened: Variant = javascript_bridge.eval(script, true) if opened != true: _reset() return false return true func _on_javascript_result(arguments: Array) -> void: var callback := _result_callback _reset() if not callback.is_valid(): return var status := str(arguments[0]) if arguments.size() > 0 else "read_error" var file_name := str(arguments[1]) if arguments.size() > 1 else "" var mime_type := str(arguments[2]) if arguments.size() > 2 else "" var base64_data := str(arguments[3]) if arguments.size() > 3 else "" callback.call(status, file_name, mime_type, base64_data) func _reset() -> void: _javascript_callback = null _result_callback = Callable() _callback_name = "" static func save_base64_file(base64_data: String, file_name: String, prefix: String) -> String: if base64_data.is_empty(): return "" var bytes := Marshalls.base64_to_raw(base64_data) if bytes.is_empty(): return "" var extension := file_name.get_extension().to_lower() if not ["png", "jpg", "jpeg", "webp"].has(extension): extension = "bin" var root := DirAccess.open("user://") if root == null: return "" if not root.dir_exists("web_uploads") and root.make_dir("web_uploads") != OK: return "" var safe_prefix := prefix.to_lower().replace(" ", "_") var path := "user://web_uploads/%s_%d.%s" % [safe_prefix, Time.get_ticks_msec(), extension] var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return "" file.store_buffer(bytes) file.close() return path