Add Stripe donation flow
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
extends Node
|
||||
|
||||
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
||||
const DEFAULT_URL := "https://s3q.ranaze.com/api/donations"
|
||||
const REQUEST_TIMEOUT_SECONDS := 30.0
|
||||
const POLL_INTERVAL_SECONDS := 3.0
|
||||
const POLL_LIMIT := 200
|
||||
|
||||
signal checkout_started(donation_id: String, checkout_url: String)
|
||||
signal donation_status_changed(status: String, donation: Dictionary)
|
||||
signal donation_failed(message: String)
|
||||
|
||||
var api_url := DEFAULT_URL
|
||||
var checkout_request: HTTPRequest
|
||||
var status_request: HTTPRequest
|
||||
var poll_timer: Timer
|
||||
var request_token := ""
|
||||
var active_donation_id := ""
|
||||
var poll_count := 0
|
||||
var checkout_pending := false
|
||||
var status_pending := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
api_url = EndpointConfig.resolve(
|
||||
str(ProjectSettings.get_setting("step3/donations_api_url", DEFAULT_URL)),
|
||||
"/api/donations")
|
||||
checkout_request = HTTPRequest.new()
|
||||
checkout_request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
checkout_request.request_completed.connect(_on_checkout_completed)
|
||||
add_child(checkout_request)
|
||||
status_request = HTTPRequest.new()
|
||||
status_request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
status_request.request_completed.connect(_on_status_completed)
|
||||
add_child(status_request)
|
||||
poll_timer = Timer.new()
|
||||
poll_timer.wait_time = POLL_INTERVAL_SECONDS
|
||||
poll_timer.timeout.connect(_poll_status)
|
||||
add_child(poll_timer)
|
||||
AuthState.auth_changed.connect(_on_auth_changed)
|
||||
|
||||
|
||||
func begin_checkout(amount_cents: int) -> void:
|
||||
if checkout_pending:
|
||||
return
|
||||
if not AuthState.can_send_authenticated_request():
|
||||
donation_failed.emit("Log in before making a contribution.")
|
||||
return
|
||||
_prepare_browser_window()
|
||||
checkout_pending = true
|
||||
request_token = AuthState.access_token
|
||||
var headers := PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer %s" % request_token
|
||||
])
|
||||
var error := checkout_request.request(
|
||||
"%s/checkout" % api_url.rstrip("/"), headers, HTTPClient.METHOD_POST,
|
||||
JSON.stringify({"amountCents": amount_cents}))
|
||||
if error != OK:
|
||||
checkout_pending = false
|
||||
_close_browser_window()
|
||||
donation_failed.emit("The secure checkout request could not be started.")
|
||||
|
||||
|
||||
func stop_polling() -> void:
|
||||
poll_timer.stop()
|
||||
poll_count = 0
|
||||
if status_pending and status_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
status_request.cancel_request()
|
||||
status_pending = false
|
||||
|
||||
|
||||
func resume_polling() -> void:
|
||||
if active_donation_id.is_empty() or poll_timer == null:
|
||||
return
|
||||
poll_timer.start()
|
||||
_poll_status()
|
||||
|
||||
|
||||
func _on_checkout_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
checkout_pending = false
|
||||
if response_code == 401:
|
||||
AuthState.handle_unauthorized(request_token)
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
_close_browser_window()
|
||||
donation_failed.emit(_failure_message("Checkout could not be started.", result, response_code, body))
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
_close_browser_window()
|
||||
donation_failed.emit("The donation service returned an invalid response.")
|
||||
return
|
||||
var donation_id := str(parsed.get("donationId", ""))
|
||||
var checkout_url := str(parsed.get("checkoutUrl", ""))
|
||||
if donation_id.is_empty() or not checkout_url.begins_with("https://"):
|
||||
_close_browser_window()
|
||||
donation_failed.emit("The donation service returned an invalid checkout link.")
|
||||
return
|
||||
active_donation_id = donation_id
|
||||
_open_checkout_url(checkout_url)
|
||||
checkout_started.emit(donation_id, checkout_url)
|
||||
poll_count = 0
|
||||
poll_timer.start()
|
||||
_poll_status()
|
||||
|
||||
|
||||
func _poll_status() -> void:
|
||||
if status_pending or active_donation_id.is_empty() or not AuthState.can_send_authenticated_request():
|
||||
return
|
||||
poll_count += 1
|
||||
if poll_count > POLL_LIMIT:
|
||||
stop_polling()
|
||||
return
|
||||
status_pending = true
|
||||
request_token = AuthState.access_token
|
||||
var headers := PackedStringArray(["Authorization: Bearer %s" % request_token])
|
||||
var error := status_request.request("%s/%s" % [api_url.rstrip("/"), active_donation_id], headers)
|
||||
if error != OK:
|
||||
status_pending = false
|
||||
|
||||
|
||||
func _on_status_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
status_pending = false
|
||||
if response_code == 401:
|
||||
AuthState.handle_unauthorized(request_token)
|
||||
stop_polling()
|
||||
return
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return
|
||||
var status := str(parsed.get("status", ""))
|
||||
donation_status_changed.emit(status, parsed)
|
||||
if status in ["paid", "failed", "expired"]:
|
||||
stop_polling()
|
||||
|
||||
|
||||
func _prepare_browser_window() -> void:
|
||||
if OS.has_feature("web"):
|
||||
JavaScriptBridge.eval(
|
||||
"window.medmindDonationWindow = window.open('about:blank', 'medmind_donation_checkout');",
|
||||
true)
|
||||
|
||||
|
||||
func _open_checkout_url(url: String) -> void:
|
||||
if OS.has_feature("web"):
|
||||
JavaScriptBridge.eval(
|
||||
"if (window.medmindDonationWindow && !window.medmindDonationWindow.closed) { window.medmindDonationWindow.location.href = %s; window.medmindDonationWindow.focus(); } else { window.open(%s, '_blank'); }" % [JSON.stringify(url), JSON.stringify(url)],
|
||||
true)
|
||||
else:
|
||||
OS.shell_open(url)
|
||||
|
||||
|
||||
func _close_browser_window() -> void:
|
||||
if OS.has_feature("web"):
|
||||
JavaScriptBridge.eval(
|
||||
"if (window.medmindDonationWindow && !window.medmindDonationWindow.closed) { window.medmindDonationWindow.close(); } window.medmindDonationWindow = null;",
|
||||
true)
|
||||
|
||||
|
||||
func _failure_message(prefix: String, result: int, response_code: int, body: PackedByteArray) -> String:
|
||||
if result != HTTPRequest.RESULT_SUCCESS and response_code == 0:
|
||||
return "%s Check your connection and try again." % prefix
|
||||
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
var detail := str(parsed.get("detail", "")).strip_edges()
|
||||
if not detail.is_empty():
|
||||
return detail
|
||||
return "%s Status %d." % [prefix, response_code]
|
||||
|
||||
|
||||
func _on_auth_changed() -> void:
|
||||
if not AuthState.is_logged_in:
|
||||
stop_polling()
|
||||
active_donation_id = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://b20e6ch7a4dtb
|
||||
@@ -0,0 +1,18 @@
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func parse_cents(value: String) -> int:
|
||||
var cleaned := value.strip_edges().trim_prefix("$").replace(",", "")
|
||||
var matcher := RegEx.new()
|
||||
matcher.compile("^[0-9]+(?:\\.[0-9]{1,2})?$")
|
||||
if matcher.search(cleaned) == null:
|
||||
return -1
|
||||
var parts := cleaned.split(".")
|
||||
var dollars := int(parts[0])
|
||||
var cents := 0
|
||||
if parts.size() == 2:
|
||||
var fraction := str(parts[1])
|
||||
cents = int(fraction) * (10 if fraction.length() == 1 else 1)
|
||||
if dollars > 10000 or (dollars == 10000 and cents > 0):
|
||||
return -1
|
||||
return dollars * 100 + cents
|
||||
@@ -0,0 +1 @@
|
||||
uid://2ha4l3mmlb61
|
||||
@@ -0,0 +1,316 @@
|
||||
extends MarginContainer
|
||||
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const DonationAmount := preload("res://scripts/ui/donation_amount.gd")
|
||||
const MINIMUM_CENTS := 500
|
||||
const MAXIMUM_CENTS := 1000000
|
||||
const DEFAULT_CENTS := 2500
|
||||
|
||||
var main: Control
|
||||
var content_grid: GridContainer
|
||||
var mission_panel: PanelContainer
|
||||
var contribution_panel: PanelContainer
|
||||
var preset_grid: GridContainer
|
||||
var amount_buttons: Array[Button] = []
|
||||
var amount_group := ButtonGroup.new()
|
||||
var custom_amount: LineEdit
|
||||
var donate_button: Button
|
||||
var status_label: Label
|
||||
var selected_amount_cents := DEFAULT_CENTS
|
||||
var donation_service: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
donation_service = get_node("/root/DonationService")
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("margin_bottom", 8)
|
||||
_build_ui()
|
||||
resized.connect(_update_layout)
|
||||
donation_service.checkout_started.connect(_on_checkout_started)
|
||||
donation_service.donation_status_changed.connect(_on_donation_status_changed)
|
||||
donation_service.donation_failed.connect(_on_donation_failed)
|
||||
UIThemeService.theme_changed.connect(_on_theme_changed)
|
||||
_update_layout()
|
||||
_update_donate_button()
|
||||
refresh_theme_controls()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(scroll)
|
||||
|
||||
var page := VBoxContainer.new()
|
||||
page.custom_minimum_size = Vector2(0, 0)
|
||||
page.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
page.add_theme_constant_override("separation", 24)
|
||||
scroll.add_child(page)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Support MedMind"
|
||||
title.add_theme_font_override("font", Brand.font)
|
||||
title.add_theme_font_size_override("font_size", 36)
|
||||
page.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Help us build accessible, clinically grounded tools for Step 3 learners."
|
||||
subtitle.add_theme_font_override("font", Brand.font)
|
||||
subtitle.add_theme_font_size_override("font_size", 18)
|
||||
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
page.add_child(subtitle)
|
||||
|
||||
content_grid = GridContainer.new()
|
||||
content_grid.columns = 2
|
||||
content_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_grid.add_theme_constant_override("h_separation", 24)
|
||||
content_grid.add_theme_constant_override("v_separation", 24)
|
||||
page.add_child(content_grid)
|
||||
|
||||
mission_panel = _panel()
|
||||
mission_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_grid.add_child(mission_panel)
|
||||
var mission := VBoxContainer.new()
|
||||
mission.add_theme_constant_override("separation", 18)
|
||||
mission_panel.add_child(mission)
|
||||
mission.add_child(_heading("Our mission", 26))
|
||||
mission.add_child(_body("MedMind exists to make rigorous, clinically grounded medical education more accessible. We are building an independent Step 3 study experience that helps learners develop clinical judgment through original questions, realistic CCS cases, transparent explanations, and continuous improvement driven by learner feedback."))
|
||||
mission.add_child(HSeparator.new())
|
||||
mission.add_child(_heading("What your support makes possible", 20))
|
||||
for item_text in [
|
||||
"Original question and CCS case development",
|
||||
"Clinical source review and content verification",
|
||||
"Reliable hosting and continued pre-alpha access",
|
||||
"Accessibility, usability, and learning-tool improvements"
|
||||
]:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
var marker := Label.new()
|
||||
marker.text = "-"
|
||||
marker.add_theme_font_size_override("font_size", 20)
|
||||
marker.add_theme_color_override("font_color", Brand.TEAL)
|
||||
row.add_child(marker)
|
||||
var item := _body(item_text)
|
||||
item.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(item)
|
||||
mission.add_child(row)
|
||||
|
||||
contribution_panel = _panel()
|
||||
contribution_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content_grid.add_child(contribution_panel)
|
||||
var contribution := VBoxContainer.new()
|
||||
contribution.add_theme_constant_override("separation", 18)
|
||||
contribution_panel.add_child(contribution)
|
||||
contribution.add_child(_heading("Make a one-time contribution", 26))
|
||||
contribution.add_child(_body("Choose an amount. Secure payment and email receipts are handled by Stripe."))
|
||||
|
||||
preset_grid = GridContainer.new()
|
||||
preset_grid.columns = 4
|
||||
preset_grid.add_theme_constant_override("h_separation", 10)
|
||||
preset_grid.add_theme_constant_override("v_separation", 10)
|
||||
contribution.add_child(preset_grid)
|
||||
for amount in [500, 1000, 2500, 5000]:
|
||||
var button := Button.new()
|
||||
button.text = "$%d" % int(amount / 100.0)
|
||||
button.toggle_mode = true
|
||||
button.button_group = amount_group
|
||||
button.custom_minimum_size = Vector2(64, 48)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.set_meta("amount_cents", amount)
|
||||
button.pressed.connect(_select_preset.bind(button))
|
||||
preset_grid.add_child(button)
|
||||
amount_buttons.append(button)
|
||||
if amount == DEFAULT_CENTS:
|
||||
button.set_pressed_no_signal(true)
|
||||
|
||||
var custom_label := Label.new()
|
||||
custom_label.text = "Custom amount (USD)"
|
||||
custom_label.add_theme_font_override("font", Brand.font)
|
||||
custom_label.add_theme_font_size_override("font_size", 16)
|
||||
contribution.add_child(custom_label)
|
||||
|
||||
custom_amount = LineEdit.new()
|
||||
custom_amount.placeholder_text = "$5.00 minimum"
|
||||
custom_amount.max_length = 12
|
||||
custom_amount.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER_DECIMAL
|
||||
custom_amount.custom_minimum_size = Vector2(0, 56)
|
||||
custom_amount.text_changed.connect(_on_custom_amount_changed)
|
||||
contribution.add_child(custom_amount)
|
||||
|
||||
donate_button = Button.new()
|
||||
donate_button.text = "Donate $25 securely"
|
||||
donate_button.custom_minimum_size = Vector2(0, 58)
|
||||
donate_button.pressed.connect(_on_donate_pressed)
|
||||
contribution.add_child(donate_button)
|
||||
|
||||
status_label = Label.new()
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
status_label.add_theme_font_override("font", Brand.font)
|
||||
status_label.add_theme_font_size_override("font_size", 15)
|
||||
status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
||||
status_label.custom_minimum_size.y = 44
|
||||
contribution.add_child(status_label)
|
||||
|
||||
var disclosure := _body("Contributions are voluntary, one-time, and not tax-deductible. Card details are entered only on Stripe's secure checkout page.")
|
||||
disclosure.add_theme_font_size_override("font_size", 14)
|
||||
contribution.add_child(disclosure)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
donate_button.disabled = donation_service.checkout_pending
|
||||
if donation_service.active_donation_id.is_empty():
|
||||
status_label.text = ""
|
||||
else:
|
||||
donation_service.resume_polling()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
donation_service.stop_polling()
|
||||
|
||||
|
||||
static func parse_amount_cents(value: String) -> int:
|
||||
return DonationAmount.parse_cents(value)
|
||||
|
||||
|
||||
func _select_preset(button: Button) -> void:
|
||||
selected_amount_cents = int(button.get_meta("amount_cents", DEFAULT_CENTS))
|
||||
custom_amount.text = ""
|
||||
_set_validation_state("")
|
||||
_update_donate_button()
|
||||
refresh_theme_controls()
|
||||
|
||||
|
||||
func _on_custom_amount_changed(value: String) -> void:
|
||||
if value.strip_edges().is_empty():
|
||||
return
|
||||
for button in amount_buttons:
|
||||
button.set_pressed_no_signal(false)
|
||||
selected_amount_cents = parse_amount_cents(value)
|
||||
_set_validation_state("")
|
||||
_update_donate_button()
|
||||
refresh_theme_controls()
|
||||
|
||||
|
||||
func _on_donate_pressed() -> void:
|
||||
if selected_amount_cents < MINIMUM_CENTS:
|
||||
_set_validation_state("Enter an amount of at least $5.00.", true)
|
||||
return
|
||||
if selected_amount_cents > MAXIMUM_CENTS:
|
||||
_set_validation_state("The maximum contribution is $10,000.00.", true)
|
||||
return
|
||||
donate_button.disabled = true
|
||||
_set_validation_state("Opening secure checkout...")
|
||||
donation_service.begin_checkout(selected_amount_cents)
|
||||
|
||||
|
||||
func _on_checkout_started(_donation_id: String, _checkout_url: String) -> void:
|
||||
donate_button.disabled = false
|
||||
_set_validation_state("Checkout opened in your browser. This page will confirm when Stripe completes the payment.")
|
||||
|
||||
|
||||
func _on_donation_status_changed(status: String, _donation: Dictionary) -> void:
|
||||
match status:
|
||||
"paid": _set_validation_state("Thank you for supporting MedMind. Your contribution has been confirmed.", false, Brand.SUCCESS)
|
||||
"failed": _set_validation_state("Stripe could not complete the contribution. No payment was confirmed.", true)
|
||||
"expired": _set_validation_state("The checkout session expired. Start a new checkout when you are ready.", true)
|
||||
|
||||
|
||||
func _on_donation_failed(message: String) -> void:
|
||||
donate_button.disabled = false
|
||||
_set_validation_state(message, true)
|
||||
|
||||
|
||||
func _update_donate_button() -> void:
|
||||
donate_button.disabled = selected_amount_cents < MINIMUM_CENTS or selected_amount_cents > MAXIMUM_CENTS
|
||||
if selected_amount_cents >= MINIMUM_CENTS and selected_amount_cents <= MAXIMUM_CENTS:
|
||||
donate_button.text = "Donate %s securely" % _format_amount(selected_amount_cents)
|
||||
else:
|
||||
donate_button.text = "Donate securely"
|
||||
|
||||
|
||||
func _format_amount(amount_cents: int) -> String:
|
||||
return "$%d.%02d" % [int(amount_cents / 100.0), amount_cents % 100]
|
||||
|
||||
|
||||
func _set_validation_state(message: String, is_error: bool = false, color: Color = Color.TRANSPARENT) -> void:
|
||||
status_label.text = message
|
||||
var text_color := color
|
||||
if text_color == Color.TRANSPARENT:
|
||||
text_color = Style.RED if is_error else Brand.muted_color(UIThemeService.is_dark_mode())
|
||||
status_label.add_theme_color_override("font_color", text_color)
|
||||
|
||||
|
||||
func _panel() -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
var box := Brand.content_card_box(UIThemeService.is_dark_mode(), false)
|
||||
box.content_margin_left = 28
|
||||
box.content_margin_right = 28
|
||||
box.content_margin_top = 28
|
||||
box.content_margin_bottom = 28
|
||||
panel.add_theme_stylebox_override("panel", box)
|
||||
return panel
|
||||
|
||||
|
||||
func _heading(value: String, font_size: int) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = value
|
||||
label.add_theme_font_override("font", Brand.font)
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
return label
|
||||
|
||||
|
||||
func _body(value: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = value
|
||||
label.add_theme_font_override("font", Brand.font)
|
||||
label.add_theme_font_size_override("font_size", 17)
|
||||
label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
return label
|
||||
|
||||
|
||||
func _update_layout() -> void:
|
||||
if content_grid != null:
|
||||
content_grid.columns = 1 if size.x < 1050.0 else 2
|
||||
if preset_grid != null:
|
||||
preset_grid.columns = 2 if size.x < 520.0 else 4
|
||||
|
||||
|
||||
func refresh_theme_controls() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
for panel in [mission_panel, contribution_panel]:
|
||||
if panel != null:
|
||||
var box := Brand.content_card_box(dark, false)
|
||||
box.content_margin_left = 28
|
||||
box.content_margin_right = 28
|
||||
box.content_margin_top = 28
|
||||
box.content_margin_bottom = 28
|
||||
panel.add_theme_stylebox_override("panel", box)
|
||||
if custom_amount != null:
|
||||
custom_amount.add_theme_stylebox_override("normal", Brand.field_box(dark))
|
||||
custom_amount.add_theme_stylebox_override("focus", Brand.field_box(dark, true))
|
||||
custom_amount.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
custom_amount.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark))
|
||||
for button in amount_buttons:
|
||||
var selected := button.button_pressed
|
||||
var normal := Brand.secondary_button_box(dark, selected)
|
||||
if selected:
|
||||
normal.bg_color = Color(Brand.ROYAL_BLUE, 0.22 if dark else 0.10)
|
||||
normal.border_color = Brand.ROYAL_BLUE
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
button.add_theme_stylebox_override("pressed", normal)
|
||||
button.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
button.add_theme_color_override("font_pressed_color", Brand.text_color(dark))
|
||||
if donate_button != null:
|
||||
donate_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
donate_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
donate_button.add_theme_color_override("font_color", Color.WHITE)
|
||||
donate_button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
|
||||
|
||||
func _on_theme_changed(_is_dark_mode: bool) -> void:
|
||||
refresh_theme_controls()
|
||||
@@ -0,0 +1 @@
|
||||
uid://i2fix2vkf8au
|
||||
+12
-1
@@ -314,6 +314,11 @@ func _build_ui() -> void:
|
||||
bookmarks.main = self
|
||||
screens_container.add_child(bookmarks)
|
||||
screens["bookmarks"] = bookmarks
|
||||
var donations := preload("res://scripts/ui/donation_screen.gd").new()
|
||||
donations.name = "DonationScreen"
|
||||
donations.main = self
|
||||
screens_container.add_child(donations)
|
||||
screens["donations"] = donations
|
||||
|
||||
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
|
||||
feedback_dialog.name = "AppFeedbackDialog"
|
||||
@@ -386,6 +391,7 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
container.add_child(spacer)
|
||||
container.add_child(HSeparator.new())
|
||||
_add_nav_button(container, "donations", "Support MedMind", "heart-pulse", true)
|
||||
_add_nav_button(container, "performance", "Profile", "user-round", true)
|
||||
_add_nav_button(container, "settings", "Settings", "settings", true)
|
||||
_add_nav_button(container, "logout", "Log out", "log-out", true)
|
||||
@@ -426,6 +432,7 @@ func _on_navigation_pressed(key: String) -> void:
|
||||
"settings": show_screen("settings")
|
||||
"notes": show_screen("notes")
|
||||
"bookmarks": show_screen("bookmarks")
|
||||
"donations": show_screen("donations")
|
||||
"logout": _logout()
|
||||
|
||||
|
||||
@@ -440,7 +447,8 @@ func _update_header_title(screen_name: String) -> void:
|
||||
"profile": "Profile",
|
||||
"settings": "Settings",
|
||||
"notes": "Notes",
|
||||
"bookmarks": "Bookmarks"
|
||||
"bookmarks": "Bookmarks",
|
||||
"donations": "Support MedMind"
|
||||
}
|
||||
header_title.text = str(titles.get(screen_name, ""))
|
||||
|
||||
@@ -735,6 +743,9 @@ func _on_theme_changed(is_dark_mode: bool) -> void:
|
||||
var selector = screens.get("selection")
|
||||
if selector != null and selector.has_method("refresh_theme_controls"):
|
||||
selector.refresh_theme_controls()
|
||||
var donations = screens.get("donations")
|
||||
if donations != null and donations.has_method("refresh_theme_controls"):
|
||||
donations.refresh_theme_controls()
|
||||
var player = screens.get("player")
|
||||
if player != null and player.has_method("refresh_theme_controls"):
|
||||
player.refresh_theme_controls()
|
||||
|
||||
@@ -23,4 +23,6 @@ static func key_for_screen(screen_name: String, payload: Variant = null) -> Stri
|
||||
key = "notes"
|
||||
elif screen_name == "bookmarks":
|
||||
key = "bookmarks"
|
||||
elif screen_name == "donations":
|
||||
key = "donations"
|
||||
return key
|
||||
|
||||
Reference in New Issue
Block a user