Apply MedMind branding and authentication flows

This commit is contained in:
hz
2026-07-16 00:22:22 -05:00
parent aec56f168e
commit 0eca0c4cbf
15 changed files with 907 additions and 286 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cggvp10nl7dkw"
path="res://.godot/imported/brand-guide.png-f5bf305eb515449b2b41095c31a95f1b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/branding/brand-guide.png"
dest_files=["res://.godot/imported/brand-guide.png-f5bf305eb515449b2b41095c31a95f1b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dh5mmxgokpxsi"
path="res://.godot/imported/login-reference.png-ffc946c8cb9eb0b3ef4572dae6a411fc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/branding/login-reference.png"
dest_files=["res://.godot/imported/login-reference.png-ffc946c8cb9eb0b3ef4572dae6a411fc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://7xw8fhj28e7r"
path="res://.godot/imported/medmind-icon.png-01bef25038ace5255da752667f77403f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/branding/medmind-icon.png"
dest_files=["res://.godot/imported/medmind-icon.png-01bef25038ace5255da752667f77403f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ck2hsnyyktfpr"
path="res://.godot/imported/Inter-Variable.ttf-e500b369badb17500bd3445af6919693.fontdata"
[deps]
source_file="res://assets/fonts/Inter-Variable.ttf"
dest_files=["res://.godot/imported/Inter-Variable.ttf-e500b369badb17500bd3445af6919693.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+1 -1
View File
@@ -22,7 +22,7 @@ vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=true
html/export_icon=true
html/custom_html_shell=""
html/head_include="<meta name=\"description\" content=\"RanaZe's Doctor Maker pre-alpha\">"
html/head_include="<meta name=\"description\" content=\"MedMind pre-alpha - Think like a physician.\">"
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
+2 -2
View File
@@ -10,11 +10,11 @@ config_version=5
[application]
config/name="step3"
config/name="MedMind"
config/version="pre-alpha"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.6", "GL Compatibility")
config/icon="res://icon.svg"
config/icon="res://assets/branding/medmind-icon.png"
[autoload]
+565 -281
View File
@@ -1,348 +1,632 @@
extends VBoxContainer
extends Control
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
const DEFAULT_AUTH_LOGIN_URL := "https://pauth.ranaze.com/api/Auth/login"
const DEFAULT_AUTH_REGISTER_URL := "https://pauth.ranaze.com/api/Auth/register"
const AUTH_BASE := "https://pauth.ranaze.com/api/Auth"
var main: Control
var auth_base_url := AUTH_BASE
var background: ColorRect
var left_brand: VBoxContainer
var auth_panel: PanelContainer
var auth_prompt_label: Label
var switch_mode_button: Button
var login_fields: VBoxContainer
var register_fields: VBoxContainer
var username_input: LineEdit
var panel_content: VBoxContainer
var heading: Label
var subheading: Label
var message_label: Label
var theme_button: Button
var login_view: VBoxContainer
var register_view: VBoxContainer
var reset_view: VBoxContainer
var steam_view: VBoxContainer
var login_input: LineEdit
var password_input: LineEdit
var registration_username_input: LineEdit
var registration_email_input: LineEdit
var registration_password_input: LineEdit
var registration_confirm_input: LineEdit
var login_button: Button
var register_button: Button
var error_label: Label
var success_label: Label
var login_request: HTTPRequest
var register_request: HTTPRequest
var auth_mode := "login"
var reset_login_input: LineEdit
var reset_code_input: LineEdit
var reset_password_input: LineEdit
var reset_confirm_input: LineEdit
var reset_confirm_group: VBoxContainer
var steam_action: OptionButton
var steam_username_input: LineEdit
var steam_email_input: LineEdit
var steam_password_input: LineEdit
var submit_buttons: Array[Button] = []
var fields: Array[LineEdit] = []
var requests: Dictionary = {}
var poll_timer: Timer
var mode := "login"
var reset_requested := false
var steam_transaction_id := ""
var steam_poll_attempts := 0
var automatic_login_pending := false
var login_attempt_username := ""
var auth_login_url := DEFAULT_AUTH_LOGIN_URL
var auth_register_url := DEFAULT_AUTH_REGISTER_URL
var compact_layout := false
var layout_updating := false
func _ready() -> void:
auth_login_url = EndpointConfig.resolve(DEFAULT_AUTH_LOGIN_URL, "/api/Auth/login")
auth_register_url = EndpointConfig.resolve(DEFAULT_AUTH_REGISTER_URL, "/api/Auth/register")
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 18)
var spacer_top := Control.new()
spacer_top.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(spacer_top)
var center := CenterContainer.new()
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_child(center)
auth_panel = PanelContainer.new()
auth_panel.custom_minimum_size = Vector2(540, 470)
auth_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
center.add_child(auth_panel)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 14)
auth_panel.add_child(content)
var title := Label.new()
title.text = "RanaZe's Doctor Maker"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
Style.set_label(title, 34, Style.TEXT)
content.add_child(title)
auth_prompt_label = Label.new()
auth_prompt_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
auth_prompt_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(auth_prompt_label, 16, Style.MUTED)
content.add_child(auth_prompt_label)
login_fields = VBoxContainer.new()
login_fields.add_theme_constant_override("separation", 10)
content.add_child(login_fields)
username_input = _add_text_field(login_fields, "Username", "enter username")
username_input.text_submitted.connect(_on_login_input_submitted)
password_input = _add_text_field(login_fields, "Password", "enter password", true)
password_input.text_submitted.connect(_on_login_input_submitted)
login_button = Button.new()
login_button.text = "Log In"
login_button.custom_minimum_size = Vector2(0, 50)
Style.style_primary_button(login_button)
login_button.pressed.connect(_on_login_pressed)
login_fields.add_child(login_button)
register_fields = VBoxContainer.new()
register_fields.add_theme_constant_override("separation", 10)
content.add_child(register_fields)
registration_username_input = _add_text_field(register_fields, "Username", "3-24 letters, numbers, or underscores")
registration_email_input = _add_text_field(register_fields, "Email", "[email protected]")
registration_password_input = _add_text_field(register_fields, "Password", "at least 8 characters", true)
registration_confirm_input = _add_text_field(register_fields, "Confirm password", "re-enter password", true)
registration_confirm_input.text_submitted.connect(_on_register_input_submitted)
register_button = Button.new()
register_button.text = "Create Account"
register_button.custom_minimum_size = Vector2(0, 50)
Style.style_primary_button(register_button)
register_button.pressed.connect(_on_register_pressed)
register_fields.add_child(register_button)
success_label = Label.new()
success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
success_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(success_label, 15, Style.LIME)
content.add_child(success_label)
error_label = Label.new()
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(error_label, 15, Style.RED)
content.add_child(error_label)
var switch_spacer := Control.new()
switch_spacer.custom_minimum_size = Vector2(0, 8)
content.add_child(switch_spacer)
switch_mode_button = Button.new()
switch_mode_button.custom_minimum_size = Vector2(0, 46)
Style.style_chip(switch_mode_button, Style.CYAN)
switch_mode_button.pressed.connect(_toggle_auth_mode)
content.add_child(switch_mode_button)
var spacer_bottom := Control.new()
spacer_bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(spacer_bottom)
login_request = HTTPRequest.new()
login_request.name = "LoginRequest"
login_request.request_completed.connect(_on_login_request_completed)
add_child(login_request)
register_request = HTTPRequest.new()
register_request.name = "RegisterRequest"
register_request.request_completed.connect(_on_register_request_completed)
add_child(register_request)
_set_auth_mode("login")
auth_base_url = EndpointConfig.resolve(AUTH_BASE, "/api/Auth")
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build_ui()
_create_request("login", _on_login_completed)
_create_request("register", _on_register_completed)
_create_request("reset_request", _on_reset_requested)
_create_request("reset_confirm", _on_reset_confirmed)
_create_request("steam_start", _on_steam_started)
_create_request("steam_status", _on_steam_status)
_create_request("steam_complete", _on_steam_completed)
poll_timer = Timer.new()
poll_timer.wait_time = 2.0
poll_timer.timeout.connect(_poll_steam)
add_child(poll_timer)
get_viewport().size_changed.connect(_update_responsive_layout)
UIThemeService.theme_changed.connect(_on_theme_changed)
_update_responsive_layout()
_set_mode("login")
_apply_theme()
func on_show(_payload: Variant = null) -> void:
_set_busy(false)
_set_auth_mode("login")
_clear_messages()
username_input.grab_focus()
_set_mode("login")
_clear_message()
login_input.grab_focus()
func _add_text_field(parent: VBoxContainer, field_name: String, placeholder: String, secret: bool = false) -> LineEdit:
func on_hide() -> void:
poll_timer.stop()
func _build_ui() -> void:
background = ColorRect.new()
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
background.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(background)
_add_dot_grid(Vector2(22, 24))
theme_button = Button.new()
theme_button.tooltip_text = "Switch color theme"
theme_button.custom_minimum_size = Vector2(44, 44)
theme_button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
theme_button.position = Vector2(-58, 14)
theme_button.pressed.connect(UIThemeService.toggle_theme)
add_child(theme_button)
var page_margin := MarginContainer.new()
page_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page_margin.add_theme_constant_override("margin_left", 42)
page_margin.add_theme_constant_override("margin_right", 42)
page_margin.add_theme_constant_override("margin_top", 38)
page_margin.add_theme_constant_override("margin_bottom", 38)
add_child(page_margin)
var columns := HBoxContainer.new()
columns.add_theme_constant_override("separation", 54)
page_margin.add_child(columns)
left_brand = VBoxContainer.new()
left_brand.size_flags_horizontal = Control.SIZE_EXPAND_FILL
left_brand.alignment = BoxContainer.ALIGNMENT_CENTER
left_brand.add_theme_constant_override("separation", 24)
columns.add_child(left_brand)
var brand_row := HBoxContainer.new()
brand_row.alignment = BoxContainer.ALIGNMENT_CENTER
brand_row.add_theme_constant_override("separation", 24)
left_brand.add_child(brand_row)
var large_icon := TextureRect.new()
large_icon.texture = preload("res://assets/branding/medmind-icon.png")
large_icon.custom_minimum_size = Vector2(172, 172)
large_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
large_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
brand_row.add_child(large_icon)
var wordmark := Label.new()
wordmark.text = "MedMind"
wordmark.add_theme_font_size_override("font_size", 76)
wordmark.add_theme_font_override("font", Brand.font)
brand_row.add_child(wordmark)
var tagline := Label.new()
tagline.text = "Think like a physician."
tagline.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
tagline.add_theme_font_size_override("font_size", 22)
tagline.add_theme_font_override("font", Brand.font)
left_brand.add_child(tagline)
var panel_center := CenterContainer.new()
panel_center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_center.size_flags_vertical = Control.SIZE_EXPAND_FILL
columns.add_child(panel_center)
auth_panel = PanelContainer.new()
auth_panel.custom_minimum_size = Vector2(610, 760)
panel_center.add_child(auth_panel)
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
auth_panel.add_child(scroll)
panel_content = VBoxContainer.new()
panel_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_content.add_theme_constant_override("separation", 16)
scroll.add_child(panel_content)
var panel_header := HBoxContainer.new()
panel_header.add_theme_constant_override("separation", 18)
panel_content.add_child(panel_header)
var small_icon := TextureRect.new()
small_icon.texture = preload("res://assets/branding/medmind-icon.png")
small_icon.custom_minimum_size = Vector2(82, 82)
small_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
small_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
panel_header.add_child(small_icon)
var titles := VBoxContainer.new()
titles.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_header.add_child(titles)
heading = Label.new()
heading.add_theme_font_size_override("font_size", 34)
heading.add_theme_font_override("font", Brand.font)
titles.add_child(heading)
subheading = Label.new()
subheading.add_theme_font_size_override("font_size", 18)
subheading.add_theme_font_override("font", Brand.font)
titles.add_child(subheading)
login_view = _build_login_view()
register_view = _build_register_view()
reset_view = _build_reset_view()
steam_view = _build_steam_view()
for view in [login_view, register_view, reset_view, steam_view]:
panel_content.add_child(view)
message_label = Label.new()
message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
message_label.add_theme_font_size_override("font_size", 14)
message_label.add_theme_font_override("font", Brand.font)
panel_content.add_child(message_label)
func _build_login_view() -> VBoxContainer:
var view := _new_form()
login_input = _add_field(view, "Email or Username", "Enter your email or username")
password_input = _add_field(view, "Password", "Enter your password", true)
_add_show_password(view, [password_input])
var forgot := Button.new()
forgot.text = "Forgot password?"
forgot.flat = true
forgot.alignment = HORIZONTAL_ALIGNMENT_RIGHT
forgot.pressed.connect(func(): _set_mode("reset"))
view.add_child(forgot)
var sign_in := _add_primary_button(view, "Sign In", _on_login_pressed)
password_input.text_submitted.connect(func(_text: String): _on_login_pressed())
var divider := Label.new()
divider.text = "or"
divider.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
view.add_child(divider)
var steam := Button.new()
steam.text = "Sign in with Steam"
steam.custom_minimum_size = Vector2(0, 58)
steam.pressed.connect(_start_steam)
view.add_child(steam)
submit_buttons.append(steam)
var create := Button.new()
create.text = "New to MedMind? Create an account"
create.flat = true
create.pressed.connect(func(): _set_mode("register"))
view.add_child(create)
submit_buttons.append(sign_in)
return view
func _build_register_view() -> VBoxContainer:
var view := _new_form()
registration_username_input = _add_field(view, "Username", "3-24 letters, numbers, or underscores")
registration_email_input = _add_field(view, "Email", "[email protected]")
registration_password_input = _add_field(view, "Password", "At least 8 characters", true)
registration_confirm_input = _add_field(view, "Confirm password", "Re-enter your password", true)
_add_show_password(view, [registration_password_input, registration_confirm_input])
_add_primary_button(view, "Create Account", _on_register_pressed)
_add_back_button(view, "Already have an account? Sign in", "login")
registration_confirm_input.text_submitted.connect(func(_text: String): _on_register_pressed())
return view
func _build_reset_view() -> VBoxContainer:
var view := _new_form()
reset_login_input = _add_field(view, "Email or Username", "Enter your account email or username")
_add_primary_button(view, "Send Verification Code", _request_reset)
reset_confirm_group = _new_form()
view.add_child(reset_confirm_group)
reset_code_input = _add_field(reset_confirm_group, "Verification code", "6-digit code")
reset_code_input.max_length = 6
reset_password_input = _add_field(reset_confirm_group, "New password", "At least 8 characters", true)
reset_confirm_input = _add_field(reset_confirm_group, "Confirm new password", "Re-enter your new password", true)
_add_show_password(reset_confirm_group, [reset_password_input, reset_confirm_input])
_add_primary_button(reset_confirm_group, "Update Password", _confirm_reset)
_add_back_button(view, "Back to sign in", "login")
reset_confirm_group.visible = false
return view
func _build_steam_view() -> VBoxContainer:
var view := _new_form()
var help := Label.new()
help.text = "Steam was verified. Create a MedMind account or link an existing one."
help.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
view.add_child(help)
steam_action = OptionButton.new()
steam_action.add_item("Create a new MedMind account")
steam_action.add_item("Link an existing MedMind account")
steam_action.item_selected.connect(_on_steam_action_changed)
view.add_child(steam_action)
steam_username_input = _add_field(view, "Username or email", "Choose a username")
steam_email_input = _add_field(view, "Email", "[email protected]")
steam_password_input = _add_field(view, "Password", "At least 8 characters", true)
_add_show_password(view, [steam_password_input])
_add_primary_button(view, "Continue", _complete_steam)
_add_back_button(view, "Cancel Steam sign in", "login")
return view
func _new_form() -> VBoxContainer:
var form := VBoxContainer.new()
form.add_theme_constant_override("separation", 12)
return form
func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, secret := false) -> LineEdit:
var label := Label.new()
label.text = field_name
Style.set_label(label, 15, Style.CYAN)
label.text = label_text
label.add_theme_font_size_override("font_size", 15)
label.add_theme_font_override("font", Brand.font)
parent.add_child(label)
var input := LineEdit.new()
input.placeholder_text = placeholder
input.secret = secret
input.custom_minimum_size = Vector2(0, 42)
input.custom_minimum_size = Vector2(0, 54)
input.add_theme_font_size_override("font_size", 16)
input.add_theme_font_override("font", Brand.font)
parent.add_child(input)
fields.append(input)
return input
func _set_auth_mode(next_mode: String) -> void:
auth_mode = next_mode
if login_fields == null:
return
login_fields.visible = auth_mode == "login"
register_fields.visible = auth_mode == "register"
auth_panel.custom_minimum_size.y = 535 if auth_mode == "login" else 650
if auth_mode == "login":
auth_prompt_label.text = "Log in to continue studying."
switch_mode_button.text = "Create new account"
username_input.grab_focus()
else:
auth_prompt_label.text = "Create your account"
switch_mode_button.text = "Already have an account? Log in"
registration_username_input.grab_focus()
_clear_messages()
func _add_show_password(parent: VBoxContainer, targets: Array) -> void:
var toggle := CheckButton.new()
toggle.text = "Show password"
toggle.toggled.connect(func(show: bool):
for target in targets:
target.secret = not show
)
parent.add_child(toggle)
func _toggle_auth_mode() -> void:
_set_auth_mode("register" if auth_mode == "login" else "login")
func _add_primary_button(parent: VBoxContainer, text_value: String, callback: Callable) -> Button:
var button := Button.new()
button.text = text_value
button.custom_minimum_size = Vector2(0, 58)
button.pressed.connect(callback)
parent.add_child(button)
submit_buttons.append(button)
return button
func _add_back_button(parent: VBoxContainer, text_value: String, target_mode: String) -> void:
var button := Button.new()
button.text = text_value
button.flat = true
button.pressed.connect(func(): _set_mode(target_mode))
parent.add_child(button)
func _add_dot_grid(origin: Vector2) -> void:
for row in range(4):
for column in range(5):
var dot := ColorRect.new()
dot.color = Color(Brand.ROYAL_BLUE, 0.45)
dot.position = origin + Vector2(column * 30, row * 30)
dot.size = Vector2(5, 5)
dot.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(dot)
func _set_mode(next_mode: String) -> void:
mode = next_mode
if login_view == null: return
login_view.visible = mode == "login"
register_view.visible = mode == "register"
reset_view.visible = mode == "reset"
steam_view.visible = mode == "steam"
match mode:
"register": heading.text = "Create your account"; subheading.text = "Start studying with MedMind"
"reset": heading.text = "Reset your password"; subheading.text = "We will email you a verification code"
"steam": heading.text = "Finish Steam sign in"; subheading.text = "Connect Steam to MedMind"
_: heading.text = "Welcome back"; subheading.text = "Sign in to continue"
_clear_message()
_update_responsive_layout()
func _update_responsive_layout() -> void:
if left_brand == null or layout_updating: return
layout_updating = true
var narrow := size.x < 1120.0
if OS.has_feature("web"):
var browser_width: Variant = JavaScriptBridge.eval("window.innerWidth", true)
narrow = float(browser_width) < 760.0
left_brand.visible = not narrow
compact_layout = narrow
theme_button.custom_minimum_size = Vector2(160, 160) if narrow else Vector2(44, 44)
theme_button.position = Vector2(-190, 20) if narrow else Vector2(-58, 14)
theme_button.add_theme_font_size_override("font_size", 64 if narrow else 20)
auth_panel.custom_minimum_size = Vector2(1700, 2300) if narrow else Vector2(610, 780)
_apply_compact_scale(panel_content, 3.8 if narrow else 1.0)
_apply_theme()
layout_updating = false
func _apply_compact_scale(control: Control, factor: float) -> void:
if not control.has_meta("medmind_base_minimum"):
control.set_meta("medmind_base_minimum", control.custom_minimum_size)
var base_minimum: Vector2 = control.get_meta("medmind_base_minimum")
control.custom_minimum_size = base_minimum * factor
if control is Label or control is Button or control is LineEdit or control is OptionButton:
if not control.has_meta("medmind_base_font_size"):
control.set_meta("medmind_base_font_size", control.get_theme_font_size("font_size"))
control.add_theme_font_size_override("font_size", int(round(float(control.get_meta("medmind_base_font_size")) * factor)))
for child in control.get_children():
if child is Control:
_apply_compact_scale(child, factor)
func _apply_theme() -> void:
var dark := UIThemeService.is_dark_mode()
background.color = Brand.background_color(dark)
auth_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
theme_button.text = "☀" if dark else "☾"
theme_button.tooltip_text = "Use light theme" if dark else "Use dark theme"
theme_button.add_theme_font_override("font", Brand.font)
for node in find_children("*", "Label", true, false):
(node as Label).add_theme_color_override("font_color", Brand.text_color(dark))
for input in fields:
input.add_theme_color_override("font_color", Brand.text_color(dark))
input.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark))
input.add_theme_stylebox_override("normal", Brand.field_box(dark))
input.add_theme_stylebox_override("focus", Brand.field_box(dark, true))
for button in find_children("*", "Button", true, false):
var typed := button as Button
typed.add_theme_font_override("font", Brand.font)
typed.add_theme_color_override("font_color", Brand.text_color(dark))
if not typed.flat:
typed.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
typed.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
for button in submit_buttons:
if button.text not in ["Sign in with Steam"]:
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
button.add_theme_color_override("font_color", Color.WHITE)
subheading.add_theme_color_override("font_color", Brand.muted_color(dark))
message_label.add_theme_color_override("font_color", Brand.muted_color(dark))
func _on_theme_changed(_is_dark: bool) -> void:
_apply_theme()
func _on_login_pressed() -> void:
_send_login(username_input.text.strip_edges(), password_input.text, false)
_send_login(login_input.text.strip_edges(), password_input.text, false)
func _send_login(username: String, password: String, is_automatic: bool) -> void:
if username.is_empty() or password.is_empty():
_show_error("Username and password required.")
return
func _send_login(identifier: String, password: String, automatic: bool) -> void:
if identifier.is_empty() or password.is_empty(): return _show_error("Email or username and password are required.")
automatic_login_pending = automatic
login_attempt_username = identifier
_send_json("login", "/login", {"username": identifier, "password": password}, "Signing in...")
automatic_login_pending = is_automatic
login_attempt_username = username
_set_busy(true)
login_button.text = "Signing in..."
_clear_messages()
if is_automatic:
success_label.text = "Account created. Signing you in..."
var payload := {"username": username, "password": password}
var headers := PackedStringArray(["Content-Type: application/json"])
var request_error := login_request.request(auth_login_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
if request_error != OK:
func _on_login_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
login_button.text = "Log In"
_show_error("Failed to send login request.")
func _on_login_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
login_button.text = "Log In"
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
if automatic_login_pending:
_set_auth_mode("login")
username_input.text = login_attempt_username
password_input.text = ""
_show_error("Account created, but automatic login failed. Please log in with your new account.")
else:
_show_error("Login failed. Check your credentials and try again.")
_set_mode("login"); login_input.text = login_attempt_username
_show_error("Account created, but automatic sign in failed. Please sign in.")
else: _show_error("Sign in failed. Check your credentials and connection.")
automatic_login_pending = false
return
var response: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(response) != TYPE_DICTIONARY:
_show_error("Login response was not valid JSON.")
return
var token := String(response.get("accessToken", ""))
var response_username := String(response.get("username", login_attempt_username))
if token.is_empty():
_show_error("Login response did not include an access token.")
return
AuthState.set_session(
response_username,
token,
String(response.get("refreshToken", "")),
String(response.get("role", "")),
String(response.get("exp", ""))
)
UISoundService.play_sound("correct")
password_input.text = ""
registration_password_input.text = ""
registration_confirm_input.text = ""
automatic_login_pending = false
main.show_screen("selection", "exam")
_accept_session(body, login_attempt_username)
func _on_register_pressed() -> void:
var username := registration_username_input.text.strip_edges()
var email := registration_email_input.text.strip_edges()
var password := registration_password_input.text
var confirmation := registration_confirm_input.text
var validation_error := _validate_registration(username, email, password, confirmation)
if validation_error != "":
_show_error(validation_error)
return
var error := _validate_account(username, email, password, registration_confirm_input.text)
if not error.is_empty(): return _show_error(error)
login_attempt_username = username
_send_json("register", "/register", {"username": username, "email": email, "password": password}, "Creating account...")
func _on_register_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return _show_error(_response_message(body, "Account creation failed."))
_send_login(login_attempt_username, registration_password_input.text, true)
func _request_reset() -> void:
if reset_login_input.text.strip_edges().is_empty(): return _show_error("Enter your email or username.")
_send_json("reset_request", "/password-reset/request", {"login": reset_login_input.text.strip_edges()}, "Sending code...")
func _on_reset_requested(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return _show_error(_response_message(body, "Unable to send a verification code."))
reset_requested = true
reset_confirm_group.visible = true
_show_success("If the account exists, a verification code has been sent.")
reset_code_input.grab_focus()
func _confirm_reset() -> void:
if reset_code_input.text.length() != 6: return _show_error("Enter the 6-digit verification code.")
if reset_password_input.text.length() < 8: return _show_error("Password must be at least 8 characters.")
if reset_password_input.text != reset_confirm_input.text: return _show_error("Passwords do not match.")
_send_json("reset_confirm", "/password-reset/confirm", {
"login": reset_login_input.text.strip_edges(), "code": reset_code_input.text,
"newPassword": reset_password_input.text
}, "Updating password...")
func _on_reset_confirmed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return _show_error(_response_message(body, "The verification code is invalid or expired."))
var identifier := reset_login_input.text
_set_mode("login")
login_input.text = identifier
_show_success("Password updated. You can now sign in.")
func _start_steam() -> void:
if OS.has_feature("web"):
JavaScriptBridge.eval("window.medmindSteamWindow = window.open('about:blank', 'medmind_steam');", true)
_send_json("steam_start", "/steam/start", {}, "Opening Steam...")
func _on_steam_started(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return _show_error("Unable to start Steam sign in.")
var data := _parse_dictionary(body)
steam_transaction_id = str(data.get("transactionId", ""))
var auth_url := str(data.get("authUrl", ""))
if steam_transaction_id.is_empty() or auth_url.is_empty(): return _show_error("Steam returned an invalid sign-in response.")
if OS.has_feature("web"):
JavaScriptBridge.eval("if (window.medmindSteamWindow) { window.medmindSteamWindow.location.href = %s; } else { window.location.href = %s; }" % [JSON.stringify(auth_url), JSON.stringify(auth_url)], true)
else:
OS.shell_open(auth_url)
steam_poll_attempts = 0
_show_success("Complete sign in in the Steam window. Waiting for verification...")
poll_timer.start()
func _poll_steam() -> void:
var status_request: HTTPRequest = requests["steam_status"]
if steam_transaction_id.is_empty() or status_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return
steam_poll_attempts += 1
if steam_poll_attempts > 150:
poll_timer.stop(); return _show_error("Steam sign in timed out. Try again when ready.")
var error: Error = status_request.request("%s/steam/status/%s" % [auth_base_url, steam_transaction_id])
if error != OK: _show_error("Waiting for a stable connection to Steam...")
func _on_steam_status(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS or code >= 500: return
if code == 404:
poll_timer.stop(); return _show_error("Steam sign in expired. Please try again.")
var data := _parse_dictionary(body)
match str(data.get("status", "pending")):
"requires_account": poll_timer.stop(); _set_mode("steam")
"complete": poll_timer.stop(); _accept_session(body, str(data.get("username", "")))
func _on_steam_action_changed(index: int) -> void:
steam_email_input.visible = index == 0
steam_username_input.placeholder_text = "Choose a username" if index == 0 else "Existing email or username"
func _complete_steam() -> void:
var action := "create" if steam_action.selected == 0 else "link"
if action == "create":
var validation := _validate_account(steam_username_input.text.strip_edges(), steam_email_input.text.strip_edges(), steam_password_input.text, steam_password_input.text)
if not validation.is_empty(): return _show_error(validation)
elif steam_username_input.text.strip_edges().is_empty() or steam_password_input.text.is_empty():
return _show_error("Enter your existing account credentials.")
_send_json("steam_complete", "/steam/complete", {
"transactionId": steam_transaction_id, "action": action,
"username": steam_username_input.text.strip_edges(), "email": steam_email_input.text.strip_edges(),
"password": steam_password_input.text
}, "Connecting account...")
func _on_steam_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return _show_error(_response_message(body, "Unable to connect this Steam account."))
steam_poll_attempts = 0
poll_timer.start()
_poll_steam()
func _accept_session(body: PackedByteArray, fallback_username: String) -> void:
var response := _parse_dictionary(body)
var token := str(response.get("accessToken", ""))
if token.is_empty(): return _show_error("Sign in response did not include an access token.")
AuthState.set_session(str(response.get("username", fallback_username)), token,
str(response.get("refreshToken", "")), str(response.get("role", "")), str(response.get("exp", "")))
password_input.text = ""
_set_busy(false)
UISoundService.play_sound("correct")
main.show_screen("selection", "exam")
func _create_request(key: String, callback: Callable) -> void:
var request := HTTPRequest.new()
request.timeout = 20.0
request.request_completed.connect(callback)
add_child(request)
requests[key] = request
func _send_json(key: String, path: String, payload: Dictionary, status: String) -> void:
_clear_message()
_set_busy(true)
register_button.text = "Creating account..."
_clear_messages()
var payload := {"username": username, "email": email, "password": password}
var headers := PackedStringArray(["Content-Type: application/json"])
var request_error := register_request.request(auth_register_url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
if request_error != OK:
message_label.text = status
var request: HTTPRequest = requests[key]
var error := request.request(auth_base_url + path, PackedStringArray(["Content-Type: application/json"]), HTTPClient.METHOD_POST, JSON.stringify(payload))
if error != OK:
_set_busy(false)
register_button.text = "Create Account"
_show_error("Failed to send account creation request.")
_show_error("Unable to send the request. Check your connection and try again.")
func _on_register_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_set_busy(false)
register_button.text = "Create Account"
if result != HTTPRequest.RESULT_SUCCESS:
_clear_registration_passwords()
_show_error("Network error. Please try again.")
return
if response_code < 200 or response_code >= 300:
_clear_registration_passwords()
var server_message := _response_message(body)
_show_error(server_message if server_message != "" else "Account creation failed.")
return
var username := registration_username_input.text.strip_edges()
var password := registration_password_input.text
username_input.text = username
_send_login(username, password, true)
func _set_busy(busy: bool) -> void:
for button in submit_buttons: button.disabled = busy
for field in fields: field.editable = not busy
func _validate_registration(username: String, email: String, password: String, confirmation: String) -> String:
var username_pattern := RegEx.new()
username_pattern.compile("^[A-Za-z0-9_]{3,24}$")
if username_pattern.search(username) == null:
registration_username_input.grab_focus()
return "Username must be 3-24 characters using only letters, numbers, or underscores."
var email_pattern := RegEx.new()
email_pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
if email_pattern.search(email) == null:
registration_email_input.grab_focus()
return "Enter a valid email address."
if password.length() < 8:
registration_password_input.grab_focus()
return "Password must be at least 8 characters."
if password != confirmation:
registration_confirm_input.grab_focus()
return "Passwords do not match."
func _validate_account(username: String, email: String, password: String, confirmation: String) -> String:
var username_regex := RegEx.new(); username_regex.compile("^[A-Za-z0-9_]{3,24}$")
if username_regex.search(username) == null: return "Username must be 3-24 letters, numbers, or underscores."
var email_regex := RegEx.new(); email_regex.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
if email_regex.search(email) == null: return "Enter a valid email address."
if password.length() < 8: return "Password must be at least 8 characters."
if password != confirmation: return "Passwords do not match."
return ""
func _set_busy(is_busy: bool) -> void:
if login_button == null:
return
login_button.disabled = is_busy
register_button.disabled = is_busy
switch_mode_button.disabled = is_busy
username_input.editable = not is_busy
password_input.editable = not is_busy
registration_username_input.editable = not is_busy
registration_email_input.editable = not is_busy
registration_password_input.editable = not is_busy
registration_confirm_input.editable = not is_busy
func _parse_dictionary(body: PackedByteArray) -> Dictionary:
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
return parsed if typeof(parsed) == TYPE_DICTIONARY else {}
func _clear_registration_passwords() -> void:
registration_password_input.text = ""
registration_confirm_input.text = ""
func _response_message(body: PackedByteArray) -> String:
func _response_message(body: PackedByteArray, fallback: String) -> String:
var raw := body.get_string_from_utf8().strip_edges()
var parsed: Variant = JSON.parse_string(raw)
return String(parsed) if typeof(parsed) == TYPE_STRING else raw
if typeof(parsed) == TYPE_STRING and not str(parsed).is_empty(): return str(parsed)
if typeof(parsed) == TYPE_DICTIONARY and parsed.has("message"): return str(parsed.message)
return fallback
func _on_login_input_submitted(_new_text: String) -> void:
_on_login_pressed()
func _clear_message() -> void:
if message_label != null: message_label.text = ""
func _on_register_input_submitted(_new_text: String) -> void:
_on_register_pressed()
func _clear_messages() -> void:
error_label.text = ""
success_label.text = ""
func _show_error(message: String) -> void:
func _show_error(value: String) -> void:
_set_busy(false)
message_label.text = value
message_label.add_theme_color_override("font_color", Color("ef4444"))
UISoundService.play_sound("error")
success_label.text = ""
error_label.text = message
func _show_success(value: String) -> void:
message_label.text = value
message_label.add_theme_color_override("font_color", Brand.SUCCESS)
+9 -1
View File
@@ -14,6 +14,7 @@ var feedback_button: Button
var feedback_dialog: Control
var theme_service
var current_screen_name := ""
var screens_margin: MarginContainer
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
@@ -47,6 +48,12 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
return
current_screen_name = screen_name
if screens_margin != null:
var margin := 0 if screen_name == "login" else 24
screens_margin.add_theme_constant_override("margin_left", margin)
screens_margin.add_theme_constant_override("margin_right", margin)
screens_margin.add_theme_constant_override("margin_top", 0 if screen_name == "login" else 18)
screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else 22)
screen.visible = true
if header_panel != null:
header_panel.visible = screen_name not in ["login", "player", "ccs_player"]
@@ -77,7 +84,7 @@ func _build_ui() -> void:
header_panel.add_child(header)
var title := Label.new()
title.text = "RanaZe's Doctor Maker"
title.text = "MedMind"
Style.set_label(title, 31, Style.TEXT)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
@@ -115,6 +122,7 @@ func _build_ui() -> void:
_refresh_theme_button()
var screens_container := MarginContainer.new()
screens_margin = screens_container
screens_container.name = "Screens"
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
screens_container.add_theme_constant_override("margin_left", 24)
+79
View File
@@ -0,0 +1,79 @@
extends RefCounted
const DEEP_NAVY := Color("0b1324")
const ROYAL_BLUE := Color("2563eb")
const SKY_BLUE := Color("38bdf8")
const PURPLE := Color("7c3aed")
const MAGENTA := Color("d946ef")
const TEAL := Color("14b8a6")
const SUCCESS := Color("22c55e")
const AMBER := Color("f59e0b")
const SLATE := Color("94a3b8")
const DARK_TEXT := Color("f8fafc")
const LIGHT_TEXT := Color("0f172a")
static var font: Font = preload("res://assets/fonts/Inter-Variable.ttf")
static func text_color(dark: bool) -> Color:
return DARK_TEXT if dark else LIGHT_TEXT
static func muted_color(dark: bool) -> Color:
return Color("a9b5ca") if dark else Color("53627a")
static func background_color(dark: bool) -> Color:
return DEEP_NAVY if dark else Color("f5f8ff")
static func panel_box(dark: bool) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0b1427cc") if dark else Color("ffffffee")
box.border_color = Color("33415d") if dark else Color("d4def0")
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = 54
box.content_margin_right = 54
box.content_margin_top = 48
box.content_margin_bottom = 42
return box
static func field_box(dark: bool, focused: bool = false) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0d172a") if dark else Color("f8faff")
box.border_color = ROYAL_BLUE if focused else (Color("33415d") if dark else Color("cbd6e8"))
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = 18
box.content_margin_right = 18
box.content_margin_top = 15
box.content_margin_bottom = 15
return box
static func secondary_button_box(dark: bool, hover: bool = false) -> StyleBoxFlat:
var box := field_box(dark, hover)
box.bg_color = Color("111b30") if dark else Color("ffffff")
return box
static func gradient_button_box() -> StyleBoxTexture:
var gradient := Gradient.new()
gradient.colors = PackedColorArray([Color("1677ff"), Color("8f3ff4")])
var texture := GradientTexture2D.new()
texture.gradient = gradient
texture.width = 512
texture.height = 64
texture.fill_from = Vector2(0, 0.5)
texture.fill_to = Vector2(1, 0.5)
var box := StyleBoxTexture.new()
box.texture = texture
box.content_margin_top = 14
box.content_margin_bottom = 14
return box
static func apply_font(control: Control) -> void:
control.add_theme_font_override("font", font)
+1
View File
@@ -0,0 +1 @@
uid://defm4b6tk5n1h