From b588a61998a71aadbf2fd7b013212742bf67b6ca Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:43:27 +0300 Subject: [PATCH] file:// support --- flumi/Scripts/Utils/FileUtils.gd | 52 +++++++++++++++++ flumi/Scripts/Utils/FileUtils.gd.uid | 1 + flumi/Scripts/Utils/URLUtils.gd | 54 +++++++++++++++++- flumi/Scripts/main.gd | 85 +++++++++++++++++++++++++++- 4 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 flumi/Scripts/Utils/FileUtils.gd create mode 100644 flumi/Scripts/Utils/FileUtils.gd.uid diff --git a/flumi/Scripts/Utils/FileUtils.gd b/flumi/Scripts/Utils/FileUtils.gd new file mode 100644 index 0000000..9d327fc --- /dev/null +++ b/flumi/Scripts/Utils/FileUtils.gd @@ -0,0 +1,52 @@ +class_name FileUtils +extends RefCounted + +static func read_local_file(file_path: String) -> Dictionary: + var result = {"success": false, "content": PackedByteArray(), "error": ""} + + if not FileAccess.file_exists(file_path): + result.error = "File not found: " + file_path + return result + + var file = FileAccess.open(file_path, FileAccess.READ) + if not file: + result.error = "Cannot open file: " + file_path + return result + + var content = file.get_buffer(file.get_length()) + file.close() + + result.success = true + result.content = content + return result + +static func is_directory(path: String) -> bool: + return DirAccess.dir_exists_absolute(path) + +static func is_html_file(file_path: String) -> bool: + var extension = file_path.get_extension().to_lower() + return extension in ["html", "htm"] + +static func is_supported_file(file_path: String) -> bool: + var extension = file_path.get_extension().to_lower() + return extension in ["html", "htm", "txt", "css", "js"] + +static func create_error_page(title: String, error_message: String) -> PackedByteArray: + var html = """ + """ + title + """ - File Browser + + + +
+

""" + title + """

+

""" + error_message + """

+
+""" + + return html.to_utf8_buffer() diff --git a/flumi/Scripts/Utils/FileUtils.gd.uid b/flumi/Scripts/Utils/FileUtils.gd.uid new file mode 100644 index 0000000..a49fddf --- /dev/null +++ b/flumi/Scripts/Utils/FileUtils.gd.uid @@ -0,0 +1 @@ +uid://djcyjusk64cyr diff --git a/flumi/Scripts/Utils/URLUtils.gd b/flumi/Scripts/Utils/URLUtils.gd index cb3f9f5..6961dec 100644 --- a/flumi/Scripts/Utils/URLUtils.gd +++ b/flumi/Scripts/Utils/URLUtils.gd @@ -3,7 +3,7 @@ extends RefCounted static func resolve_url(base_url: String, relative_url: String) -> String: # If relative_url is already absolute, return it as-is - if relative_url.begins_with("http://") or relative_url.begins_with("https://") or relative_url.begins_with("gurt://"): + if relative_url.begins_with("http://") or relative_url.begins_with("https://") or relative_url.begins_with("gurt://") or relative_url.begins_with("file://"): return relative_url # If empty, treat as relative to current domain @@ -20,6 +20,23 @@ static func resolve_url(base_url: String, relative_url: String) -> String: var scheme = clean_base.substr(0, scheme_end + 3) var remainder = clean_base.substr(scheme_end + 3) + if scheme == "file://": + var file_path = remainder + + if OS.get_name() == "Windows": + file_path = file_path.replace("/", "\\") + + if relative_url.begins_with("/"): + return scheme + relative_url.substr(1) + else: + var base_dir = file_path.get_base_dir() + if base_dir.is_empty(): + return scheme + relative_url + else: + var resolved_path = base_dir + "/" + relative_url + resolved_path = resolved_path.replace("\\", "/") + return scheme + resolved_path + # Split remainder into host and path var first_slash = remainder.find("/") var host = "" @@ -77,9 +94,44 @@ static func extract_domain(url: String) -> String: clean_url = clean_url.substr(8) elif clean_url.begins_with("http://"): clean_url = clean_url.substr(7) + elif clean_url.begins_with("file://"): + return "localhost" var slash_pos = clean_url.find("/") if slash_pos != -1: clean_url = clean_url.substr(0, slash_pos) return clean_url + +static func is_local_file_url(url: String) -> bool: + return url.begins_with("file://") + +static func file_url_to_path(url: String) -> String: + if not is_local_file_url(url): + return "" + + var path = url.substr(7) # Remove "file://" + + if path.begins_with("/") and path.length() > 2 and path.substr(2, 1) == ":": + path = path.substr(1) + elif path.length() > 1 and path.substr(1, 1) == ":": + pass + + if OS.get_name() == "Windows": + path = path.replace("/", "\\") + + return path + +static func path_to_file_url(path: String) -> String: + var clean_path = path + + clean_path = clean_path.replace("\\", "/") + + if OS.get_name() == "Windows": + if not clean_path.begins_with("/"): + clean_path = "/" + clean_path + else: + if not clean_path.begins_with("/"): + clean_path = "/" + clean_path + + return "file://" + clean_path diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 90f4764..0ba7435 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -93,7 +93,9 @@ func handle_link_click(meta: Variant) -> void: var resolved_url = resolve_url(href) - if GurtProtocol.is_gurt_domain(resolved_url): + if URLUtils.is_local_file_url(resolved_url): + _on_search_submitted(resolved_url) + elif GurtProtocol.is_gurt_domain(resolved_url): _on_search_submitted(resolved_url) else: OS.shell_open(resolved_url) @@ -103,7 +105,12 @@ func _on_search_submitted(url: String, add_to_history: bool = true) -> void: search_bar.release_focus() - if GurtProtocol.is_gurt_domain(url): + if URLUtils.is_local_file_url(url): + var tab = tab_container.tabs[tab_container.active_tab] + tab.start_loading() + + await fetch_local_file_content_async(url, tab, url, add_to_history) + elif GurtProtocol.is_gurt_domain(url): print("Processing as GURT domain") var tab = tab_container.tabs[tab_container.active_tab] @@ -168,6 +175,74 @@ func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary: return {"success": true, "html_bytes": response.body} +func fetch_local_file_content_async(file_url: String, tab: Tab, original_url: String, add_to_history: bool = true) -> void: + var file_path = URLUtils.file_url_to_path(file_url) + + if FileUtils.is_directory(file_path): + handle_local_file_error("Directory browsing is not supported. Please specify a file.", tab) + return + + if not FileAccess.file_exists(file_path): + handle_local_file_error("File not found: " + file_path, tab) + return + + if not FileUtils.is_supported_file(file_path): + handle_local_file_error("Unsupported file type: " + file_path.get_extension(), tab) + return + + var result = FileUtils.read_local_file(file_path) + + if result.success: + if FileUtils.is_html_file(file_path): + handle_local_file_result({"success": true, "html_bytes": result.content}, tab, original_url, file_url, add_to_history) + else: + var content_str = result.content.get_string_from_utf8() + var wrapped_html = """ + """ + file_path.get_file() + """ + + + +

""" + file_path.get_file() + """

+
+
""" + content_str.xml_escape() + """
+
+""" + handle_local_file_result({"success": true, "html_bytes": wrapped_html.to_utf8_buffer()}, tab, original_url, file_url, add_to_history) + else: + handle_local_file_error(result.error, tab) + +func handle_local_file_result(result: Dictionary, tab: Tab, original_url: String, file_url: String, add_to_history: bool = true) -> void: + var html_bytes = result.html_bytes + + current_domain = file_url + if not search_bar.has_focus(): + search_bar.text = original_url + + render_content(html_bytes) + + tab.stop_loading() + + if add_to_history: + add_to_history(file_url, tab) + else: + update_navigation_buttons() + +func handle_local_file_error(error_message: String, tab: Tab) -> void: + var error_html = FileUtils.create_error_page("File Access Error", error_message) + + render_content(error_html) + + const FOLDER_ICON = preload("res://Assets/Icons/folder.svg") + tab.stop_loading() + if FOLDER_ICON: + tab.set_icon(FOLDER_ICON) + else: + var GLOBE_ICON = preload("res://Assets/Icons/globe.svg") + tab.set_icon(GLOBE_ICON) + func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gurt_url: String, add_to_history: bool = true) -> void: if not result.success: print("GURT request failed: ", result.error) @@ -216,6 +291,8 @@ func _on_search_focus_exited() -> void: var display_text = current_domain if display_text.begins_with("gurt://"): display_text = display_text.substr(7) + elif display_text.begins_with("file://"): + display_text = URLUtils.file_url_to_path(display_text) search_bar.text = display_text func render() -> void: @@ -747,7 +824,7 @@ func reload_current_page() -> void: _on_search_submitted(current_domain) func navigate_to_url(url: String, add_to_history: bool = true) -> void: - if url.begins_with("gurt://"): + if url.begins_with("gurt://") or url.begins_with("file://"): _on_search_submitted(url, add_to_history) else: var resolved_url = resolve_url(url) @@ -758,6 +835,8 @@ func update_search_bar_from_current_domain() -> void: var display_text = current_domain if display_text.begins_with("gurt://"): display_text = display_text.substr(7) + elif display_text.begins_with("file://"): + display_text = URLUtils.file_url_to_path(display_text) search_bar.text = display_text func get_active_tab() -> Tab: