From e19d4b7f61b9a63f3dbdfc600e62d69b27cbf22d Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:45:45 +0300 Subject: [PATCH] use hbox instead of richtextlabel for nested text tags, complete invite creation --- README.md | 1 - dns/frontend/index.html | 2 +- dns/frontend/register.html | 2 +- dns/frontend/register.lua | 15 ++- dns/frontend/signup.html | 2 +- dns/src/gurt_server/auth_routes.rs | 73 ++++++++------ dns/src/gurt_server/routes.rs | 31 +++--- flumi/Scenes/Tags/p.tscn | 15 +-- flumi/Scripts/B9/HTMLParser.gd | 8 +- flumi/Scripts/B9/Lua.gd | 30 ++++-- flumi/Scripts/Tags/input.gd | 10 +- flumi/Scripts/Tags/p.gd | 150 +++++++++++++++++++---------- 12 files changed, 205 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index eb9a709..fcbabf2 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,4 @@ Issues: Notes: - **< input />** is sort-of inline in normal web. We render it as a block element (new-line). -- A single `RichTextLabel` for inline text tags should stop, we should use invididual ones so it's easier to style and achieve separation through a `vboxcontainer`. - Fonts use **Flash of Unstyled Text (FOUT)** as opposed to **Flash of Invisible Text (FOIT)**, meaning the text with custom fonts will render with a generic font (sans-serif) while the custom ones downloads. diff --git a/dns/frontend/index.html b/dns/frontend/index.html index d97961d..3de60b0 100644 --- a/dns/frontend/index.html +++ b/dns/frontend/index.html @@ -47,7 +47,7 @@ -

Don't have an account? Register here

+

Don't have an account? Register here

diff --git a/dns/frontend/register.html b/dns/frontend/register.html index 95c035c..9402319 100644 --- a/dns/frontend/register.html +++ b/dns/frontend/register.html @@ -46,7 +46,7 @@ } .form-input { - w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500 + w-64 p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500 } .card { diff --git a/dns/frontend/register.lua b/dns/frontend/register.lua index 9a6d90c..29c2f01 100644 --- a/dns/frontend/register.lua +++ b/dns/frontend/register.lua @@ -7,6 +7,7 @@ local tldSelector = gurt.select('#tld-selector') local loadingElement = gurt.select('#tld-loading') local displayElement = gurt.select('#invite-code-display') local remainingElement = gurt.select('#remaining') +local redeemBtn = gurt.select('#redeem-invite-btn') local options @@ -169,6 +170,10 @@ local function createInvite() displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)' displayElement:show() Clipboard.write(inviteCode) + + user.registrations_remaining = user.registrations_remaining - 1 + updateUserInfo() + print('Invite code created and copied to clipboard: ' .. inviteCode) else print('Failed to create invite: ' .. response:text()) @@ -197,7 +202,11 @@ local function redeemInvite(code) updateUserInfo() -- Clear form - gurt.select('#invite-code-input').text = '' + gurt.select('#invite-code-input').value = '' + redeemBtn.text = 'Success!' + gurt.setTimeout(function() + redeemBtn.text = 'Redeem' + end, 1000) else local error = response:text() showError('redeem-error', 'Failed to redeem invite: ' .. error) @@ -236,8 +245,8 @@ end) gurt.select('#create-invite-btn'):on('click', createInvite) -gurt.select('#redeem-invite-btn'):on('click', function() - local code = gurt.select('#invite-code-input').text +redeemBtn:on('click', function() + local code = gurt.select('#invite-code-input').value if code and code ~= '' then redeemInvite(code) end diff --git a/dns/frontend/signup.html b/dns/frontend/signup.html index e86ade0..f3680b3 100644 --- a/dns/frontend/signup.html +++ b/dns/frontend/signup.html @@ -61,7 +61,7 @@ -

Already have an account? Login here

+

Already have an account? Login here

diff --git a/dns/src/gurt_server/auth_routes.rs b/dns/src/gurt_server/auth_routes.rs index b54afdc..c9e6d88 100644 --- a/dns/src/gurt_server/auth_routes.rs +++ b/dns/src/gurt_server/auth_routes.rs @@ -197,17 +197,32 @@ pub(crate) async fn create_invite(_ctx: &ServerContext, app_state: AppState, cla .collect() }; - // Insert invite code into database - let insert_result = sqlx::query( - "INSERT INTO invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)" - ) - .bind(&invite_code) - .bind(claims.user_id) - .bind(Utc::now()) - .execute(&app_state.db) - .await; + let mut tx = app_state.db.begin().await + .map_err(|_| GurtError::invalid_message("Database error"))?; - match insert_result { + let affected_rows = sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0") + .bind(claims.user_id) + .execute(&mut *tx) + .await + .map_err(|_| GurtError::invalid_message("Database error"))? + .rows_affected(); + + if affected_rows == 0 { + return Ok(GurtResponse::bad_request().with_json_body(&Error { + msg: "No registrations remaining to create invite", + error: "INSUFFICIENT_REGISTRATIONS".into(), + })?); + } + + sqlx::query("INSERT INTO invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)") + .bind(&invite_code) + .bind(claims.user_id) + .bind(Utc::now()) + .execute(&mut *tx) + .await + .map_err(|_| GurtError::invalid_message("Database error"))?; + + match tx.commit().await { Ok(_) => { let response = serde_json::json!({ "invite_code": invite_code @@ -252,7 +267,7 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai .await .map_err(|_| GurtError::invalid_message("Database error"))?; - sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 3 WHERE id = $1") + sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 1 WHERE id = $1") .bind(claims.user_id) .execute(&mut *tx) .await @@ -262,7 +277,7 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai .map_err(|_| GurtError::invalid_message("Database error"))?; let response = serde_json::json!({ - "registrations_added": 3 + "registrations_added": 1 }); Ok(GurtResponse::ok().with_json_body(&response)?) } @@ -282,20 +297,6 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai } pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result { - // Check if user has domain invite codes remaining - let user: (i32,) = sqlx::query_as("SELECT domain_invite_codes FROM users WHERE id = $1") - .bind(claims.user_id) - .fetch_one(&app_state.db) - .await - .map_err(|_| GurtError::invalid_message("User not found"))?; - - if user.0 <= 0 { - return Ok(GurtResponse::bad_request().with_json_body(&Error { - msg: "No domain invite codes remaining", - error: "NO_INVITES_REMAINING".into(), - })?); - } - // Generate random domain invite code let invite_code: String = { use rand::Rng; @@ -312,6 +313,20 @@ pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppSta let mut tx = app_state.db.begin().await .map_err(|_| GurtError::invalid_message("Database error"))?; + let affected_rows = sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = $1 AND domain_invite_codes > 0") + .bind(claims.user_id) + .execute(&mut *tx) + .await + .map_err(|_| GurtError::invalid_message("Database error"))? + .rows_affected(); + + if affected_rows == 0 { + return Ok(GurtResponse::bad_request().with_json_body(&Error { + msg: "No domain invite codes remaining", + error: "NO_INVITES_REMAINING".into(), + })?); + } + sqlx::query( "INSERT INTO domain_invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)" ) @@ -322,12 +337,6 @@ pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppSta .await .map_err(|_| GurtError::invalid_message("Database error"))?; - sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = $1") - .bind(claims.user_id) - .execute(&mut *tx) - .await - .map_err(|_| GurtError::invalid_message("Database error"))?; - tx.commit().await .map_err(|_| GurtError::invalid_message("Database error"))?; diff --git a/dns/src/gurt_server/routes.rs b/dns/src/gurt_server/routes.rs index 5b8031d..76fafc9 100644 --- a/dns/src/gurt_server/routes.rs +++ b/dns/src/gurt_server/routes.rs @@ -85,14 +85,23 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) - let domain_id = domain_row.0; - // Decrease user's registrations remaining - sqlx::query( - "UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1", + let affected_rows = sqlx::query( + "UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0", ) .bind(user_id) .execute(&app.db) .await - .map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?; + .map_err(|_| GurtError::invalid_message("Failed to update user registrations"))? + .rows_affected(); + + if affected_rows == 0 { + sqlx::query("DELETE FROM domains WHERE id = $1") + .bind(domain_id) + .execute(&app.db) + .await + .map_err(|_| GurtError::invalid_message("Database cleanup error"))?; + return Err(GurtError::invalid_message("No registrations remaining")); + } if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 { let domain_registration = DomainRegistration { @@ -123,20 +132,6 @@ pub(crate) async fn create_domain( app_state: AppState, claims: Claims, ) -> Result { - // Check if user has registrations remaining - let user: (i32,) = sqlx::query_as("SELECT registrations_remaining FROM users WHERE id = $1") - .bind(claims.user_id) - .fetch_one(&app_state.db) - .await - .map_err(|_| GurtError::invalid_message("User not found"))?; - - if user.0 <= 0 { - return Ok(GurtResponse::bad_request().with_json_body(&Error { - msg: "Failed to create domain", - error: "No registrations remaining".into(), - })?); - } - let domain: Domain = serde_json::from_slice(ctx.body()) .map_err(|_| GurtError::invalid_message("Invalid JSON"))?; diff --git a/flumi/Scenes/Tags/p.tscn b/flumi/Scenes/Tags/p.tscn index a529b6e..7603fb0 100644 --- a/flumi/Scenes/Tags/p.tscn +++ b/flumi/Scenes/Tags/p.tscn @@ -1,21 +1,12 @@ -[gd_scene load_steps=3 format=3 uid="uid://fr6lovy4ikok"] +[gd_scene load_steps=2 format=3 uid="uid://fr6lovy4ikok"] [ext_resource type="Script" uid="uid://cg6kjvlx3an1j" path="res://Scripts/Tags/p.gd" id="1_pnbfg"] -[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_1glvj"] -[node name="p" type="RichTextLabel"] +[node name="p" type="HBoxContainer"] anchors_preset = 10 anchor_right = 1.0 offset_bottom = 19.0 grow_horizontal = 2 size_flags_horizontal = 3 -focus_mode = 2 -mouse_default_cursor_shape = 1 -theme = ExtResource("2_1glvj") -theme_override_colors/default_color = Color(0, 0, 0, 1) -bbcode_enabled = true -text = "Placeholder" -fit_content = true -vertical_alignment = 1 -selection_enabled = true +size_flags_vertical = 8 script = ExtResource("1_pnbfg") diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd index 58b9086..86d9bed 100644 --- a/flumi/Scripts/B9/HTMLParser.gd +++ b/flumi/Scripts/B9/HTMLParser.gd @@ -436,7 +436,9 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio color = "#" + color.to_html(false) else: color = str(color) - formatted_content = "[color=%s]%s[/color]" % [color, formatted_content] + var color_pattern = "[color=%s]" % color + if not formatted_content.contains(color_pattern): + formatted_content = "[color=%s]%s[/color]" % [color, formatted_content] # Apply tag-specific formatting match element.tag_name: @@ -471,8 +473,8 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio var href = element.get_attribute("href") if href.length() > 0: - # Pass raw href - URL resolution happens in handle_link_click - formatted_content = "[url=%s]%s[/url]" % [href, formatted_content] + if not formatted_content.contains("[url="): + formatted_content = "[url=%s]%s[/url]" % [href, formatted_content] return formatted_content diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index 715e2d8..e04ca64 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -489,6 +489,10 @@ func _on_input_focus_lost(subscription: EventSubscription) -> void: current_text = dom_node.get_text() elif "text" in dom_node: current_text = dom_node.text + else: + var element = dom_parser.find_by_id(subscription.element_id) + if element: + current_text = element.text_content var event_info = {"value": current_text} _execute_lua_callback(subscription, [event_info]) @@ -723,10 +727,7 @@ func _handle_text_setting(operation: Dictionary): var element = SelectorUtils.find_first_matching(selector, dom_parser.parse_result.all_elements) if element: - # Always update the HTML element's text content first - element.text_content = text - - # If the element has a DOM node, update it too + # If the element has a DOM node, update it directly without updating text_content var element_id = get_or_assign_element_id(element) var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) if dom_node: @@ -734,24 +735,37 @@ func _handle_text_setting(operation: Dictionary): var button_node = dom_node.get_node_or_null("ButtonNode") if button_node and button_node is Button: button_node.text = text + element.text_content = text return + if element.tag_name == "p" and dom_node.has_method("set_text"): + dom_node.set_text(text) + element.text_content = text + return + + element.text_content = text + + if dom_node: var text_node = get_dom_node(dom_node, "text") if text_node: if text_node is RichTextLabel: StyleManager.apply_styles_to_label(text_node, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text) - text_node.call_deferred("_auto_resize_to_content") + try_apply_auto_resize(text_node) elif text_node.has_method("set_text"): text_node.set_text(text) elif "text" in text_node: text_node.text = text - if text_node.has_method("_auto_resize_to_content"): - text_node.call_deferred("_auto_resize_to_content") + try_apply_auto_resize(text_node) else: var rich_text_label = _find_rich_text_label_recursive(dom_node) if rich_text_label: StyleManager.apply_styles_to_label(rich_text_label, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text) - rich_text_label.call_deferred("_auto_resize_to_content") + try_apply_auto_resize(rich_text_label) + +func try_apply_auto_resize(text_node: Node) -> void: + var parent = text_node.get_parent() + if parent and parent.has_method("_apply_auto_resize_to_label"): + parent.call_deferred("_apply_auto_resize_to_label", text_node) func _find_rich_text_label_recursive(node: Node) -> RichTextLabel: if node is RichTextLabel: diff --git a/flumi/Scripts/Tags/input.gd b/flumi/Scripts/Tags/input.gd index 740442f..17b824d 100644 --- a/flumi/Scripts/Tags/input.gd +++ b/flumi/Scripts/Tags/input.gd @@ -464,17 +464,17 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> var new_height = max(active_child.custom_minimum_size.y, active_child.size.y) if width: - if width == "100%": + if typeof(width) == TYPE_STRING and width == "100%": active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL size_flags_horizontal = Control.SIZE_EXPAND_FILL new_width = 0 - elif SizingUtils.is_percentage(width): + elif typeof(width) == TYPE_STRING and 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): + if typeof(height) == TYPE_STRING and SizingUtils.is_percentage(height): new_height = SizingUtils.calculate_percentage_size(height, SizingUtils.DEFAULT_VIEWPORT_HEIGHT) else: new_height = height @@ -483,7 +483,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> active_child.custom_minimum_size = new_child_size - if width and width != "100%": + if width and not (typeof(width) == TYPE_STRING and width == "100%"): active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN if height: active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN @@ -494,7 +494,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> custom_minimum_size = new_child_size # Root Control adjusts size flags to match child - if width and width != "100%": + if width and not (typeof(width) == TYPE_STRING and width == "100%"): size_flags_horizontal = Control.SIZE_SHRINK_BEGIN else: size_flags_horizontal = Control.SIZE_EXPAND_FILL diff --git a/flumi/Scripts/Tags/p.gd b/flumi/Scripts/Tags/p.gd index fca015b..c8f24d1 100644 --- a/flumi/Scripts/Tags/p.gd +++ b/flumi/Scripts/Tags/p.gd @@ -1,75 +1,127 @@ class_name HTMLP -extends RichTextLabel +extends HBoxContainer -var element_styles: Dictionary = {} +var _element: HTMLParser.HTMLElement +var _parser: HTMLParser func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void: - element_styles = parser.get_element_styles_with_inheritance(element, "", []) - - text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser) - - # Allow mouse events to pass through to parent containers for hover effects while keeping text selection - mouse_filter = Control.MOUSE_FILTER_PASS - - autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - - call_deferred("_auto_resize_to_content") - + _element = element + _parser = parser size_flags_horizontal = Control.SIZE_EXPAND_FILL - set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) + size_flags_vertical = Control.SIZE_SHRINK_BEGIN -func _auto_resize_to_content(): - if not is_inside_tree(): - await tree_entered + if get_child_count() > 0: + return + + var content_parts = [] + var current_text = "" + + var element_text = element.text_content + var child_texts = [] + + for child in element.children: + child_texts.append(child.text_content) + + var parent_only_text = element_text + for child_text in child_texts: + parent_only_text = parent_only_text.replace(child_text, "") + + if not parent_only_text.strip_edges().is_empty(): + var parent_label = create_styled_label(parent_only_text.strip_edges(), element, parser) + + for child in element.children: + var child_label = create_styled_label(child.get_bbcode_formatted_text(parser), element, parser) + + if contains_hyperlink(child): + child_label.meta_clicked.connect(_on_meta_clicked) + +func create_styled_label(text: String, element: HTMLParser.HTMLElement, parser: HTMLParser) -> RichTextLabel: + var label = RichTextLabel.new() + label.fit_content = true + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.size_flags_vertical = Control.SIZE_SHRINK_CENTER + label.bbcode_enabled = true + add_child(label) + + var styles = parser.get_element_styles_with_inheritance(element, "", []) + StyleManager.apply_styles_to_label(label, styles, element, parser, text) + + call_deferred("_apply_auto_resize_to_label", label) + return label + +func _apply_auto_resize_to_label(label: RichTextLabel): + if not label.is_inside_tree(): + await label.tree_entered var min_width = 20 var max_width = 800 var min_height = 30 - fit_content = true + label.fit_content = true - var original_autowrap = autowrap_mode - autowrap_mode = TextServer.AUTOWRAP_OFF + var original_autowrap = label.autowrap_mode + label.autowrap_mode = TextServer.AUTOWRAP_OFF await get_tree().process_frame - var natural_width = size.x - - var font_weight_multiplier = _get_font_weight_multiplier() - natural_width *= font_weight_multiplier + var natural_width = label.size.x + natural_width *= 1.0 # font weight multiplier simplified var desired_width = clampf(natural_width, min_width, max_width) - autowrap_mode = original_autowrap + label.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 0.0 - var final_height = explicit_height if explicit_height > 0 else max(content_height, min_height) - custom_minimum_size = Vector2(desired_width, final_height) + label.custom_minimum_size = Vector2(desired_width, 0) - queue_redraw() + label.queue_redraw() -func _get_font_weight_multiplier() -> float: - #if element_styles.has("font-black"): - # return 1.12 - #elif element_styles.has("font-extrabold"): - # return 1.10 - #elif element_styles.has("font-bold"): - # return 1.04 - #elif element_styles.has("font-semibold"): - # return 1.06 - #elif element_styles.has("font-medium"): - # return 1.03 - #elif element_styles.has("font-light"): - # return 0.98 - #elif element_styles.has("font-extralight") or element_styles.has("font-thin"): - # return 0.95 +func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool: + if element.tag_name == "a": + return true - #var text_content = get_parsed_text() + for child in element.children: + if contains_hyperlink(child): + return true - #if text_content.contains("[b]"): - # return 1.08 + return false + +func _on_meta_clicked(meta: Variant) -> void: + var current = get_parent() + while current: + if current.has_method("handle_link_click"): + current.handle_link_click(meta) + break + current = current.get_parent() + +func get_text() -> String: + var text_parts = [] + for child in get_children(): + if child is RichTextLabel: + text_parts.append(child.get_parsed_text()) + return " ".join(text_parts) + +func set_text(new_text: String) -> void: + # Clear existing children immediately + for child in get_children(): + remove_child(child) + child.queue_free() - return 1.0 + if _element and _parser: + var label = create_styled_label(new_text, _element, _parser) + else: + var label = create_label(new_text) + +func create_label(text: String) -> RichTextLabel: + var label = RichTextLabel.new() + label.text = text + label.fit_content = true + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.size_flags_vertical = Control.SIZE_SHRINK_CENTER + label.bbcode_enabled = true + add_child(label) + call_deferred("_apply_auto_resize_to_label", label) + return label