diff --git a/flumi/Scenes/DevTools.tscn b/flumi/Scenes/DevTools.tscn index ace2907..fb2401e 100644 --- a/flumi/Scenes/DevTools.tscn +++ b/flumi/Scenes/DevTools.tscn @@ -489,6 +489,11 @@ visible = false layout_mode = 2 metadata/_tab_index = 2 +[node name="Messages" type="VBoxContainer" parent="DevTools/TabContainer/Network/MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 3 + [node name="Application" type="Label" parent="DevTools/TabContainer"] visible = false layout_mode = 2 diff --git a/flumi/Scripts/Browser/NetworkManager.gd b/flumi/Scripts/Browser/NetworkManager.gd index bc970b0..4316012 100644 --- a/flumi/Scripts/Browser/NetworkManager.gd +++ b/flumi/Scripts/Browser/NetworkManager.gd @@ -138,4 +138,54 @@ func add_completed_request(url: String, method: String, is_from_lua: bool, statu all_requests.append(request) if dev_tools_network_tab: - dev_tools_network_tab.add_network_request(request) \ No newline at end of file + dev_tools_network_tab.add_network_request(request) + +func start_websocket_connection(url: String, websocket_id: String) -> NetworkRequest: + var request = NetworkRequest.create_websocket_connection(url, websocket_id) + active_requests[request.id] = request + all_requests.append(request) + + if dev_tools_network_tab: + dev_tools_network_tab.add_network_request(request) + + request_started.emit(request) + return request + +func add_websocket_message(url: String, websocket_id: String, direction: String, message: String): + var connection_request: NetworkRequest = null + + for request in active_requests.values(): + if request.websocket_id == websocket_id and request.websocket_event_type == "connection": + connection_request = request + break + + if not connection_request: + for request in all_requests: + if request.websocket_id == websocket_id and request.websocket_event_type == "connection": + connection_request = request + break + + if connection_request: + connection_request.add_websocket_message(direction, message) + + if dev_tools_network_tab: + dev_tools_network_tab.update_request_item(connection_request) + + request_completed.emit(connection_request) + +func update_websocket_connection(websocket_id: String, status: String, status_code: int = 200, status_text: String = "OK"): + for request in active_requests.values(): + if request.websocket_id == websocket_id and request.websocket_event_type == "connection": + request.update_websocket_status(status, status_code, status_text) + + if status in ["closed", "error"]: + active_requests.erase(request.id) + + 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) + break \ No newline at end of file diff --git a/flumi/Scripts/Browser/NetworkRequest.gd b/flumi/Scripts/Browser/NetworkRequest.gd index 571b303..73f0125 100644 --- a/flumi/Scripts/Browser/NetworkRequest.gd +++ b/flumi/Scripts/Browser/NetworkRequest.gd @@ -42,6 +42,31 @@ var response_body_bytes: PackedByteArray = [] var mime_type: String = "" var is_from_lua: bool = false +var websocket_id: String = "" +var websocket_event_type: String = "" # "connection", "close", "error" +var connection_status: String = "" # "connecting", "open", "closing", "closed" +var websocket_messages: Array[WebSocketMessage] = [] + +class WebSocketMessage: + var hour: int + var minute: int + var second: int + var direction: String # "sent" or "received" + var content: String + var size: int + + func _init(dir: String, msg: String): + var local_time = Time.get_datetime_dict_from_system(false) + hour = local_time.hour + minute = local_time.minute + second = local_time.second + direction = dir + content = msg + size = msg.length() + + func get_formatted_time() -> String: + return "%02d:%02d:%02d" % [hour, minute, second] + func _init(request_url: String = "", request_method: String = "GET"): id = generate_id() url = request_url @@ -62,6 +87,16 @@ func generate_id() -> String: func extract_name_from_url(request_url: String) -> String: if request_url.is_empty(): return "Unknown" + + if request_url.begins_with("ws://") or request_url.begins_with("wss://"): + if not websocket_event_type.is_empty(): + match websocket_event_type: + "connection": + return "WebSocket" + "close": + return "WebSocket Close" + "error": + return "WebSocket Error" var parts = request_url.split("/") if parts.size() > 0: @@ -177,7 +212,7 @@ static func format_bytes(given_size: int) -> String: elif given_size < 1024 * 1024: return str(given_size / 1024) + " KB" else: - return str(given_size / (1024 * 1024)) + " MB" + return str(given_size / (1024.0 * 1024)) + " MB" func get_time_display() -> String: if status == RequestStatus.PENDING: @@ -205,3 +240,40 @@ func get_icon_texture() -> Texture2D: return load("res://Assets/Icons/arrow-down-up.svg") _: return load("res://Assets/Icons/search.svg") + +static func create_websocket_connection(ws_url: String, ws_id: String) -> NetworkRequest: + var request = NetworkRequest.new(ws_url, "WS") + request.type = RequestType.SOCKET + request.websocket_id = ws_id + request.websocket_event_type = "connection" + request.connection_status = "connecting" + request.is_from_lua = true + return request + +func add_websocket_message(direction: String, message: String): + var ws_message = WebSocketMessage.new(direction, message) + websocket_messages.append(ws_message) + + var total_message_size = 0 + for msg in websocket_messages: + total_message_size += msg.size + size = total_message_size + +func update_websocket_status(new_status: String, status_code: int = 200, status_text: String = "OK"): + connection_status = new_status + self.status_code = status_code + self.status_text = status_text + + match new_status: + "open": + status = RequestStatus.SUCCESS + "closed": + if status_code >= 1000 and status_code < 1100: + status = RequestStatus.SUCCESS + else: + status = RequestStatus.ERROR + "error": + status = RequestStatus.ERROR + + end_time = Time.get_ticks_msec() + time_ms = end_time - start_time diff --git a/flumi/Scripts/Browser/NetworkTab.gd b/flumi/Scripts/Browser/NetworkTab.gd index bc15d84..00fb8e9 100644 --- a/flumi/Scripts/Browser/NetworkTab.gd +++ b/flumi/Scripts/Browser/NetworkTab.gd @@ -11,6 +11,7 @@ const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn") @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 +@onready var messages_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Messages # Header components @onready var status_header: Label = %StatusHeader @@ -31,7 +32,7 @@ const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn") @onready var syntax_highlighter = preload("res://Resources/LuaSyntaxHighlighter.tres") var network_requests: Array[NetworkRequest] = [] -var current_filter: NetworkRequest.RequestType = -1 # -1 means all +var current_filter: int = -1 # -1 means all, otherwise NetworkRequest.RequestType var selected_request: NetworkRequest = null var request_items: Dictionary = {} @@ -66,7 +67,7 @@ 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) + var should_show = (current_filter == -1) or (int(request.type) == current_filter) item.visible = should_show func update_request_item(request: NetworkRequest): @@ -76,6 +77,9 @@ func update_request_item(request: NetworkRequest): request_item.update_display() + if selected_request == request and details_panel.visible: + update_details_panel(request) + apply_filter() update_status_bar() @@ -84,11 +88,19 @@ func update_details_panel(request: NetworkRequest): update_headers_tab(request) update_preview_tab(request) update_response_tab(request) + update_messages_tab(request) + + if request.type == NetworkRequest.RequestType.SOCKET: + messages_tab.visible = true + details_tab_container.set_tab_title(3, "Messages (" + str(request.websocket_messages.size()) + ")") + else: + messages_tab.visible = false 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() + for child in messages_tab.get_children(): child.queue_free() func create_collapsible_section(title: String, expanded: bool = false) -> VBoxContainer: var section = VBoxContainer.new() @@ -154,6 +166,17 @@ func update_headers_tab(request: NetworkRequest): add_header_row(general_content, "Request Method:", request.method) add_header_row(general_content, "Status Code:", str(request.status_code) + " " + request.status_text) + # WebSocket information + if request.type == NetworkRequest.RequestType.SOCKET: + var ws_section = create_collapsible_section("WebSocket Information", true) + headers_tab.add_child(ws_section) + + var ws_content = ws_section.get_child(1) + add_header_row(ws_content, "WebSocket ID:", request.websocket_id) + add_header_row(ws_content, "Event Type:", request.websocket_event_type) + add_header_row(ws_content, "Connection Status:", request.connection_status) + add_header_row(ws_content, "Total Messages:", str(request.websocket_messages.size())) + # Request Headers section if not request.request_headers.is_empty(): var request_headers_section = create_collapsible_section("Request Headers", false) @@ -173,6 +196,32 @@ func update_headers_tab(request: NetworkRequest): add_header_row(response_headers_content, header_name + ":", str(request.response_headers[header_name])) func update_preview_tab(request: NetworkRequest): + if request.type == NetworkRequest.RequestType.SOCKET: + var content_to_show = "" + + match request.websocket_event_type: + "connection": + content_to_show = "WebSocket connection event\n" + content_to_show += "URL: " + request.url + "\n" + content_to_show += "Status: " + request.connection_status + "\n" + if request.status_code > 0: + content_to_show += "Status Code: " + str(request.status_code) + " " + request.status_text + "\n" + content_to_show += "Messages exchanged: " + str(request.websocket_messages.size()) + "close", "error": + content_to_show = "WebSocket " + request.websocket_event_type + " event\n" + content_to_show += "Status Code: " + str(request.status_code) + "\n" + content_to_show += "Reason: " + request.status_text + + if not content_to_show.is_empty(): + var code_edit = CodeEditUtils.create_code_edit({ + "text": content_to_show, + "editable": false, + "show_line_numbers": false, + "syntax_highlighter": null + }) + preview_tab.add_child(code_edit) + return + # 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() @@ -240,6 +289,42 @@ func update_preview_tab(request: NetworkRequest): preview_tab.add_child(code_edit) func update_response_tab(request: NetworkRequest): + if request.type == NetworkRequest.RequestType.SOCKET: + var content_to_show = "" + + match request.websocket_event_type: + "connection": + content_to_show = "WebSocket Connection Details\n\n" + content_to_show += "This is a WebSocket connection request.\n" + content_to_show += "Connection Status: " + request.connection_status + "\n" + content_to_show += "WebSocket ID: " + request.websocket_id + "\n" + content_to_show += "Total Messages: " + str(request.websocket_messages.size()) + "\n" + if request.status_code > 0: + content_to_show += "Status Code: " + str(request.status_code) + " " + request.status_text + "\n" + content_to_show += "\nNote: Individual messages can be viewed in the 'Messages' tab." + "close", "error": + content_to_show = "WebSocket " + request.websocket_event_type.capitalize() + " Event\n\n" + content_to_show += "Status Code: " + str(request.status_code) + "\n" + content_to_show += "Reason: " + request.status_text + "\n" + content_to_show += "WebSocket ID: " + request.websocket_id + "\n" + content_to_show += "Total Messages Exchanged: " + str(request.websocket_messages.size()) + + if not content_to_show.is_empty(): + var code_edit = CodeEditUtils.create_code_edit({ + "text": content_to_show, + "editable": false, + "show_line_numbers": false, + "syntax_highlighter": null + }) + response_tab.add_child(code_edit) + else: + var label = Label.new() + label.text = "No WebSocket data to display" + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + response_tab.add_child(label) + return + 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." @@ -280,6 +365,165 @@ func update_response_tab(request: NetworkRequest): response_tab.add_child(code_edit) +func update_messages_tab(request: NetworkRequest): + if request.type != NetworkRequest.RequestType.SOCKET: + return + + if request.websocket_messages.is_empty(): + var label = Label.new() + label.text = "No WebSocket messages yet" + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + messages_tab.add_child(label) + return + + var scroll_container = ScrollContainer.new() + scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL + + var messages_container = VBoxContainer.new() + messages_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll_container.add_child(messages_container) + + var header_container = VBoxContainer.new() + header_container.add_theme_constant_override("separation", 8) + + var search_container = HBoxContainer.new() + search_container.add_theme_constant_override("separation", 8) + + var search_input = LineEdit.new() + search_input.placeholder_text = "Filter" + search_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + var focus_style = StyleBoxFlat.new() + focus_style.content_margin_left = 16.0 + focus_style.content_margin_right = 8.0 + focus_style.bg_color = Color(0.168627, 0.168627, 0.168627, 1) + focus_style.border_width_left = 1 + focus_style.border_width_top = 1 + focus_style.border_width_right = 1 + focus_style.border_width_bottom = 1 + focus_style.border_color = Color(0.247059, 0.466667, 0.807843, 1) + focus_style.corner_radius_top_left = 15 + focus_style.corner_radius_top_right = 15 + focus_style.corner_radius_bottom_right = 15 + focus_style.corner_radius_bottom_left = 15 + search_input.add_theme_stylebox_override("focus", focus_style) + + var normal_style = StyleBoxFlat.new() + normal_style.content_margin_left = 16.0 + normal_style.content_margin_right = 8.0 + normal_style.bg_color = Color(0.168627, 0.168627, 0.168627, 1) + normal_style.corner_radius_top_left = 15 + normal_style.corner_radius_top_right = 15 + normal_style.corner_radius_bottom_right = 15 + normal_style.corner_radius_bottom_left = 15 + search_input.add_theme_stylebox_override("normal", normal_style) + + search_container.add_child(search_input) + + header_container.add_child(search_container) + + var spacer = Control.new() + spacer.custom_minimum_size.y = 8 + header_container.add_child(spacer) + + messages_container.add_child(header_container) + + var message_rows: Array[Control] = [] + var search_term = "" + + var update_search = func(): + var filter_text = search_input.text.to_lower() + + for row_index in range(message_rows.size()): + var row = message_rows[row_index] + var message = request.websocket_messages[row_index] + var should_show = filter_text.is_empty() or message.content.to_lower().contains(filter_text) + row.visible = should_show + + search_input.text_changed.connect(func(_text): update_search.call()) + + for i in range(request.websocket_messages.size()): + var message = request.websocket_messages[i] + + var message_panel = PanelContainer.new() + message_panel.custom_minimum_size.y = 32 + + var button = Button.new() + button.flat = true + button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND + button.focus_mode = Control.FOCUS_NONE + + button.anchors_preset = Control.PRESET_FULL_RECT + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.size_flags_vertical = Control.SIZE_EXPAND_FILL + + var panel_style = StyleBoxFlat.new() + if message.direction == "sent": + panel_style.bg_color = Color(0.2, 0.3, 0.5, 0.3) + else: + panel_style.bg_color = Color(0.0, 0.0, 0.0, 0.0) + + panel_style.content_margin_left = 6 + panel_style.content_margin_right = 6 + panel_style.content_margin_top = 2 + panel_style.content_margin_bottom = 2 + message_panel.add_theme_stylebox_override("panel", panel_style) + + var message_row = HBoxContainer.new() + message_row.add_theme_constant_override("separation", 8) + + var direction_label = Label.new() + var direction_icon = "↑" if message.direction == "sent" else "↓" + var direction_color = Color(0.7, 0.9, 1.0) if message.direction == "sent" else Color(1.0, 0.7, 0.7) + + direction_label.text = direction_icon + direction_label.add_theme_font_size_override("font_size", 16) + direction_label.add_theme_color_override("font_color", direction_color) + direction_label.custom_minimum_size.x = 16 + direction_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + message_row.add_child(direction_label) + + var timestamp_label = Label.new() + timestamp_label.text = message.get_formatted_time() + timestamp_label.add_theme_font_size_override("font_size", 14) + timestamp_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0)) + timestamp_label.custom_minimum_size.x = 80 + message_row.add_child(timestamp_label) + + var content_label = Label.new() + var content_text = message.content + if content_text.length() > 60: + content_text = content_text.substr(0, 57) + "..." + content_text = content_text.replace("\n", " ").replace("\r", " ") + + content_label.text = content_text + content_label.add_theme_font_size_override("font_size", 16) + content_label.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0, 1.0)) + content_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + content_label.clip_contents = true + content_label.autowrap_mode = TextServer.AUTOWRAP_OFF + message_row.add_child(content_label) + + var size_label = Label.new() + size_label.text = str(message.size) + "b" + size_label.add_theme_font_size_override("font_size", 14) + size_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0)) + size_label.custom_minimum_size.x = 40 + size_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + message_row.add_child(size_label) + + button.pressed.connect(func(): DisplayServer.clipboard_set(message.content)) + + message_panel.add_child(message_row) + message_panel.add_child(button) + + messages_container.add_child(message_panel) + message_rows.append(message_panel) + + messages_tab.add_child(scroll_container) + func update_status_bar(): var total_requests = network_requests.size() var total_size = 0 diff --git a/flumi/Scripts/Utils/Lua/WebSocket.gd b/flumi/Scripts/Utils/Lua/WebSocket.gd index 71dfc83..0a1f7ff 100644 --- a/flumi/Scripts/Utils/Lua/WebSocket.gd +++ b/flumi/Scripts/Utils/Lua/WebSocket.gd @@ -23,6 +23,8 @@ class WebSocketWrapper: if connection_status: return + NetworkManager.call_deferred("start_websocket_connection", url, instance_id) + var error = websocket.connect_to_url(url) if error == OK: @@ -42,8 +44,10 @@ class WebSocketWrapper: timer.call_deferred("start") else: trigger_event("error", {"message": "No scene available for WebSocket timer"}) + NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "No scene available") else: trigger_event("error", {"message": "Failed to connect to " + url + " (error: " + str(error) + ")"}) + NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "Connection failed: " + str(error)) func _poll_websocket(): if not websocket: @@ -57,17 +61,25 @@ class WebSocketWrapper: if not connection_status: connection_status = true trigger_event("open", {}) + # Update NetworkManager with successful connection + NetworkManager.call_deferred("update_websocket_connection", instance_id, "open", 200, "Connected") # Check for messages while websocket.get_available_packet_count() > 0: var packet = websocket.get_packet() var message = packet.get_string_from_utf8() trigger_event("message", {"data": message}) + # Track received message in NetworkManager + NetworkManager.call_deferred("add_websocket_message", url, instance_id, "received", message) WebSocketPeer.STATE_CLOSED: if connection_status: connection_status = false trigger_event("close", {}) + # Update NetworkManager with closed connection + var close_code = websocket.get_close_code() + var close_reason = websocket.get_close_reason() + NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", close_code, close_reason) # Clean up timer if timer: @@ -82,25 +94,33 @@ class WebSocketWrapper: # Connection is closing if connection_status: connection_status = false + NetworkManager.call_deferred("update_websocket_connection", instance_id, "closing", 0, "Closing") _: # Unknown state or connection failed if connection_status: connection_status = false trigger_event("close", {}) + NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", 0, "Unexpected disconnection") elif not connection_status: # This might be a connection failure trigger_event("error", {"message": "Connection failed or was rejected by server"}) + NetworkManager.call_deferred("update_websocket_connection", instance_id, "error", 0, "Connection failed or rejected") func send_message(message: String): if connection_status and websocket: websocket.send_text(message) + # Track sent message in NetworkManager + NetworkManager.call_deferred("add_websocket_message", url, instance_id, "sent", message) func close_connection(): if websocket: websocket.close() connection_status = false + # Update NetworkManager with manual close + NetworkManager.call_deferred("update_websocket_connection", instance_id, "closed", 1000, "Manually closed") + if timer: timer.queue_free() timer = null