rearrange scripts in folders
This commit is contained in:
91
flumi/Scripts/Browser/BrowserHistory.gd
Normal file
91
flumi/Scripts/Browser/BrowserHistory.gd
Normal file
@@ -0,0 +1,91 @@
|
||||
extends Node
|
||||
|
||||
const HISTORY_FILE_PATH = "user://browser_history.json"
|
||||
const MAX_HISTORY_ENTRIES = 1000
|
||||
|
||||
func get_history_data() -> Array:
|
||||
var history_file = FileAccess.open(HISTORY_FILE_PATH, FileAccess.READ)
|
||||
if not history_file:
|
||||
return []
|
||||
|
||||
var json_string = history_file.get_as_text()
|
||||
history_file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
|
||||
if parse_result != OK:
|
||||
return []
|
||||
|
||||
var history_data = json.data
|
||||
if not history_data is Array:
|
||||
return []
|
||||
|
||||
return history_data
|
||||
|
||||
func save_history_data(history_data: Array):
|
||||
var history_file = FileAccess.open(HISTORY_FILE_PATH, FileAccess.WRITE)
|
||||
if not history_file:
|
||||
push_error("Failed to open history file for writing")
|
||||
return
|
||||
|
||||
var json_string = JSON.stringify(history_data)
|
||||
history_file.store_string(json_string)
|
||||
history_file.close()
|
||||
|
||||
func add_entry(url: String, title: String, icon_url: String = ""):
|
||||
if url.is_empty():
|
||||
return
|
||||
|
||||
var history_data = get_history_data()
|
||||
var timestamp = Time.get_datetime_string_from_system()
|
||||
|
||||
var existing_index = -1
|
||||
for i in range(history_data.size()):
|
||||
if history_data[i].url == url:
|
||||
existing_index = i
|
||||
break
|
||||
|
||||
var entry = {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"timestamp": timestamp,
|
||||
"icon_url": icon_url
|
||||
}
|
||||
|
||||
if existing_index >= 0:
|
||||
history_data.remove_at(existing_index)
|
||||
|
||||
history_data.insert(0, entry)
|
||||
|
||||
if history_data.size() > MAX_HISTORY_ENTRIES:
|
||||
history_data = history_data.slice(0, MAX_HISTORY_ENTRIES)
|
||||
|
||||
save_history_data(history_data)
|
||||
|
||||
func remove_entry(url: String):
|
||||
var history_data = get_history_data()
|
||||
|
||||
for i in range(history_data.size() - 1, -1, -1):
|
||||
if history_data[i].url == url:
|
||||
history_data.remove_at(i)
|
||||
|
||||
save_history_data(history_data)
|
||||
|
||||
func clear_all():
|
||||
save_history_data([])
|
||||
|
||||
func search_history(query: String) -> Array:
|
||||
var history_data = get_history_data()
|
||||
var results = []
|
||||
|
||||
query = query.to_lower()
|
||||
|
||||
for entry in history_data:
|
||||
var title = entry.get("title", "").to_lower()
|
||||
var url = entry.get("url", "").to_lower()
|
||||
|
||||
if title.contains(query) or url.contains(query):
|
||||
results.append(entry)
|
||||
|
||||
return results
|
||||
1
flumi/Scripts/Browser/BrowserHistory.gd.uid
Normal file
1
flumi/Scripts/Browser/BrowserHistory.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c8wiwde042pkx
|
||||
49
flumi/Scripts/Browser/CertificateManager.gd
Normal file
49
flumi/Scripts/Browser/CertificateManager.gd
Normal file
@@ -0,0 +1,49 @@
|
||||
extends RefCounted
|
||||
class_name CertificateManager
|
||||
|
||||
static var trusted_ca_certificates: Array[String] = []
|
||||
static var ca_cache: Dictionary = {}
|
||||
|
||||
static func fetch_cert_via_http(url: String) -> String:
|
||||
var http_request = HTTPRequest.new()
|
||||
|
||||
var main_scene = Engine.get_main_loop().current_scene
|
||||
if not main_scene:
|
||||
return ""
|
||||
|
||||
main_scene.add_child(http_request)
|
||||
|
||||
var error = http_request.request(url)
|
||||
if error != OK:
|
||||
http_request.queue_free()
|
||||
return ""
|
||||
|
||||
var response = await http_request.request_completed
|
||||
http_request.queue_free()
|
||||
|
||||
var result = response[0]
|
||||
var response_code = response[1]
|
||||
var body = response[3]
|
||||
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
|
||||
return ""
|
||||
|
||||
return body.get_string_from_utf8()
|
||||
|
||||
static func initialize():
|
||||
load_builtin_ca()
|
||||
print("Certificate Manager initialized with ", trusted_ca_certificates.size(), " trusted CAs")
|
||||
|
||||
static func load_builtin_ca():
|
||||
var ca_file = FileAccess.open("res://Assets/gurted-ca.crt", FileAccess.READ)
|
||||
if ca_file:
|
||||
var ca_cert_pem = ca_file.get_as_text()
|
||||
ca_file.close()
|
||||
|
||||
if not ca_cert_pem.is_empty():
|
||||
trusted_ca_certificates.append(ca_cert_pem)
|
||||
print("Loaded built-in GURT CA certificate")
|
||||
else:
|
||||
print("Built-in CA certificate not yet configured")
|
||||
else:
|
||||
print("Could not load built-in CA certificate")
|
||||
1
flumi/Scripts/Browser/CertificateManager.gd.uid
Normal file
1
flumi/Scripts/Browser/CertificateManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bhnsb8ttn6f7n
|
||||
4
flumi/Scripts/Browser/DevTools.gd
Normal file
4
flumi/Scripts/Browser/DevTools.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
extends Control
|
||||
|
||||
func _on_close_button_pressed():
|
||||
Engine.get_main_loop().current_scene._toggle_dev_tools()
|
||||
1
flumi/Scripts/Browser/DevTools.gd.uid
Normal file
1
flumi/Scripts/Browser/DevTools.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://21a6ds271vmb
|
||||
553
flumi/Scripts/Browser/DevToolsConsole.gd
Normal file
553
flumi/Scripts/Browser/DevToolsConsole.gd
Normal file
@@ -0,0 +1,553 @@
|
||||
class_name DevToolsConsole
|
||||
extends VBoxContainer
|
||||
|
||||
@onready var log_container: VBoxContainer = $ScrollContainer/LogContainer
|
||||
@onready var input_line: CodeEdit = $InputContainer/InputLine
|
||||
@onready var scroll_container: ScrollContainer = $ScrollContainer
|
||||
@onready var clear_button: Button = $Toolbar/ClearButton
|
||||
@onready var filter_input: LineEdit = $Toolbar/LineEdit
|
||||
|
||||
var log_entries: Array[Dictionary] = []
|
||||
var current_filter: String = ""
|
||||
var last_log_item: Control = null
|
||||
var last_log_entry: Dictionary = {}
|
||||
var input_history: Array[String] = []
|
||||
var history_index: int = -1
|
||||
|
||||
func _ready():
|
||||
connect_signals()
|
||||
initialize_filter()
|
||||
load_existing_logs()
|
||||
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
|
||||
func initialize_filter() -> void:
|
||||
filter_input.placeholder_text = "Filter"
|
||||
current_filter = ""
|
||||
|
||||
func connect_signals() -> void:
|
||||
clear_button.pressed.connect(_on_clear_pressed)
|
||||
|
||||
input_line.gui_input.connect(_on_input_gui_input)
|
||||
filter_input.text_changed.connect(_on_filter_changed)
|
||||
|
||||
func load_existing_logs() -> void:
|
||||
var existing_messages = Trace.get_all_messages()
|
||||
for msg in existing_messages:
|
||||
add_log_entry(msg.message, msg.level, msg.timestamp)
|
||||
|
||||
func _on_lua_print(message: String) -> void:
|
||||
add_log_entry(message, "lua", Time.get_ticks_msec() / 1000.0)
|
||||
|
||||
func add_log_entry(message: Variant, level: String, timestamp: float) -> void:
|
||||
var entry = {
|
||||
"message": message,
|
||||
"level": level,
|
||||
"timestamp": timestamp,
|
||||
"count": 1
|
||||
}
|
||||
|
||||
if can_group_with_last_entry(entry):
|
||||
last_log_entry.count += 1
|
||||
last_log_entry.timestamp = timestamp
|
||||
log_entries[log_entries.size() - 1] = last_log_entry
|
||||
|
||||
update_log_item_display(last_log_item, last_log_entry)
|
||||
return
|
||||
|
||||
log_entries.append(entry)
|
||||
last_log_entry = entry
|
||||
|
||||
var should_add_separator = false
|
||||
if level == "log" or level == "lua" or level == "input":
|
||||
if log_container.get_child_count() > 0:
|
||||
var last_displayed_entry = get_last_displayed_entry()
|
||||
if last_displayed_entry and last_displayed_entry.level != "warning" and last_displayed_entry.level != "error":
|
||||
should_add_separator = true
|
||||
|
||||
if should_add_separator:
|
||||
var separator = HSeparator.new()
|
||||
separator.add_theme_color_override("separator", Color.GRAY * 0.3)
|
||||
log_container.add_child(separator)
|
||||
|
||||
var log_item = create_log_item(entry)
|
||||
log_container.add_child(log_item)
|
||||
last_log_item = log_item
|
||||
|
||||
apply_filter_to_item(log_item, entry)
|
||||
|
||||
call_deferred("_scroll_to_bottom")
|
||||
|
||||
func create_log_item(entry: Dictionary) -> Control:
|
||||
if entry.level == "input":
|
||||
var input_display_text = get_display_text_for_entry(entry)
|
||||
var message_code_edit = CodeEditUtils.create_code_edit({
|
||||
"text": input_display_text,
|
||||
"scroll_fit_content_height": true,
|
||||
"transparent_background": true,
|
||||
"syntax_highlighter": input_line.syntax_highlighter.duplicate(),
|
||||
"block_editing_signals": true
|
||||
})
|
||||
|
||||
message_code_edit.gui_input.connect(_on_log_code_edit_gui_input)
|
||||
message_code_edit.focus_entered.connect(_on_log_code_edit_focus_entered.bind(message_code_edit))
|
||||
message_code_edit.text_changed.connect(_on_log_code_edit_text_changed.bind(message_code_edit, input_display_text))
|
||||
|
||||
return message_code_edit
|
||||
|
||||
if entry.level == "lua" and entry.message is Dictionary and entry.message.has("parts"):
|
||||
return create_structured_log_item(entry)
|
||||
|
||||
var panel = PanelContainer.new()
|
||||
var style_box = StyleBoxFlat.new()
|
||||
|
||||
match entry.level:
|
||||
"warning":
|
||||
style_box.bg_color = Color.YELLOW
|
||||
style_box.bg_color.a = 0.2
|
||||
style_box.corner_radius_top_left = 6
|
||||
style_box.corner_radius_top_right = 6
|
||||
style_box.corner_radius_bottom_left = 6
|
||||
style_box.corner_radius_bottom_right = 6
|
||||
style_box.content_margin_left = 8
|
||||
style_box.content_margin_right = 8
|
||||
style_box.content_margin_top = 4
|
||||
style_box.content_margin_bottom = 4
|
||||
"error":
|
||||
style_box.bg_color = Color.RED
|
||||
style_box.bg_color.a = 0.2
|
||||
style_box.corner_radius_top_left = 6
|
||||
style_box.corner_radius_top_right = 6
|
||||
style_box.corner_radius_bottom_left = 6
|
||||
style_box.corner_radius_bottom_right = 6
|
||||
style_box.content_margin_left = 8
|
||||
style_box.content_margin_right = 8
|
||||
style_box.content_margin_top = 4
|
||||
style_box.content_margin_bottom = 4
|
||||
_:
|
||||
style_box.bg_color = Color.TRANSPARENT
|
||||
|
||||
panel.add_theme_stylebox_override("panel", style_box)
|
||||
|
||||
var container: Control
|
||||
if entry.level == "warning" or entry.level == "error":
|
||||
var margin_container = MarginContainer.new()
|
||||
margin_container.add_child(panel)
|
||||
container = margin_container
|
||||
else:
|
||||
container = panel
|
||||
|
||||
var message_label = Label.new()
|
||||
var display_text = get_display_text_for_entry(entry)
|
||||
message_label.text = display_text
|
||||
message_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
|
||||
match entry.level:
|
||||
"warning":
|
||||
message_label.add_theme_color_override("font_color", Color.YELLOW)
|
||||
"error":
|
||||
message_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
_:
|
||||
message_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
|
||||
panel.add_child(message_label)
|
||||
return container
|
||||
|
||||
func _scroll_to_bottom() -> void:
|
||||
if scroll_container:
|
||||
scroll_container.scroll_vertical = int(scroll_container.get_v_scroll_bar().max_value)
|
||||
|
||||
func _on_clear_pressed() -> void:
|
||||
for child in log_container.get_children():
|
||||
child.queue_free()
|
||||
log_entries.clear()
|
||||
|
||||
last_log_item = null
|
||||
last_log_entry = {}
|
||||
|
||||
Trace.clear_messages()
|
||||
|
||||
func _on_input_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_ENTER and event.ctrl_pressed:
|
||||
var text = input_line.text.strip_edges()
|
||||
if not text.is_empty():
|
||||
_on_input_submitted(text)
|
||||
input_line.text = ""
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.keycode == KEY_UP and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
|
||||
if input_line.get_caret_column() == 0 and input_line.get_caret_line() == 0:
|
||||
navigate_history_up()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.keycode == KEY_DOWN and not event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed:
|
||||
var caret_pos = input_line.get_caret_column()
|
||||
var last_line = input_line.get_line_count() - 1
|
||||
var last_line_length = input_line.get_line(last_line).length()
|
||||
if input_line.get_caret_line() == last_line and caret_pos == last_line_length:
|
||||
navigate_history_down()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func navigate_history_up() -> void:
|
||||
if input_history.is_empty():
|
||||
return
|
||||
if history_index == -1:
|
||||
history_index = input_history.size() - 1
|
||||
elif history_index > 0:
|
||||
history_index -= 1
|
||||
|
||||
if history_index >= 0 and history_index < input_history.size():
|
||||
input_line.text = input_history[history_index]
|
||||
call_deferred("move_caret_to_end")
|
||||
|
||||
func navigate_history_down() -> void:
|
||||
if input_history.is_empty() or history_index == -1:
|
||||
return
|
||||
|
||||
history_index += 1
|
||||
|
||||
if history_index >= input_history.size():
|
||||
history_index = -1
|
||||
input_line.text = ""
|
||||
else:
|
||||
input_line.text = input_history[history_index]
|
||||
call_deferred("move_caret_to_end")
|
||||
|
||||
func move_caret_to_end() -> void:
|
||||
var last_line = input_line.get_line_count() - 1
|
||||
var last_line_length = input_line.get_line(last_line).length()
|
||||
input_line.set_caret_line(last_line)
|
||||
input_line.set_caret_column(last_line_length)
|
||||
|
||||
func _on_input_submitted(text: String) -> void:
|
||||
if text.strip_edges().is_empty():
|
||||
return
|
||||
|
||||
if input_history.is_empty() or input_history[input_history.size() - 1] != text:
|
||||
input_history.append(text)
|
||||
if input_history.size() > 100:
|
||||
input_history.pop_front()
|
||||
|
||||
history_index = -1
|
||||
|
||||
add_log_entry("> " + text, "input", Time.get_ticks_msec() / 1000.0)
|
||||
|
||||
execute_lua_command(text)
|
||||
|
||||
func execute_lua_command(code: String) -> void:
|
||||
var main_scene = Engine.get_main_loop().current_scene
|
||||
if main_scene and main_scene.has_method("get_active_tab"):
|
||||
var active_tab = main_scene.get_active_tab()
|
||||
if active_tab and active_tab.lua_apis.size() > 0:
|
||||
var lua_api = active_tab.lua_apis[0]
|
||||
if lua_api:
|
||||
var is_expression = is_likely_expression(code)
|
||||
if is_expression:
|
||||
var wrapped_code = "print(" + code + ")"
|
||||
lua_api.execute_lua_script(wrapped_code)
|
||||
else:
|
||||
lua_api.execute_lua_script(code)
|
||||
return
|
||||
|
||||
add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0)
|
||||
|
||||
func is_likely_expression(code: String) -> bool:
|
||||
var trimmed = code.strip_edges()
|
||||
var statement_keywords = ["if", "for", "while", "do", "function", "local", "return", "break"]
|
||||
for keyword in statement_keywords:
|
||||
if trimmed.begins_with(keyword + " ") or trimmed.begins_with(keyword + "("):
|
||||
return false
|
||||
if "=" in trimmed and not ("==" in trimmed or "!=" in trimmed or ">=" in trimmed or "<=" in trimmed):
|
||||
return false
|
||||
if "print(" in trimmed or "console.log(" in trimmed or "_trace_" in trimmed:
|
||||
return false
|
||||
if trimmed.ends_with(")") or trimmed.ends_with("]") or not (" " in trimmed):
|
||||
return true
|
||||
return true
|
||||
|
||||
func _on_filter_changed(new_text: String) -> void:
|
||||
current_filter = new_text.strip_edges()
|
||||
refresh_log_display()
|
||||
|
||||
func refresh_log_display() -> void:
|
||||
for i in range(log_container.get_child_count()):
|
||||
var child = log_container.get_child(i)
|
||||
if child is HSeparator:
|
||||
child.visible = should_separator_be_visible(i)
|
||||
else:
|
||||
var entry_index = get_entry_index_for_child(i)
|
||||
if entry_index >= 0 and entry_index < log_entries.size():
|
||||
var entry = log_entries[entry_index]
|
||||
apply_filter_to_item(child, entry)
|
||||
|
||||
call_deferred("_scroll_to_bottom")
|
||||
|
||||
func apply_filter_to_item(item: Control, entry: Dictionary) -> void:
|
||||
item.visible = entry_matches_filter(entry)
|
||||
|
||||
func entry_matches_filter(entry: Dictionary) -> bool:
|
||||
if current_filter.is_empty():
|
||||
return true
|
||||
|
||||
var message_text = ""
|
||||
if entry.message is Dictionary and entry.message.has("parts"):
|
||||
message_text = get_display_text_for_entry(entry)
|
||||
else:
|
||||
message_text = str(entry.message)
|
||||
|
||||
if current_filter.to_lower() in message_text.to_lower():
|
||||
return true
|
||||
|
||||
if current_filter.to_lower() == entry.level.to_lower():
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func get_last_displayed_entry() -> Dictionary:
|
||||
for i in range(log_entries.size() - 2, -1, -1): # Start from second-to-last entry
|
||||
var entry = log_entries[i]
|
||||
if entry_matches_filter(entry):
|
||||
return entry
|
||||
return {}
|
||||
|
||||
func should_separator_be_visible(separator_index: int) -> bool:
|
||||
var before_visible = false
|
||||
var after_visible = false
|
||||
|
||||
if separator_index > 0:
|
||||
var before_child = log_container.get_child(separator_index - 1)
|
||||
before_visible = before_child.visible
|
||||
|
||||
|
||||
if separator_index < log_container.get_child_count() - 1:
|
||||
var after_child = log_container.get_child(separator_index + 1)
|
||||
after_visible = after_child.visible
|
||||
|
||||
return before_visible and after_visible
|
||||
|
||||
func get_entry_index_for_child(child_index: int) -> int:
|
||||
var entry_index = 0
|
||||
for i in range(child_index):
|
||||
var child = log_container.get_child(i)
|
||||
if not child is HSeparator:
|
||||
entry_index += 1
|
||||
return entry_index
|
||||
|
||||
func can_group_with_last_entry(entry: Dictionary) -> bool:
|
||||
if last_log_entry.is_empty() or last_log_item == null:
|
||||
return false
|
||||
|
||||
if entry.level != last_log_entry.level:
|
||||
return false
|
||||
|
||||
var current_message_text = ""
|
||||
var last_message_text = ""
|
||||
|
||||
if entry.message is Dictionary and entry.message.has("parts"):
|
||||
current_message_text = get_display_text_for_entry(entry)
|
||||
else:
|
||||
current_message_text = str(entry.message)
|
||||
|
||||
if last_log_entry.message is Dictionary and last_log_entry.message.has("parts"):
|
||||
last_message_text = get_display_text_for_entry(last_log_entry)
|
||||
else:
|
||||
last_message_text = str(last_log_entry.message)
|
||||
|
||||
if current_message_text != last_message_text:
|
||||
return false
|
||||
|
||||
if entry.level == "input":
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func update_log_item_display(log_item: Control, entry: Dictionary) -> void:
|
||||
if entry.level == "input":
|
||||
var code_edit = log_item as CodeEdit
|
||||
if code_edit:
|
||||
code_edit.text = get_display_text_for_entry(entry)
|
||||
else:
|
||||
var label = find_message_label_in_item(log_item)
|
||||
if label:
|
||||
label.text = get_display_text_for_entry(entry)
|
||||
|
||||
func find_message_code_edit_in_item(item: Control) -> CodeEdit:
|
||||
if item is CodeEdit:
|
||||
return item as CodeEdit
|
||||
|
||||
if item is MarginContainer:
|
||||
var panel = item.get_child(0) as PanelContainer
|
||||
if panel:
|
||||
return panel.get_child(0) as CodeEdit
|
||||
elif item is PanelContainer:
|
||||
return item.get_child(0) as CodeEdit
|
||||
return null
|
||||
|
||||
func find_message_label_in_item(item: Control) -> Label:
|
||||
if item is MarginContainer:
|
||||
var panel = item.get_child(0) as PanelContainer
|
||||
if panel:
|
||||
return panel.get_child(0) as Label
|
||||
elif item is PanelContainer:
|
||||
return item.get_child(0) as Label
|
||||
return null
|
||||
|
||||
func _on_log_code_edit_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
var key = event.keycode
|
||||
if event.ctrl_pressed and (key == KEY_C or key == KEY_A):
|
||||
return
|
||||
if key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]:
|
||||
return
|
||||
if event.shift_pressed and key in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END]:
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseButton:
|
||||
return
|
||||
|
||||
func _on_log_code_edit_focus_entered(code_edit: CodeEdit) -> void:
|
||||
code_edit.release_focus()
|
||||
|
||||
func _on_log_code_edit_text_changed(code_edit: CodeEdit, original_text: String) -> void:
|
||||
if code_edit.text != original_text:
|
||||
code_edit.text = original_text
|
||||
|
||||
func get_display_text_for_entry(entry: Dictionary) -> String:
|
||||
var count = entry.get("count", 1)
|
||||
var message = entry.message
|
||||
|
||||
var base_text = ""
|
||||
if message is Dictionary and message.has("parts"):
|
||||
var parts = message.parts
|
||||
var text_parts = []
|
||||
for part in parts:
|
||||
if part.type == "primitive":
|
||||
text_parts.append(str(part.data))
|
||||
elif part.type == "table":
|
||||
var key_count = part.data.keys().size()
|
||||
text_parts.append("Object {" + str(key_count) + "}")
|
||||
base_text = "\t".join(text_parts)
|
||||
else:
|
||||
base_text = str(message)
|
||||
|
||||
if count > 1:
|
||||
return "(" + str(count) + ") " + base_text
|
||||
else:
|
||||
return base_text
|
||||
|
||||
func create_structured_log_item(entry) -> Control:
|
||||
var container = VBoxContainer.new()
|
||||
var parts = entry.message.parts
|
||||
if parts.size() == 1 and parts[0].type == "primitive":
|
||||
var simple_panel = PanelContainer.new()
|
||||
var style_box = StyleBoxFlat.new()
|
||||
style_box.bg_color = Color.TRANSPARENT
|
||||
style_box.content_margin_left = 10
|
||||
style_box.content_margin_top = 5
|
||||
style_box.content_margin_right = 10
|
||||
style_box.content_margin_bottom = 5
|
||||
simple_panel.add_theme_stylebox_override("panel", style_box)
|
||||
var label = Label.new()
|
||||
label.text = str(parts[0].data)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
simple_panel.add_child(label)
|
||||
container.add_child(simple_panel)
|
||||
return container
|
||||
|
||||
for i in range(parts.size()):
|
||||
var part = parts[i]
|
||||
|
||||
if part.type == "primitive":
|
||||
var text_panel = PanelContainer.new()
|
||||
var style_box = StyleBoxFlat.new()
|
||||
style_box.bg_color = Color.TRANSPARENT
|
||||
style_box.content_margin_left = 10
|
||||
style_box.content_margin_top = 2
|
||||
style_box.content_margin_right = 10
|
||||
style_box.content_margin_bottom = 2
|
||||
text_panel.add_theme_stylebox_override("panel", style_box)
|
||||
|
||||
var label = Label.new()
|
||||
label.text = str(part.data)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
text_panel.add_child(label)
|
||||
container.add_child(text_panel)
|
||||
|
||||
elif part.type == "table":
|
||||
var table_item = create_expandable_table_item(part.data)
|
||||
container.add_child(table_item)
|
||||
|
||||
return container
|
||||
|
||||
func create_expandable_table_item(table_data) -> Control:
|
||||
var main_container = VBoxContainer.new()
|
||||
|
||||
var header_container = HBoxContainer.new()
|
||||
header_container.custom_minimum_size.y = 24
|
||||
|
||||
var chevron_button = Button.new()
|
||||
chevron_button.text = "▶"
|
||||
chevron_button.custom_minimum_size = Vector2(20, 20)
|
||||
chevron_button.flat = true
|
||||
chevron_button.focus_mode = Control.FOCUS_NONE
|
||||
header_container.add_child(chevron_button)
|
||||
|
||||
var summary_label = Label.new()
|
||||
var key_count = table_data.keys().size()
|
||||
if table_data.has("__type"):
|
||||
summary_label.text = str(table_data.get("__type", "Object")) + " {" + str(key_count) + "}"
|
||||
else:
|
||||
summary_label.text = "Object {" + str(key_count) + "}"
|
||||
summary_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
summary_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header_container.add_child(summary_label)
|
||||
|
||||
main_container.add_child(header_container)
|
||||
|
||||
var content_container = VBoxContainer.new()
|
||||
content_container.visible = false
|
||||
|
||||
for key in table_data.keys():
|
||||
if key == "__type":
|
||||
continue
|
||||
|
||||
var value = table_data[key]
|
||||
var row_container = HBoxContainer.new()
|
||||
|
||||
var key_label = Label.new()
|
||||
key_label.text = str(key) + ": "
|
||||
key_label.custom_minimum_size.x = 80
|
||||
key_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
row_container.add_child(key_label)
|
||||
|
||||
var value_label = Label.new()
|
||||
if value is Dictionary:
|
||||
var nested_count = value.keys().size()
|
||||
value_label.text = "Object {" + str(nested_count) + "}"
|
||||
value_label.modulate = Color.GRAY
|
||||
elif value is Array:
|
||||
value_label.text = "Array [" + str(value.size()) + "]"
|
||||
value_label.modulate = Color.GRAY
|
||||
else:
|
||||
value_label.text = str(value)
|
||||
|
||||
value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row_container.add_child(value_label)
|
||||
|
||||
content_container.add_child(row_container)
|
||||
|
||||
main_container.add_child(content_container)
|
||||
|
||||
chevron_button.pressed.connect(func():
|
||||
content_container.visible = !content_container.visible
|
||||
chevron_button.text = "▼" if content_container.visible else "▶"
|
||||
)
|
||||
|
||||
return main_container
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if visible:
|
||||
input_line.grab_focus()
|
||||
1
flumi/Scripts/Browser/DevToolsConsole.gd.uid
Normal file
1
flumi/Scripts/Browser/DevToolsConsole.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vrobqac6makc
|
||||
112
flumi/Scripts/Browser/GurtProtocol.gd
Normal file
112
flumi/Scripts/Browser/GurtProtocol.gd
Normal file
@@ -0,0 +1,112 @@
|
||||
extends RefCounted
|
||||
class_name GurtProtocol
|
||||
|
||||
const DNS_SERVER_IP: String = "135.125.163.131"
|
||||
const DNS_SERVER_PORT: int = 4878
|
||||
|
||||
static func is_gurt_domain(url: String) -> bool:
|
||||
if url.begins_with("gurt://"):
|
||||
return true
|
||||
|
||||
if not url.contains("://"):
|
||||
# Extract just the domain part (before any path)
|
||||
var domain = url.split("/")[0]
|
||||
var parts = domain.split(".")
|
||||
return parts.size() == 2
|
||||
|
||||
return false
|
||||
|
||||
static func is_direct_address(domain: String) -> bool:
|
||||
# Check if it's already an IP address or localhost
|
||||
if domain.contains(":"):
|
||||
var parts = domain.split(":")
|
||||
domain = parts[0]
|
||||
|
||||
return domain == "localhost" or domain == "127.0.0.1" or is_ip_address(domain)
|
||||
|
||||
static func is_ip_address(address: String) -> bool:
|
||||
var parts = address.split(".")
|
||||
if parts.size() != 4:
|
||||
return false
|
||||
|
||||
for part in parts:
|
||||
if not part.is_valid_int():
|
||||
return false
|
||||
var num = part.to_int()
|
||||
if num < 0 or num > 255:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
static func resolve_gurt_domain(domain: String) -> String:
|
||||
if is_direct_address(domain):
|
||||
if domain == "localhost":
|
||||
return "127.0.0.1"
|
||||
return domain
|
||||
|
||||
return domain
|
||||
|
||||
static func get_error_type(error_message: String) -> Dictionary:
|
||||
if "DNS server is not responding" in error_message or "Domain not found" in error_message:
|
||||
return {"code": "ERR_NAME_NOT_RESOLVED", "title": "This site can't be reached", "icon": "? :("}
|
||||
elif "timeout" in error_message.to_lower() or "timed out" in error_message.to_lower():
|
||||
return {"code": "ERR_CONNECTION_TIMED_OUT", "title": "This site can't be reached", "icon": "...?"}
|
||||
elif "Failed to fetch" in error_message or "No response" in error_message:
|
||||
return {"code": "ERR_CONNECTION_REFUSED", "title": "This site can't be reached", "icon": ">:("}
|
||||
elif "Invalid domain format" in error_message:
|
||||
return {"code": "ERR_INVALID_URL", "title": "This page isn't working", "icon": ":|"}
|
||||
else:
|
||||
return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": ">_<"}
|
||||
|
||||
static func create_error_page(error_message: String) -> PackedByteArray:
|
||||
var error_info = get_error_type(error_message)
|
||||
|
||||
return ("""<head>
|
||||
<title>""" + error_info.title + """ - GURT</title>
|
||||
<meta name="theme-color" content="#f8f9fa">
|
||||
<style>
|
||||
body { bg-[#ffffff] text-[#202124] font-sans p-0 m-0 }
|
||||
.error-container { flex flex-col items-center justify-center max-w-[600px] mx-auto px-6 text-center }
|
||||
.error-icon { text-6xl mb-6 opacity-60 w-32 h-32 }
|
||||
.error-title { text-[#202124] text-2xl font-normal mb-4 line-height-1.3 }
|
||||
.error-subtitle { text-[#5f6368] text-base mb-6 line-height-1.4 }
|
||||
.error-code { bg-[#f8f9fa] text-[#5f6368] px-3 py-2 rounded-md font-mono text-sm inline-block mb-6 }
|
||||
.suggestions { text-left max-w-[400px] w-[500px] }
|
||||
.suggestion-title { text-[#202124] text-lg font-normal mb-3 }
|
||||
.suggestion-list { text-[#5f6368] text-sm line-height-1.6 }
|
||||
.suggestion-item { mb-2 pl-4 relative }
|
||||
.suggestion-item:before { content-"•" absolute left-0 top-0 text-[#5f6368] }
|
||||
.retry-button { bg-[#1a73e8] text-[#ffffff] px-6 py-3 rounded-md font-medium text-sm hover:bg-[#1557b0] active:bg-[#1246a0] cursor-pointer border-none mt-4 }
|
||||
.details-section { mt-8 pt-6 border-t border-[#e8eaed] }
|
||||
.details-toggle { text-[#1a73e8] text-sm cursor-pointer hover:underline }
|
||||
.details-content { bg-[#f8f9fa] text-[#5f6368] text-xs font-mono p-4 rounded-md mt-3 text-left display-none }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
gurt.select("#reload"):on("click", function()
|
||||
gurt.location.reload()
|
||||
end)
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="error-container">
|
||||
<p style="error-icon">""" + error_info.icon + """</p>
|
||||
|
||||
<h1 style="error-title">""" + error_info.title + """</h1>
|
||||
|
||||
<p style="error-subtitle">""" + error_message + """</p>
|
||||
|
||||
<div style="error-code">""" + error_info.code + """</div>
|
||||
|
||||
<div style="suggestions">
|
||||
<h2 style="suggestion-title">Try:</h2>
|
||||
<ul style="suggestion-list">
|
||||
<li style="suggestion-item">Checking if the domain is correctly registered</li>
|
||||
<li style="suggestion-item">Verifying your DNS server is running</li>
|
||||
<li style="suggestion-item">Checking your internet connection</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button style="retry-button" id="reload">Reload</button>
|
||||
</div>
|
||||
</body>""").to_utf8_buffer()
|
||||
1
flumi/Scripts/Browser/GurtProtocol.gd.uid
Normal file
1
flumi/Scripts/Browser/GurtProtocol.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://clhivwjs3eujk
|
||||
189
flumi/Scripts/Browser/LuaSyntaxHighlighter.gd
Normal file
189
flumi/Scripts/Browser/LuaSyntaxHighlighter.gd
Normal file
@@ -0,0 +1,189 @@
|
||||
@tool
|
||||
class_name LuaSyntaxHighlighter
|
||||
extends SyntaxHighlighter
|
||||
|
||||
@export_group("Colors")
|
||||
@export var font_color: Color = Color.from_string("#d4d4d4", Color.WHITE)
|
||||
@export var keyword_color: Color = Color.from_string("#c586c0", Color.WHITE)
|
||||
@export var gurt_globals_color: Color = Color.from_string("#569cd6", Color.WHITE)
|
||||
@export var function_color: Color = Color.from_string("#dcdcaa", Color.WHITE)
|
||||
@export var member_color: Color = Color.from_string("#9cdcfe", Color.WHITE)
|
||||
@export var number_color: Color = Color.from_string("#b5cea8", Color.WHITE)
|
||||
@export var string_color: Color = Color.from_string("#ce9178", Color.WHITE)
|
||||
@export var comment_color: Color = Color.from_string("#6a9955", Color.WHITE)
|
||||
@export var symbol_color: Color = Color.from_string("#c586c0", Color.WHITE)
|
||||
|
||||
enum State { DEFAULT, IN_MULTILINE_COMMENT, IN_MULTILINE_STRING }
|
||||
var _state_cache: Dictionary = {}
|
||||
|
||||
var _keywords: Dictionary = {}
|
||||
var _built_in_functions: Dictionary = {}
|
||||
var _gurt_globals: Dictionary = {}
|
||||
var _member_keywords: Dictionary = {}
|
||||
|
||||
func _init() -> void:
|
||||
for k in ["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"]:
|
||||
_keywords[k] = true
|
||||
|
||||
for f in ["assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber", "tostring", "type", "xpcall"]:
|
||||
_built_in_functions[f] = true
|
||||
|
||||
for g in ["gurt", "trace", "JSON", "Time", "WebSocket", "Clipboard", "Regex", "setTimeout", "setInterval", "clearTimeout", "clearInterval", "fetch", "urlEncode", "urlDecode"]:
|
||||
_gurt_globals[g] = true
|
||||
|
||||
var members = [
|
||||
"select", "selectAll", "create", "body", "location", "href", "reload", "goto", "query", "get", "has", "getAll",
|
||||
"log", "warn", "error", "text", "value", "visible", "children", "parent", "nextSibling", "previousSibling",
|
||||
"firstChild", "lastChild", "classList", "on", "append", "remove", "insertBefore", "insertAfter", "replace",
|
||||
"clone", "getAttribute", "setAttribute", "show", "hide", "focus", "unfocus", "createTween", "unsubscribe",
|
||||
"add", "remove", "toggle", "contains", "item", "length", "to", "duration", "easing", "transition", "play",
|
||||
"pause", "stop", "currentTime", "volume", "loop", "src", "playing", "paused", "withContext", "fillRect",
|
||||
"strokeRect", "clearRect", "drawCircle", "drawText", "setFont", "measureText", "beginPath", "moveTo",
|
||||
"lineTo", "closePath", "stroke", "fill", "arc", "quadraticCurveTo", "bezierCurveTo", "setStrokeStyle",
|
||||
"setFillStyle", "setLineWidth", "save", "translate", "rotate", "scale", "restore", "source", "ok", "json",
|
||||
"status", "statusText", "headers", "stringify", "parse", "now", "format", "date", "sleep", "benchmark",
|
||||
"timer", "delay", "elapsed", "reset", "complete", "remaining", "new", "send", "close", "readyState",
|
||||
"test", "match", "tostring", "replace", "replaceAll", "trim",
|
||||
]
|
||||
for m in members:
|
||||
_member_keywords[m] = true
|
||||
|
||||
func _is_whitespace(ch: String) -> bool:
|
||||
return ch == " " or ch == "\t"
|
||||
|
||||
func _clear_highlighting_cache():
|
||||
_state_cache.clear()
|
||||
|
||||
func _get_initial_state() -> int:
|
||||
return State.DEFAULT
|
||||
|
||||
func _get_line_state(p_line: int, p_state: int) -> int:
|
||||
var current_state: int = p_state
|
||||
var line_text: String = get_text_edit().get_line(p_line)
|
||||
var line_len: int = line_text.length()
|
||||
var i := 0
|
||||
while i < line_len:
|
||||
if current_state == State.DEFAULT:
|
||||
if i + 3 < line_len and line_text.substr(i, 4) == "--[[":
|
||||
current_state = State.IN_MULTILINE_COMMENT
|
||||
i += 4
|
||||
continue
|
||||
if i + 1 < line_len and line_text.substr(i, 2) == "[[":
|
||||
current_state = State.IN_MULTILINE_STRING
|
||||
i += 2
|
||||
continue
|
||||
if line_text[i] == "'" or line_text[i] == "\"":
|
||||
var quote = line_text[i]
|
||||
i += 1
|
||||
while i < line_len:
|
||||
if line_text[i] == "\\": i += 2; continue
|
||||
if line_text[i] == quote: break
|
||||
i += 1
|
||||
else:
|
||||
var end_idx = line_text.find("]]", i)
|
||||
if end_idx != -1:
|
||||
current_state = State.DEFAULT
|
||||
i = end_idx + 2
|
||||
continue
|
||||
else:
|
||||
i = line_len
|
||||
i += 1
|
||||
_state_cache[p_line] = current_state
|
||||
return current_state
|
||||
|
||||
func _get_line_syntax_highlighting(p_line: int) -> Dictionary:
|
||||
var color_map := {}
|
||||
var start_state: int = _state_cache.get(p_line - 1, _get_initial_state())
|
||||
|
||||
var line_text: String = get_text_edit().get_line(p_line)
|
||||
var line_len: int = line_text.length()
|
||||
|
||||
color_map[0] = {"color": font_color}
|
||||
|
||||
var i := 0
|
||||
if start_state != State.DEFAULT:
|
||||
var end_idx = line_text.find("]]")
|
||||
var region_color = comment_color if start_state == State.IN_MULTILINE_COMMENT else string_color
|
||||
|
||||
color_map[0] = {"color": region_color}
|
||||
if end_idx == -1:
|
||||
return color_map
|
||||
else:
|
||||
i = end_idx + 2
|
||||
if i < line_len:
|
||||
color_map[i] = {"color": font_color}
|
||||
start_state = State.DEFAULT
|
||||
while i < line_len:
|
||||
var start_col: int = i
|
||||
var current_char: String = line_text[i]
|
||||
|
||||
if current_char == "-" and i + 1 < line_len and line_text[i+1] == "-":
|
||||
if not (i + 3 < line_len and line_text.substr(i, 4) == "--[["):
|
||||
color_map[i] = {"color": comment_color}
|
||||
return color_map
|
||||
|
||||
if current_char == "\"" or current_char == "'":
|
||||
var quote = current_char
|
||||
var string_start = i
|
||||
i += 1
|
||||
while i < line_len:
|
||||
if line_text[i] == "\\": i += 2; continue
|
||||
if line_text[i] == quote: i += 1; break
|
||||
i += 1
|
||||
var string_end = i
|
||||
color_map[string_start] = {"color": string_color}
|
||||
if string_end < line_len:
|
||||
color_map[string_end] = {"color": font_color}
|
||||
continue
|
||||
|
||||
if current_char.is_valid_int() or (current_char == "." and i + 1 < line_len and line_text[i+1].is_valid_int()):
|
||||
var is_hex = false
|
||||
if current_char == "0" and i + 1 < line_len and line_text[i+1].to_lower() == "x":
|
||||
i += 2; is_hex = true
|
||||
while i < line_len:
|
||||
var ch = line_text[i]
|
||||
if (is_hex and ch.is_valid_hex_number(false)) or \
|
||||
(not is_hex and (ch.is_valid_int() or ch in "Ee.-+")):
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
var number_end = i
|
||||
color_map[start_col] = {"color": number_color}
|
||||
if number_end < line_len:
|
||||
color_map[number_end] = {"color": font_color}
|
||||
continue
|
||||
|
||||
if current_char.is_valid_identifier() and not current_char.is_valid_int():
|
||||
while i < line_len and line_text[i].is_valid_identifier():
|
||||
i += 1
|
||||
var word = line_text.substr(start_col, i - start_col)
|
||||
|
||||
var color = font_color
|
||||
if _keywords.has(word): color = keyword_color
|
||||
elif _gurt_globals.has(word): color = gurt_globals_color
|
||||
elif _built_in_functions.has(word): color = function_color
|
||||
else:
|
||||
var prev_char_idx = start_col - 1
|
||||
while prev_char_idx >= 0 and _is_whitespace(line_text[prev_char_idx]):
|
||||
prev_char_idx -= 1
|
||||
var next_char_idx = i
|
||||
while next_char_idx < line_len and _is_whitespace(line_text[next_char_idx]):
|
||||
next_char_idx += 1
|
||||
|
||||
var is_member = prev_char_idx >= 0 and line_text[prev_char_idx] in [".", ":"]
|
||||
var is_function_call = next_char_idx < line_len and line_text[next_char_idx] == "("
|
||||
|
||||
if is_member and _member_keywords.has(word): color = member_color
|
||||
elif is_function_call: color = function_color
|
||||
|
||||
if color != font_color: color_map[start_col] = {"color": color}
|
||||
continue
|
||||
|
||||
if not _is_whitespace(current_char):
|
||||
color_map[i] = {"color": symbol_color}
|
||||
if i + 1 < line_len:
|
||||
color_map[i + 1] = {"color": font_color}
|
||||
|
||||
i += 1
|
||||
|
||||
return color_map
|
||||
1
flumi/Scripts/Browser/LuaSyntaxHighlighter.gd.uid
Normal file
1
flumi/Scripts/Browser/LuaSyntaxHighlighter.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://qicpfnrmje1v
|
||||
124
flumi/Scripts/Browser/NetworkManager.gd
Normal file
124
flumi/Scripts/Browser/NetworkManager.gd
Normal file
@@ -0,0 +1,124 @@
|
||||
extends Node
|
||||
|
||||
signal request_started(request: NetworkRequest)
|
||||
signal request_completed(request: NetworkRequest)
|
||||
signal request_failed(request: NetworkRequest)
|
||||
|
||||
var active_requests: Dictionary = {} # request_id -> NetworkRequest
|
||||
var all_requests: Array[NetworkRequest] = []
|
||||
var dev_tools_network_tab: NetworkTab = null
|
||||
|
||||
func register_dev_tools_network_tab(network_tab: NetworkTab):
|
||||
dev_tools_network_tab = network_tab
|
||||
|
||||
func start_request(url: String, method: String = "GET", is_from_lua: bool = false) -> NetworkRequest:
|
||||
var request = NetworkRequest.new(url, method)
|
||||
request.is_from_lua = is_from_lua
|
||||
|
||||
active_requests[request.id] = request
|
||||
all_requests.append(request)
|
||||
|
||||
# Notify dev tools
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.add_network_request(request)
|
||||
|
||||
request_started.emit(request)
|
||||
return request
|
||||
|
||||
func complete_request(request_id: String, status_code: int, status_text: String, headers: Dictionary, body: String, body_bytes: PackedByteArray = []):
|
||||
var request = active_requests.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
request.set_response(status_code, status_text, headers, body, body_bytes)
|
||||
active_requests.erase(request_id)
|
||||
|
||||
# Update dev tools UI
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.update_request_item(request)
|
||||
|
||||
if request.status == NetworkRequest.RequestStatus.SUCCESS:
|
||||
request_completed.emit(request)
|
||||
else:
|
||||
request_failed.emit(request)
|
||||
|
||||
func fail_request(request_id: String, error_message: String):
|
||||
var request = active_requests.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
request.set_error(error_message)
|
||||
active_requests.erase(request_id)
|
||||
|
||||
# Update dev tools UI
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.update_request_item(request)
|
||||
|
||||
request_failed.emit(request)
|
||||
|
||||
func set_request_headers(request_id: String, headers: Dictionary):
|
||||
var request = active_requests.get(request_id)
|
||||
if request:
|
||||
request.request_headers = headers
|
||||
|
||||
func set_request_body(request_id: String, body: String):
|
||||
var request = active_requests.get(request_id)
|
||||
if request:
|
||||
request.request_body = body
|
||||
|
||||
func get_all_requests() -> Array[NetworkRequest]:
|
||||
return all_requests
|
||||
|
||||
func clear_all_requests():
|
||||
active_requests.clear()
|
||||
all_requests.clear()
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.clear_all_requests()
|
||||
|
||||
func clear_all_requests_except(preserve_request_id: String):
|
||||
# Remove from active_requests but preserve specific request
|
||||
var preserved_active = null
|
||||
if active_requests.has(preserve_request_id):
|
||||
preserved_active = active_requests[preserve_request_id]
|
||||
|
||||
active_requests.clear()
|
||||
if preserved_active:
|
||||
active_requests[preserve_request_id] = preserved_active
|
||||
|
||||
# Remove from all_requests but preserve specific request
|
||||
var preserved_request = null
|
||||
for request in all_requests:
|
||||
if request.id == preserve_request_id:
|
||||
preserved_request = request
|
||||
break
|
||||
|
||||
all_requests.clear()
|
||||
if preserved_request:
|
||||
all_requests.append(preserved_request)
|
||||
|
||||
if dev_tools_network_tab:
|
||||
dev_tools_network_tab.clear_all_requests_except(preserve_request_id)
|
||||
|
||||
func get_request_stats() -> Dictionary:
|
||||
var total_requests = all_requests.size()
|
||||
var total_size = 0
|
||||
var successful_requests = 0
|
||||
var failed_requests = 0
|
||||
var pending_requests = active_requests.size()
|
||||
|
||||
for request in all_requests:
|
||||
total_size += request.size
|
||||
match request.status:
|
||||
NetworkRequest.RequestStatus.SUCCESS:
|
||||
successful_requests += 1
|
||||
NetworkRequest.RequestStatus.ERROR:
|
||||
failed_requests += 1
|
||||
|
||||
return {
|
||||
"total": total_requests,
|
||||
"successful": successful_requests,
|
||||
"failed": failed_requests,
|
||||
"pending": pending_requests,
|
||||
"total_size": total_size
|
||||
}
|
||||
1
flumi/Scripts/Browser/NetworkManager.gd.uid
Normal file
1
flumi/Scripts/Browser/NetworkManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ggm1sq7h64sr
|
||||
207
flumi/Scripts/Browser/NetworkRequest.gd
Normal file
207
flumi/Scripts/Browser/NetworkRequest.gd
Normal file
@@ -0,0 +1,207 @@
|
||||
class_name NetworkRequest
|
||||
extends RefCounted
|
||||
|
||||
enum RequestType {
|
||||
FETCH,
|
||||
DOC,
|
||||
CSS,
|
||||
LUA,
|
||||
FONT,
|
||||
IMG,
|
||||
SOCKET,
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum RequestStatus {
|
||||
PENDING,
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
var id: String
|
||||
var name: String
|
||||
var url: String
|
||||
var method: String
|
||||
var type: RequestType
|
||||
var status: RequestStatus
|
||||
var status_code: int
|
||||
var status_text: String
|
||||
var size: int
|
||||
var time_ms: float
|
||||
var start_time: float
|
||||
var end_time: float
|
||||
|
||||
var request_headers: Dictionary = {}
|
||||
var response_headers: Dictionary = {}
|
||||
|
||||
var request_body: String = ""
|
||||
var response_body: String = ""
|
||||
var response_body_bytes: PackedByteArray = []
|
||||
|
||||
var mime_type: String = ""
|
||||
var is_from_lua: bool = false
|
||||
|
||||
func _init(request_url: String = "", request_method: String = "GET"):
|
||||
id = generate_id()
|
||||
url = request_url
|
||||
method = request_method.to_upper()
|
||||
name = extract_name_from_url(url)
|
||||
type = determine_type_from_url(url)
|
||||
status = RequestStatus.PENDING
|
||||
status_code = 0
|
||||
status_text = ""
|
||||
size = 0
|
||||
time_ms = 0.0
|
||||
start_time = Time.get_ticks_msec()
|
||||
end_time = 0.0
|
||||
|
||||
func generate_id() -> String:
|
||||
return str(Time.get_ticks_msec()) + "_" + str(randi())
|
||||
|
||||
func extract_name_from_url(request_url: String) -> String:
|
||||
if request_url.is_empty():
|
||||
return "Unknown"
|
||||
|
||||
var parts = request_url.split("/")
|
||||
if parts.size() > 0:
|
||||
var filename = parts[-1]
|
||||
if filename.is_empty() and parts.size() > 1:
|
||||
filename = parts[-2]
|
||||
if "?" in filename:
|
||||
filename = filename.split("?")[0]
|
||||
if "#" in filename:
|
||||
filename = filename.split("#")[0]
|
||||
return filename if not filename.is_empty() else "/"
|
||||
|
||||
return request_url
|
||||
|
||||
func determine_type_from_url(request_url: String) -> RequestType:
|
||||
var lower_url = request_url.to_lower()
|
||||
|
||||
if lower_url.ends_with(".html") or lower_url.ends_with(".htm"):
|
||||
return RequestType.DOC
|
||||
elif lower_url.ends_with(".css"):
|
||||
return RequestType.CSS
|
||||
elif lower_url.ends_with(".lua") or lower_url.ends_with(".luau"):
|
||||
return RequestType.LUA
|
||||
elif lower_url.ends_with(".woff") or lower_url.ends_with(".woff2") or lower_url.ends_with(".ttf") or lower_url.ends_with(".otf"):
|
||||
return RequestType.FONT
|
||||
elif lower_url.ends_with(".png") or lower_url.ends_with(".jpg") or lower_url.ends_with(".jpeg") or lower_url.ends_with(".gif") or lower_url.ends_with(".webp") or lower_url.ends_with(".svg") or lower_url.ends_with(".bmp"):
|
||||
return RequestType.IMG
|
||||
elif lower_url.begins_with("ws://") or lower_url.begins_with("wss://"):
|
||||
return RequestType.SOCKET
|
||||
|
||||
if not mime_type.is_empty():
|
||||
var lower_mime = mime_type.to_lower()
|
||||
if lower_mime.begins_with("text/html"):
|
||||
return RequestType.DOC
|
||||
elif lower_mime.begins_with("text/css"):
|
||||
return RequestType.CSS
|
||||
elif lower_mime.begins_with("image/"):
|
||||
return RequestType.IMG
|
||||
elif lower_mime.begins_with("font/") or lower_mime == "application/font-woff" or lower_mime == "application/font-woff2":
|
||||
return RequestType.FONT
|
||||
|
||||
if is_from_lua:
|
||||
return RequestType.FETCH
|
||||
|
||||
return RequestType.OTHER
|
||||
|
||||
func set_response(response_status_code: int, response_status_text: String, response_headers_dict: Dictionary, response_body_content: String, body_bytes: PackedByteArray = []):
|
||||
end_time = Time.get_ticks_msec()
|
||||
time_ms = end_time - start_time
|
||||
|
||||
status_code = response_status_code
|
||||
status_text = response_status_text
|
||||
response_headers = response_headers_dict
|
||||
response_body = response_body_content
|
||||
response_body_bytes = body_bytes if not body_bytes.is_empty() else response_body_content.to_utf8_buffer()
|
||||
size = response_body_bytes.size()
|
||||
|
||||
for header_name in response_headers:
|
||||
if header_name.to_lower() == "content-type":
|
||||
mime_type = response_headers[header_name].split(";")[0].strip_edges()
|
||||
break
|
||||
|
||||
type = determine_type_from_url(url)
|
||||
|
||||
if response_status_code >= 200 and response_status_code < 300:
|
||||
status = RequestStatus.SUCCESS
|
||||
else:
|
||||
status = RequestStatus.ERROR
|
||||
|
||||
func set_error(error_message: String):
|
||||
end_time = Time.get_ticks_msec()
|
||||
time_ms = end_time - start_time
|
||||
status = RequestStatus.ERROR
|
||||
status_text = error_message
|
||||
|
||||
func get_status_display() -> String:
|
||||
match status:
|
||||
RequestStatus.PENDING:
|
||||
return "Pending"
|
||||
RequestStatus.SUCCESS:
|
||||
return str(status_code)
|
||||
RequestStatus.ERROR:
|
||||
return str(status_code) if status_code > 0 else "Failed"
|
||||
RequestStatus.CANCELLED:
|
||||
return "Cancelled"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
func get_type_display() -> String:
|
||||
match type:
|
||||
RequestType.FETCH:
|
||||
return "Fetch"
|
||||
RequestType.DOC:
|
||||
return "Doc"
|
||||
RequestType.CSS:
|
||||
return "CSS"
|
||||
RequestType.LUA:
|
||||
return "Lua"
|
||||
RequestType.FONT:
|
||||
return "Font"
|
||||
RequestType.IMG:
|
||||
return "Img"
|
||||
RequestType.SOCKET:
|
||||
return "Socket"
|
||||
RequestType.OTHER:
|
||||
return "Other"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
static func format_bytes(given_size: int) -> String:
|
||||
if given_size < 1024:
|
||||
return str(given_size) + " B"
|
||||
elif given_size < 1024 * 1024:
|
||||
return str(given_size / 1024) + " KB"
|
||||
else:
|
||||
return str(given_size / (1024 * 1024)) + " MB"
|
||||
|
||||
func get_time_display() -> String:
|
||||
if status == RequestStatus.PENDING:
|
||||
return "Pending"
|
||||
if time_ms < 1000:
|
||||
return str(int(time_ms)) + " ms"
|
||||
else:
|
||||
return "%.1f s" % (time_ms / 1000.0)
|
||||
|
||||
func get_icon_texture() -> Texture2D:
|
||||
match type:
|
||||
RequestType.FETCH:
|
||||
return load("res://Assets/Icons/download.svg")
|
||||
RequestType.DOC:
|
||||
return load("res://Assets/Icons/file-text.svg")
|
||||
RequestType.CSS:
|
||||
return load("res://Assets/Icons/palette.svg")
|
||||
RequestType.LUA:
|
||||
return load("res://Assets/Icons/braces.svg")
|
||||
RequestType.FONT:
|
||||
return load("res://Assets/Icons/braces.svg")
|
||||
RequestType.IMG:
|
||||
return load("res://Assets/Icons/image.svg")
|
||||
RequestType.SOCKET:
|
||||
return load("res://Assets/Icons/arrow-down-up.svg")
|
||||
_:
|
||||
return load("res://Assets/Icons/search.svg")
|
||||
1
flumi/Scripts/Browser/NetworkRequest.gd.uid
Normal file
1
flumi/Scripts/Browser/NetworkRequest.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://r6h4tvud6yne
|
||||
70
flumi/Scripts/Browser/NetworkRequestItem.gd
Normal file
70
flumi/Scripts/Browser/NetworkRequestItem.gd
Normal file
@@ -0,0 +1,70 @@
|
||||
class_name NetworkRequestItem
|
||||
extends PanelContainer
|
||||
|
||||
@onready var icon: TextureRect = $HBoxContainer/IconContainer/Icon
|
||||
@onready var name_label: Label = $HBoxContainer/NameLabel
|
||||
@onready var status_label: Label = $HBoxContainer/StatusLabel
|
||||
@onready var type_label: Label = $HBoxContainer/TypeLabel
|
||||
@onready var size_label: Label = $HBoxContainer/SizeLabel
|
||||
@onready var time_label: Label = $HBoxContainer/TimeLabel
|
||||
|
||||
var request: NetworkRequest
|
||||
var network_tab: NetworkTab
|
||||
|
||||
@onready var normal_style: StyleBox = get_meta("normal_style")
|
||||
@onready var selected_style: StyleBox = get_meta("selected_style")
|
||||
|
||||
@onready var success_color: Color = status_label.get_meta("success_color")
|
||||
@onready var error_color: Color = status_label.get_meta("error_color")
|
||||
@onready var pending_color: Color = status_label.get_meta("pending_color")
|
||||
|
||||
signal item_clicked(request: NetworkRequest)
|
||||
|
||||
func _ready():
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
gui_input.connect(_on_gui_input)
|
||||
|
||||
func init(network_request: NetworkRequest, parent_tab: NetworkTab):
|
||||
request = network_request
|
||||
network_tab = parent_tab
|
||||
update_display()
|
||||
|
||||
func update_display():
|
||||
if not request:
|
||||
return
|
||||
|
||||
# Update icon
|
||||
icon.texture = request.get_icon_texture()
|
||||
|
||||
# Update labels
|
||||
name_label.text = request.name
|
||||
status_label.text = request.get_status_display()
|
||||
type_label.text = request.get_type_display()
|
||||
size_label.text = NetworkRequest.format_bytes(request.size)
|
||||
time_label.text = request.get_time_display()
|
||||
|
||||
# Color code status
|
||||
match request.status:
|
||||
NetworkRequest.RequestStatus.SUCCESS:
|
||||
status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
NetworkRequest.RequestStatus.ERROR:
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
NetworkRequest.RequestStatus.PENDING:
|
||||
status_label.add_theme_color_override("font_color", Color.YELLOW)
|
||||
|
||||
func _on_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
item_clicked.emit(request)
|
||||
|
||||
func set_selected(selected: bool):
|
||||
if selected:
|
||||
add_theme_stylebox_override("panel", selected_style)
|
||||
else:
|
||||
add_theme_stylebox_override("panel", normal_style)
|
||||
|
||||
func hide_columns(should_hide: bool):
|
||||
status_label.visible = !should_hide
|
||||
type_label.visible = !should_hide
|
||||
size_label.visible = !should_hide
|
||||
time_label.visible = !should_hide
|
||||
1
flumi/Scripts/Browser/NetworkRequestItem.gd.uid
Normal file
1
flumi/Scripts/Browser/NetworkRequestItem.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bcs7m624uvv3x
|
||||
398
flumi/Scripts/Browser/NetworkTab.gd
Normal file
398
flumi/Scripts/Browser/NetworkTab.gd
Normal file
@@ -0,0 +1,398 @@
|
||||
class_name NetworkTab
|
||||
extends VBoxContainer
|
||||
|
||||
const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn")
|
||||
|
||||
@onready var filter_dropdown: OptionButton = %FilterDropdown
|
||||
|
||||
# Details panel components
|
||||
@onready var close_button: Button = %CloseButton
|
||||
@onready var details_tab_container: TabContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer
|
||||
@onready var headers_tab: VBoxContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Headers
|
||||
@onready var preview_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Preview
|
||||
@onready var response_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Response
|
||||
|
||||
# Header components
|
||||
@onready var status_header: Label = %StatusHeader
|
||||
@onready var type_header: Label = %TypeHeader
|
||||
@onready var size_header: Label = %SizeHeader
|
||||
@onready var time_header: Label = %TimeHeader
|
||||
|
||||
# Main components
|
||||
@onready var main_container: HSplitContainer = $MainContainer
|
||||
@onready var request_list: VBoxContainer = $MainContainer/LeftPanel/ScrollContainer/RequestList
|
||||
@onready var scroll_container: ScrollContainer = $MainContainer/LeftPanel/ScrollContainer
|
||||
@onready var details_panel: Control = $MainContainer/RightPanel
|
||||
@onready var status_bar: HBoxContainer = $HBoxContainer/StatusBar
|
||||
@onready var request_count_label: Label = $HBoxContainer/StatusBar/RequestCount
|
||||
@onready var transfer_label: Label = $HBoxContainer/StatusBar/Transfer
|
||||
@onready var loaded_label: Label = $HBoxContainer/StatusBar/Loaded
|
||||
|
||||
@onready var syntax_highlighter = preload("res://Resources/LuaSyntaxHighlighter.tres")
|
||||
|
||||
var network_requests: Array[NetworkRequest] = []
|
||||
var current_filter: NetworkRequest.RequestType = -1 # -1 means all
|
||||
var selected_request: NetworkRequest = null
|
||||
var request_items: Dictionary = {}
|
||||
|
||||
signal request_selected(request: NetworkRequest)
|
||||
|
||||
func _ready():
|
||||
details_panel.visible = false
|
||||
|
||||
if main_container and main_container.size.x > 0:
|
||||
main_container.split_offset = int(main_container.size.x)
|
||||
|
||||
update_status_bar()
|
||||
|
||||
NetworkManager.register_dev_tools_network_tab(self)
|
||||
|
||||
func add_network_request(request: NetworkRequest):
|
||||
network_requests.append(request)
|
||||
|
||||
var request_item = NetworkRequestItemScene.instantiate() as NetworkRequestItem
|
||||
request_list.add_child(request_item)
|
||||
request_item.init(request, self)
|
||||
request_item.item_clicked.connect(_on_request_item_clicked)
|
||||
|
||||
request_items[request.id] = request_item
|
||||
|
||||
apply_filter()
|
||||
update_status_bar()
|
||||
|
||||
func apply_filter():
|
||||
for request in network_requests:
|
||||
var item = request_items.get(request.id)
|
||||
if item:
|
||||
var should_show = (current_filter == -1) or (request.type == current_filter)
|
||||
item.visible = should_show
|
||||
|
||||
func update_request_item(request: NetworkRequest):
|
||||
var request_item = request_items.get(request.id) as NetworkRequestItem
|
||||
if not request_item:
|
||||
return
|
||||
|
||||
request_item.update_display()
|
||||
|
||||
apply_filter()
|
||||
update_status_bar()
|
||||
|
||||
func update_details_panel(request: NetworkRequest):
|
||||
clear_details_panel()
|
||||
update_headers_tab(request)
|
||||
update_preview_tab(request)
|
||||
update_response_tab(request)
|
||||
|
||||
func clear_details_panel():
|
||||
for child in headers_tab.get_children(): child.queue_free()
|
||||
for child in preview_tab.get_children(): child.queue_free()
|
||||
for child in response_tab.get_children(): child.queue_free()
|
||||
|
||||
func create_collapsible_section(title: String, expanded: bool = false) -> VBoxContainer:
|
||||
var section = VBoxContainer.new()
|
||||
|
||||
# Header w/ toggle button
|
||||
var header = HBoxContainer.new()
|
||||
header.custom_minimum_size.y = 28
|
||||
|
||||
var toggle_button = Button.new()
|
||||
toggle_button.text = "▼" if expanded else "▶"
|
||||
toggle_button.custom_minimum_size = Vector2(20, 20)
|
||||
toggle_button.flat = true
|
||||
toggle_button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
header.add_child(toggle_button)
|
||||
|
||||
var title_label = Label.new()
|
||||
title_label.text = title
|
||||
title_label.add_theme_font_size_override("font_size", 14)
|
||||
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
header.add_child(title_label)
|
||||
|
||||
section.add_child(header)
|
||||
|
||||
# Content container
|
||||
var content = VBoxContainer.new()
|
||||
content.visible = expanded
|
||||
section.add_child(content)
|
||||
|
||||
toggle_button.pressed.connect(func():
|
||||
content.visible = !content.visible
|
||||
toggle_button.text = "▼" if content.visible else "▶"
|
||||
)
|
||||
|
||||
return section
|
||||
|
||||
func add_header_row(parent: VBoxContainer, header_name: String, value: String):
|
||||
var row = HBoxContainer.new()
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = header_name
|
||||
name_label.custom_minimum_size.x = 200
|
||||
name_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
name_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
name_label.clip_text = true
|
||||
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
row.add_child(name_label)
|
||||
|
||||
var value_label = Label.new()
|
||||
value_label.text = value
|
||||
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
row.add_child(value_label)
|
||||
|
||||
parent.add_child(row)
|
||||
|
||||
func update_headers_tab(request: NetworkRequest):
|
||||
var general_section = create_collapsible_section("General", true)
|
||||
headers_tab.add_child(general_section)
|
||||
|
||||
var general_content = general_section.get_child(1)
|
||||
add_header_row(general_content, "Request URL:", request.url)
|
||||
add_header_row(general_content, "Request Method:", request.method)
|
||||
add_header_row(general_content, "Status Code:", str(request.status_code) + " " + request.status_text)
|
||||
|
||||
# Request Headers section
|
||||
if not request.request_headers.is_empty():
|
||||
var request_headers_section = create_collapsible_section("Request Headers", false)
|
||||
headers_tab.add_child(request_headers_section)
|
||||
|
||||
var request_headers_content = request_headers_section.get_child(1)
|
||||
for header_name in request.request_headers:
|
||||
add_header_row(request_headers_content, header_name + ":", str(request.request_headers[header_name]))
|
||||
|
||||
# Response Headers section
|
||||
if not request.response_headers.is_empty():
|
||||
var response_headers_section = create_collapsible_section("Response Headers", false)
|
||||
headers_tab.add_child(response_headers_section)
|
||||
|
||||
var response_headers_content = response_headers_section.get_child(1)
|
||||
for header_name in request.response_headers:
|
||||
add_header_row(response_headers_content, header_name + ":", str(request.response_headers[header_name]))
|
||||
|
||||
func update_preview_tab(request: NetworkRequest):
|
||||
# For images, show the image in the preview tab
|
||||
if request.type == NetworkRequest.RequestType.IMG and request.status == NetworkRequest.RequestStatus.SUCCESS:
|
||||
var image = Image.new()
|
||||
var response_bytes = request.response_body_bytes
|
||||
var load_error = ERR_UNAVAILABLE
|
||||
|
||||
load_error = image.load_png_from_buffer(response_bytes)
|
||||
if load_error != OK:
|
||||
load_error = image.load_jpg_from_buffer(response_bytes)
|
||||
if load_error != OK:
|
||||
load_error = image.load_webp_from_buffer(response_bytes)
|
||||
if load_error != OK:
|
||||
load_error = image.load_bmp_from_buffer(response_bytes)
|
||||
if load_error != OK:
|
||||
load_error = image.load_tga_from_buffer(response_bytes)
|
||||
|
||||
if load_error == OK:
|
||||
var texture = ImageTexture.create_from_image(image)
|
||||
|
||||
var container = VBoxContainer.new()
|
||||
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
container.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
|
||||
var texture_rect = TextureRect.new()
|
||||
texture_rect.texture = texture
|
||||
texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
texture_rect.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
texture_rect.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
|
||||
var img_size = image.get_size()
|
||||
var max_size = 200.0
|
||||
var scale_factor = min(max_size / img_size.x, max_size / img_size.y, 1.0)
|
||||
texture_rect.custom_minimum_size = Vector2(img_size.x * scale_factor, img_size.y * scale_factor)
|
||||
|
||||
container.add_child(texture_rect)
|
||||
preview_tab.add_child(container)
|
||||
return
|
||||
else:
|
||||
var label = Label.new()
|
||||
label.text = "Failed to load image data (Error: " + str(load_error) + ")"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
preview_tab.add_child(label)
|
||||
return
|
||||
|
||||
# For non-images, show request body
|
||||
if request.request_body.is_empty():
|
||||
var label = Label.new()
|
||||
label.text = "No request body"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
preview_tab.add_child(label)
|
||||
return
|
||||
|
||||
# CodeEdit for request body
|
||||
# TODO: Syntax highlight based on Content-Type, we need a JSON, HTML and CSS highlighter too
|
||||
var code_edit = CodeEditUtils.create_code_edit({
|
||||
"text": request.request_body,
|
||||
"editable": false,
|
||||
"show_line_numbers": true,
|
||||
"syntax_highlighter": syntax_highlighter.duplicate()
|
||||
})
|
||||
|
||||
preview_tab.add_child(code_edit)
|
||||
|
||||
func update_response_tab(request: NetworkRequest):
|
||||
if request.type == NetworkRequest.RequestType.IMG:
|
||||
var label = Label.new()
|
||||
label.text = "This response contains image data. See the \"Preview\" tab to view the image."
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
response_tab.add_child(label)
|
||||
return
|
||||
|
||||
if request.response_body.is_empty():
|
||||
var label = Label.new()
|
||||
label.text = "No response body"
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
response_tab.add_child(label)
|
||||
return
|
||||
|
||||
# Check if we can display the content
|
||||
var can_display = true
|
||||
if request.mime_type.begins_with("video/") or request.mime_type.begins_with("audio/"):
|
||||
can_display = false
|
||||
|
||||
if not can_display:
|
||||
var label = Label.new()
|
||||
label.text = "Cannot preview this content type: " + request.mime_type
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
response_tab.add_child(label)
|
||||
return
|
||||
|
||||
# Create CodeEdit for response body
|
||||
var code_edit = CodeEditUtils.create_code_edit({
|
||||
"text": request.response_body,
|
||||
"editable": false,
|
||||
"show_line_numbers": true,
|
||||
"syntax_highlighter": syntax_highlighter.duplicate()
|
||||
})
|
||||
|
||||
response_tab.add_child(code_edit)
|
||||
|
||||
func update_status_bar():
|
||||
var total_requests = network_requests.size()
|
||||
var total_size = 0
|
||||
var loaded_resources = 0
|
||||
|
||||
for request in network_requests:
|
||||
total_size += request.size
|
||||
if request.status == NetworkRequest.RequestStatus.SUCCESS:
|
||||
loaded_resources += 1
|
||||
|
||||
request_count_label.text = str(total_requests) + " requests"
|
||||
transfer_label.text = NetworkRequest.format_bytes(total_size) + " transferred"
|
||||
loaded_label.text = str(loaded_resources) + " resources loaded"
|
||||
|
||||
func hide_details_panel():
|
||||
# Hide details panel and show columns again
|
||||
details_panel.visible = false
|
||||
hide_columns(false)
|
||||
selected_request = null
|
||||
if main_container.size.x > 0:
|
||||
main_container.split_offset = int(main_container.size.x)
|
||||
|
||||
# Clear selection visual
|
||||
for req_id in request_items:
|
||||
var request_item = request_items[req_id] as NetworkRequestItem
|
||||
request_item.set_selected(false)
|
||||
|
||||
func hide_columns(should_hide: bool):
|
||||
# Hide/show header labels
|
||||
status_header.visible = !should_hide
|
||||
type_header.visible = !should_hide
|
||||
size_header.visible = !should_hide
|
||||
time_header.visible = !should_hide
|
||||
|
||||
# Hide/show status, type, size, time columns for all request items
|
||||
for request_item in request_items.values():
|
||||
var network_request_item = request_item as NetworkRequestItem
|
||||
network_request_item.hide_columns(should_hide)
|
||||
|
||||
func clear_all_requests():
|
||||
for item in request_items.values():
|
||||
item.queue_free()
|
||||
|
||||
network_requests.clear()
|
||||
request_items.clear()
|
||||
selected_request = null
|
||||
|
||||
# Hide details panel and show columns again
|
||||
details_panel.visible = false
|
||||
hide_columns(false)
|
||||
if main_container.size.x > 0:
|
||||
main_container.split_offset = int(main_container.size.x)
|
||||
|
||||
update_status_bar()
|
||||
|
||||
func clear_all_requests_except(preserve_request_id: String):
|
||||
# Remove all items except the preserved one
|
||||
var preserved_request = null
|
||||
var preserved_item = null
|
||||
|
||||
for request in network_requests:
|
||||
if request.id == preserve_request_id:
|
||||
preserved_request = request
|
||||
preserved_item = request_items.get(preserve_request_id)
|
||||
break
|
||||
|
||||
# Clear all items except preserved one
|
||||
for item_id in request_items:
|
||||
if item_id != preserve_request_id:
|
||||
var item = request_items[item_id]
|
||||
item.queue_free()
|
||||
|
||||
network_requests.clear()
|
||||
request_items.clear()
|
||||
|
||||
# Re-add preserved request and item
|
||||
if preserved_request and preserved_item:
|
||||
network_requests.append(preserved_request)
|
||||
request_items[preserve_request_id] = preserved_item
|
||||
|
||||
selected_request = null
|
||||
|
||||
# Hide details panel and show columns again
|
||||
details_panel.visible = false
|
||||
hide_columns(false)
|
||||
if main_container.size.x > 0:
|
||||
main_container.split_offset = int(main_container.size.x)
|
||||
|
||||
update_status_bar()
|
||||
|
||||
func _on_request_item_clicked(request: NetworkRequest):
|
||||
if selected_request == request:
|
||||
hide_details_panel()
|
||||
return
|
||||
|
||||
selected_request = request
|
||||
request_selected.emit(request)
|
||||
|
||||
for req_id in request_items:
|
||||
var request_item = request_items[req_id] as NetworkRequestItem
|
||||
request_item.set_selected(req_id == request.id)
|
||||
|
||||
details_panel.visible = true
|
||||
if main_container.size.x > 0:
|
||||
# Give 6/8 (3/4) of space to details panel, 2/8 (1/4) to left panel
|
||||
main_container.split_offset = int(main_container.size.x * 0.25)
|
||||
|
||||
hide_columns(true)
|
||||
update_details_panel(request)
|
||||
|
||||
func _on_filter_selected(index: int):
|
||||
var filter_type = index - 1 # 0 -> -1 (All), 1 -> 0 (Fetch)...
|
||||
|
||||
if current_filter == filter_type:
|
||||
return
|
||||
|
||||
current_filter = filter_type
|
||||
apply_filter()
|
||||
1
flumi/Scripts/Browser/NetworkTab.gd.uid
Normal file
1
flumi/Scripts/Browser/NetworkTab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dh3jdrot4r7m3
|
||||
55
flumi/Scripts/Browser/OptionButton.gd
Normal file
55
flumi/Scripts/Browser/OptionButton.gd
Normal file
@@ -0,0 +1,55 @@
|
||||
extends Button
|
||||
|
||||
const HISTORY = preload("res://Scenes/BrowserMenus/history.tscn")
|
||||
|
||||
@onready var tab_container: TabManager = $"../../TabContainer"
|
||||
@onready var main: Main = $"../../../"
|
||||
|
||||
var history_scene: PopupPanel = null
|
||||
|
||||
func _on_pressed() -> void:
|
||||
%OptionsMenu.show()
|
||||
|
||||
func _input(_event: InputEvent) -> void:
|
||||
if _event is InputEventKey and _event.pressed and _event.ctrl_pressed:
|
||||
if _event.keycode == KEY_N:
|
||||
if _event.shift_pressed:
|
||||
# CTRL+SHIFT+N - New incognito window
|
||||
_on_options_menu_id_pressed(2)
|
||||
get_viewport().set_input_as_handled()
|
||||
else:
|
||||
# CTRL+N - New window
|
||||
_on_options_menu_id_pressed(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event.keycode == KEY_H:
|
||||
# CTRL+H - History
|
||||
_on_options_menu_id_pressed(4)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_options_menu_id_pressed(id: int) -> void:
|
||||
if id == 0: # new tab
|
||||
tab_container.create_tab()
|
||||
if id == 1: # new window
|
||||
OS.create_process(OS.get_executable_path(), [])
|
||||
if id == 2: # new ingonito window
|
||||
# TODO: handle incognito
|
||||
OS.create_process(OS.get_executable_path(), ["--incognito"])
|
||||
if id == 4: # history
|
||||
show_history()
|
||||
if id == 10: # exit
|
||||
get_tree().quit()
|
||||
|
||||
func show_history() -> void:
|
||||
if history_scene == null:
|
||||
history_scene = HISTORY.instantiate()
|
||||
history_scene.navigate_to_url.connect(main.navigate_to_url)
|
||||
main.add_child(history_scene)
|
||||
|
||||
history_scene.connect("popup_hide", _on_history_closed)
|
||||
else:
|
||||
history_scene.load_history()
|
||||
history_scene.show()
|
||||
|
||||
func _on_history_closed() -> void:
|
||||
if history_scene:
|
||||
history_scene.hide()
|
||||
1
flumi/Scripts/Browser/OptionButton.gd.uid
Normal file
1
flumi/Scripts/Browser/OptionButton.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vjjhljlftlbk
|
||||
11
flumi/Scripts/Browser/SearchBar.gd
Normal file
11
flumi/Scripts/Browser/SearchBar.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
extends LineEdit
|
||||
|
||||
# NOTE: this should be implemented to every Control element,
|
||||
# so that it defocuses on click outside element, instead of focusing on another control node,
|
||||
# but I find it impractical to simply paste to every script in Tags.
|
||||
# Will hold onto the above for now, and only implement it in SearchBar for now
|
||||
func _input(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1:
|
||||
var evLocal = make_input_local(event)
|
||||
if !Rect2(Vector2(0,0), size).has_point(evLocal.position):
|
||||
release_focus()
|
||||
1
flumi/Scripts/Browser/SearchBar.gd.uid
Normal file
1
flumi/Scripts/Browser/SearchBar.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://nve723radqih
|
||||
295
flumi/Scripts/Browser/Tab.gd
Normal file
295
flumi/Scripts/Browser/Tab.gd
Normal file
@@ -0,0 +1,295 @@
|
||||
class_name Tab
|
||||
extends Control
|
||||
|
||||
signal tab_pressed
|
||||
signal tab_closed
|
||||
|
||||
@onready var gradient_texture: TextureRect = %GradientTexture
|
||||
@onready var button: Button = %Button
|
||||
@onready var close_button: Button = %CloseButton
|
||||
@onready var icon: TextureRect = %Icon
|
||||
@onready var animation: AnimationPlayer = $AnimationPlayer
|
||||
var appear_tween: Tween
|
||||
|
||||
const TAB_GRADIENT: GradientTexture2D = preload("res://Scenes/Styles/TabGradient.tres")
|
||||
const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/TabGradientDefault.tres")
|
||||
const TAB_GRADIENT_INACTIVE: GradientTexture2D = preload("res://Scenes/Styles/TabGradientInactive.tres")
|
||||
|
||||
const TAB_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/TabHover.tres")
|
||||
const TAB_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabDefault.tres")
|
||||
const CLOSE_BUTTON_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonHover.tres")
|
||||
const CLOSE_BUTTON_NORMAL: StyleBoxFlat = preload("res://Scenes/Styles/CloseButtonNormal.tres")
|
||||
|
||||
const CLOSE_BUTTON_HIDE_THRESHOLD := 100
|
||||
const TEXT_HIDE_THRESHOLD := 50
|
||||
const GRADIENT_WIDTH := 64
|
||||
const GRADIENT_OFFSET := 72
|
||||
const CLOSE_BUTTON_OFFSET := 34
|
||||
const ICON_OFFSET := 8
|
||||
const APPEAR_ANIMATION_DURATION := 0.25
|
||||
|
||||
var is_active := false
|
||||
var mouse_over_tab := false
|
||||
var loading_tween: Tween
|
||||
|
||||
var scroll_container: ScrollContainer = null
|
||||
var website_container: VBoxContainer = null
|
||||
var background_panel: PanelContainer = null
|
||||
var main_hbox: HSplitContainer = null
|
||||
var dev_tools: Control = null
|
||||
var dev_tools_visible: bool = false
|
||||
var lua_apis: Array[LuaAPI] = []
|
||||
var current_url: String = ""
|
||||
var has_content: bool = false
|
||||
var navigation_history: Array[String] = []
|
||||
var history_index: int = -1
|
||||
|
||||
func _ready():
|
||||
add_to_group("tabs")
|
||||
gradient_texture.texture = gradient_texture.texture.duplicate()
|
||||
gradient_texture.texture.gradient = gradient_texture.texture.gradient.duplicate()
|
||||
|
||||
func _process(_delta):
|
||||
# NOTE: probably very inefficient
|
||||
if mouse_over_tab:
|
||||
var mouse_pos = get_global_mouse_position()
|
||||
var close_button_rect = Rect2(close_button.global_position, close_button.size * close_button.scale)
|
||||
|
||||
if close_button_rect.has_point(mouse_pos):
|
||||
close_button.add_theme_stylebox_override("normal", CLOSE_BUTTON_HOVER)
|
||||
else:
|
||||
close_button.add_theme_stylebox_override("normal", CLOSE_BUTTON_NORMAL)
|
||||
|
||||
func set_title(title: String) -> void:
|
||||
button.text = title
|
||||
button.set_meta("original_text", title)
|
||||
|
||||
func set_icon(new_icon: Texture) -> void:
|
||||
icon.texture = new_icon
|
||||
icon.rotation = 0
|
||||
|
||||
func update_icon_from_url(icon_url: String) -> void:
|
||||
if icon_url.is_empty():
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
set_icon(GLOBE_ICON)
|
||||
remove_meta("original_icon_url")
|
||||
return
|
||||
|
||||
set_meta("original_icon_url", icon_url)
|
||||
|
||||
var icon_resource = await Network.fetch_image(icon_url)
|
||||
|
||||
if is_instance_valid(self) and icon_resource:
|
||||
set_icon(icon_resource)
|
||||
elif is_instance_valid(self):
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
set_icon(GLOBE_ICON)
|
||||
|
||||
func _on_button_mouse_entered() -> void:
|
||||
mouse_over_tab = true
|
||||
if is_active: return
|
||||
gradient_texture.texture = TAB_GRADIENT_INACTIVE
|
||||
|
||||
func _on_button_mouse_exited() -> void:
|
||||
mouse_over_tab = false
|
||||
if is_active: return
|
||||
gradient_texture.texture = TAB_GRADIENT_DEFAULT
|
||||
|
||||
func start_loading() -> void:
|
||||
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
|
||||
|
||||
stop_loading()
|
||||
|
||||
set_icon(LOADER_CIRCLE)
|
||||
icon.pivot_offset = Vector2(11.5, 11.5)
|
||||
|
||||
loading_tween = create_tween()
|
||||
if loading_tween:
|
||||
loading_tween.set_loops(0)
|
||||
loading_tween.tween_method(func(angle):
|
||||
if !is_instance_valid(icon):
|
||||
if loading_tween: loading_tween.kill()
|
||||
return
|
||||
icon.rotation = angle
|
||||
, 0.0, TAU, 1.0)
|
||||
|
||||
func stop_loading() -> void:
|
||||
if loading_tween:
|
||||
loading_tween.kill()
|
||||
loading_tween = null
|
||||
|
||||
func _exit_tree():
|
||||
if loading_tween:
|
||||
loading_tween.kill()
|
||||
loading_tween = null
|
||||
|
||||
for lua_api in lua_apis:
|
||||
if is_instance_valid(lua_api):
|
||||
lua_api.kill_script_execution()
|
||||
lua_api.queue_free()
|
||||
lua_apis.clear()
|
||||
|
||||
if scroll_container and is_instance_valid(scroll_container):
|
||||
if scroll_container.get_parent():
|
||||
scroll_container.get_parent().remove_child(scroll_container)
|
||||
scroll_container.queue_free()
|
||||
|
||||
if background_panel and is_instance_valid(background_panel):
|
||||
if background_panel.get_parent():
|
||||
background_panel.get_parent().remove_child(background_panel)
|
||||
background_panel.queue_free()
|
||||
|
||||
if dev_tools and is_instance_valid(dev_tools):
|
||||
dev_tools.queue_free()
|
||||
|
||||
remove_from_group("tabs")
|
||||
|
||||
func init_scene(parent_container: Control) -> void:
|
||||
if not scroll_container:
|
||||
background_panel = PanelContainer.new()
|
||||
background_panel.name = "Tab_Background_" + str(get_instance_id())
|
||||
background_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
background_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var style_box = StyleBoxFlat.new()
|
||||
style_box.bg_color = Color(1, 1, 1, 1) # White background
|
||||
background_panel.add_theme_stylebox_override("panel", style_box)
|
||||
|
||||
main_hbox = HSplitContainer.new()
|
||||
main_hbox.name = "Tab_MainHBox_" + str(get_instance_id())
|
||||
main_hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
main_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
scroll_container = ScrollContainer.new()
|
||||
scroll_container.name = "Tab_ScrollContainer_" + str(get_instance_id())
|
||||
scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Take 2/3 of space
|
||||
|
||||
website_container = VBoxContainer.new()
|
||||
website_container.name = "Tab_WebsiteContainer_" + str(get_instance_id())
|
||||
website_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
website_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
website_container.add_theme_constant_override("separation", 22)
|
||||
|
||||
var dev_tools_scene = preload("res://Scenes/DevTools.tscn")
|
||||
dev_tools = dev_tools_scene.instantiate()
|
||||
dev_tools.name = "Tab_DevTools_" + str(get_instance_id())
|
||||
dev_tools.visible = false
|
||||
|
||||
parent_container.call_deferred("add_child", background_panel)
|
||||
background_panel.call_deferred("add_child", main_hbox)
|
||||
main_hbox.call_deferred("add_child", scroll_container)
|
||||
main_hbox.call_deferred("add_child", dev_tools)
|
||||
scroll_container.call_deferred("add_child", website_container)
|
||||
|
||||
background_panel.visible = is_active
|
||||
|
||||
func show_content() -> void:
|
||||
if background_panel:
|
||||
background_panel.visible = true
|
||||
|
||||
func play_appear_animation(target_width: float) -> void:
|
||||
var should_hide_close = target_width < CLOSE_BUTTON_HIDE_THRESHOLD
|
||||
var should_hide_text = target_width < TEXT_HIDE_THRESHOLD
|
||||
|
||||
close_button.visible = not should_hide_close
|
||||
button.text = "" if should_hide_text else button.get_meta("original_text", "New Tab")
|
||||
|
||||
var should_show_gradient = not should_hide_text and not should_hide_close
|
||||
gradient_texture.visible = should_show_gradient
|
||||
|
||||
if should_show_gradient:
|
||||
gradient_texture.size.x = GRADIENT_WIDTH
|
||||
gradient_texture.position.x = target_width - GRADIENT_OFFSET
|
||||
|
||||
if not should_hide_close:
|
||||
close_button.position.x = target_width - CLOSE_BUTTON_OFFSET
|
||||
|
||||
icon.position.x = ICON_OFFSET
|
||||
custom_minimum_size.x = 0.0
|
||||
size.x = 0.0
|
||||
button.custom_minimum_size.x = 0.0
|
||||
button.size.x = 0.0
|
||||
|
||||
if appear_tween:
|
||||
appear_tween.kill()
|
||||
|
||||
appear_tween = create_tween()
|
||||
appear_tween.set_ease(Tween.EASE_OUT)
|
||||
appear_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
appear_tween.parallel().tween_property(self, "custom_minimum_size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(self, "size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(button, "custom_minimum_size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
appear_tween.parallel().tween_property(button, "size:x", target_width, APPEAR_ANIMATION_DURATION)
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
tab_pressed.emit()
|
||||
|
||||
func _on_close_button_pressed() -> void:
|
||||
var close_tween = create_tween()
|
||||
close_tween.set_ease(Tween.EASE_IN)
|
||||
close_tween.set_trans(Tween.TRANS_CUBIC)
|
||||
|
||||
close_tween.parallel().tween_property(self, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(self, "size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "custom_minimum_size:x", 0.0, 0.15)
|
||||
close_tween.parallel().tween_property(button, "size:x", 0.0, 0.15)
|
||||
|
||||
await close_tween.finished
|
||||
tab_closed.emit()
|
||||
queue_free()
|
||||
|
||||
func toggle_dev_tools() -> void:
|
||||
if not dev_tools:
|
||||
return
|
||||
|
||||
dev_tools_visible = not dev_tools_visible
|
||||
dev_tools.visible = dev_tools_visible
|
||||
|
||||
if dev_tools_visible:
|
||||
scroll_container.size_flags_stretch_ratio = 2.0
|
||||
dev_tools.size_flags_stretch_ratio = 1.0
|
||||
else:
|
||||
scroll_container.size_flags_stretch_ratio = 1.0
|
||||
|
||||
func get_dev_tools_console() -> DevToolsConsole:
|
||||
if dev_tools and dev_tools.has_method("get_console"):
|
||||
return dev_tools.get_console()
|
||||
return null
|
||||
|
||||
func add_to_navigation_history(url: String) -> void:
|
||||
if url.is_empty():
|
||||
return
|
||||
|
||||
# If we're not at the end of history, remove everything after current position
|
||||
if history_index < navigation_history.size() - 1:
|
||||
navigation_history = navigation_history.slice(0, history_index + 1)
|
||||
|
||||
# Don't add duplicate consecutive entries
|
||||
if navigation_history.is_empty() or navigation_history[-1] != url:
|
||||
navigation_history.append(url)
|
||||
history_index = navigation_history.size() - 1
|
||||
|
||||
func can_go_back() -> bool:
|
||||
return history_index > 0
|
||||
|
||||
func can_go_forward() -> bool:
|
||||
return history_index < navigation_history.size() - 1
|
||||
|
||||
func go_back() -> String:
|
||||
if can_go_back():
|
||||
history_index -= 1
|
||||
return navigation_history[history_index]
|
||||
return ""
|
||||
|
||||
func go_forward() -> String:
|
||||
if can_go_forward():
|
||||
history_index += 1
|
||||
return navigation_history[history_index]
|
||||
return ""
|
||||
|
||||
func get_current_history_url() -> String:
|
||||
if history_index >= 0 and history_index < navigation_history.size():
|
||||
return navigation_history[history_index]
|
||||
return ""
|
||||
1
flumi/Scripts/Browser/Tab.gd.uid
Normal file
1
flumi/Scripts/Browser/Tab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://crpnnfqm3k5xv
|
||||
251
flumi/Scripts/Browser/TabContainer.gd
Normal file
251
flumi/Scripts/Browser/TabContainer.gd
Normal file
@@ -0,0 +1,251 @@
|
||||
class_name TabManager
|
||||
extends HBoxContainer
|
||||
|
||||
var tabs: Array[Tab] = []
|
||||
var active_tab := 0
|
||||
|
||||
@onready var main: Main = $"../.."
|
||||
|
||||
const TAB = preload("res://Scenes/Tab.tscn")
|
||||
|
||||
const TAB_NORMAL: StyleBoxFlat = preload("res://Scenes/Styles/TabNormal.tres")
|
||||
const TAB_HOVER: StyleBoxFlat = preload("res://Scenes/Styles/TabHover.tres")
|
||||
|
||||
const TAB_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabDefault.tres")
|
||||
const TAB_HOVER_DEFAULT: StyleBoxFlat = preload("res://Scenes/Styles/TabHoverDefault.tres")
|
||||
|
||||
const TAB_GRADIENT: GradientTexture2D = preload("res://Scenes/Styles/TabGradient.tres")
|
||||
const TAB_GRADIENT_DEFAULT: GradientTexture2D = preload("res://Scenes/Styles/TabGradientDefault.tres")
|
||||
|
||||
@onready var h_box_container: HBoxContainer = $HBoxContainer
|
||||
|
||||
const MIN_TAB_WIDTH = 24 # Minimum width (icon only)
|
||||
const MIN_TAB_WIDTH_WITH_CLOSE = 60 # Minimum width when close button is visible
|
||||
const MAX_TAB_WIDTH = 320 # Max width
|
||||
const CLOSE_BUTTON_HIDE_THRESHOLD = 100 # When to hide close button
|
||||
const TEXT_HIDE_THRESHOLD = 50 # When to hide text
|
||||
const POPUP_BUTTON_WIDTH = 50 # Width of + button
|
||||
const NEW_TAB_BUTTON_WIDTH = 50 # Width of new tab button
|
||||
const OTHER_UI_PADDING = 200 # Space for other UI elements
|
||||
|
||||
func _ready() -> void:
|
||||
tabs.assign(get_tree().get_nodes_in_group("tabs"))
|
||||
|
||||
call_deferred("_initialize_tab_containers")
|
||||
|
||||
set_active_tab(0)
|
||||
|
||||
for i in tabs.size():
|
||||
tabs[i].tab_pressed.connect(_tab_pressed.bind(i))
|
||||
tabs[i].tab_closed.connect(_tab_closed.bind(i))
|
||||
|
||||
get_viewport().size_changed.connect(_on_viewport_resized)
|
||||
|
||||
call_deferred("update_tab_widths")
|
||||
call_deferred("_delayed_update")
|
||||
|
||||
func _initialize_tab_containers() -> void:
|
||||
for tab in tabs:
|
||||
trigger_init_scene(tab)
|
||||
|
||||
func trigger_init_scene(tab: Tab) -> void:
|
||||
var main_vbox = main.get_node("VBoxContainer")
|
||||
tab.init_scene(main_vbox)
|
||||
|
||||
func _tab_pressed(index: int) -> void:
|
||||
set_active_tab(index)
|
||||
|
||||
func _tab_closed(index: int) -> void:
|
||||
tabs.remove_at(index)
|
||||
|
||||
if tabs.is_empty():
|
||||
get_tree().quit()
|
||||
return
|
||||
|
||||
if index <= active_tab:
|
||||
if index == active_tab:
|
||||
# Closed tab was active, select right neighbor (or last tab if at end)
|
||||
if index >= tabs.size():
|
||||
active_tab = tabs.size() - 1
|
||||
else:
|
||||
active_tab = index
|
||||
else:
|
||||
# Closed tab was before active tab, shift active index down
|
||||
active_tab -= 1
|
||||
|
||||
# Reconnect signals with updated indices
|
||||
for i in tabs.size():
|
||||
tabs[i].tab_pressed.disconnect(_tab_pressed)
|
||||
tabs[i].tab_closed.disconnect(_tab_closed)
|
||||
tabs[i].tab_pressed.connect(_tab_pressed.bind(i))
|
||||
tabs[i].tab_closed.connect(_tab_closed.bind(i))
|
||||
|
||||
set_active_tab(active_tab)
|
||||
update_tab_widths()
|
||||
|
||||
func _on_viewport_resized() -> void:
|
||||
update_tab_widths()
|
||||
|
||||
func _delayed_update() -> void:
|
||||
update_tab_widths()
|
||||
|
||||
func update_tab_widths() -> void:
|
||||
if tabs.is_empty():
|
||||
return
|
||||
|
||||
var viewport_width = get_viewport().get_visible_rect().size.x
|
||||
var available_width = viewport_width - POPUP_BUTTON_WIDTH - NEW_TAB_BUTTON_WIDTH - OTHER_UI_PADDING
|
||||
|
||||
var tab_width = available_width / float(tabs.size())
|
||||
tab_width = clamp(tab_width, MIN_TAB_WIDTH, MAX_TAB_WIDTH)
|
||||
|
||||
var should_hide_close = tab_width < CLOSE_BUTTON_HIDE_THRESHOLD
|
||||
var should_hide_text = tab_width < TEXT_HIDE_THRESHOLD
|
||||
|
||||
h_box_container.custom_minimum_size.x = 0
|
||||
h_box_container.size.x = 0
|
||||
|
||||
for tab in tabs:
|
||||
if tab.appear_tween and tab.appear_tween.is_valid():
|
||||
continue
|
||||
|
||||
tab.custom_minimum_size.x = tab_width
|
||||
tab.size.x = tab_width
|
||||
|
||||
tab.button.custom_minimum_size.x = tab_width
|
||||
tab.button.size.x = tab_width
|
||||
|
||||
tab.close_button.visible = not should_hide_close
|
||||
tab.button.text = "" if should_hide_text else tab.button.get_meta("original_text", tab.button.text)
|
||||
|
||||
if not tab.button.has_meta("original_text"):
|
||||
tab.button.set_meta("original_text", tab.button.text)
|
||||
|
||||
update_tab_internal_elements(tab, tab_width, should_hide_close, should_hide_text)
|
||||
|
||||
func calculate_visible_tab_count(available_width: float) -> int:
|
||||
var all_tabs_width = calculate_tab_width(available_width, tabs.size())
|
||||
if all_tabs_width >= MIN_TAB_WIDTH:
|
||||
return tabs.size()
|
||||
|
||||
for tab_count in range(tabs.size(), 0, -1):
|
||||
var tab_width = calculate_tab_width(available_width, tab_count)
|
||||
if tab_width >= MIN_TAB_WIDTH:
|
||||
return tab_count
|
||||
|
||||
return max(1, tabs.size())
|
||||
|
||||
func calculate_tab_width(available_width: float, tab_count: int) -> float:
|
||||
if tab_count == 0:
|
||||
return MAX_TAB_WIDTH
|
||||
|
||||
var ideal_width = available_width / tab_count
|
||||
return clamp(ideal_width, MIN_TAB_WIDTH, MAX_TAB_WIDTH)
|
||||
|
||||
func get_hidden_tabs() -> Array[Tab]:
|
||||
var hidden_tabs: Array[Tab] = []
|
||||
for tab in tabs:
|
||||
if not tab.visible:
|
||||
hidden_tabs.append(tab)
|
||||
return hidden_tabs
|
||||
|
||||
func has_hidden_tabs() -> bool:
|
||||
return get_hidden_tabs().size() > 0
|
||||
|
||||
func update_tab_internal_elements(tab: Tab, width: float, hide_close_button: bool = false, hide_text: bool = false) -> void:
|
||||
var should_show_gradient = not hide_text and not hide_close_button
|
||||
tab.gradient_texture.visible = should_show_gradient
|
||||
|
||||
if should_show_gradient:
|
||||
var gradient_start_offset = 72
|
||||
var gradient_width = 64
|
||||
var gradient_start_x = width - gradient_start_offset
|
||||
|
||||
tab.gradient_texture.position.x = gradient_start_x
|
||||
tab.gradient_texture.size.x = gradient_width
|
||||
|
||||
if not hide_close_button:
|
||||
var close_button_x = width - 34
|
||||
tab.close_button.position.x = close_button_x
|
||||
|
||||
func set_active_tab(index: int) -> void:
|
||||
if index < 0 or index >= tabs.size():
|
||||
return
|
||||
|
||||
if active_tab >= 0 and active_tab < tabs.size():
|
||||
tabs[active_tab].is_active = false
|
||||
tabs[active_tab].button.add_theme_stylebox_override("normal", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("pressed", TAB_DEFAULT)
|
||||
tabs[active_tab].button.add_theme_stylebox_override("hover", TAB_HOVER_DEFAULT)
|
||||
tabs[active_tab].gradient_texture.texture = TAB_GRADIENT_DEFAULT
|
||||
if tabs[active_tab].background_panel:
|
||||
tabs[active_tab].background_panel.visible = false
|
||||
|
||||
tabs[index].is_active = true
|
||||
tabs[index].button.add_theme_stylebox_override("normal", TAB_NORMAL)
|
||||
tabs[index].button.add_theme_stylebox_override("pressed", TAB_NORMAL)
|
||||
tabs[index].button.add_theme_stylebox_override("hover", TAB_NORMAL)
|
||||
tabs[index].gradient_texture.texture = TAB_GRADIENT
|
||||
tabs[index].show_content()
|
||||
|
||||
if not tabs[index].website_container:
|
||||
if main:
|
||||
trigger_init_scene(tabs[index])
|
||||
|
||||
active_tab = index
|
||||
|
||||
if main and main.search_bar:
|
||||
if tabs[index].has_content:
|
||||
main.current_domain = tabs[index].current_url
|
||||
var display_text = main.current_domain
|
||||
if display_text.begins_with("gurt://"):
|
||||
display_text = display_text.substr(7)
|
||||
main.search_bar.text = display_text
|
||||
else:
|
||||
main.current_domain = ""
|
||||
main.search_bar.text = ""
|
||||
main.search_bar.grab_focus()
|
||||
|
||||
main.update_navigation_buttons()
|
||||
|
||||
func create_tab() -> void:
|
||||
var index = tabs.size();
|
||||
var tab = TAB.instantiate()
|
||||
tabs.append(tab)
|
||||
h_box_container.add_child(tab)
|
||||
tab.tab_pressed.connect(_tab_pressed.bind(index))
|
||||
tab.tab_closed.connect(_tab_closed.bind(index))
|
||||
var viewport_width = get_viewport().get_visible_rect().size.x
|
||||
var available_width = viewport_width - POPUP_BUTTON_WIDTH - NEW_TAB_BUTTON_WIDTH - OTHER_UI_PADDING
|
||||
var visible_count = calculate_visible_tab_count(available_width)
|
||||
var tab_width = calculate_tab_width(available_width, visible_count)
|
||||
|
||||
tab.play_appear_animation(tab_width)
|
||||
|
||||
set_active_tab(index)
|
||||
|
||||
await get_tree().process_frame
|
||||
update_tab_widths()
|
||||
|
||||
trigger_init_scene(tab)
|
||||
|
||||
# WARNING: temporary
|
||||
main.render()
|
||||
|
||||
func _input(_event: InputEvent) -> void:
|
||||
if Input.is_action_just_pressed("NewTab"):
|
||||
create_tab()
|
||||
if Input.is_action_just_pressed("CloseTab"):
|
||||
tabs[active_tab]._on_close_button_pressed()
|
||||
if Input.is_action_just_pressed("NextTab"):
|
||||
var next_tab = (active_tab + 1) % tabs.size()
|
||||
set_active_tab(next_tab)
|
||||
if Input.is_action_just_pressed("PreviousTab"):
|
||||
var prev_tab = (active_tab - 1 + tabs.size()) % tabs.size()
|
||||
set_active_tab(prev_tab - 1)
|
||||
if Input.is_action_just_pressed("FocusSearch"):
|
||||
main.search_bar.grab_focus()
|
||||
main.search_bar.select_all()
|
||||
|
||||
func _on_new_tab_button_pressed() -> void:
|
||||
create_tab()
|
||||
1
flumi/Scripts/Browser/TabContainer.gd.uid
Normal file
1
flumi/Scripts/Browser/TabContainer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cy0c74thgjwok
|
||||
120
flumi/Scripts/Browser/history.gd
Normal file
120
flumi/Scripts/Browser/history.gd
Normal file
@@ -0,0 +1,120 @@
|
||||
extends PopupPanel
|
||||
signal navigate_to_url(url: String)
|
||||
|
||||
@onready var history_entry_container: VBoxContainer = $Main/PanelContainer2/ScrollContainer/HistoryEntryContainer
|
||||
@onready var delete_menu: PanelContainer = $Main/DeleteMenu
|
||||
@onready var line_edit: LineEdit = $Main/LineEdit
|
||||
@onready var entries_label: RichTextLabel = $Main/DeleteMenu/HBoxContainer/RichTextLabel
|
||||
@onready var cancel_button: Button = $Main/DeleteMenu/HBoxContainer/CancelButton
|
||||
@onready var delete_button: Button = $Main/DeleteMenu/HBoxContainer/DeleteButton
|
||||
|
||||
var toggled_entries = []
|
||||
var history_entry_scene = preload("res://Scenes/BrowserMenus/history_entry.tscn")
|
||||
|
||||
func _ready():
|
||||
delete_button.pressed.connect(_on_delete_button_pressed)
|
||||
line_edit.text_changed.connect(_on_search_text_changed)
|
||||
load_history()
|
||||
|
||||
func history_toggle(toggled: bool, entry) -> void:
|
||||
print('toggling ', entry, ' to :', toggled)
|
||||
if toggled:
|
||||
toggled_entries.append(entry)
|
||||
else:
|
||||
toggled_entries.remove_at(toggled_entries.find(entry))
|
||||
|
||||
entries_label.text = str(toggled_entries.size()) + " selected"
|
||||
|
||||
if toggled_entries.size() != 0:
|
||||
delete_menu.show()
|
||||
line_edit.hide()
|
||||
else:
|
||||
delete_menu.hide()
|
||||
line_edit.show()
|
||||
|
||||
func _on_cancel_button_pressed() -> void:
|
||||
var entries_to_reset = toggled_entries.duplicate()
|
||||
toggled_entries.clear()
|
||||
|
||||
for entry in entries_to_reset:
|
||||
entry.reset()
|
||||
|
||||
delete_menu.hide()
|
||||
line_edit.show()
|
||||
|
||||
func _on_delete_button_pressed() -> void:
|
||||
var urls_to_delete = []
|
||||
for entry in toggled_entries:
|
||||
if entry.has_meta("history_url"):
|
||||
urls_to_delete.append(entry.get_meta("history_url"))
|
||||
|
||||
for url in urls_to_delete:
|
||||
remove_history_entry(url)
|
||||
|
||||
var entries_to_remove = toggled_entries.duplicate()
|
||||
toggled_entries.clear()
|
||||
|
||||
for entry in entries_to_remove:
|
||||
history_entry_container.remove_child(entry)
|
||||
entry.queue_free()
|
||||
|
||||
delete_menu.hide()
|
||||
line_edit.show()
|
||||
|
||||
func _on_search_text_changed(search_text: String) -> void:
|
||||
filter_history_entries(search_text)
|
||||
|
||||
func load_history():
|
||||
var history_data = BrowserHistory.get_history_data()
|
||||
var existing_entries = history_entry_container.get_children()
|
||||
|
||||
var needs_update = existing_entries.size() != history_data.size()
|
||||
|
||||
if not needs_update and history_data.size() > 0 and existing_entries.size() > 0:
|
||||
var first_entry = existing_entries[0]
|
||||
if first_entry.has_meta("history_url"):
|
||||
var stored_url = first_entry.get_meta("history_url")
|
||||
if stored_url != history_data[0].url:
|
||||
needs_update = true
|
||||
|
||||
if needs_update:
|
||||
clear_displayed_entries()
|
||||
for entry in history_data:
|
||||
add_history_entry_to_display(entry.url, entry.title, entry.timestamp, entry.icon_url)
|
||||
|
||||
show()
|
||||
|
||||
func clear_displayed_entries():
|
||||
for child in history_entry_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func add_history_entry_to_display(url: String, title_: String, timestamp: String, icon_url: String = ""):
|
||||
var entry_instance = history_entry_scene.instantiate()
|
||||
history_entry_container.add_child(entry_instance)
|
||||
entry_instance.setup_entry(url, title_, timestamp, icon_url)
|
||||
entry_instance.connect("checkbox_toggle", history_toggle.bind(entry_instance))
|
||||
entry_instance.connect("entry_clicked", _on_entry_clicked)
|
||||
entry_instance.set_meta("history_url", url)
|
||||
|
||||
func filter_history_entries(search_text: String):
|
||||
if search_text.is_empty():
|
||||
# Show all entries
|
||||
for child in history_entry_container.get_children():
|
||||
child.visible = true
|
||||
return
|
||||
|
||||
# Filter existing entries by showing/hiding them
|
||||
var query = search_text.to_lower()
|
||||
for child in history_entry_container.get_children():
|
||||
if child.has_method("get_title") and child.has_method("get_url"):
|
||||
var title_ = child.get_title().to_lower()
|
||||
var url = child.get_url().to_lower()
|
||||
child.visible = title_.contains(query) or url.contains(query)
|
||||
else:
|
||||
child.visible = false
|
||||
|
||||
func remove_history_entry(url: String):
|
||||
BrowserHistory.remove_entry(url)
|
||||
|
||||
func _on_entry_clicked(url: String):
|
||||
navigate_to_url.emit(url)
|
||||
1
flumi/Scripts/Browser/history.gd.uid
Normal file
1
flumi/Scripts/Browser/history.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ektopbvnhfga
|
||||
84
flumi/Scripts/Browser/history_entry.gd
Normal file
84
flumi/Scripts/Browser/history_entry.gd
Normal file
@@ -0,0 +1,84 @@
|
||||
extends HBoxContainer
|
||||
signal checkbox_toggle
|
||||
signal entry_clicked(url: String)
|
||||
|
||||
@onready var check_box: CheckBox = $CheckBox
|
||||
@onready var time_label: RichTextLabel = $RichTextLabel
|
||||
@onready var icon: TextureRect = $TextureRect
|
||||
@onready var title_label: RichTextLabel = $RichTextLabel2
|
||||
@onready var domain_label: RichTextLabel = $DomainLabel
|
||||
|
||||
var entry_url: String = ""
|
||||
var entry_title: String = ""
|
||||
|
||||
func reset() -> void:
|
||||
check_box.set_pressed_no_signal(false)
|
||||
|
||||
func _on_check_box_toggled(toggled_on: bool) -> void:
|
||||
checkbox_toggle.emit(toggled_on)
|
||||
|
||||
func setup_entry(url: String, title: String, timestamp: String, icon_url: String = ""):
|
||||
entry_url = url
|
||||
entry_title = title
|
||||
|
||||
title_label.text = title if not title.is_empty() else url
|
||||
|
||||
var domain = URLUtils.extract_domain(url)
|
||||
if domain.is_empty():
|
||||
domain = url
|
||||
domain_label.text = domain
|
||||
|
||||
var datetime_dict = Time.get_datetime_dict_from_datetime_string(timestamp, false)
|
||||
if datetime_dict.has("hour") and datetime_dict.has("minute"):
|
||||
var hour = datetime_dict.hour
|
||||
var minute = datetime_dict.minute
|
||||
var am_pm = "AM"
|
||||
|
||||
if hour == 0:
|
||||
hour = 12
|
||||
elif hour > 12:
|
||||
hour -= 12
|
||||
am_pm = "PM"
|
||||
elif hour == 12:
|
||||
am_pm = "PM"
|
||||
|
||||
time_label.text = "%d:%02d%s" % [hour, minute, am_pm]
|
||||
else:
|
||||
time_label.text = ""
|
||||
|
||||
if not icon_url.is_empty():
|
||||
_load_icon(icon_url)
|
||||
else:
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
icon.texture = GLOBE_ICON
|
||||
|
||||
func _load_icon(icon_url: String):
|
||||
if icon_url.is_empty():
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
icon.texture = GLOBE_ICON
|
||||
return
|
||||
|
||||
icon.texture = null
|
||||
|
||||
var icon_resource = await Network.fetch_image(icon_url)
|
||||
|
||||
if is_instance_valid(self) and icon_resource:
|
||||
icon.texture = icon_resource
|
||||
elif is_instance_valid(self):
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
icon.texture = GLOBE_ICON
|
||||
|
||||
func get_title() -> String:
|
||||
return entry_title
|
||||
|
||||
func get_url() -> String:
|
||||
return entry_url
|
||||
|
||||
func _ready():
|
||||
title_label.gui_input.connect(_on_title_clicked)
|
||||
domain_label.gui_input.connect(_on_title_clicked)
|
||||
|
||||
func _on_title_clicked(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
entry_clicked.emit(entry_url)
|
||||
1
flumi/Scripts/Browser/history_entry.gd.uid
Normal file
1
flumi/Scripts/Browser/history_entry.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bw5pr4wrf780h
|
||||
Reference in New Issue
Block a user