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: