From 4d16a16fe4ed5d5dd2f553863c6d2edd23fb4e59 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:07:21 +0300 Subject: [PATCH] network tab --- dns/src/gurt_server.rs | 5 - dns/src/gurt_server/routes.rs | 12 +- flumi/Assets/Icons/arrow-down-up.svg | 1 + flumi/Assets/Icons/arrow-down-up.svg.import | 37 ++ flumi/Assets/Icons/braces.svg | 1 + flumi/Assets/Icons/braces.svg.import | 37 ++ flumi/Assets/Icons/case-sensitive.svg | 1 + flumi/Assets/Icons/case-sensitive.svg.import | 37 ++ flumi/Assets/Icons/file-text.svg | 1 + flumi/Assets/Icons/file-text.svg.import | 37 ++ flumi/Assets/Icons/image.svg | 1 + flumi/Assets/Icons/image.svg.import | 37 ++ flumi/Assets/Icons/palette.svg | 1 + flumi/Assets/Icons/palette.svg.import | 37 ++ .../Resources/NetworkRequestItem_Normal.tres | 12 + .../NetworkRequestItem_Selected.tres | 12 + flumi/Scenes/DevTools.tscn | 268 +++++++++++- flumi/Scenes/NetworkRequestItem.tscn | 33 +- flumi/Scripts/Constants.gd | 6 +- flumi/Scripts/DevToolsConsole.gd | 35 +- flumi/Scripts/GurtProtocol.gd | 2 +- flumi/Scripts/LuaCodeEdit.gd.uid | 1 - flumi/Scripts/LuaSyntaxHighlighter.gd | 13 +- flumi/Scripts/Network.gd | 54 ++- flumi/Scripts/NetworkManager.gd | 124 ++++++ flumi/Scripts/NetworkManager.gd.uid | 1 + flumi/Scripts/NetworkRequest.gd | 207 +++++++++ flumi/Scripts/NetworkRequest.gd.uid | 1 + flumi/Scripts/NetworkRequestItem.gd | 50 +-- flumi/Scripts/NetworkRequestItem.gd.uid | 1 + flumi/Scripts/NetworkTab.gd | 398 ++++++++++++++++++ flumi/Scripts/NetworkTab.gd.uid | 1 + flumi/Scripts/Tab.gd | 4 +- flumi/Scripts/Utils/CodeEditUtils.gd | 94 +++++ flumi/Scripts/Utils/CodeEditUtils.gd.uid | 1 + flumi/Scripts/Utils/Lua/Network.gd | 8 +- flumi/Scripts/main.gd | 27 +- flumi/project.godot | 1 + protocol/cli/README.md | 2 +- protocol/gurtca/src/challenges.rs | 2 +- protocol/gurtca/src/main.rs | 2 +- tests/websocket.html | 12 +- 42 files changed, 1463 insertions(+), 154 deletions(-) create mode 100644 flumi/Assets/Icons/arrow-down-up.svg create mode 100644 flumi/Assets/Icons/arrow-down-up.svg.import create mode 100644 flumi/Assets/Icons/braces.svg create mode 100644 flumi/Assets/Icons/braces.svg.import create mode 100644 flumi/Assets/Icons/case-sensitive.svg create mode 100644 flumi/Assets/Icons/case-sensitive.svg.import create mode 100644 flumi/Assets/Icons/file-text.svg create mode 100644 flumi/Assets/Icons/file-text.svg.import create mode 100644 flumi/Assets/Icons/image.svg create mode 100644 flumi/Assets/Icons/image.svg.import create mode 100644 flumi/Assets/Icons/palette.svg create mode 100644 flumi/Assets/Icons/palette.svg.import create mode 100644 flumi/Resources/NetworkRequestItem_Normal.tres create mode 100644 flumi/Resources/NetworkRequestItem_Selected.tres delete mode 100644 flumi/Scripts/LuaCodeEdit.gd.uid create mode 100644 flumi/Scripts/NetworkManager.gd create mode 100644 flumi/Scripts/NetworkManager.gd.uid create mode 100644 flumi/Scripts/NetworkRequest.gd create mode 100644 flumi/Scripts/NetworkRequest.gd.uid create mode 100644 flumi/Scripts/NetworkRequestItem.gd.uid create mode 100644 flumi/Scripts/NetworkTab.gd create mode 100644 flumi/Scripts/NetworkTab.gd.uid create mode 100644 flumi/Scripts/Utils/CodeEditUtils.gd create mode 100644 flumi/Scripts/Utils/CodeEditUtils.gd.uid diff --git a/dns/src/gurt_server.rs b/dns/src/gurt_server.rs index ae2e83d..aed0bc7 100644 --- a/dns/src/gurt_server.rs +++ b/dns/src/gurt_server.rs @@ -127,7 +127,6 @@ impl GurtHandler for AppHandler { }; log::info!("Handler started for {} {} from {}", ctx.method(), ctx.path(), ctx.remote_addr); - log::info!("Handler type will be: {:?}", handler_type); let result = match handler_type { HandlerType::Index => routes::index(app_state).await, @@ -337,7 +336,6 @@ async fn get_ca_certificate_content(app_state: &AppState) -> std::result::Result async fn serve_static_file(ctx: &ServerContext) -> Result { let path = ctx.path(); - log::info!("Static file request for path: '{}'", path); // Strip query parameters from the path for static file serving let path_without_query = if let Some(query_pos) = path.find('?') { @@ -355,7 +353,6 @@ async fn serve_static_file(ctx: &ServerContext) -> Result { path_without_query } }; - log::info!("Resolved file_path: '{}'", file_path); if file_path.contains("..") || file_path.contains('/') || file_path.contains('\\') { log::warn!("Invalid file path requested: '{}'", file_path); @@ -367,11 +364,9 @@ async fn serve_static_file(ctx: &ServerContext) -> Result { .map_err(|_| GurtError::invalid_message("Failed to get current directory"))?; let frontend_dir = current_dir.join("frontend"); let full_path = frontend_dir.join(file_path); - log::info!("Attempting to read file: '{}'", full_path.display()); match tokio::fs::read_to_string(&full_path).await { Ok(content) => { - log::info!("Successfully read file, content length: {} bytes", content.len()); let content_type = match full_path.extension().and_then(|ext| ext.to_str()) { Some("html") => "text/html", Some("lua") => "text/plain", diff --git a/dns/src/gurt_server/routes.rs b/dns/src/gurt_server/routes.rs index 7689346..043b2c3 100644 --- a/dns/src/gurt_server/routes.rs +++ b/dns/src/gurt_server/routes.rs @@ -21,30 +21,22 @@ fn parse_query_string(query: &str) -> HashMap { } pub(crate) async fn index(_app_state: AppState) -> Result { - log::info!("Index handler called - attempting to serve index.html"); - let current_dir = std::env::current_dir() .map_err(|_| GurtError::invalid_message("Failed to get current directory"))?; - log::info!("Current directory: {}", current_dir.display()); - let frontend_dir = current_dir.join("frontend"); let index_path = frontend_dir.join("index.html"); - log::info!("Looking for index.html at: {}", index_path.display()); match tokio::fs::read_to_string(&index_path).await { Ok(content) => { - log::info!("Successfully read index.html, content length: {} bytes", content.len()); Ok(GurtResponse::ok() .with_header("Content-Type", "text/html") .with_string_body(&content)) } - Err(e) => { - log::error!("Failed to read index.html: {}", e); + Err(_) => { let body = format!( "GurtDNS v{}!\n\nThe available endpoints are:\n\n - [GET] /domains\n - [GET] /domain/{{name}}/{{tld}}\n - [POST] /domain\n - [PUT] /domain/{{key}}\n - [DELETE] /domain/{{key}}\n - [GET] /tlds\n\nRatelimits are as follows: 5 requests per 10 minutes on `[POST] /domain`.\n\nCode link: https://github.com/outpoot/gurted", env!("CARGO_PKG_VERSION") ); - log::info!("Serving fallback API info, length: {} bytes", body.len()); Ok(GurtResponse::ok().with_string_body(&body)) } } @@ -353,8 +345,6 @@ pub(crate) async fn get_user_domains( app_state: AppState, claims: Claims, ) -> Result { - log::info!("get_user_domains called for user_id: {} path: {}", claims.user_id, ctx.path()); - // Parse pagination from query parameters let path = ctx.path(); let query_params = if let Some(query_start) = path.find('?') { diff --git a/flumi/Assets/Icons/arrow-down-up.svg b/flumi/Assets/Icons/arrow-down-up.svg new file mode 100644 index 0000000..caac118 --- /dev/null +++ b/flumi/Assets/Icons/arrow-down-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/arrow-down-up.svg.import b/flumi/Assets/Icons/arrow-down-up.svg.import new file mode 100644 index 0000000..f3582cc --- /dev/null +++ b/flumi/Assets/Icons/arrow-down-up.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dv4nw2ly6hcrr" +path="res://.godot/imported/arrow-down-up.svg-e4447ec644884e537fc08b18ae7bb6dc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/arrow-down-up.svg" +dest_files=["res://.godot/imported/arrow-down-up.svg-e4447ec644884e537fc08b18ae7bb6dc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/braces.svg b/flumi/Assets/Icons/braces.svg new file mode 100644 index 0000000..f0b5674 --- /dev/null +++ b/flumi/Assets/Icons/braces.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/braces.svg.import b/flumi/Assets/Icons/braces.svg.import new file mode 100644 index 0000000..cd4c9f8 --- /dev/null +++ b/flumi/Assets/Icons/braces.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://k6yp5f5io1mh" +path="res://.godot/imported/braces.svg-be0ddfb3b648ce88f7cbe89a363017a8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/braces.svg" +dest_files=["res://.godot/imported/braces.svg-be0ddfb3b648ce88f7cbe89a363017a8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/case-sensitive.svg b/flumi/Assets/Icons/case-sensitive.svg new file mode 100644 index 0000000..69d8780 --- /dev/null +++ b/flumi/Assets/Icons/case-sensitive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/case-sensitive.svg.import b/flumi/Assets/Icons/case-sensitive.svg.import new file mode 100644 index 0000000..ec2fea4 --- /dev/null +++ b/flumi/Assets/Icons/case-sensitive.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cxv5qglx3e52a" +path="res://.godot/imported/case-sensitive.svg-86490276d97b2dfa5c8d69e899cd6fc8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/case-sensitive.svg" +dest_files=["res://.godot/imported/case-sensitive.svg-86490276d97b2dfa5c8d69e899cd6fc8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/file-text.svg b/flumi/Assets/Icons/file-text.svg new file mode 100644 index 0000000..c8a1ee0 --- /dev/null +++ b/flumi/Assets/Icons/file-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/file-text.svg.import b/flumi/Assets/Icons/file-text.svg.import new file mode 100644 index 0000000..b9216b6 --- /dev/null +++ b/flumi/Assets/Icons/file-text.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bo1jlyb03xum4" +path="res://.godot/imported/file-text.svg-353070fe8162736240f258d043a74bb8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/file-text.svg" +dest_files=["res://.godot/imported/file-text.svg-353070fe8162736240f258d043a74bb8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/image.svg b/flumi/Assets/Icons/image.svg new file mode 100644 index 0000000..384b9f5 --- /dev/null +++ b/flumi/Assets/Icons/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/image.svg.import b/flumi/Assets/Icons/image.svg.import new file mode 100644 index 0000000..9dfab58 --- /dev/null +++ b/flumi/Assets/Icons/image.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bpkr68d2gousl" +path="res://.godot/imported/image.svg-cb997eeaa724d7751463205eebbff36d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/image.svg" +dest_files=["res://.godot/imported/image.svg-cb997eeaa724d7751463205eebbff36d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Assets/Icons/palette.svg b/flumi/Assets/Icons/palette.svg new file mode 100644 index 0000000..991e1c3 --- /dev/null +++ b/flumi/Assets/Icons/palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/palette.svg.import b/flumi/Assets/Icons/palette.svg.import new file mode 100644 index 0000000..da6e7af --- /dev/null +++ b/flumi/Assets/Icons/palette.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://brm4a7hb5316w" +path="res://.godot/imported/palette.svg-5fa12e1482da68f886eda54557eaaf30.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/palette.svg" +dest_files=["res://.godot/imported/palette.svg-5fa12e1482da68f886eda54557eaaf30.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Resources/NetworkRequestItem_Normal.tres b/flumi/Resources/NetworkRequestItem_Normal.tres new file mode 100644 index 0000000..9116fbb --- /dev/null +++ b/flumi/Resources/NetworkRequestItem_Normal.tres @@ -0,0 +1,12 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://bxmcq4y0i4hl4"] + +[resource] +content_margin_left = 5.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.188371, 0.188371, 0.188371, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/flumi/Resources/NetworkRequestItem_Selected.tres b/flumi/Resources/NetworkRequestItem_Selected.tres new file mode 100644 index 0000000..91af10a --- /dev/null +++ b/flumi/Resources/NetworkRequestItem_Selected.tres @@ -0,0 +1,12 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://clvnmfql36jit"] + +[resource] +content_margin_left = 5.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.2, 0.4, 0.8, 0.3) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 diff --git a/flumi/Scenes/DevTools.tscn b/flumi/Scenes/DevTools.tscn index ae30000..5027a85 100644 --- a/flumi/Scenes/DevTools.tscn +++ b/flumi/Scenes/DevTools.tscn @@ -1,17 +1,15 @@ -[gd_scene load_steps=19 format=3 uid="uid://cgav3xl2xgupb"] +[gd_scene load_steps=26 format=3 uid="uid://cgav3xl2xgupb"] [ext_resource type="Script" uid="uid://vrobqac6makc" path="res://Scripts/DevToolsConsole.gd" id="2_3m6n9"] [ext_resource type="Texture2D" uid="uid://custohlvwclqs" path="res://Assets/Icons/eraser.svg" id="3_6hj4c"] [ext_resource type="Texture2D" uid="uid://cqg4eny0nyojd" path="res://Assets/Icons/funnel.svg" id="4_ynqb1"] [ext_resource type="SyntaxHighlighter" uid="uid://d0aeuvwp0545i" path="res://Resources/LuaSyntaxHighlighter.tres" id="5_xkykt"] +[ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="6_8muo7"] +[ext_resource type="Script" uid="uid://dh3jdrot4r7m3" path="res://Scripts/NetworkTab.gd" id="7_network"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6hj4c"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6hj4c"] -content_margin_left = 2.0 -content_margin_top = 2.0 -content_margin_right = 2.0 -content_margin_bottom = 2.0 bg_color = Color(0.105882, 0.105882, 0.105882, 1) [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qb7hm"] @@ -120,8 +118,62 @@ corner_radius_bottom_left = 8 expand_margin_left = 4.0 expand_margin_right = 4.0 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y2h58"] +content_margin_left = 8.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.125911, 0.125911, 0.125911, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.498039, 0.498039, 0.498039, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_couro"] +content_margin_left = 8.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.498039, 0.498039, 0.498039, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7sefc"] +content_margin_left = 8.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.498039, 0.498039, 0.498039, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xkykt"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c62ub"] +content_margin_top = 5.0 +bg_color = Color(0.105882, 0.105882, 0.105882, 1) + [node name="DevTools" type="VBoxContainer"] -custom_minimum_size = Vector2(450, 400) +custom_minimum_size = Vector2(495, 400) size_flags_vertical = 3 [node name="TabContainer" type="TabContainer" parent="."] @@ -136,13 +188,21 @@ theme_override_styles/tab_selected = SubResource("StyleBoxFlat_ynqb1") theme_override_styles/tab_hovered = SubResource("StyleBoxFlat_8muo7") theme_override_styles/tab_unselected = SubResource("StyleBoxFlat_xkykt") tab_alignment = 1 -current_tab = 0 +current_tab = 1 drag_to_rearrange_enabled = true +[node name="Elements" type="Label" parent="TabContainer"] +visible = false +layout_mode = 2 +text = "Elements tab - Coming soon" +horizontal_alignment = 1 +vertical_alignment = 1 +metadata/_tab_index = 0 + [node name="Console" type="VBoxContainer" parent="TabContainer"] layout_mode = 2 script = ExtResource("2_3m6n9") -metadata/_tab_index = 0 +metadata/_tab_index = 1 [node name="Toolbar" type="HBoxContainer" parent="TabContainer/Console"] layout_mode = 2 @@ -196,7 +256,6 @@ layout_mode = 2 size_flags_horizontal = 3 theme_override_styles/normal = SubResource("StyleBoxFlat_up43w") theme_override_styles/focus = SubResource("StyleBoxFlat_qb3ke") -text = "test" placeholder_text = "Enter Lua code..." scroll_fit_content_height = true caret_blink = true @@ -210,14 +269,6 @@ auto_brace_completion_highlight_matching = true [node name="PositioningTimer" type="Timer" parent="TabContainer/Console/InputContainer"] -[node name="Elements" type="Label" parent="TabContainer"] -visible = false -layout_mode = 2 -text = "Elements tab - Coming soon" -horizontal_alignment = 1 -vertical_alignment = 1 -metadata/_tab_index = 1 - [node name="Sources" type="Label" parent="TabContainer"] visible = false layout_mode = 2 @@ -226,13 +277,187 @@ horizontal_alignment = 1 vertical_alignment = 1 metadata/_tab_index = 2 -[node name="Network" type="Label" parent="TabContainer"] +[node name="Network" type="VBoxContainer" parent="TabContainer"] visible = false layout_mode = 2 -text = "Network tab - Coming soon" +script = ExtResource("7_network") +metadata/_tab_index = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/Network"] +layout_mode = 2 + +[node name="StatusBar" type="HBoxContainer" parent="TabContainer/Network/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 16 + +[node name="RequestCount" type="Label" parent="TabContainer/Network/HBoxContainer/StatusBar"] +layout_mode = 2 +text = "0 requests" + +[node name="Transfer" type="Label" parent="TabContainer/Network/HBoxContainer/StatusBar"] +layout_mode = 2 +text = "0 B transferred" + +[node name="Loaded" type="Label" parent="TabContainer/Network/HBoxContainer/StatusBar"] +layout_mode = 2 +text = "0 resources loaded" + +[node name="Control" type="Control" parent="TabContainer/Network/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="FilterDropdown" type="OptionButton" parent="TabContainer/Network/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme = ExtResource("6_8muo7") +theme_override_colors/font_hover_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/font_hover_color = Color(1, 1, 1, 1) +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_colors/font_focus_color = Color(1, 1, 1, 1) +theme_override_colors/font_pressed_color = Color(1, 1, 1, 1) +theme_override_styles/hover = SubResource("StyleBoxFlat_y2h58") +theme_override_styles/pressed = SubResource("StyleBoxFlat_couro") +theme_override_styles/normal = SubResource("StyleBoxFlat_7sefc") +selected = 0 +item_count = 9 +popup/item_0/text = "All" +popup/item_0/id = 0 +popup/item_1/text = "Fetch" +popup/item_1/id = 1 +popup/item_2/text = "Doc" +popup/item_2/id = 2 +popup/item_3/text = "CSS" +popup/item_3/id = 3 +popup/item_4/text = "Lua" +popup/item_4/id = 4 +popup/item_5/text = "Font" +popup/item_5/id = 5 +popup/item_6/text = "Img" +popup/item_6/id = 6 +popup/item_7/text = "Socket" +popup/item_7/id = 7 +popup/item_8/text = "Other" +popup/item_8/id = 8 + +[node name="HSeparator2" type="HSeparator" parent="TabContainer/Network"] +layout_mode = 2 +theme_override_constants/separation = 14 + +[node name="MainContainer" type="HSplitContainer" parent="TabContainer/Network"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="LeftPanel" type="VBoxContainer" parent="TabContainer/Network/MainContainer"] +layout_mode = 2 + +[node name="HeaderRow" type="HBoxContainer" parent="TabContainer/Network/MainContainer/LeftPanel"] +custom_minimum_size = Vector2(0, 28) +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="IconHeader" type="Control" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +custom_minimum_size = Vector2(25, 20) +layout_mode = 2 + +[node name="NameHeader" type="Label" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "Name" +vertical_alignment = 1 + +[node name="StatusHeader" type="Label" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Status" horizontal_alignment = 1 vertical_alignment = 1 -metadata/_tab_index = 3 + +[node name="TypeHeader" type="Label" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(60, 0) +layout_mode = 2 +text = "Type" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="SizeHeader" type="Label" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +text = "Size" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="TimeHeader" type="Label" parent="TabContainer/Network/MainContainer/LeftPanel/HeaderRow"] +unique_name_in_owner = true +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Time" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Network/MainContainer/LeftPanel"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="RequestList" type="VBoxContainer" parent="TabContainer/Network/MainContainer/LeftPanel/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="RightPanel" type="VBoxContainer" parent="TabContainer/Network/MainContainer"] +visible = false +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PanelContainer" type="PanelContainer" parent="TabContainer/Network/MainContainer/RightPanel"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_6hj4c") + +[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 0 + +[node name="CloseButton" type="Button" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 +theme_override_styles/focus = SubResource("StyleBoxEmpty_xkykt") +text = "✕" +flat = true + +[node name="TabContainer" type="TabContainer" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/side_margin = 0 +theme_override_styles/tab_focus = SubResource("StyleBoxEmpty_6hj4c") +theme_override_styles/tabbar_background = SubResource("StyleBoxFlat_6hj4c") +theme_override_styles/panel = SubResource("StyleBoxFlat_c62ub") +theme_override_styles/tab_selected = SubResource("StyleBoxFlat_ynqb1") +theme_override_styles/tab_hovered = SubResource("StyleBoxFlat_8muo7") +theme_override_styles/tab_unselected = SubResource("StyleBoxFlat_xkykt") +current_tab = 0 + +[node name="Headers" type="VBoxContainer" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="Preview" type="VBoxContainer" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 1 + +[node name="Response" type="VBoxContainer" parent="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 2 [node name="Application" type="Label" parent="TabContainer"] visible = false @@ -241,3 +466,6 @@ text = "Application tab - Coming soon" horizontal_alignment = 1 vertical_alignment = 1 metadata/_tab_index = 4 + +[connection signal="item_selected" from="TabContainer/Network/HBoxContainer/FilterDropdown" to="TabContainer/Network" method="_on_filter_selected"] +[connection signal="pressed" from="TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/CloseButton" to="TabContainer/Network" method="hide_details_panel"] diff --git a/flumi/Scenes/NetworkRequestItem.tscn b/flumi/Scenes/NetworkRequestItem.tscn index f3f3e46..f2b86aa 100644 --- a/flumi/Scenes/NetworkRequestItem.tscn +++ b/flumi/Scenes/NetworkRequestItem.tscn @@ -1,19 +1,26 @@ -[gd_scene load_steps=2 format=3 uid="uid://dqvgywj71hfry"] +[gd_scene load_steps=4 format=3 uid="uid://dqvgywj71hfry"] -[ext_resource type="Script" path="res://Scripts/NetworkRequestItem.gd" id="1_8v2qr"] +[ext_resource type="Script" uid="uid://bcs7m624uvv3x" path="res://Scripts/NetworkRequestItem.gd" id="1_8v2qr"] +[ext_resource type="StyleBox" uid="uid://bxmcq4y0i4hl4" path="res://Resources/NetworkRequestItem_Normal.tres" id="2_normal"] +[ext_resource type="StyleBox" uid="uid://clvnmfql36jit" path="res://Resources/NetworkRequestItem_Selected.tres" id="3_selected"] [node name="NetworkRequestItem" type="PanelContainer"] custom_minimum_size = Vector2(0, 28) +theme_override_styles/panel = ExtResource("2_normal") script = ExtResource("1_8v2qr") +metadata/normal_style = ExtResource("2_normal") +metadata/selected_style = ExtResource("3_selected") [node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 2 +theme_override_constants/separation = 8 [node name="IconContainer" type="Control" parent="HBoxContainer"] -layout_mode = 2 custom_minimum_size = Vector2(20, 20) +layout_mode = 2 [node name="Icon" type="TextureRect" parent="HBoxContainer/IconContainer"] +custom_minimum_size = Vector2(16, 16) layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 @@ -26,41 +33,43 @@ offset_right = 8.0 offset_bottom = 8.0 grow_horizontal = 2 grow_vertical = 2 -custom_minimum_size = Vector2(16, 16) [node name="NameLabel" type="Label" parent="HBoxContainer"] +clip_contents = true +custom_minimum_size = Vector2(140, 0) layout_mode = 2 size_flags_horizontal = 3 -custom_minimum_size = Vector2(140, 0) text = "Request Name" -clip_contents = true -text_overrun_behavior = 3 vertical_alignment = 1 +text_overrun_behavior = 3 [node name="StatusLabel" type="Label" parent="HBoxContainer"] -layout_mode = 2 custom_minimum_size = Vector2(60, 0) +layout_mode = 2 text = "200" horizontal_alignment = 1 vertical_alignment = 1 +metadata/success_color = Color(0, 1, 0, 1) +metadata/error_color = Color(1, 0, 0, 1) +metadata/pending_color = Color(1, 1, 0, 1) [node name="TypeLabel" type="Label" parent="HBoxContainer"] -layout_mode = 2 custom_minimum_size = Vector2(60, 0) +layout_mode = 2 text = "Fetch" horizontal_alignment = 1 vertical_alignment = 1 [node name="SizeLabel" type="Label" parent="HBoxContainer"] +custom_minimum_size = Vector2(60, 0) layout_mode = 2 -custom_minimum_size = Vector2(80, 0) text = "1.2 KB" horizontal_alignment = 2 vertical_alignment = 1 [node name="TimeLabel" type="Label" parent="HBoxContainer"] -layout_mode = 2 custom_minimum_size = Vector2(80, 0) +layout_mode = 2 text = "125ms" horizontal_alignment = 2 -vertical_alignment = 1 \ No newline at end of file +vertical_alignment = 1 diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index f93bb24..06d1e28 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -39,5 +39,9 @@ var HTML_CONTENT = """ New tab -

test

+ +

Welcome to Flumi Browser!

+Test image +

This page includes a test image to verify network functionality.

+ """.to_utf8_buffer() diff --git a/flumi/Scripts/DevToolsConsole.gd b/flumi/Scripts/DevToolsConsole.gd index 8f5b82a..1effed0 100644 --- a/flumi/Scripts/DevToolsConsole.gd +++ b/flumi/Scripts/DevToolsConsole.gd @@ -26,7 +26,6 @@ func initialize_filter() -> void: current_filter = "" func connect_signals() -> void: - Trace.get_instance().log_message.connect(_on_trace_message) clear_button.pressed.connect(_on_clear_pressed) input_line.gui_input.connect(_on_input_gui_input) @@ -37,9 +36,6 @@ func load_existing_logs() -> void: for msg in existing_messages: add_log_entry(msg.message, msg.level, msg.timestamp) -func _on_trace_message(message: Variant, level: String, timestamp: float) -> void: - call_deferred("add_log_entry", message, level, timestamp) - func _on_lua_print(message: String) -> void: add_log_entry(message, "lua", Time.get_ticks_msec() / 1000.0) @@ -84,31 +80,14 @@ func add_log_entry(message: Variant, level: String, timestamp: float) -> void: func create_log_item(entry: Dictionary) -> Control: if entry.level == "input": - var message_code_edit = CodeEdit.new() var input_display_text = get_display_text_for_entry(entry) - message_code_edit.text = input_display_text - message_code_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL - message_code_edit.scroll_fit_content_height = true - message_code_edit.editable = true - message_code_edit.context_menu_enabled = true - message_code_edit.shortcut_keys_enabled = true - message_code_edit.selecting_enabled = true - message_code_edit.deselect_on_focus_loss_enabled = true - message_code_edit.drag_and_drop_selection_enabled = false - message_code_edit.virtual_keyboard_enabled = false - message_code_edit.middle_mouse_paste_enabled = false - - var code_style_normal = StyleBoxFlat.new() - code_style_normal.bg_color = Color.TRANSPARENT - code_style_normal.border_width_left = 0 - code_style_normal.border_width_top = 0 - code_style_normal.border_width_right = 0 - code_style_normal.border_width_bottom = 0 - code_style_normal.content_margin_bottom = 8 - message_code_edit.add_theme_stylebox_override("normal", code_style_normal) - message_code_edit.add_theme_stylebox_override("focus", code_style_normal) - - message_code_edit.syntax_highlighter = input_line.syntax_highlighter.duplicate() + 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)) diff --git a/flumi/Scripts/GurtProtocol.gd b/flumi/Scripts/GurtProtocol.gd index 6df9a3f..0959774 100644 --- a/flumi/Scripts/GurtProtocol.gd +++ b/flumi/Scripts/GurtProtocol.gd @@ -2,7 +2,7 @@ extends RefCounted class_name GurtProtocol const DNS_SERVER_IP: String = "135.125.163.131" -const DNS_SERVER_PORT: int = 8877 +const DNS_SERVER_PORT: int = 4878 static func is_gurt_domain(url: String) -> bool: if url.begins_with("gurt://"): diff --git a/flumi/Scripts/LuaCodeEdit.gd.uid b/flumi/Scripts/LuaCodeEdit.gd.uid deleted file mode 100644 index 1f1a4ba..0000000 --- a/flumi/Scripts/LuaCodeEdit.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cer1rniskhi24 diff --git a/flumi/Scripts/LuaSyntaxHighlighter.gd b/flumi/Scripts/LuaSyntaxHighlighter.gd index 1b0a922..1c5bceb 100644 --- a/flumi/Scripts/LuaSyntaxHighlighter.gd +++ b/flumi/Scripts/LuaSyntaxHighlighter.gd @@ -3,7 +3,7 @@ class_name LuaSyntaxHighlighter extends SyntaxHighlighter @export_group("Colors") -@export var font_color: Color = Color("#d4d4d4", Color.WHITE) +@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) @@ -48,9 +48,8 @@ func _init() -> void: for m in members: _member_keywords[m] = true -func _is_whitespace(char: String) -> bool: - return char == " " or char == "\t" - +func _is_whitespace(ch: String) -> bool: + return ch == " " or ch == "\t" func _clear_highlighting_cache(): _state_cache.clear() @@ -142,9 +141,9 @@ func _get_line_syntax_highlighting(p_line: int) -> Dictionary: 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 char = line_text[i] - if (is_hex and char.is_valid_hex_number(false)) or \ - (not is_hex and (char.is_valid_int() or char in "Ee.-+")): + 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 diff --git a/flumi/Scripts/Network.gd b/flumi/Scripts/Network.gd index 267db0f..7c50011 100644 --- a/flumi/Scripts/Network.gd +++ b/flumi/Scripts/Network.gd @@ -7,12 +7,19 @@ func fetch_image(url: String) -> ImageTexture: if url.is_empty(): return null + var network_request = NetworkManager.start_request(url, "GET", false) + var request_headers = PackedStringArray() request_headers.append("User-Agent: " + UserAgent.get_user_agent()) + var headers_dict = {} + headers_dict["User-Agent"] = UserAgent.get_user_agent() + NetworkManager.set_request_headers(network_request.id, headers_dict) + var error = http_request.request(url, request_headers) if error != OK: print("Error making HTTP request: ", error) + NetworkManager.fail_request(network_request.id, "HTTP request error: " + str(error)) http_request.queue_free() return null @@ -25,10 +32,19 @@ func fetch_image(url: String) -> ImageTexture: http_request.queue_free() + var response_headers = {} + for header in headers: + var parts = header.split(":", 1) + if parts.size() == 2: + response_headers[parts[0].strip_edges()] = parts[1].strip_edges() + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: print("Failed to fetch image. Result: ", result, " Response code: ", response_code) + NetworkManager.complete_request(network_request.id, response_code, "Request failed", response_headers, body.get_string_from_utf8(), body) return null + NetworkManager.complete_request(network_request.id, response_code, "OK", response_headers, body.get_string_from_utf8(), body) + # Get content type from headers var content_type = "" for header in headers: @@ -74,12 +90,19 @@ func fetch_text(url: String) -> String: http_request.queue_free() return "" + var network_request = NetworkManager.start_request(url, "GET", false) + var request_headers = PackedStringArray() request_headers.append("User-Agent: " + UserAgent.get_user_agent()) + var headers_dict = {} + headers_dict["User-Agent"] = UserAgent.get_user_agent() + NetworkManager.set_request_headers(network_request.id, headers_dict) + var error = http_request.request(url, request_headers) if error != OK: print("Error making HTTP request for text resource: ", url, " Error: ", error) + NetworkManager.fail_request(network_request.id, "HTTP request error: " + str(error)) http_request.queue_free() return "" @@ -87,15 +110,27 @@ func fetch_text(url: String) -> String: var result = response[0] # HTTPClient.Result var response_code = response[1] # int + var headers = response[2] # PackedStringArray var body = response[3] # PackedByteArray http_request.queue_free() + var response_headers = {} + for header in headers: + var parts = header.split(":", 1) + if parts.size() == 2: + response_headers[parts[0].strip_edges()] = parts[1].strip_edges() + + var response_body = body.get_string_from_utf8() + if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: print("Failed to fetch text resource. URL: ", url, " Result: ", result, " Response code: ", response_code) + NetworkManager.complete_request(network_request.id, response_code, "Request failed", response_headers, response_body) return "" - return body.get_string_from_utf8() + NetworkManager.complete_request(network_request.id, response_code, "OK", response_headers, response_body) + + return response_body func fetch_external_resource(url: String, base_url: String = "") -> String: var resolved_url = URLUtils.resolve_url(base_url, url) @@ -118,13 +153,15 @@ func fetch_gurt_resource(url: String) -> String: if gurt_url.contains("localhost"): gurt_url = gurt_url.replace("localhost", "127.0.0.1") + var network_request = NetworkManager.start_request(gurt_url, "GET", false) + var client = GurtProtocolClient.new() for ca_cert in CertificateManager.trusted_ca_certificates: client.add_ca_certificate(ca_cert) - if not client.create_client(30): - print("GURT resource error: Failed to create client") + if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT): + NetworkManager.fail_request(network_request.id, "Failed to create GURT client") return "" var host_domain = gurt_url @@ -142,9 +179,16 @@ func fetch_gurt_resource(url: String) -> String: if not response or not response.is_success: var error_msg = "Failed to load GURT resource" + var status_code = 0 if response: + status_code = response.status_code error_msg += ": " + str(response.status_code) + " " + response.status_message - print("GURT resource error: ", error_msg) + NetworkManager.complete_request(network_request.id, status_code, error_msg, {}, "") return "" - return response.body.get_string_from_utf8() + var response_headers = response.headers if response.headers else {} + var response_body = response.body.get_string_from_utf8() + + NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body) + + return response_body diff --git a/flumi/Scripts/NetworkManager.gd b/flumi/Scripts/NetworkManager.gd new file mode 100644 index 0000000..8dad50f --- /dev/null +++ b/flumi/Scripts/NetworkManager.gd @@ -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 + } diff --git a/flumi/Scripts/NetworkManager.gd.uid b/flumi/Scripts/NetworkManager.gd.uid new file mode 100644 index 0000000..05f8707 --- /dev/null +++ b/flumi/Scripts/NetworkManager.gd.uid @@ -0,0 +1 @@ +uid://ggm1sq7h64sr diff --git a/flumi/Scripts/NetworkRequest.gd b/flumi/Scripts/NetworkRequest.gd new file mode 100644 index 0000000..571b303 --- /dev/null +++ b/flumi/Scripts/NetworkRequest.gd @@ -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") diff --git a/flumi/Scripts/NetworkRequest.gd.uid b/flumi/Scripts/NetworkRequest.gd.uid new file mode 100644 index 0000000..eaeca1f --- /dev/null +++ b/flumi/Scripts/NetworkRequest.gd.uid @@ -0,0 +1 @@ +uid://r6h4tvud6yne diff --git a/flumi/Scripts/NetworkRequestItem.gd b/flumi/Scripts/NetworkRequestItem.gd index 7f442e8..41e67e8 100644 --- a/flumi/Scripts/NetworkRequestItem.gd +++ b/flumi/Scripts/NetworkRequestItem.gd @@ -11,24 +11,16 @@ extends PanelContainer 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(): - # Set up styles for different states - var style_normal = StyleBoxFlat.new() - style_normal.bg_color = Color.TRANSPARENT - style_normal.content_margin_left = 5 - style_normal.content_margin_bottom = 5 - style_normal.content_margin_right = 5 - style_normal.content_margin_top = 5 - style_normal.corner_radius_bottom_left = 8 - style_normal.corner_radius_bottom_right = 8 - style_normal.corner_radius_top_left = 8 - style_normal.corner_radius_top_right = 8 - - add_theme_stylebox_override("panel", style_normal) - - # Set up mouse handling mouse_filter = Control.MOUSE_FILTER_PASS gui_input.connect(_on_gui_input) @@ -48,7 +40,7 @@ func update_display(): name_label.text = request.name status_label.text = request.get_status_display() type_label.text = request.get_type_display() - size_label.text = request.get_size_display() + size_label.text = NetworkRequest.format_bytes(request.size) time_label.text = request.get_time_display() # Color code status @@ -67,32 +59,12 @@ func _on_gui_input(event: InputEvent): func set_selected(selected: bool): if selected: - var style_selected = StyleBoxFlat.new() - style_selected.bg_color = Color(0.2, 0.4, 0.8, 0.3) - style_selected.content_margin_left = 5 - style_selected.content_margin_bottom = 5 - style_selected.content_margin_right = 5 - style_selected.content_margin_top = 5 - style_selected.corner_radius_bottom_left = 8 - style_selected.corner_radius_bottom_right = 8 - style_selected.corner_radius_top_left = 8 - style_selected.corner_radius_top_right = 8 - add_theme_stylebox_override("panel", style_selected) + add_theme_stylebox_override("panel", selected_style) else: - var style_normal = StyleBoxFlat.new() - style_normal.bg_color = Color.TRANSPARENT - style_normal.content_margin_left = 5 - style_normal.content_margin_bottom = 5 - style_normal.content_margin_right = 5 - style_normal.content_margin_top = 5 - style_normal.corner_radius_bottom_left = 8 - style_normal.corner_radius_bottom_right = 8 - style_normal.corner_radius_top_left = 8 - style_normal.corner_radius_top_right = 8 - add_theme_stylebox_override("panel", style_normal) + 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 \ No newline at end of file + time_label.visible = !should_hide diff --git a/flumi/Scripts/NetworkRequestItem.gd.uid b/flumi/Scripts/NetworkRequestItem.gd.uid new file mode 100644 index 0000000..5257942 --- /dev/null +++ b/flumi/Scripts/NetworkRequestItem.gd.uid @@ -0,0 +1 @@ +uid://bcs7m624uvv3x diff --git a/flumi/Scripts/NetworkTab.gd b/flumi/Scripts/NetworkTab.gd new file mode 100644 index 0000000..0c24ddd --- /dev/null +++ b/flumi/Scripts/NetworkTab.gd @@ -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() diff --git a/flumi/Scripts/NetworkTab.gd.uid b/flumi/Scripts/NetworkTab.gd.uid new file mode 100644 index 0000000..496d8b3 --- /dev/null +++ b/flumi/Scripts/NetworkTab.gd.uid @@ -0,0 +1 @@ +uid://dh3jdrot4r7m3 diff --git a/flumi/Scripts/Tab.gd b/flumi/Scripts/Tab.gd index 7ef3ec0..893da56 100644 --- a/flumi/Scripts/Tab.gd +++ b/flumi/Scripts/Tab.gd @@ -35,7 +35,7 @@ var loading_tween: Tween var scroll_container: ScrollContainer = null var website_container: VBoxContainer = null var background_panel: PanelContainer = null -var main_hbox: HBoxContainer = null +var main_hbox: HSplitContainer = null var dev_tools: Control = null var dev_tools_visible: bool = false var lua_apis: Array[LuaAPI] = [] @@ -150,7 +150,7 @@ func init_scene(parent_container: Control) -> void: style_box.bg_color = Color(1, 1, 1, 1) # White background background_panel.add_theme_stylebox_override("panel", style_box) - main_hbox = HBoxContainer.new() + 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 diff --git a/flumi/Scripts/Utils/CodeEditUtils.gd b/flumi/Scripts/Utils/CodeEditUtils.gd new file mode 100644 index 0000000..0bc1d58 --- /dev/null +++ b/flumi/Scripts/Utils/CodeEditUtils.gd @@ -0,0 +1,94 @@ +class_name CodeEditUtils + +static func create_code_edit(options: Dictionary = {}) -> CodeEdit: + var code_edit = CodeEdit.new() + + # Default configuration + var defaults = { + "text": "", + "editable": true, + "size_flags_horizontal": Control.SIZE_EXPAND_FILL, + "size_flags_vertical": Control.SIZE_EXPAND_FILL, + "scroll_fit_content_height": false, + "context_menu_enabled": true, + "shortcut_keys_enabled": true, + "selecting_enabled": true, + "deselect_on_focus_loss_enabled": true, + "drag_and_drop_selection_enabled": false, + "virtual_keyboard_enabled": false, + "middle_mouse_paste_enabled": false, + "show_line_numbers": false, + "syntax_highlighter": null, + "transparent_background": false, + "block_editing_signals": false + } + + # Merge user options with defaults + for key in defaults: + if options.has(key): + defaults[key] = options[key] + + # Apply basic properties + code_edit.text = defaults.text + code_edit.size_flags_horizontal = defaults.size_flags_horizontal + code_edit.size_flags_vertical = defaults.size_flags_vertical + code_edit.scroll_fit_content_height = defaults.scroll_fit_content_height + code_edit.context_menu_enabled = defaults.context_menu_enabled + code_edit.shortcut_keys_enabled = defaults.shortcut_keys_enabled + code_edit.selecting_enabled = defaults.selecting_enabled + code_edit.deselect_on_focus_loss_enabled = defaults.deselect_on_focus_loss_enabled + code_edit.drag_and_drop_selection_enabled = defaults.drag_and_drop_selection_enabled + code_edit.virtual_keyboard_enabled = defaults.virtual_keyboard_enabled + code_edit.middle_mouse_paste_enabled = defaults.middle_mouse_paste_enabled + + # Line numbers + if defaults.show_line_numbers: + code_edit.gutters_draw_line_numbers = true + + # Syntax highlighter + if defaults.syntax_highlighter: + code_edit.syntax_highlighter = defaults.syntax_highlighter + + # Transparent background styling + if defaults.transparent_background: + var code_style_normal = StyleBoxFlat.new() + code_style_normal.bg_color = Color.TRANSPARENT + code_style_normal.border_width_left = 0 + code_style_normal.border_width_top = 0 + code_style_normal.border_width_right = 0 + code_style_normal.border_width_bottom = 0 + code_style_normal.content_margin_bottom = 8 + code_edit.add_theme_stylebox_override("normal", code_style_normal) + code_edit.add_theme_stylebox_override("focus", code_style_normal) + + # Block editing + # This is because Godot applies some transparency when we simply set editable=false, which I cant be bothered to fix + if defaults.block_editing_signals and defaults.editable: + code_edit.gui_input.connect(_block_editing_input) + + return code_edit + +static func _block_editing_input(event: InputEvent): + # Block text modification events while allowing selection and copy + if event is InputEventKey: + var key_event = event as InputEventKey + # Allow Ctrl+C (copy), Ctrl+A (select all), arrow keys, etc. + if key_event.pressed: + # Allow copy operations + if key_event.ctrl_pressed and key_event.keycode == KEY_C: + return + if key_event.ctrl_pressed and key_event.keycode == KEY_A: + return + # Allow navigation + if key_event.keycode in [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN]: + return + # Block all other key inputs + if key_event.keycode != KEY_ESCAPE: + event.set_input_as_handled() + +static func create_readonly_code_edit(text: String, options: Dictionary = {}) -> CodeEdit: + var readonly_options = options.duplicate() + readonly_options["text"] = text + readonly_options["editable"] = false + readonly_options["block_editing_signals"] = false + return create_code_edit(readonly_options) diff --git a/flumi/Scripts/Utils/CodeEditUtils.gd.uid b/flumi/Scripts/Utils/CodeEditUtils.gd.uid new file mode 100644 index 0000000..009177d --- /dev/null +++ b/flumi/Scripts/Utils/CodeEditUtils.gd.uid @@ -0,0 +1 @@ +uid://cc7m0rc5oxjnv diff --git a/flumi/Scripts/Utils/Lua/Network.gd b/flumi/Scripts/Utils/Lua/Network.gd index 44ae670..1e272d5 100644 --- a/flumi/Scripts/Utils/Lua/Network.gd +++ b/flumi/Scripts/Utils/Lua/Network.gd @@ -29,14 +29,14 @@ static func resolve_fetch_url(url: String) -> String: return URLUtils.resolve_url(current_domain, url) static func _lua_fetch_handler(vm: LuauVM) -> int: - var url: String = vm.luaL_checkstring(1) + var original_url: String = vm.luaL_checkstring(1) var options: Dictionary = {} if vm.lua_gettop() >= 2 and vm.lua_istable(2): options = vm.lua_todictionary(2) # Resolve relative URLs and default to gurt:// protocol - url = resolve_fetch_url(url) + var url = resolve_fetch_url(original_url) # Default options var method = options.get("method", "GET").to_upper() @@ -296,7 +296,7 @@ static func get_or_create_gurt_client(domain: String) -> GurtProtocolClient: for ca_cert in CertificateManager.trusted_ca_certificates: gurt_client.add_ca_certificate(ca_cert) - if not gurt_client.create_client(10): + if not gurt_client.create_client_with_dns(10, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT): gurt_client = null current_domain = "" return null @@ -316,8 +316,6 @@ static func make_gurt_request(url: String, method: String, headers: PackedString var domain_part = url.replace("gurt://", "") if domain_part.contains("/"): domain_part = domain_part.split("/")[0] - if domain_part.contains(":"): - domain_part = domain_part.split(":")[0] var client = get_or_create_gurt_client(domain_part) if client == null: diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 1e07ef1..652d795 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -36,6 +36,9 @@ const MIN_SIZE = Vector2i(750, 200) var font_dependent_elements: Array = [] var current_domain = "" +var main_navigation_request: NetworkRequest = null +var network_start_time: float = 0.0 +var network_end_time: float = 0.0 func should_group_as_inline(element: HTMLParser.HTMLElement) -> bool: if element.tag_name == "input": @@ -61,7 +64,7 @@ func _ready(): call_deferred("render") -func _input(event: InputEvent) -> void: +func _input(_event: InputEvent) -> void: if Input.is_action_just_pressed("DevTools"): _toggle_dev_tools() get_viewport().set_input_as_handled() @@ -104,6 +107,10 @@ func _on_search_submitted(url: String) -> void: print("Non-GURT URL entered: ", url) func fetch_gurt_content_async(gurt_url: String, tab: Tab, original_url: String) -> void: + main_navigation_request = NetworkManager.start_request(gurt_url, "GET", false) + main_navigation_request.type = NetworkRequest.RequestType.DOC + network_start_time = Time.get_ticks_msec() + var thread = Thread.new() var request_data = {"gurt_url": gurt_url} @@ -125,7 +132,7 @@ func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary: if not client.create_client_with_dns(30, GurtProtocol.DNS_SERVER_IP, GurtProtocol.DNS_SERVER_PORT): client.disconnect() - return {"success": false, "error": "Failed to connect to GURT DNS server"} + return {"success": false, "error": "Failed to connect to GURT DNS server at " + GurtProtocol.DNS_SERVER_IP + ":" + str(GurtProtocol.DNS_SERVER_PORT)} var response = client.request(gurt_url, { "method": "GET" @@ -136,7 +143,7 @@ func _perform_gurt_request_threaded(request_data: Dictionary) -> Dictionary: var error_msg = "Connection failed" if response: error_msg = "GURT %d: %s" % [response.status_code, response.status_message] - elif not response: + else: error_msg = "Request timed out or connection failed" return {"success": false, "error": error_msg} @@ -149,6 +156,7 @@ func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gur return var html_bytes = result.html_bytes + network_end_time = Time.get_ticks_msec() current_domain = gurt_url if not search_bar.has_focus(): @@ -156,6 +164,14 @@ func _handle_gurt_result(result: Dictionary, tab: Tab, original_url: String, gur render_content(html_bytes) + if main_navigation_request: + main_navigation_request.end_time = network_end_time + main_navigation_request.time_ms = network_end_time - network_start_time + var headers = {"content-type": "text/html"} + var body_text = html_bytes.get_string_from_utf8() + NetworkManager.complete_request(main_navigation_request.id, 200, "OK", headers, body_text, html_bytes) + main_navigation_request = null + tab.stop_loading() func handle_gurt_error(error_message: String, tab: Tab) -> void: @@ -182,6 +198,11 @@ func render() -> void: render_content(Constants.HTML_CONTENT) func render_content(html_bytes: PackedByteArray) -> void: + if main_navigation_request: + NetworkManager.clear_all_requests_except(main_navigation_request.id) + else: + NetworkManager.clear_all_requests() + var active_tab = get_active_tab() var target_container: Control diff --git a/flumi/project.godot b/flumi/project.godot index 97c3a98..69bfd56 100644 --- a/flumi/project.godot +++ b/flumi/project.godot @@ -22,6 +22,7 @@ config/icon="uid://ctpe0lbehepen" Constants="*res://Scripts/Constants.gd" Network="*res://Scripts/Network.gd" +NetworkManager="*res://Scripts/NetworkManager.gd" [display] diff --git a/protocol/cli/README.md b/protocol/cli/README.md index 642484a..2f75f18 100644 --- a/protocol/cli/README.md +++ b/protocol/cli/README.md @@ -44,7 +44,7 @@ For production deployments, you can use the Gurted Certificate Authority to get 3. **Follow the DNS challenge instructions:** When prompted, add the TXT record to your domain: - - Go to gurt://localhost:8877 (or your DNS server) + - Go to gurt://dns.web (or your DNS server) - Login and navigate to your domain - Add a TXT record with: - Name: `_gurtca-challenge` diff --git a/protocol/gurtca/src/challenges.rs b/protocol/gurtca/src/challenges.rs index 4fd47db..e9dc6d4 100644 --- a/protocol/gurtca/src/challenges.rs +++ b/protocol/gurtca/src/challenges.rs @@ -30,7 +30,7 @@ async fn verify_dns_txt_record(domain: &str, expected_value: &str, client: &Gurt }); let response = client - .post_json("gurt://localhost:8877/resolve-full", &request) + .post_json("gurt://dns.web/resolve-full", &request) .await?; if response.is_success() { diff --git a/protocol/gurtca/src/main.rs b/protocol/gurtca/src/main.rs index 538de32..aa46f78 100644 --- a/protocol/gurtca/src/main.rs +++ b/protocol/gurtca/src/main.rs @@ -12,7 +12,7 @@ struct Cli { #[command(subcommand)] command: Commands, - #[arg(long, default_value = "gurt://localhost:8877")] + #[arg(long, default_value = "gurt://dns.web")] ca_url: String, } diff --git a/tests/websocket.html b/tests/websocket.html index 08983d3..6a1d513 100644 --- a/tests/websocket.html +++ b/tests/websocket.html @@ -23,7 +23,6 @@