URL UI, history UI
This commit is contained in:
@@ -151,7 +151,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
|
||||
styles[property] = inline_parsed[property]
|
||||
|
||||
# Inherit certain properties from parent elements
|
||||
var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor"]
|
||||
var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor", "font-bold", "font-italic", "underline"]
|
||||
var parent_element = element.parent
|
||||
while parent_element:
|
||||
var parent_styles = get_element_styles_internal(parent_element, event)
|
||||
|
||||
@@ -28,6 +28,7 @@ func _init():
|
||||
timeout_manager = LuaTimeoutManager.new()
|
||||
threaded_vm = ThreadedLuaVM.new()
|
||||
threaded_vm.script_completed.connect(_on_threaded_script_completed)
|
||||
threaded_vm.script_error.connect(func(e): print(e))
|
||||
threaded_vm.dom_operation_request.connect(_handle_dom_operation)
|
||||
threaded_vm.print_output.connect(_on_print_output)
|
||||
|
||||
|
||||
@@ -2794,4 +2794,108 @@ audio.play()
|
||||
|
||||
# Set the active HTML content to use the audio demo
|
||||
func _ready():
|
||||
HTML_CONTENT = HTML_CONTENT_AUDIO_TEST
|
||||
HTML_CONTENT = """<head>
|
||||
<title>Login</title>
|
||||
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||
<meta name="theme-color" content="#1b1b1b">
|
||||
<meta name="description" content="Login to your account">
|
||||
|
||||
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
|
||||
|
||||
<style>
|
||||
body { bg-[#1b1b1b] text-[#ffffff] font-roboto flex items-center justify-center p-4 }
|
||||
.login-card { bg-[#2a2a2a] rounded-lg p-8 shadow-2xl mx-0 }
|
||||
h1 { text-3xl font-bold text-center mb-6 text-[#ffffff] }
|
||||
input {
|
||||
bg-[#3b3b3b]
|
||||
border-none
|
||||
rounded-md
|
||||
p-3
|
||||
w-full
|
||||
text-[#ffffff]
|
||||
placeholder:text-[#999999]
|
||||
outline-none
|
||||
focus:ring-2
|
||||
focus:ring-[#5b5b5b]
|
||||
mb-4
|
||||
}
|
||||
button {
|
||||
bg-[#4ade80]
|
||||
text-[#1b1b1b]
|
||||
font-bold
|
||||
p-3
|
||||
rounded-md
|
||||
w-full
|
||||
hover:bg-[#22c55e]
|
||||
active:bg-[#15803d]
|
||||
cursor-pointer
|
||||
}
|
||||
a { text-[#4ade80] hover:text-[#22c55e] cursor-pointer }
|
||||
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
local submitBtn = gurt.select('#submit')
|
||||
local username_input = gurt.select('#username')
|
||||
local password_input = gurt.select('#password')
|
||||
local log_output = gurt.select('#log-output')
|
||||
|
||||
function addLog(message)
|
||||
gurt.log(message)
|
||||
log_output.text = log_output.text .. message .. '\\n'
|
||||
end
|
||||
|
||||
submitBtn:on('submit', function(event)
|
||||
local username = event.data.username
|
||||
local password = event.data.password
|
||||
|
||||
local request_body = JSON.stringify({
|
||||
username = username,
|
||||
password = password
|
||||
})
|
||||
print(request_body)
|
||||
local url = 'http://localhost:8080/auth/login'
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
addLog('Attempting to log in with username: ' .. username)
|
||||
log_output.text = ''
|
||||
|
||||
local response = fetch(url, {
|
||||
method = 'POST',
|
||||
headers = headers,
|
||||
body = request_body
|
||||
})
|
||||
|
||||
addLog('Response Status: ' .. response.status .. ' ' .. response.statusText)
|
||||
|
||||
if response:ok() then
|
||||
addLog('Login successful!')
|
||||
local jsonData = response:json()
|
||||
if jsonData then
|
||||
addLog('Logged in as user: ' .. jsonData.user.username)
|
||||
addLog('Token: ' .. jsonData.token:sub(1, 20) .. '...')
|
||||
end
|
||||
else
|
||||
addLog('Request failed with status: ' .. response.status)
|
||||
local error_data = response:text()
|
||||
addLog('Error response: ' .. error_data)
|
||||
end
|
||||
end)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="login-card">
|
||||
<h1>Login</h1>
|
||||
<form id="login-form">
|
||||
<input id="username" type="text" placeholder="Username" required="true" />
|
||||
<input id="password" type="password" placeholder="Password" required="true" />
|
||||
<button type="submit" id="submit">Log In</button>
|
||||
</form>
|
||||
<p style="text-center mt-4 text-[#999999] text-base">Don't have an account? <a href="#">Register here</a></p>
|
||||
|
||||
<p id="log-output" style="min-h-24"></p>
|
||||
</div>
|
||||
</body>""".to_utf8_buffer()
|
||||
|
||||
198
flumi/Scripts/GurtProtocol.gd
Normal file
198
flumi/Scripts/GurtProtocol.gd
Normal file
@@ -0,0 +1,198 @@
|
||||
extends RefCounted
|
||||
class_name GurtProtocol
|
||||
|
||||
const DNS_API_URL = "http://localhost:8080"
|
||||
|
||||
static func is_gurt_domain(url: String) -> bool:
|
||||
if url.begins_with("gurt://"):
|
||||
return true
|
||||
|
||||
var parts = url.split(".")
|
||||
return parts.size() == 2 and not url.contains("://")
|
||||
|
||||
static func parse_gurt_domain(url: String) -> Dictionary:
|
||||
print("Parsing URL: ", url)
|
||||
|
||||
var domain_part = url
|
||||
|
||||
if url.begins_with("gurt://"):
|
||||
domain_part = url.substr(7) # Remove "gurt://"
|
||||
|
||||
var parts = domain_part.split(".")
|
||||
if parts.size() != 2:
|
||||
print("Invalid domain format: ", domain_part)
|
||||
return {}
|
||||
|
||||
print("Parsed domain - name: ", parts[0], ", tld: ", parts[1])
|
||||
return {
|
||||
"name": parts[0],
|
||||
"tld": parts[1],
|
||||
"display_url": domain_part
|
||||
}
|
||||
|
||||
static func fetch_domain_info(name: String, tld: String) -> Dictionary:
|
||||
print("Fetching domain info for: ", name, ".", tld)
|
||||
|
||||
var http_request = HTTPRequest.new()
|
||||
var tree = Engine.get_main_loop()
|
||||
tree.current_scene.add_child(http_request)
|
||||
|
||||
http_request.timeout = 5.0
|
||||
|
||||
var url = DNS_API_URL + "/domain/" + name + "/" + tld
|
||||
print("DNS API URL: ", url)
|
||||
|
||||
var error = http_request.request(url)
|
||||
|
||||
if error != OK:
|
||||
print("HTTP request failed with error: ", error)
|
||||
http_request.queue_free()
|
||||
return {"error": "Failed to make DNS request"}
|
||||
|
||||
var response = await http_request.request_completed
|
||||
http_request.queue_free()
|
||||
|
||||
if response[1] == 0 and response[3].size() == 0:
|
||||
print("DNS API request timed out")
|
||||
return {"error": "DNS server is not responding"}
|
||||
|
||||
var http_code = response[1]
|
||||
var body = response[3]
|
||||
|
||||
print("DNS API response code: ", http_code)
|
||||
print("DNS API response body: ", body.get_string_from_utf8())
|
||||
|
||||
if http_code != 200:
|
||||
return {"error": "Domain not found or not approved"}
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(body.get_string_from_utf8())
|
||||
|
||||
if parse_result != OK:
|
||||
print("JSON parse error: ", parse_result)
|
||||
return {"error": "Invalid JSON response from DNS server"}
|
||||
|
||||
print("Domain info retrieved: ", json.data)
|
||||
return json.data
|
||||
|
||||
static func fetch_index_html(ip: String) -> String:
|
||||
print("Fetching index.html from IP: ", ip)
|
||||
|
||||
var http_request = HTTPRequest.new()
|
||||
var tree = Engine.get_main_loop()
|
||||
tree.current_scene.add_child(http_request)
|
||||
|
||||
http_request.timeout = 5.0
|
||||
|
||||
var url = "http://" + ip + "/index.html"
|
||||
print("Fetching from URL: ", url)
|
||||
|
||||
var error = http_request.request(url)
|
||||
|
||||
if error != OK:
|
||||
print("HTTP request to IP failed with error: ", error)
|
||||
http_request.queue_free()
|
||||
return ""
|
||||
|
||||
var response = await http_request.request_completed
|
||||
http_request.queue_free()
|
||||
|
||||
if response[1] == 0 and response[3].size() == 0:
|
||||
print("Index.html request timed out")
|
||||
return ""
|
||||
|
||||
var http_code = response[1]
|
||||
var body = response[3]
|
||||
|
||||
print("IP response code: ", http_code)
|
||||
|
||||
if http_code != 200:
|
||||
print("Failed to fetch index.html, HTTP code: ", http_code)
|
||||
return ""
|
||||
|
||||
var html_content = body.get_string_from_utf8()
|
||||
print("Successfully fetched HTML content (", html_content.length(), " characters)")
|
||||
return html_content
|
||||
|
||||
static func handle_gurt_domain(url: String) -> Dictionary:
|
||||
print("Handling GURT domain: ", url)
|
||||
|
||||
var parsed = parse_gurt_domain(url)
|
||||
if parsed.is_empty():
|
||||
return {"error": "Invalid domain format. Use: domain.tld", "html": create_error_page("Invalid domain format. Use: domain.tld")}
|
||||
|
||||
var domain_info = await fetch_domain_info(parsed.name, parsed.tld)
|
||||
if domain_info.has("error"):
|
||||
return {"error": domain_info.error, "html": create_error_page(domain_info.error)}
|
||||
|
||||
var html_content = await fetch_index_html(domain_info.ip)
|
||||
if html_content.is_empty():
|
||||
var error_msg = "Failed to fetch index.html from " + domain_info.ip
|
||||
return {"error": error_msg, "html": create_error_page(error_msg)}
|
||||
|
||||
return {"html": html_content, "display_url": parsed.display_url}
|
||||
|
||||
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 "HTTP request failed" 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) -> String:
|
||||
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>"""
|
||||
1
flumi/Scripts/GurtProtocol.gd.uid
Normal file
1
flumi/Scripts/GurtProtocol.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://clhivwjs3eujk
|
||||
18
flumi/Scripts/OptionButton.gd
Normal file
18
flumi/Scripts/OptionButton.gd
Normal file
@@ -0,0 +1,18 @@
|
||||
extends Button
|
||||
|
||||
@onready var tab_container: TabManager = $"../../TabContainer"
|
||||
@onready var website_background: Panel = %WebsiteBackground
|
||||
|
||||
func _on_pressed() -> void:
|
||||
%OptionsMenu.show()
|
||||
|
||||
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
|
||||
website_background.modulate = Constants.SECONDARY_COLOR
|
||||
1
flumi/Scripts/OptionButton.gd.uid
Normal file
1
flumi/Scripts/OptionButton.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vjjhljlftlbk
|
||||
@@ -318,16 +318,23 @@ static func apply_margin_wrapper(node: Control, styles: Dictionary) -> Control:
|
||||
var margin_container = MarginContainer.new()
|
||||
margin_container.name = "MarginWrapper_" + node.name
|
||||
|
||||
# Copy size flags from the original node
|
||||
margin_container.size_flags_horizontal = node.size_flags_horizontal
|
||||
margin_container.size_flags_vertical = node.size_flags_vertical
|
||||
var has_explicit_width = styles.has("width")
|
||||
var has_explicit_height = styles.has("height")
|
||||
|
||||
if has_explicit_width:
|
||||
margin_container.size_flags_horizontal = node.size_flags_horizontal
|
||||
else:
|
||||
margin_container.size_flags_horizontal = node.size_flags_horizontal
|
||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
if has_explicit_height:
|
||||
margin_container.size_flags_vertical = node.size_flags_vertical
|
||||
else:
|
||||
margin_container.size_flags_vertical = node.size_flags_vertical
|
||||
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
apply_margin_styles_to_container(margin_container, styles)
|
||||
|
||||
# Reset the original node's size flags since they're now handled by the wrapper
|
||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
# Handle reparenting properly
|
||||
var original_parent = node.get_parent()
|
||||
if original_parent:
|
||||
@@ -542,9 +549,22 @@ static func parse_radius(radius_str: String) -> int:
|
||||
|
||||
static func apply_font_to_label(label: RichTextLabel, font_resource: Font) -> void:
|
||||
label.add_theme_font_override("normal_font", font_resource)
|
||||
label.add_theme_font_override("bold_font", font_resource)
|
||||
label.add_theme_font_override("italics_font", font_resource)
|
||||
label.add_theme_font_override("bold_italics_font", font_resource)
|
||||
|
||||
var bold_font = SystemFont.new()
|
||||
bold_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
||||
bold_font.font_weight = 700 # Bold weight
|
||||
label.add_theme_font_override("bold_font", bold_font)
|
||||
|
||||
var italic_font = SystemFont.new()
|
||||
italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
||||
italic_font.font_italic = true
|
||||
label.add_theme_font_override("italics_font", italic_font)
|
||||
|
||||
var bold_italic_font = SystemFont.new()
|
||||
bold_italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"]
|
||||
bold_italic_font.font_weight = 700 # Bold weight
|
||||
bold_italic_font.font_italic = true
|
||||
label.add_theme_font_override("bold_italics_font", bold_italic_font)
|
||||
|
||||
static func apply_font_to_button(button: Button, styles: Dictionary) -> void:
|
||||
if styles.has("font-family"):
|
||||
|
||||
@@ -47,6 +47,10 @@ func set_icon(new_icon: Texture) -> void:
|
||||
icon.rotation = 0
|
||||
|
||||
func update_icon_from_url(icon_url: String) -> void:
|
||||
if icon_url.is_empty():
|
||||
stop_loading()
|
||||
return
|
||||
|
||||
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
|
||||
|
||||
loading_tween = create_tween()
|
||||
@@ -68,9 +72,7 @@ func update_icon_from_url(icon_url: String) -> void:
|
||||
# Only update if tab still exists
|
||||
if is_instance_valid(self):
|
||||
set_icon(icon_resource)
|
||||
if loading_tween:
|
||||
loading_tween.kill()
|
||||
loading_tween = null
|
||||
stop_loading()
|
||||
|
||||
func _on_button_mouse_entered() -> void:
|
||||
mouse_over_tab = true
|
||||
@@ -82,6 +84,27 @@ func _on_button_mouse_exited() -> void:
|
||||
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()
|
||||
|
||||
loading_tween = create_tween()
|
||||
set_icon(LOADER_CIRCLE)
|
||||
loading_tween.set_loops()
|
||||
icon.pivot_offset = Vector2(11.5, 11.5)
|
||||
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()
|
||||
|
||||
@@ -189,10 +189,23 @@ func apply_padding_to_stylebox(style_box: StyleBoxFlat, styles: Dictionary) -> v
|
||||
|
||||
func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant) -> void:
|
||||
if width != null or height != null:
|
||||
ctrl.custom_minimum_size = Vector2(
|
||||
width if width != null else 0,
|
||||
height if height != null else 0
|
||||
)
|
||||
var new_width = 0
|
||||
var new_height = 0
|
||||
|
||||
if width != null:
|
||||
if SizingUtils.is_percentage(width):
|
||||
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
||||
else:
|
||||
new_width = width
|
||||
|
||||
if height != null:
|
||||
if SizingUtils.is_percentage(height):
|
||||
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
|
||||
else:
|
||||
new_height = height
|
||||
|
||||
ctrl.custom_minimum_size = Vector2(new_width, new_height)
|
||||
|
||||
if width != null:
|
||||
ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
if height != null:
|
||||
|
||||
@@ -404,10 +404,22 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
||||
if active_child:
|
||||
if width or height:
|
||||
# Explicit sizing from CSS
|
||||
var new_child_size = Vector2(
|
||||
width if width else active_child.custom_minimum_size.x,
|
||||
height if height else max(active_child.custom_minimum_size.y, active_child.size.y)
|
||||
)
|
||||
var new_width = active_child.custom_minimum_size.x
|
||||
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
|
||||
|
||||
if width:
|
||||
if SizingUtils.is_percentage(width):
|
||||
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
|
||||
else:
|
||||
new_width = width
|
||||
|
||||
if height:
|
||||
if SizingUtils.is_percentage(height):
|
||||
new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT)
|
||||
else:
|
||||
new_height = height
|
||||
|
||||
var new_child_size = Vector2(new_width, new_height)
|
||||
|
||||
active_child.custom_minimum_size = new_child_size
|
||||
|
||||
|
||||
@@ -7,9 +7,38 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||
# Allow mouse events to pass through to parent containers for hover effects while keeping text selection
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
|
||||
# NOTE: estimate width/height because FlexContainer removes our anchor preset (sets 0 width)
|
||||
var plain_text = element.get_collapsed_text()
|
||||
var estimated_height = 30
|
||||
var estimated_width = min(200, max(100, plain_text.length() * 12))
|
||||
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
|
||||
custom_minimum_size = Vector2(estimated_width, estimated_height)
|
||||
call_deferred("_auto_resize_to_content")
|
||||
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
|
||||
func _auto_resize_to_content():
|
||||
if not is_inside_tree():
|
||||
await tree_entered
|
||||
|
||||
var min_width = 20
|
||||
var max_width = 800
|
||||
var min_height = 30
|
||||
|
||||
fit_content = true
|
||||
|
||||
var original_autowrap = autowrap_mode
|
||||
autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
var natural_width = size.x
|
||||
var desired_width = clampf(natural_width, min_width, max_width)
|
||||
|
||||
autowrap_mode = original_autowrap
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
var content_height = get_content_height()
|
||||
var explicit_height = custom_minimum_size.y if custom_minimum_size.y > 0 else null
|
||||
var final_height = explicit_height if explicit_height != null else max(content_height, min_height)
|
||||
custom_minimum_size = Vector2(desired_width, final_height)
|
||||
|
||||
queue_redraw()
|
||||
|
||||
39
flumi/Scripts/history.gd
Normal file
39
flumi/Scripts/history.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
extends MarginContainer
|
||||
|
||||
@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
|
||||
|
||||
var toggled_entries = []
|
||||
|
||||
func _ready():
|
||||
for entry in history_entry_container.get_children():
|
||||
entry.connect("checkbox_toggle", history_toggle.bind(entry))
|
||||
|
||||
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()
|
||||
1
flumi/Scripts/history.gd.uid
Normal file
1
flumi/Scripts/history.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ektopbvnhfga
|
||||
10
flumi/Scripts/history_entry.gd
Normal file
10
flumi/Scripts/history_entry.gd
Normal file
@@ -0,0 +1,10 @@
|
||||
extends HBoxContainer
|
||||
signal checkbox_toggle
|
||||
|
||||
@onready var check_box: CheckBox = $CheckBox
|
||||
|
||||
func reset() -> void:
|
||||
check_box.set_pressed_no_signal(false)
|
||||
|
||||
func _on_check_box_toggled(toggled_on: bool) -> void:
|
||||
checkbox_toggle.emit(toggled_on)
|
||||
1
flumi/Scripts/history_entry.gd.uid
Normal file
1
flumi/Scripts/history_entry.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bw5pr4wrf780h
|
||||
@@ -4,6 +4,8 @@ extends Control
|
||||
@onready var website_container: Control = %WebsiteContainer
|
||||
@onready var website_background: Control = %WebsiteBackground
|
||||
@onready var tab_container: TabManager = $VBoxContainer/TabContainer
|
||||
@onready var search_bar: LineEdit = $VBoxContainer/HBoxContainer/LineEdit
|
||||
|
||||
const LOADER_CIRCLE = preload("res://Assets/Icons/loader-circle.svg")
|
||||
const AUTO_SIZING_FLEX_CONTAINER = preload("res://Scripts/AutoSizingFlexContainer.gd")
|
||||
|
||||
@@ -53,6 +55,8 @@ func _ready():
|
||||
DisplayServer.window_set_min_size(MIN_SIZE)
|
||||
|
||||
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
||||
|
||||
call_deferred("render")
|
||||
|
||||
func _on_viewport_size_changed():
|
||||
recalculate_percentage_elements(website_container)
|
||||
@@ -64,7 +68,49 @@ func recalculate_percentage_elements(node: Node):
|
||||
for child in node.get_children():
|
||||
recalculate_percentage_elements(child)
|
||||
|
||||
var current_domain = "" # Store current domain for display
|
||||
|
||||
func _on_search_submitted(url: String) -> void:
|
||||
print("Search submitted: ", url)
|
||||
|
||||
if GurtProtocol.is_gurt_domain(url):
|
||||
print("Processing as GURT domain")
|
||||
|
||||
var tab = tab_container.tabs[tab_container.active_tab]
|
||||
tab.start_loading()
|
||||
|
||||
var result = await GurtProtocol.handle_gurt_domain(url)
|
||||
|
||||
if result.has("error"):
|
||||
print("GURT domain error: ", result.error)
|
||||
const GLOBE_ICON = preload("res://Assets/Icons/globe.svg")
|
||||
tab.stop_loading()
|
||||
tab.set_icon(GLOBE_ICON)
|
||||
|
||||
var html_bytes = result.html.to_utf8_buffer()
|
||||
render_content(html_bytes)
|
||||
|
||||
if result.has("display_url"):
|
||||
current_domain = result.display_url
|
||||
if not search_bar.has_focus():
|
||||
search_bar.text = current_domain
|
||||
else:
|
||||
print("Non-GURT URL entered: ", url)
|
||||
|
||||
func _on_search_focus_entered() -> void:
|
||||
if not current_domain.is_empty():
|
||||
search_bar.text = "gurt://" + current_domain
|
||||
|
||||
func _on_search_focus_exited() -> void:
|
||||
if not current_domain.is_empty():
|
||||
search_bar.text = current_domain
|
||||
|
||||
|
||||
func render() -> void:
|
||||
render_content(Constants.HTML_CONTENT)
|
||||
|
||||
func render_content(html_bytes: PackedByteArray) -> void:
|
||||
|
||||
# Clear existing content
|
||||
for child in website_container.get_children():
|
||||
child.queue_free()
|
||||
@@ -73,8 +119,6 @@ func render() -> void:
|
||||
FontManager.clear_fonts()
|
||||
FontManager.set_refresh_callback(refresh_fonts)
|
||||
|
||||
var html_bytes = Constants.HTML_CONTENT
|
||||
|
||||
var parser: HTMLParser = HTMLParser.new(html_bytes)
|
||||
var parse_result = parser.parse()
|
||||
|
||||
@@ -109,57 +153,59 @@ func render() -> void:
|
||||
add_child(lua_api)
|
||||
|
||||
var i = 0
|
||||
while i < body.children.size():
|
||||
var element: HTMLParser.HTMLElement = body.children[i]
|
||||
|
||||
if should_group_as_inline(element):
|
||||
# Create an HBoxContainer for consecutive inline elements
|
||||
var inline_elements: Array[HTMLParser.HTMLElement] = []
|
||||
|
||||
while i < body.children.size() and should_group_as_inline(body.children[i]):
|
||||
inline_elements.append(body.children[i])
|
||||
i += 1
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 4)
|
||||
|
||||
for inline_element in inline_elements:
|
||||
var inline_node = await create_element_node(inline_element, parser)
|
||||
if inline_node:
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
|
||||
parser.register_dom_node(inline_element, inline_node)
|
||||
|
||||
safe_add_child(hbox, inline_node)
|
||||
# Handle hyperlinks for all inline elements
|
||||
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
|
||||
inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
else:
|
||||
print("Failed to create inline element node: ", inline_element.tag_name)
|
||||
|
||||
safe_add_child(website_container, hbox)
|
||||
continue
|
||||
|
||||
var element_node = await create_element_node(element, parser)
|
||||
if element_node:
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
|
||||
parser.register_dom_node(element, element_node)
|
||||
if body:
|
||||
while i < body.children.size():
|
||||
var element: HTMLParser.HTMLElement = body.children[i]
|
||||
|
||||
# ul/ol handle their own adding
|
||||
if element.tag_name != "ul" and element.tag_name != "ol":
|
||||
safe_add_child(website_container, element_node)
|
||||
if should_group_as_inline(element):
|
||||
# Create an HBoxContainer for consecutive inline elements
|
||||
var inline_elements: Array[HTMLParser.HTMLElement] = []
|
||||
|
||||
# Handle hyperlinks for all elements
|
||||
if contains_hyperlink(element):
|
||||
if element_node is RichTextLabel:
|
||||
element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
elif element_node.has_method("get") and element_node.get("rich_text_label"):
|
||||
element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
else:
|
||||
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
|
||||
|
||||
i += 1
|
||||
while i < body.children.size() and should_group_as_inline(body.children[i]):
|
||||
inline_elements.append(body.children[i])
|
||||
i += 1
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 4)
|
||||
|
||||
for inline_element in inline_elements:
|
||||
var inline_node = await create_element_node(inline_element, parser)
|
||||
if inline_node:
|
||||
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
|
||||
parser.register_dom_node(inline_element, inline_node)
|
||||
|
||||
safe_add_child(hbox, inline_node)
|
||||
# Handle hyperlinks for all inline elements
|
||||
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
|
||||
inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
else:
|
||||
print("Failed to create inline element node: ", inline_element.tag_name)
|
||||
|
||||
safe_add_child(website_container, hbox)
|
||||
continue
|
||||
|
||||
var element_node = await create_element_node(element, parser)
|
||||
if element_node:
|
||||
|
||||
# Input elements register their own DOM nodes in their init() function
|
||||
if element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
|
||||
parser.register_dom_node(element, element_node)
|
||||
|
||||
# ul/ol handle their own adding
|
||||
if element.tag_name != "ul" and element.tag_name != "ol":
|
||||
safe_add_child(website_container, element_node)
|
||||
|
||||
if contains_hyperlink(element):
|
||||
if element_node is RichTextLabel:
|
||||
element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
elif element_node.has_method("get") and element_node.get("rich_text_label"):
|
||||
element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
else:
|
||||
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
|
||||
|
||||
i += 1
|
||||
|
||||
if scripts.size() > 0 and lua_api:
|
||||
parser.process_scripts(lua_api, null)
|
||||
@@ -381,6 +427,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
||||
var p_node = P.instantiate()
|
||||
p_node.init(element, parser)
|
||||
|
||||
var div_styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||
StyleManager.apply_styles_to_label(p_node, div_styles, element, parser)
|
||||
|
||||
var container_for_children = node
|
||||
if node is PanelContainer and node.get_child_count() > 0:
|
||||
container_for_children = node.get_child(0) # The VBoxContainer inside
|
||||
|
||||
Reference in New Issue
Block a user