extends Control const Style := preload("res://scripts/ui/ui_style.gd") const MIN_DETAILS := 10 const MAX_DETAILS := 4000 const CATEGORIES := [ {"id": "bug", "label": "Bug / technical issue"}, {"id": "usability", "label": "Usability"}, {"id": "feature_request", "label": "Feature request"}, {"id": "account", "label": "Account"}, {"id": "other", "label": "Other feedback"} ] var panel: PanelContainer var form: VBoxContainer var success: VBoxContainer var group: ButtonGroup var details: TextEdit var count: Label var error_label: Label var submit_button: Button var cancel_button: Button var receipt_label: Label var context_label: Label var current_context := {} func _ready() -> void: visible = false top_level = true mouse_filter = Control.MOUSE_FILTER_STOP set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var dim := ColorRect.new() dim.color = Color(0, 0, 0, 0.72) dim.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(dim) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(center) panel = PanelContainer.new() panel.custom_minimum_size = Vector2(700, 590) panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE_DARK, Style.CYAN)) center.add_child(panel) var root := VBoxContainer.new() root.add_theme_constant_override("separation", 12) panel.add_child(root) var title := Label.new() title.text = "Send feedback" Style.set_label(title, 26, Style.TEXT) root.add_child(title) form = VBoxContainer.new() form.add_theme_constant_override("separation", 10) root.add_child(form) var prompt := Label.new() prompt.text = "What would you like to report?" Style.set_label(prompt, 17, Style.CYAN) form.add_child(prompt) group = ButtonGroup.new() group.allow_unpress = false var category_grid := GridContainer.new() category_grid.columns = 2 form.add_child(category_grid) for category in CATEGORIES: var option := CheckBox.new() option.text = str(category["label"]) option.set_meta("category", str(category["id"])) option.button_group = group option.toggled.connect(func(_pressed: bool): _refresh()) category_grid.add_child(option) var detail_header := HBoxContainer.new() form.add_child(detail_header) var detail_title := Label.new() detail_title.text = "Details" detail_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL detail_header.add_child(detail_title) count = Label.new() detail_header.add_child(count) details = TextEdit.new() details.placeholder_text = "Describe what happened, what you expected, and how to reproduce it when applicable." details.custom_minimum_size = Vector2(0, 210) details.size_flags_vertical = Control.SIZE_EXPAND_FILL details.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY details.text_changed.connect(_refresh) form.add_child(details) context_label = Label.new() context_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(context_label, 13, Style.MUTED) form.add_child(context_label) error_label = Label.new() error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(error_label, 14, Style.RED) form.add_child(error_label) var actions := HBoxContainer.new() actions.add_theme_constant_override("separation", 10) form.add_child(actions) cancel_button = Button.new() cancel_button.text = "Cancel" cancel_button.pressed.connect(hide_dialog) actions.add_child(cancel_button) submit_button = Button.new() submit_button.text = "Send feedback" submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL Style.style_primary_button(submit_button) submit_button.pressed.connect(_submit) actions.add_child(submit_button) success = VBoxContainer.new() success.visible = false success.alignment = BoxContainer.ALIGNMENT_CENTER success.add_theme_constant_override("separation", 16) root.add_child(success) receipt_label = Label.new() receipt_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER receipt_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(receipt_label, 21, Style.LIME) success.add_child(receipt_label) var close := Button.new() close.text = "Close" Style.style_primary_button(close) close.pressed.connect(_close_success) success.add_child(close) FeedbackService.feedback_submitted.connect(_submitted) FeedbackService.feedback_failed.connect(_failed) resized.connect(_refresh_size) _refresh_size() _refresh() func show_dialog(context: Dictionary) -> void: current_context = context.duplicate(true) form.visible = true success.visible = false error_label.text = "" context_label.text = "Included context: %s, %s, %dx%d, version %s (%s)." % [ context.get("screen", "unknown"), context.get("platform", "unknown"), context.get("viewportWidth", 0), context.get("viewportHeight", 0), context.get("appVersion", "unknown"), context.get("buildRevision", "unknown")] visible = true move_to_front() BlockSessionService.pause_current_question_timer() _refresh() func hide_dialog() -> void: if FeedbackService.is_submitting: return visible = false BlockSessionService.resume_current_question_timer() func _close_success() -> void: details.text = "" for button in group.get_buttons(): button.button_pressed = false hide_dialog() func _unhandled_input(event: InputEvent) -> void: if visible and event.is_action_pressed("ui_cancel"): hide_dialog() get_viewport().set_input_as_handled() func _refresh_size() -> void: if panel != null: panel.custom_minimum_size = Vector2(minf(700.0, maxf(300.0, size.x - 32.0)), minf(590.0, maxf(430.0, size.y - 32.0))) func _refresh() -> void: if details == null: return var length := details.text.strip_edges().length() count.text = "%d / %d" % [length, MAX_DETAILS] var selected := group.get_pressed_button() var category := "" if selected == null else str(selected.get_meta("category", "")) submit_button.disabled = FeedbackService.is_submitting or not is_valid_submission(category, details.text) cancel_button.disabled = FeedbackService.is_submitting static func is_valid_submission(category: String, value: String) -> bool: var valid_categories := ["bug", "usability", "feature_request", "account", "other"] var length := value.strip_edges().length() return valid_categories.has(category) and length >= MIN_DETAILS and length <= MAX_DETAILS func _submit() -> void: var selected := group.get_pressed_button() if selected == null: return error_label.text = "" submit_button.text = "Sending..." _refresh() FeedbackService.submit(str(selected.get_meta("category", "")), details.text.strip_edges(), current_context) func _submitted(receipt_id: String) -> void: submit_button.text = "Send feedback" form.visible = false success.visible = true receipt_label.text = "Feedback sent\nReceipt: %s" % receipt_id UISoundService.play_sound("confirmation") func _failed(message: String) -> void: submit_button.text = "Send feedback" error_label.text = message _refresh() UISoundService.play_sound("error")