Apply MedMind branding and authentication flows
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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 |
@@ -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.
@@ -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={}
|
||||||
@@ -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
@@ -22,7 +22,7 @@ vram_texture_compression/for_desktop=true
|
|||||||
vram_texture_compression/for_mobile=true
|
vram_texture_compression/for_mobile=true
|
||||||
html/export_icon=true
|
html/export_icon=true
|
||||||
html/custom_html_shell=""
|
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/canvas_resize_policy=2
|
||||||
html/focus_canvas_on_start=true
|
html/focus_canvas_on_start=true
|
||||||
html/experimental_virtual_keyboard=false
|
html/experimental_virtual_keyboard=false
|
||||||
|
|||||||
+2
-2
@@ -10,11 +10,11 @@ config_version=5
|
|||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="step3"
|
config/name="MedMind"
|
||||||
config/version="pre-alpha"
|
config/version="pre-alpha"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main.tscn"
|
||||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://assets/branding/medmind-icon.png"
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
|
|||||||
+566
-282
@@ -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 EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
||||||
const DEFAULT_AUTH_LOGIN_URL := "https://pauth.ranaze.com/api/Auth/login"
|
const AUTH_BASE := "https://pauth.ranaze.com/api/Auth"
|
||||||
const DEFAULT_AUTH_REGISTER_URL := "https://pauth.ranaze.com/api/Auth/register"
|
|
||||||
|
|
||||||
var main: Control
|
var main: Control
|
||||||
|
var auth_base_url := AUTH_BASE
|
||||||
|
var background: ColorRect
|
||||||
|
var left_brand: VBoxContainer
|
||||||
var auth_panel: PanelContainer
|
var auth_panel: PanelContainer
|
||||||
var auth_prompt_label: Label
|
var panel_content: VBoxContainer
|
||||||
var switch_mode_button: Button
|
var heading: Label
|
||||||
var login_fields: VBoxContainer
|
var subheading: Label
|
||||||
var register_fields: VBoxContainer
|
var message_label: Label
|
||||||
var username_input: LineEdit
|
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 password_input: LineEdit
|
||||||
var registration_username_input: LineEdit
|
var registration_username_input: LineEdit
|
||||||
var registration_email_input: LineEdit
|
var registration_email_input: LineEdit
|
||||||
var registration_password_input: LineEdit
|
var registration_password_input: LineEdit
|
||||||
var registration_confirm_input: LineEdit
|
var registration_confirm_input: LineEdit
|
||||||
var login_button: Button
|
var reset_login_input: LineEdit
|
||||||
var register_button: Button
|
var reset_code_input: LineEdit
|
||||||
var error_label: Label
|
var reset_password_input: LineEdit
|
||||||
var success_label: Label
|
var reset_confirm_input: LineEdit
|
||||||
var login_request: HTTPRequest
|
var reset_confirm_group: VBoxContainer
|
||||||
var register_request: HTTPRequest
|
var steam_action: OptionButton
|
||||||
var auth_mode := "login"
|
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 automatic_login_pending := false
|
||||||
var login_attempt_username := ""
|
var login_attempt_username := ""
|
||||||
var auth_login_url := DEFAULT_AUTH_LOGIN_URL
|
var compact_layout := false
|
||||||
var auth_register_url := DEFAULT_AUTH_REGISTER_URL
|
var layout_updating := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
auth_login_url = EndpointConfig.resolve(DEFAULT_AUTH_LOGIN_URL, "/api/Auth/login")
|
auth_base_url = EndpointConfig.resolve(AUTH_BASE, "/api/Auth")
|
||||||
auth_register_url = EndpointConfig.resolve(DEFAULT_AUTH_REGISTER_URL, "/api/Auth/register")
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
_build_ui()
|
||||||
add_theme_constant_override("separation", 18)
|
_create_request("login", _on_login_completed)
|
||||||
|
_create_request("register", _on_register_completed)
|
||||||
var spacer_top := Control.new()
|
_create_request("reset_request", _on_reset_requested)
|
||||||
spacer_top.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_create_request("reset_confirm", _on_reset_confirmed)
|
||||||
add_child(spacer_top)
|
_create_request("steam_start", _on_steam_started)
|
||||||
|
_create_request("steam_status", _on_steam_status)
|
||||||
var center := CenterContainer.new()
|
_create_request("steam_complete", _on_steam_completed)
|
||||||
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
poll_timer = Timer.new()
|
||||||
add_child(center)
|
poll_timer.wait_time = 2.0
|
||||||
|
poll_timer.timeout.connect(_poll_steam)
|
||||||
auth_panel = PanelContainer.new()
|
add_child(poll_timer)
|
||||||
auth_panel.custom_minimum_size = Vector2(540, 470)
|
get_viewport().size_changed.connect(_update_responsive_layout)
|
||||||
auth_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
|
UIThemeService.theme_changed.connect(_on_theme_changed)
|
||||||
center.add_child(auth_panel)
|
_update_responsive_layout()
|
||||||
|
_set_mode("login")
|
||||||
var content := VBoxContainer.new()
|
_apply_theme()
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
func on_show(_payload: Variant = null) -> void:
|
func on_show(_payload: Variant = null) -> void:
|
||||||
_set_busy(false)
|
_set_busy(false)
|
||||||
_set_auth_mode("login")
|
_set_mode("login")
|
||||||
_clear_messages()
|
_clear_message()
|
||||||
username_input.grab_focus()
|
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()
|
var label := Label.new()
|
||||||
label.text = field_name
|
label.text = label_text
|
||||||
Style.set_label(label, 15, Style.CYAN)
|
label.add_theme_font_size_override("font_size", 15)
|
||||||
|
label.add_theme_font_override("font", Brand.font)
|
||||||
parent.add_child(label)
|
parent.add_child(label)
|
||||||
|
|
||||||
var input := LineEdit.new()
|
var input := LineEdit.new()
|
||||||
input.placeholder_text = placeholder
|
input.placeholder_text = placeholder
|
||||||
input.secret = secret
|
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)
|
parent.add_child(input)
|
||||||
|
fields.append(input)
|
||||||
return input
|
return input
|
||||||
|
|
||||||
|
|
||||||
func _set_auth_mode(next_mode: String) -> void:
|
func _add_show_password(parent: VBoxContainer, targets: Array) -> void:
|
||||||
auth_mode = next_mode
|
var toggle := CheckButton.new()
|
||||||
if login_fields == null:
|
toggle.text = "Show password"
|
||||||
return
|
toggle.toggled.connect(func(show: bool):
|
||||||
login_fields.visible = auth_mode == "login"
|
for target in targets:
|
||||||
register_fields.visible = auth_mode == "register"
|
target.secret = not show
|
||||||
auth_panel.custom_minimum_size.y = 535 if auth_mode == "login" else 650
|
)
|
||||||
if auth_mode == "login":
|
parent.add_child(toggle)
|
||||||
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 _toggle_auth_mode() -> void:
|
func _add_primary_button(parent: VBoxContainer, text_value: String, callback: Callable) -> Button:
|
||||||
_set_auth_mode("register" if auth_mode == "login" else "login")
|
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:
|
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:
|
func _send_login(identifier: String, password: String, automatic: bool) -> void:
|
||||||
if username.is_empty() or password.is_empty():
|
if identifier.is_empty() or password.is_empty(): return _show_error("Email or username and password are required.")
|
||||||
_show_error("Username and password required.")
|
automatic_login_pending = automatic
|
||||||
return
|
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:
|
|
||||||
_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:
|
func _on_login_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
_set_busy(false)
|
_set_busy(false)
|
||||||
login_button.text = "Log In"
|
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
|
||||||
|
|
||||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
|
||||||
if automatic_login_pending:
|
if automatic_login_pending:
|
||||||
_set_auth_mode("login")
|
_set_mode("login"); login_input.text = login_attempt_username
|
||||||
username_input.text = login_attempt_username
|
_show_error("Account created, but automatic sign in failed. Please sign in.")
|
||||||
password_input.text = ""
|
else: _show_error("Sign in failed. Check your credentials and connection.")
|
||||||
_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.")
|
|
||||||
automatic_login_pending = false
|
automatic_login_pending = false
|
||||||
return
|
return
|
||||||
|
_accept_session(body, login_attempt_username)
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_register_pressed() -> void:
|
func _on_register_pressed() -> void:
|
||||||
var username := registration_username_input.text.strip_edges()
|
var username := registration_username_input.text.strip_edges()
|
||||||
var email := registration_email_input.text.strip_edges()
|
var email := registration_email_input.text.strip_edges()
|
||||||
var password := registration_password_input.text
|
var password := registration_password_input.text
|
||||||
var confirmation := registration_confirm_input.text
|
var error := _validate_account(username, email, password, registration_confirm_input.text)
|
||||||
var validation_error := _validate_registration(username, email, password, confirmation)
|
if not error.is_empty(): return _show_error(error)
|
||||||
if validation_error != "":
|
login_attempt_username = username
|
||||||
_show_error(validation_error)
|
_send_json("register", "/register", {"username": username, "email": email, "password": password}, "Creating account...")
|
||||||
return
|
|
||||||
|
|
||||||
_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:
|
|
||||||
_set_busy(false)
|
|
||||||
register_button.text = "Create Account"
|
|
||||||
_show_error("Failed to send account creation request.")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_register_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
func _on_register_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
_set_busy(false)
|
_set_busy(false)
|
||||||
register_button.text = "Create Account"
|
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
|
||||||
if result != HTTPRequest.RESULT_SUCCESS:
|
return _show_error(_response_message(body, "Account creation failed."))
|
||||||
_clear_registration_passwords()
|
_send_login(login_attempt_username, registration_password_input.text, true)
|
||||||
_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 _validate_registration(username: String, email: String, password: String, confirmation: String) -> String:
|
func _request_reset() -> void:
|
||||||
var username_pattern := RegEx.new()
|
if reset_login_input.text.strip_edges().is_empty(): return _show_error("Enter your email or username.")
|
||||||
username_pattern.compile("^[A-Za-z0-9_]{3,24}$")
|
_send_json("reset_request", "/password-reset/request", {"login": reset_login_input.text.strip_edges()}, "Sending code...")
|
||||||
if username_pattern.search(username) == null:
|
|
||||||
registration_username_input.grab_focus()
|
|
||||||
return "Username must be 3-24 characters using only letters, numbers, or underscores."
|
func _on_reset_requested(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
var email_pattern := RegEx.new()
|
_set_busy(false)
|
||||||
email_pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
|
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
|
||||||
if email_pattern.search(email) == null:
|
return _show_error(_response_message(body, "Unable to send a verification code."))
|
||||||
registration_email_input.grab_focus()
|
reset_requested = true
|
||||||
return "Enter a valid email address."
|
reset_confirm_group.visible = true
|
||||||
if password.length() < 8:
|
_show_success("If the account exists, a verification code has been sent.")
|
||||||
registration_password_input.grab_focus()
|
reset_code_input.grab_focus()
|
||||||
return "Password must be at least 8 characters."
|
|
||||||
if password != confirmation:
|
|
||||||
registration_confirm_input.grab_focus()
|
func _confirm_reset() -> void:
|
||||||
return "Passwords do not match."
|
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)
|
||||||
|
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)
|
||||||
|
_show_error("Unable to send the request. Check your connection and try again.")
|
||||||
|
|
||||||
|
|
||||||
|
func _set_busy(busy: bool) -> void:
|
||||||
|
for button in submit_buttons: button.disabled = busy
|
||||||
|
for field in fields: field.editable = not busy
|
||||||
|
|
||||||
|
|
||||||
|
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 ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func _set_busy(is_busy: bool) -> void:
|
func _parse_dictionary(body: PackedByteArray) -> Dictionary:
|
||||||
if login_button == null:
|
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||||
return
|
return parsed if typeof(parsed) == TYPE_DICTIONARY else {}
|
||||||
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 _clear_registration_passwords() -> void:
|
func _response_message(body: PackedByteArray, fallback: String) -> String:
|
||||||
registration_password_input.text = ""
|
|
||||||
registration_confirm_input.text = ""
|
|
||||||
|
|
||||||
|
|
||||||
func _response_message(body: PackedByteArray) -> String:
|
|
||||||
var raw := body.get_string_from_utf8().strip_edges()
|
var raw := body.get_string_from_utf8().strip_edges()
|
||||||
var parsed: Variant = JSON.parse_string(raw)
|
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:
|
func _clear_message() -> void:
|
||||||
_on_login_pressed()
|
if message_label != null: message_label.text = ""
|
||||||
|
|
||||||
|
|
||||||
func _on_register_input_submitted(_new_text: String) -> void:
|
func _show_error(value: String) -> void:
|
||||||
_on_register_pressed()
|
_set_busy(false)
|
||||||
|
message_label.text = value
|
||||||
|
message_label.add_theme_color_override("font_color", Color("ef4444"))
|
||||||
func _clear_messages() -> void:
|
|
||||||
error_label.text = ""
|
|
||||||
success_label.text = ""
|
|
||||||
|
|
||||||
|
|
||||||
func _show_error(message: String) -> void:
|
|
||||||
UISoundService.play_sound("error")
|
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
@@ -14,6 +14,7 @@ var feedback_button: Button
|
|||||||
var feedback_dialog: Control
|
var feedback_dialog: Control
|
||||||
var theme_service
|
var theme_service
|
||||||
var current_screen_name := ""
|
var current_screen_name := ""
|
||||||
|
var screens_margin: MarginContainer
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
theme_service = get_node("/root/UIThemeService")
|
theme_service = get_node("/root/UIThemeService")
|
||||||
@@ -47,6 +48,12 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
current_screen_name = screen_name
|
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
|
screen.visible = true
|
||||||
if header_panel != null:
|
if header_panel != null:
|
||||||
header_panel.visible = screen_name not in ["login", "player", "ccs_player"]
|
header_panel.visible = screen_name not in ["login", "player", "ccs_player"]
|
||||||
@@ -77,7 +84,7 @@ func _build_ui() -> void:
|
|||||||
header_panel.add_child(header)
|
header_panel.add_child(header)
|
||||||
|
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "RanaZe's Doctor Maker"
|
title.text = "MedMind"
|
||||||
Style.set_label(title, 31, Style.TEXT)
|
Style.set_label(title, 31, Style.TEXT)
|
||||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
header.add_child(title)
|
header.add_child(title)
|
||||||
@@ -115,6 +122,7 @@ func _build_ui() -> void:
|
|||||||
_refresh_theme_button()
|
_refresh_theme_button()
|
||||||
|
|
||||||
var screens_container := MarginContainer.new()
|
var screens_container := MarginContainer.new()
|
||||||
|
screens_margin = screens_container
|
||||||
screens_container.name = "Screens"
|
screens_container.name = "Screens"
|
||||||
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
screens_container.add_theme_constant_override("margin_left", 24)
|
screens_container.add_theme_constant_override("margin_left", 24)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://defm4b6tk5n1h
|
||||||
Reference in New Issue
Block a user