diff --git a/flumi/Scripts/Utils/Lua/Crumbs.gd b/flumi/Scripts/Utils/Lua/Crumbs.gd new file mode 100644 index 0000000..eda9f05 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Crumbs.gd @@ -0,0 +1,257 @@ +class_name LuaCrumbsUtils +extends RefCounted + +const CRUMBS_DIR_PATH = "user://crumbs/" + +class Crumb: + var name: String + var value: String + var created_at: float + var lifespan: float = -1.0 # -1 = no expiry, otherwise lifespan in seconds + + func _init(n: String, v: String, lifetime: float = -1.0): + name = n + value = v + created_at = Time.get_unix_time_from_system() + lifespan = lifetime + + func is_expired() -> bool: + if lifespan < 0: + return false + var current_time = Time.get_unix_time_from_system() + return current_time > (created_at + lifespan) + + func get_expiry_time() -> float: + if lifespan < 0: + return -1.0 + return created_at + lifespan + + func to_dict() -> Dictionary: + return { + "name": name, + "value": value, + "created_at": created_at, + "lifespan": lifespan + } + + static func from_dict(data: Dictionary) -> Crumb: + var crumb = Crumb.new(data.get("name", ""), data.get("value", "")) + crumb.created_at = data.get("created_at", Time.get_unix_time_from_system()) + crumb.lifespan = data.get("lifespan", -1.0) + return crumb + +static func setup_crumbs_api(vm: LuauVM): + # Ensure crumbs directory exists + if not DirAccess.dir_exists_absolute(CRUMBS_DIR_PATH): + DirAccess.make_dir_recursive_absolute(CRUMBS_DIR_PATH) + + vm.lua_newtable() + + vm.lua_pushcallable(_crumbs_set_handler, "gurt.crumbs.set") + vm.lua_setfield(-2, "set") + + vm.lua_pushcallable(_crumbs_get_handler, "gurt.crumbs.get") + vm.lua_setfield(-2, "get") + + vm.lua_pushcallable(_crumbs_delete_handler, "gurt.crumbs.delete") + vm.lua_setfield(-2, "delete") + + vm.lua_pushcallable(_crumbs_get_all_handler, "gurt.crumbs.getAll") + vm.lua_setfield(-2, "getAll") + + vm.lua_getglobal("gurt") + if vm.lua_isnil(-1): + vm.lua_pop(1) + vm.lua_newtable() + vm.lua_setglobal("gurt") + vm.lua_getglobal("gurt") + + vm.lua_pushvalue(-2) + vm.lua_setfield(-2, "crumbs") + vm.lua_pop(2) + +static func get_current_domain() -> String: + var main_node = Engine.get_main_loop().current_scene + if main_node and main_node.has_method("get_current_url"): + var current_url = main_node.get_current_url() + return sanitize_domain_for_filename(current_url) + return "default" + +static func sanitize_domain_for_filename(domain: String) -> String: + # Remove protocol prefix + if domain.begins_with("gurt://"): + domain = domain.substr(7) + elif domain.contains("://"): + var parts = domain.split("://") + if parts.size() > 1: + domain = parts[1] + + # Extract only the domain part (remove path) + if domain.contains("/"): + domain = domain.split("/")[0] + + # Replace invalid filename characters (mainly colons for ports) + domain = domain.replace(":", "_") + domain = domain.replace("\\", "_") + domain = domain.replace("*", "_") + domain = domain.replace("?", "_") + domain = domain.replace("\"", "_") + domain = domain.replace("<", "_") + domain = domain.replace(">", "_") + domain = domain.replace("|", "_") + + # Ensure it's not empty + if domain.is_empty(): + domain = "default" + + return domain + +static func get_domain_file_path(domain: String) -> String: + return CRUMBS_DIR_PATH + domain + ".json" + +static func _crumbs_set_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "name") + if vm.lua_isnil(-1): + vm.luaL_error("crumb 'name' field is required") + return 0 + var name: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + vm.lua_getfield(1, "value") + if vm.lua_isnil(-1): + vm.luaL_error("crumb 'value' field is required") + return 0 + var value: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var lifetime: float = -1.0 + vm.lua_getfield(1, "lifetime") + if not vm.lua_isnil(-1): + lifetime = vm.lua_tonumber(-1) + vm.lua_pop(1) + + var domain = get_current_domain() + var crumb = Crumb.new(name, value, lifetime) + save_crumb(domain, crumb) + + return 0 + +static func _crumbs_get_handler(vm: LuauVM) -> int: + var name: String = vm.luaL_checkstring(1) + var domain = get_current_domain() + var crumb = load_crumb(domain, name) + + if crumb and not crumb.is_expired(): + vm.lua_pushstring(crumb.value) + else: + vm.lua_pushnil() + + return 1 + +static func _crumbs_delete_handler(vm: LuauVM) -> int: + var name: String = vm.luaL_checkstring(1) + var domain = get_current_domain() + var existed = delete_crumb(domain, name) + + vm.lua_pushboolean(existed) + return 1 + +static func _crumbs_get_all_handler(vm: LuauVM) -> int: + var domain = get_current_domain() + var all_crumbs = load_all_crumbs(domain) + + vm.lua_newtable() + + for crumb_name in all_crumbs: + var crumb = all_crumbs[crumb_name] + if not crumb.is_expired(): + vm.lua_newtable() + vm.lua_pushstring(crumb.name) + vm.lua_setfield(-2, "name") + vm.lua_pushstring(crumb.value) + vm.lua_setfield(-2, "value") + + # Include expiry time if it exists (but not created_at) + var expiry_time = crumb.get_expiry_time() + if expiry_time > 0: + vm.lua_pushnumber(expiry_time) + vm.lua_setfield(-2, "expiry") + + vm.lua_setfield(-2, crumb_name) + + return 1 + +static func load_all_crumbs(domain: String) -> Dictionary: + var file_path = get_domain_file_path(domain) + if not FileAccess.file_exists(file_path): + return {} + + var file = FileAccess.open(file_path, FileAccess.READ) + if not file: + return {} + + var json_string = file.get_as_text() + file.close() + + var json = JSON.new() + var parse_result = json.parse(json_string) + if parse_result != OK: + return {} + + var crumbs_data = json.data + if not crumbs_data is Dictionary: + return {} + + var crumbs = {} + var current_time = Time.get_ticks_msec() / 1000.0 + var changed = false + + for crumb_name in crumbs_data: + var crumb_dict = crumbs_data[crumb_name] + if crumb_dict is Dictionary: + var crumb = Crumb.from_dict(crumb_dict) + if crumb.is_expired(): + changed = true + else: + crumbs[crumb_name] = crumb + + # Save back if we removed expired crumbs + if changed: + save_all_crumbs(domain, crumbs) + + return crumbs + +static func save_all_crumbs(domain: String, crumbs: Dictionary): + var crumbs_data = {} + for crumb_name in crumbs: + var crumb = crumbs[crumb_name] + crumbs_data[crumb_name] = crumb.to_dict() + + var file_path = get_domain_file_path(domain) + var file = FileAccess.open(file_path, FileAccess.WRITE) + if not file: + push_error("Failed to open crumbs file for writing: " + file_path) + return + + var json_string = JSON.stringify(crumbs_data) + file.store_string(json_string) + file.close() + +static func load_crumb(domain: String, name: String) -> Crumb: + var all_crumbs = load_all_crumbs(domain) + return all_crumbs.get(name, null) + +static func save_crumb(domain: String, crumb: Crumb): + var all_crumbs = load_all_crumbs(domain) + all_crumbs[crumb.name] = crumb + save_all_crumbs(domain, all_crumbs) + +static func delete_crumb(domain: String, name: String) -> bool: + var all_crumbs = load_all_crumbs(domain) + var existed = all_crumbs.has(name) + if existed: + all_crumbs.erase(name) + save_all_crumbs(domain, all_crumbs) + return existed diff --git a/flumi/Scripts/Utils/Lua/Crumbs.gd.uid b/flumi/Scripts/Utils/Lua/Crumbs.gd.uid new file mode 100644 index 0000000..3d561d4 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Crumbs.gd.uid @@ -0,0 +1 @@ +uid://bf63lfs770plv diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index 6c71831..5e8b52d 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -120,9 +120,8 @@ static func handle_element_append(operation: Dictionary, dom_parser: HTMLParser, # Handle visual rendering if parent is already rendered var parent_dom_node: Node = null if parent_id == "body": - var main_scene = lua_api.get_node("/root/Main") - if main_scene: - parent_dom_node = main_scene.website_container + var main_scene = Engine.get_main_loop().current_scene + parent_dom_node = main_scene.website_container else: parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_id, null) @@ -184,7 +183,7 @@ static func handle_insert_before(operation: Dictionary, dom_parser: HTMLParser, # Handle visual rendering var parent_dom_node: Node = null if parent_id == "body": - var main_scene = lua_api.get_node("/root/Main") + var main_scene = Engine.get_main_loop().current_scene if main_scene: parent_dom_node = main_scene.website_container else: @@ -223,7 +222,7 @@ static func handle_insert_after(operation: Dictionary, dom_parser: HTMLParser, l # Handle visual rendering var parent_dom_node: Node = null if parent_id == "body": - var main_scene = lua_api.get_node("/root/Main") + var main_scene = Engine.get_main_loop().current_scene if main_scene: parent_dom_node = main_scene.website_container else: @@ -265,7 +264,7 @@ static func handle_replace_child(operation: Dictionary, dom_parser: HTMLParser, static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Node, dom_parser: HTMLParser, lua_api) -> void: # Get reference to main scene for rendering - var main_scene = lua_api.get_node("/root/Main") + var main_scene = Engine.get_main_loop().current_scene if not main_scene: return @@ -318,7 +317,7 @@ static func clone_element(element: HTMLParser.HTMLElement, deep: bool) -> HTMLPa static func handle_visual_insertion_by_reference(parent_element_id: String, new_child_element: HTMLParser.HTMLElement, reference_element_id: String, insert_before: bool, dom_parser: HTMLParser, lua_api) -> void: var parent_dom_node: Node = null if parent_element_id == "body": - var main_scene = lua_api.get_node("/root/Main") + var main_scene = Engine.get_main_loop().current_scene if main_scene: parent_dom_node = main_scene.website_container else: diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index 36b4efb..7bc9e07 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -344,6 +344,7 @@ func _setup_additional_lua_apis(): LuaJSONUtils.setup_json_api(lua_vm) LuaWebSocketUtils.setup_websocket_api(lua_vm) LuaAudioUtils.setup_audio_api(lua_vm) + LuaCrumbsUtils.setup_crumbs_api(lua_vm) func _table_tostring_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 5795395..be1dc4c 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -135,6 +135,16 @@ func render() -> void: func render_content(html_bytes: PackedByteArray) -> void: + var existing_lua_apis = [] + for child in get_children(): + if child is LuaAPI: + existing_lua_apis.append(child) + + for lua_api in existing_lua_apis: + lua_api.kill_script_execution() + remove_child(lua_api) + lua_api.queue_free() + # Clear existing content for child in website_container.get_children(): child.queue_free() diff --git a/flumi/project.godot b/flumi/project.godot index 9f74a71..ccf6adc 100644 --- a/flumi/project.godot +++ b/flumi/project.godot @@ -13,6 +13,7 @@ config_version=5 config/name="Flumi" config/version="1.0.0" run/main_scene="uid://bytm7bt2s4ak8" +config/use_custom_user_dir=true config/features=PackedStringArray("4.4", "Forward Plus") boot_splash/show_image=false config/icon="uid://ctpe0lbehepen" diff --git a/tests/crumbs.html b/tests/crumbs.html new file mode 100644 index 0000000..e9e2b43 --- /dev/null +++ b/tests/crumbs.html @@ -0,0 +1,223 @@ +
+Test setting crumbs with different lifetimes
+ + + + +Test retrieving existing and non-existent crumbs
+ + +Test deleting crumbs and handling non-existent deletions
+ + + +All stored crumbs (updates every 2 seconds to show expiry)
+ +