Groups it with Profile/Settings/Log out instead of the primary study nav, and shortens the label from "RZ Coin Shop" to "RZ Shop".
983 lines
37 KiB
GDScript
983 lines
37 KiB
GDScript
extends Control
|
|
|
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
|
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
|
const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd")
|
|
const CreditsBadge := preload("res://scripts/ui/credits_badge.gd")
|
|
|
|
var screens := {}
|
|
var _account_profile_synced := false
|
|
var header_panel: PanelContainer
|
|
var header_title: Label
|
|
var environment_badge: Label
|
|
var app_menu: PanelContainer
|
|
var menu_exit_button: Button
|
|
var theme_button: Button
|
|
var feedback_button: Button
|
|
var feedback_dialog: Control
|
|
var notes_dialog: Control
|
|
var session_expiry_dialog: Control
|
|
var legal_document_dialog: Control
|
|
var theme_service
|
|
var current_screen_name := ""
|
|
var screens_margin: MarginContainer
|
|
var screens_host: MarginContainer
|
|
var shell_root: HBoxContainer
|
|
var sidebar: PanelContainer
|
|
var sidebar_content: VBoxContainer
|
|
var drawer_backdrop: ColorRect
|
|
var drawer_open := false
|
|
var menu_button: Button
|
|
var account_button: Button
|
|
var account_chevron_button: Button
|
|
var credits_badge: PanelContainer
|
|
var account_menu: PanelContainer
|
|
var nav_buttons: Dictionary = {}
|
|
var current_nav_key := "home"
|
|
var compact_shell := false
|
|
var app_background: ColorRect
|
|
|
|
func _ready() -> void:
|
|
theme_service = get_node("/root/UIThemeService")
|
|
Style.apply_app_theme(self)
|
|
_build_ui()
|
|
theme_service.theme_changed.connect(_on_theme_changed)
|
|
resized.connect(_update_responsive_layout)
|
|
_update_responsive_layout()
|
|
show_screen(("legal_consent" if AuthState.requires_legal_acceptance else "dashboard") if AuthState.is_logged_in else "login")
|
|
|
|
|
|
func _update_responsive_layout() -> void:
|
|
compact_shell = size.x < 900.0
|
|
if sidebar != null:
|
|
sidebar.visible = AuthState.is_logged_in and not compact_shell and _screen_uses_shell(current_screen_name)
|
|
if menu_button != null:
|
|
menu_button.visible = AuthState.is_logged_in and compact_shell and _screen_uses_shell(current_screen_name)
|
|
_apply_screen_margins(current_screen_name)
|
|
if feedback_button != null:
|
|
feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0
|
|
if account_chevron_button != null:
|
|
account_chevron_button.visible = size.x >= 480.0
|
|
if drawer_open and not compact_shell:
|
|
_close_drawer()
|
|
if app_menu != null:
|
|
app_menu.custom_minimum_size.x = minf(340.0, maxf(280.0, size.x - 32.0))
|
|
if app_menu.visible:
|
|
app_menu.size = app_menu.custom_minimum_size
|
|
app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5
|
|
|
|
|
|
func show_screen(screen_name: String, payload: Variant = null) -> void:
|
|
_close_app_menu()
|
|
_close_account_menu()
|
|
_close_drawer()
|
|
for child in screens_host.get_children():
|
|
if child.visible and child.has_method("on_hide"):
|
|
child.on_hide()
|
|
child.visible = false
|
|
|
|
var screen = screens.get(screen_name)
|
|
if screen == null:
|
|
return
|
|
|
|
current_screen_name = screen_name
|
|
_update_header_title(screen_name)
|
|
_apply_screen_margins(screen_name)
|
|
screen.visible = true
|
|
if header_panel != null:
|
|
header_panel.visible = _screen_uses_shell(screen_name)
|
|
_update_shell_visibility(screen_name)
|
|
_update_active_navigation(screen_name, payload)
|
|
if screen.has_method("on_show"):
|
|
screen.on_show(payload)
|
|
|
|
|
|
func _apply_screen_margins(screen_name: String) -> void:
|
|
if screens_margin == null or screen_name.is_empty():
|
|
return
|
|
var is_fullscreen_study := screen_name == "player"
|
|
var margin := 0 if screen_name == "login" else (12 if is_fullscreen_study else (20 if compact_shell else 44))
|
|
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 (12 if is_fullscreen_study else (20 if compact_shell else 34)))
|
|
screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else (12 if is_fullscreen_study else 28))
|
|
|
|
|
|
func _build_ui() -> void:
|
|
app_background = ColorRect.new()
|
|
app_background.color = Brand.background_color(UIThemeService.is_dark_mode())
|
|
app_background.material = Brand.background_material(UIThemeService.is_dark_mode())
|
|
app_background.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
add_child(app_background)
|
|
|
|
shell_root = HBoxContainer.new()
|
|
shell_root.name = "Root"
|
|
shell_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
shell_root.add_theme_constant_override("separation", 0)
|
|
add_child(shell_root)
|
|
|
|
sidebar = _build_sidebar()
|
|
shell_root.add_child(sidebar)
|
|
|
|
var main_column := VBoxContainer.new()
|
|
main_column.name = "MainColumn"
|
|
main_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
main_column.add_theme_constant_override("separation", 0)
|
|
shell_root.add_child(main_column)
|
|
|
|
header_panel = PanelContainer.new()
|
|
header_panel.name = "Header"
|
|
header_panel.add_theme_stylebox_override("panel", _top_bar_box())
|
|
main_column.add_child(header_panel)
|
|
|
|
var header := HBoxContainer.new()
|
|
header.custom_minimum_size = Vector2(0, 80)
|
|
header.add_theme_constant_override("separation", 14)
|
|
header.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
header_panel.add_child(header)
|
|
|
|
menu_button = Button.new()
|
|
menu_button.icon = preload("res://assets/icons/lucide/menu.svg")
|
|
menu_button.add_theme_constant_override("icon_max_width", 24)
|
|
menu_button.tooltip_text = "Open navigation"
|
|
menu_button.custom_minimum_size = Vector2(48, 48)
|
|
menu_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
menu_button.visible = false
|
|
menu_button.pressed.connect(_open_drawer)
|
|
header.add_child(menu_button)
|
|
|
|
header_title = Label.new()
|
|
header_title.add_theme_font_override("font", Brand.font)
|
|
header_title.add_theme_font_size_override("font_size", 22)
|
|
header_title.add_theme_color_override("font_color", Brand.text_color(UIThemeService.is_dark_mode()))
|
|
header.add_child(header_title)
|
|
|
|
environment_badge = Label.new()
|
|
environment_badge.text = "UAT"
|
|
environment_badge.tooltip_text = "User acceptance testing environment"
|
|
environment_badge.visible = Brand.is_uat_environment()
|
|
environment_badge.add_theme_font_override("font", Brand.font)
|
|
environment_badge.add_theme_font_size_override("font_size", 13)
|
|
environment_badge.add_theme_color_override("font_color", Color("052e16"))
|
|
environment_badge.add_theme_stylebox_override("normal", _environment_badge_box())
|
|
header.add_child(environment_badge)
|
|
|
|
var header_spacer := Control.new()
|
|
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
header.add_child(header_spacer)
|
|
|
|
feedback_button = Button.new()
|
|
feedback_button.text = "Feedback"
|
|
feedback_button.tooltip_text = "Send general feedback or report a bug"
|
|
feedback_button.custom_minimum_size = Vector2(124, 42)
|
|
feedback_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
_style_header_feedback_button(feedback_button)
|
|
feedback_button.pressed.connect(open_feedback_dialog)
|
|
header.add_child(feedback_button)
|
|
|
|
theme_button = Button.new()
|
|
theme_button.tooltip_text = "Switch color theme"
|
|
theme_button.custom_minimum_size = Vector2(44, 44)
|
|
theme_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
theme_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
theme_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
theme_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
theme_button.focus_mode = Control.FOCUS_NONE
|
|
theme_button.pressed.connect(theme_service.toggle_theme)
|
|
_style_shell_theme_button(theme_button)
|
|
header.add_child(theme_button)
|
|
_refresh_theme_button()
|
|
|
|
account_button = Button.new()
|
|
account_button.tooltip_text = "Open account menu"
|
|
account_button.custom_minimum_size = Vector2(44, 44)
|
|
account_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
account_button.pressed.connect(_toggle_account_menu)
|
|
_style_account_avatar(account_button)
|
|
header.add_child(account_button)
|
|
|
|
account_chevron_button = Button.new()
|
|
account_chevron_button.icon = preload("res://assets/icons/lucide/chevron-down.svg")
|
|
account_chevron_button.tooltip_text = "Open account menu"
|
|
account_chevron_button.custom_minimum_size = Vector2(28, 44)
|
|
account_chevron_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
account_chevron_button.add_theme_constant_override("icon_max_width", 17)
|
|
account_chevron_button.pressed.connect(_toggle_account_menu)
|
|
_style_account_chevron(account_chevron_button)
|
|
header.add_child(account_chevron_button)
|
|
|
|
credits_badge = CreditsBadge.new()
|
|
credits_badge.store_requested.connect(func(): show_screen("rz_store"))
|
|
header.add_child(credits_badge)
|
|
|
|
var screens_container := MarginContainer.new()
|
|
screens_margin = screens_container
|
|
screens_host = screens_container
|
|
screens_container.name = "Screens"
|
|
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
screens_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
screens_container.add_theme_constant_override("margin_left", 24)
|
|
screens_container.add_theme_constant_override("margin_right", 24)
|
|
screens_container.add_theme_constant_override("margin_top", 18)
|
|
screens_container.add_theme_constant_override("margin_bottom", 22)
|
|
main_column.add_child(screens_container)
|
|
|
|
var menu := preload("res://scripts/ui/main_menu.gd").new()
|
|
menu.name = "MainMenu"
|
|
menu.main = self
|
|
screens_container.add_child(menu)
|
|
screens["menu"] = menu
|
|
|
|
var login := preload("res://scripts/ui/login_screen.gd").new()
|
|
login.name = "LoginScreen"
|
|
login.main = self
|
|
screens_container.add_child(login)
|
|
screens["login"] = login
|
|
|
|
var legal_consent := preload("res://scripts/ui/legal_consent_screen.gd").new()
|
|
legal_consent.name = "LegalConsentScreen"
|
|
legal_consent.main = self
|
|
screens_container.add_child(legal_consent)
|
|
screens["legal_consent"] = legal_consent
|
|
|
|
var dashboard := preload("res://scripts/ui/dashboard_screen.gd").new()
|
|
dashboard.name = "DashboardScreen"
|
|
dashboard.main = self
|
|
screens_container.add_child(dashboard)
|
|
screens["dashboard"] = dashboard
|
|
|
|
var selector := preload("res://scripts/ui/study_selector.gd").new()
|
|
selector.name = "StudySelector"
|
|
selector.main = self
|
|
selector.stage_changed.connect(_on_selector_stage_changed)
|
|
screens_container.add_child(selector)
|
|
screens["selection"] = selector
|
|
|
|
var dynamic_questions := preload("res://scripts/ui/dynamic_questions_screen.gd").new()
|
|
dynamic_questions.name = "DynamicQuestionsScreen"
|
|
dynamic_questions.main = self
|
|
screens_container.add_child(dynamic_questions)
|
|
screens["dynamic_questions"] = dynamic_questions
|
|
|
|
var map := preload("res://scripts/ui/study_map.gd").new()
|
|
map.name = "StudyMap"
|
|
map.main = self
|
|
screens_container.add_child(map)
|
|
screens["map"] = map
|
|
|
|
var setup := preload("res://scripts/ui/block_setup.gd").new()
|
|
setup.name = "BlockSetup"
|
|
setup.main = self
|
|
screens_container.add_child(setup)
|
|
screens["setup"] = setup
|
|
|
|
var player := preload("res://scripts/ui/question_player.gd").new()
|
|
player.name = "QuestionPlayer"
|
|
player.main = self
|
|
screens_container.add_child(player)
|
|
screens["player"] = player
|
|
|
|
var review := preload("res://scripts/ui/review_results.gd").new()
|
|
review.name = "ReviewResults"
|
|
review.main = self
|
|
screens_container.add_child(review)
|
|
screens["review"] = review
|
|
|
|
var review_queue_results := preload("res://scripts/ui/review_queue_results.gd").new()
|
|
review_queue_results.name = "ReviewQueueResults"
|
|
review_queue_results.main = self
|
|
screens_container.add_child(review_queue_results)
|
|
screens["review_queue_results"] = review_queue_results
|
|
|
|
var simulation_break := preload("res://scripts/ui/simulation_break.gd").new()
|
|
simulation_break.name = "SimulationBreak"
|
|
simulation_break.main = self
|
|
screens_container.add_child(simulation_break)
|
|
screens["simulation_break"] = simulation_break
|
|
|
|
var simulation_results := preload("res://scripts/ui/simulation_results.gd").new()
|
|
simulation_results.name = "SimulationResults"
|
|
simulation_results.main = self
|
|
screens_container.add_child(simulation_results)
|
|
screens["simulation_results"] = simulation_results
|
|
|
|
var simulation_review := preload("res://scripts/ui/simulation_review.gd").new()
|
|
simulation_review.name = "SimulationReview"
|
|
simulation_review.main = self
|
|
screens_container.add_child(simulation_review)
|
|
screens["simulation_review"] = simulation_review
|
|
|
|
var simulation_history := preload("res://scripts/ui/simulation_history.gd").new()
|
|
simulation_history.name = "SimulationHistory"
|
|
simulation_history.main = self
|
|
screens_container.add_child(simulation_history)
|
|
screens["simulation_history"] = simulation_history
|
|
|
|
var ccs_hub := preload("res://scripts/ui/ccs_hub.gd").new()
|
|
ccs_hub.name = "CcsHub"; ccs_hub.main = self; screens_container.add_child(ccs_hub); screens["ccs_hub"] = ccs_hub
|
|
var ccs_player := preload("res://scripts/ui/ccs_player.gd").new()
|
|
ccs_player.name = "CcsPlayer"; ccs_player.main = self; screens_container.add_child(ccs_player); screens["ccs_player"] = ccs_player
|
|
var ccs_review := preload("res://scripts/ui/ccs_review.gd").new()
|
|
ccs_review.name = "CcsReview"; ccs_review.main = self; screens_container.add_child(ccs_review); screens["ccs_review"] = ccs_review
|
|
var ccs_history := preload("res://scripts/ui/ccs_history.gd").new()
|
|
ccs_history.name = "CcsHistory"; ccs_history.main = self; screens_container.add_child(ccs_history); screens["ccs_history"] = ccs_history
|
|
|
|
var flashcards_hub := preload("res://scripts/ui/flashcards_hub.gd").new()
|
|
flashcards_hub.name = "FlashcardsHub"; flashcards_hub.main = self
|
|
screens_container.add_child(flashcards_hub); screens["flashcards_hub"] = flashcards_hub
|
|
var flashcards_study := preload("res://scripts/ui/flashcards_study.gd").new()
|
|
flashcards_study.name = "FlashcardsStudy"; flashcards_study.main = self
|
|
screens_container.add_child(flashcards_study); screens["flashcards_study"] = flashcards_study
|
|
|
|
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
|
profile.name = "ProgressProfile"
|
|
profile.main = self
|
|
screens_container.add_child(profile)
|
|
screens["profile"] = profile
|
|
|
|
var settings := preload("res://scripts/ui/settings_screen.gd").new()
|
|
settings.name = "SettingsScreen"
|
|
settings.main = self
|
|
screens_container.add_child(settings)
|
|
screens["settings"] = settings
|
|
|
|
var notes := preload("res://scripts/ui/notes_screen.gd").new()
|
|
notes.name = "NotesScreen"
|
|
notes.main = self
|
|
screens_container.add_child(notes)
|
|
screens["notes"] = notes
|
|
var bookmarks := preload("res://scripts/ui/bookmarks_screen.gd").new()
|
|
bookmarks.name = "BookmarksScreen"
|
|
bookmarks.main = self
|
|
screens_container.add_child(bookmarks)
|
|
screens["bookmarks"] = bookmarks
|
|
var donations := preload("res://scripts/ui/donation_screen.gd").new()
|
|
donations.name = "DonationScreen"
|
|
donations.main = self
|
|
screens_container.add_child(donations)
|
|
screens["donations"] = donations
|
|
var rz_store := preload("res://scripts/ui/rz_store_screen.gd").new()
|
|
rz_store.name = "RzCoinStore"
|
|
rz_store.main = self
|
|
screens_container.add_child(rz_store)
|
|
screens["rz_store"] = rz_store
|
|
|
|
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
|
|
feedback_dialog.name = "AppFeedbackDialog"
|
|
add_child(feedback_dialog)
|
|
notes_dialog = preload("res://scripts/ui/notes_dialog.gd").new()
|
|
notes_dialog.name = "NotesDialog"
|
|
add_child(notes_dialog)
|
|
session_expiry_dialog = preload("res://scripts/ui/session_expiry_dialog.gd").new()
|
|
session_expiry_dialog.name = "SessionExpiryDialog"
|
|
session_expiry_dialog.logout_requested.connect(_logout)
|
|
add_child(session_expiry_dialog)
|
|
legal_document_dialog = preload("res://scripts/ui/legal_document_dialog.gd").new()
|
|
legal_document_dialog.name = "LegalDocumentDialog"
|
|
add_child(legal_document_dialog)
|
|
_build_account_menu()
|
|
_build_drawer()
|
|
|
|
AuthState.auth_changed.connect(_on_auth_changed)
|
|
AuthState.session_expired.connect(_on_session_expired)
|
|
_refresh_header_controls()
|
|
_build_app_menu()
|
|
|
|
|
|
func _build_sidebar() -> PanelContainer:
|
|
var panel := PanelContainer.new()
|
|
panel.name = "Sidebar"
|
|
panel.custom_minimum_size = Vector2(304, 0)
|
|
panel.add_theme_stylebox_override("panel", _sidebar_box())
|
|
sidebar_content = VBoxContainer.new()
|
|
sidebar_content.add_theme_constant_override("separation", 8)
|
|
panel.add_child(sidebar_content)
|
|
_populate_navigation(sidebar_content, true)
|
|
return panel
|
|
|
|
|
|
func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void:
|
|
if include_brand:
|
|
var brand_row := HBoxContainer.new()
|
|
brand_row.custom_minimum_size = Vector2(0, 56)
|
|
brand_row.add_theme_constant_override("separation", 10)
|
|
container.add_child(brand_row)
|
|
var mark := TextureRect.new()
|
|
mark.texture = Brand.ranaze_mark_texture()
|
|
mark.custom_minimum_size = Vector2(46, 46)
|
|
mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
brand_row.add_child(mark)
|
|
var wordmark := HBoxContainer.new()
|
|
wordmark.add_theme_constant_override("separation", 0)
|
|
brand_row.add_child(wordmark)
|
|
var rana := Label.new()
|
|
rana.text = "Rana"
|
|
rana.add_theme_font_override("font", Brand.font)
|
|
rana.add_theme_font_size_override("font_size", 27)
|
|
wordmark.add_child(rana)
|
|
var ze := Label.new()
|
|
ze.text = "Ze"
|
|
ze.add_theme_font_override("font", Brand.font)
|
|
ze.add_theme_font_size_override("font_size", 27)
|
|
ze.add_theme_color_override("font_color", Brand.SKY_BLUE)
|
|
wordmark.add_child(ze)
|
|
container.add_child(HSeparator.new())
|
|
|
|
_add_nav_button(container, "home", "Dashboard", "chart-no-axes-column-increasing", true)
|
|
_add_nav_button(container, "question_bank", "Question Bank", "clipboard-list", true)
|
|
_add_nav_button(container, "dynamic_questions", "Dynamic Questions", "activity", true, Brand.is_uat_environment())
|
|
_add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true, Brand.is_uat_environment())
|
|
_add_nav_button(container, "exams", "Exam Simulations", "clipboard-list", true)
|
|
_add_nav_button(container, "flashcards", "Flashcards", "clipboard-plus", true, Brand.is_uat_environment())
|
|
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", true)
|
|
_add_nav_button(container, "notes", "Notes", "notebook-pen", true)
|
|
|
|
var spacer := Control.new()
|
|
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
container.add_child(spacer)
|
|
container.add_child(HSeparator.new())
|
|
_add_nav_button(container, "donations", "Support RanaZe", "heart-pulse", true)
|
|
_add_nav_button(container, "performance", "Profile", "user-round", true)
|
|
_add_nav_button(container, "rz_store", "RZ Shop", "star", true)
|
|
_add_nav_button(container, "settings", "Settings", "settings", true)
|
|
_add_nav_button(container, "logout", "Log out", "log-out", true)
|
|
|
|
|
|
func _add_nav_button(container: VBoxContainer, key: String, label_text: String, icon_name: String, enabled: bool, visible_flag: bool = true) -> void:
|
|
var button := Button.new()
|
|
button.text = label_text
|
|
button.name = "Nav%s" % key.to_pascal_case()
|
|
button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name)
|
|
button.expand_icon = true
|
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
button.custom_minimum_size = Vector2(0, 58)
|
|
button.add_theme_constant_override("icon_max_width", 25)
|
|
button.add_theme_constant_override("h_separation", 18)
|
|
button.add_theme_font_override("font", Brand.font)
|
|
button.add_theme_font_size_override("font_size", 18)
|
|
button.disabled = not enabled
|
|
button.tooltip_text = "Coming Soon" if not enabled else label_text
|
|
button.visible = visible_flag
|
|
button.pressed.connect(_on_navigation_pressed.bind(key))
|
|
container.add_child(button)
|
|
if not nav_buttons.has(key):
|
|
nav_buttons[key] = []
|
|
var instances: Array = nav_buttons[key]
|
|
instances.append(button)
|
|
nav_buttons[key] = instances
|
|
_style_nav_button(button, key == current_nav_key)
|
|
|
|
|
|
func _on_navigation_pressed(key: String) -> void:
|
|
match key:
|
|
"home": show_screen("dashboard")
|
|
"question_bank": show_screen("selection", "question_bank")
|
|
"dynamic_questions": show_screen("dynamic_questions")
|
|
"exams": show_screen("simulation_history")
|
|
"ccs": show_screen("ccs_hub")
|
|
"flashcards": show_screen("flashcards_study")
|
|
"performance": show_screen("profile")
|
|
"study_plan": show_screen("map")
|
|
"settings": show_screen("settings")
|
|
"notes": show_screen("notes")
|
|
"bookmarks": show_screen("bookmarks")
|
|
"donations": show_screen("donations")
|
|
"rz_store": show_screen("rz_store")
|
|
"logout": _logout()
|
|
|
|
|
|
func _update_header_title(screen_name: String) -> void:
|
|
if header_title == null:
|
|
return
|
|
var titles := {
|
|
"dashboard": "Dashboard",
|
|
"selection": "Question Bank",
|
|
"dynamic_questions": "Dynamic Questions",
|
|
"simulation_history": "Exam Simulations",
|
|
"ccs_hub": "CCS Cases",
|
|
"flashcards_hub": "Flashcards",
|
|
"flashcards_study": "Flashcards",
|
|
"profile": "Profile",
|
|
"settings": "Settings",
|
|
"notes": "Notes",
|
|
"bookmarks": "Bookmarks",
|
|
"donations": "Support RanaZe"
|
|
}
|
|
header_title.text = str(titles.get(screen_name, ""))
|
|
|
|
|
|
func _build_account_menu() -> void:
|
|
account_menu = PanelContainer.new()
|
|
account_menu.visible = false
|
|
account_menu.top_level = true
|
|
account_menu.custom_minimum_size = Vector2(230, 230)
|
|
account_menu.add_theme_stylebox_override("panel", Brand.panel_box(UIThemeService.is_dark_mode()))
|
|
add_child(account_menu)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 8)
|
|
account_menu.add_child(content)
|
|
for spec in [["Profile", "user-round", "performance"], ["Settings", "settings", "settings"], ["Log out", "log-out", "logout"]]:
|
|
var button := Button.new()
|
|
button.text = spec[0]
|
|
button.icon = load("res://assets/icons/lucide/%s.svg" % spec[1])
|
|
button.add_theme_constant_override("icon_max_width", 22)
|
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
button.custom_minimum_size = Vector2(0, 46)
|
|
button.pressed.connect(_on_navigation_pressed.bind(spec[2]))
|
|
content.add_child(button)
|
|
|
|
|
|
func _build_drawer() -> void:
|
|
drawer_backdrop = ColorRect.new()
|
|
drawer_backdrop.visible = false
|
|
drawer_backdrop.top_level = true
|
|
drawer_backdrop.color = Color(0, 0, 0, 0.62)
|
|
drawer_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
drawer_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
drawer_backdrop.gui_input.connect(_on_drawer_backdrop_input)
|
|
add_child(drawer_backdrop)
|
|
var drawer := PanelContainer.new()
|
|
drawer.custom_minimum_size = Vector2(304, 0)
|
|
drawer.set_anchors_preset(Control.PRESET_LEFT_WIDE)
|
|
drawer.offset_right = 304
|
|
drawer.add_theme_stylebox_override("panel", _sidebar_box())
|
|
drawer.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
drawer_backdrop.add_child(drawer)
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 8)
|
|
drawer.add_child(content)
|
|
_populate_navigation(content, true)
|
|
|
|
|
|
func _open_drawer() -> void:
|
|
if drawer_backdrop == null:
|
|
return
|
|
drawer_open = true
|
|
drawer_backdrop.visible = true
|
|
|
|
|
|
func _close_drawer() -> void:
|
|
drawer_open = false
|
|
if drawer_backdrop != null:
|
|
drawer_backdrop.visible = false
|
|
|
|
|
|
func _on_drawer_backdrop_input(event: InputEvent) -> void:
|
|
var click := event as InputEventMouseButton
|
|
if click != null and click.pressed:
|
|
_close_drawer()
|
|
|
|
|
|
func _toggle_account_menu() -> void:
|
|
if account_menu == null:
|
|
return
|
|
if account_menu.visible:
|
|
_close_account_menu()
|
|
return
|
|
account_menu.visible = true
|
|
account_menu.size = account_menu.custom_minimum_size
|
|
account_menu.position = Vector2(
|
|
get_viewport_rect().size.x - account_menu.size.x - 24,
|
|
82
|
|
)
|
|
|
|
|
|
func _close_account_menu() -> void:
|
|
if account_menu != null:
|
|
account_menu.visible = false
|
|
|
|
|
|
func _screen_uses_shell(screen_name: String) -> bool:
|
|
return AuthState.is_logged_in and screen_name not in ["login", "legal_consent", "player", "ccs_player"]
|
|
|
|
|
|
func _update_shell_visibility(screen_name: String) -> void:
|
|
var show_shell := _screen_uses_shell(screen_name)
|
|
if sidebar != null:
|
|
sidebar.visible = show_shell and not compact_shell
|
|
if header_panel != null:
|
|
header_panel.visible = show_shell
|
|
if menu_button != null:
|
|
menu_button.visible = show_shell and compact_shell
|
|
|
|
|
|
func _update_active_navigation(screen_name: String, payload: Variant = null) -> void:
|
|
var key := NavigationRoutes.key_for_screen(screen_name, payload)
|
|
current_nav_key = key
|
|
for nav_key in nav_buttons:
|
|
for button in nav_buttons[nav_key]:
|
|
_style_nav_button(button, nav_key == current_nav_key)
|
|
|
|
|
|
func _on_selector_stage_changed(stage_name: String) -> void:
|
|
if current_screen_name == "selection":
|
|
_update_active_navigation("selection", stage_name)
|
|
|
|
|
|
func _style_nav_button(button: Button, active: bool) -> void:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var normal := StyleBoxFlat.new()
|
|
normal.bg_color = (Color("14255b") if dark else Color("e5edff")) if active else Color(0, 0, 0, 0)
|
|
normal.set_corner_radius_all(10)
|
|
normal.content_margin_left = 18
|
|
normal.content_margin_right = 18
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
var hover := normal.duplicate() as StyleBoxFlat
|
|
hover.bg_color = Color("172d68") if dark else Color("d8e5ff")
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
var disabled := normal.duplicate() as StyleBoxFlat
|
|
disabled.bg_color = Color(0, 0, 0, 0)
|
|
button.add_theme_stylebox_override("disabled", disabled)
|
|
button.add_theme_color_override("font_color", (Color("f8fafc") if dark else Color("0f172a")) if active else (Color("b8c2d6") if dark else Color("53627a")))
|
|
button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if active else (Color("b8c2d6") if dark else Color("53627a")))
|
|
button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE)
|
|
button.add_theme_color_override("font_disabled_color", Color("596982") if dark else Color("94a3b8"))
|
|
button.add_theme_color_override("icon_disabled_color", Color("596982") if dark else Color("94a3b8"))
|
|
|
|
|
|
func _sidebar_box() -> StyleBoxFlat:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var box := Brand.shell_box(dark, "right")
|
|
box.content_margin_left = 28
|
|
box.content_margin_right = 28
|
|
box.content_margin_top = 12
|
|
box.content_margin_bottom = 26
|
|
return box
|
|
|
|
|
|
func _top_bar_box() -> StyleBoxFlat:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var box := Brand.shell_box(dark, "bottom")
|
|
box.content_margin_left = 28
|
|
box.content_margin_right = 28
|
|
return box
|
|
|
|
|
|
func _environment_badge_box() -> StyleBoxFlat:
|
|
var box := StyleBoxFlat.new()
|
|
box.bg_color = Color("86efac")
|
|
box.set_corner_radius_all(6)
|
|
box.content_margin_left = 9
|
|
box.content_margin_right = 9
|
|
box.content_margin_top = 4
|
|
box.content_margin_bottom = 4
|
|
return box
|
|
|
|
|
|
func _style_header_feedback_button(button: Button) -> void:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var normal := Style.button_box(Color(0, 0, 0, 0), Brand.ROYAL_BLUE)
|
|
var hover := Style.button_box(Color(Brand.ROYAL_BLUE, 0.16), Brand.SKY_BLUE)
|
|
var pressed := Style.button_box(Color(Brand.ROYAL_BLUE, 0.24), Brand.SKY_BLUE)
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
button.add_theme_stylebox_override("pressed", pressed)
|
|
button.add_theme_color_override("font_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE)
|
|
button.add_theme_color_override("font_hover_color", Brand.SKY_BLUE)
|
|
button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE)
|
|
button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE)
|
|
|
|
|
|
func _style_account_avatar(button: Button) -> void:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var normal := StyleBoxFlat.new()
|
|
normal.bg_color = Color("26324f") if dark else Color("dce5f5")
|
|
normal.corner_radius_top_left = 22
|
|
normal.corner_radius_top_right = 22
|
|
normal.corner_radius_bottom_left = 22
|
|
normal.corner_radius_bottom_right = 22
|
|
var hover := normal.duplicate() as StyleBoxFlat
|
|
hover.bg_color = Color("344465") if dark else Color("cad7ed")
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
button.add_theme_stylebox_override("pressed", hover)
|
|
button.add_theme_color_override("font_color", Color("f8fafc") if dark else Color("172033"))
|
|
button.add_theme_color_override("font_hover_color", Color("ffffff") if dark else Color("0f172a"))
|
|
|
|
|
|
func _style_account_chevron(button: Button) -> void:
|
|
var empty := StyleBoxEmpty.new()
|
|
button.add_theme_stylebox_override("normal", empty)
|
|
button.add_theme_stylebox_override("hover", empty)
|
|
button.add_theme_stylebox_override("pressed", empty)
|
|
button.add_theme_color_override("icon_normal_color", Style.MUTED)
|
|
button.add_theme_color_override("icon_hover_color", Style.TEXT)
|
|
|
|
|
|
func _style_shell_theme_button(button: Button) -> void:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
var normal := StyleBoxFlat.new()
|
|
normal.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE
|
|
normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER
|
|
normal.set_border_width_all(1)
|
|
normal.set_corner_radius_all(22)
|
|
normal.content_margin_left = 0
|
|
normal.content_margin_right = 0
|
|
normal.content_margin_top = 0
|
|
normal.content_margin_bottom = 0
|
|
var hover := normal.duplicate() as StyleBoxFlat
|
|
hover.border_color = Brand.AMBER
|
|
var focus := StyleBoxEmpty.new()
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
button.add_theme_stylebox_override("pressed", hover)
|
|
button.add_theme_stylebox_override("focus", focus)
|
|
button.add_theme_constant_override("h_separation", 0)
|
|
button.add_theme_color_override("font_color", Brand.AMBER)
|
|
button.add_theme_color_override("font_hover_color", Brand.AMBER)
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if not AuthState.is_logged_in or current_screen_name == "login":
|
|
return
|
|
if event.is_action_pressed("ui_cancel"):
|
|
if drawer_open:
|
|
_close_drawer()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if account_menu != null and account_menu.visible:
|
|
_close_account_menu()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if current_screen_name in ["player", "ccs_player", "flashcards_study"]:
|
|
_toggle_app_menu()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _refresh_header_controls() -> void:
|
|
if account_button != null:
|
|
account_button.text = _account_initials(AuthState.username)
|
|
if feedback_button != null:
|
|
feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0
|
|
feedback_button.icon = preload("res://assets/icons/lucide/message-square.svg")
|
|
feedback_button.add_theme_constant_override("icon_max_width", 21)
|
|
|
|
|
|
func _account_initials(username: String) -> String:
|
|
var cleaned := ""
|
|
for character in username:
|
|
if character.to_lower() != character.to_upper():
|
|
cleaned += character
|
|
elif character.is_valid_int():
|
|
cleaned += character
|
|
if cleaned.length() >= 2:
|
|
break
|
|
return cleaned.to_upper() if not cleaned.is_empty() else "ME"
|
|
|
|
|
|
func open_feedback_dialog() -> void:
|
|
if not AuthState.is_logged_in or feedback_dialog == null:
|
|
return
|
|
feedback_dialog.show_dialog(FeedbackService.build_context(current_screen_name, get_viewport_rect().size))
|
|
|
|
|
|
func open_legal_document(kind: String) -> void:
|
|
legal_document_dialog.show_document(kind)
|
|
|
|
|
|
func _on_auth_changed() -> void:
|
|
_refresh_header_controls()
|
|
if AuthState.is_logged_in:
|
|
if not _account_profile_synced:
|
|
_account_profile_synced = true
|
|
AccountProfileService.refresh()
|
|
CreditsService.refresh()
|
|
else:
|
|
_account_profile_synced = false
|
|
if AuthState.is_logged_in and AuthState.requires_legal_acceptance and current_screen_name not in ["login", "legal_consent"]:
|
|
show_screen("legal_consent")
|
|
|
|
|
|
func open_notes_dialog() -> void:
|
|
if not AuthState.is_logged_in or notes_dialog == null:
|
|
return
|
|
if current_screen_name != "player" or BlockSessionService.session_kind == "simulation":
|
|
return
|
|
notes_dialog.show_dialog()
|
|
|
|
|
|
func _on_theme_changed(is_dark_mode: bool) -> void:
|
|
Style.refresh_tree(self, not is_dark_mode)
|
|
_refresh_theme_button()
|
|
if app_background != null:
|
|
app_background.color = Brand.background_color(is_dark_mode)
|
|
app_background.material = Brand.background_material(is_dark_mode)
|
|
if account_menu != null:
|
|
account_menu.add_theme_stylebox_override("panel", Brand.panel_box(is_dark_mode))
|
|
if feedback_button != null:
|
|
_style_header_feedback_button(feedback_button)
|
|
if account_button != null:
|
|
_style_account_avatar(account_button)
|
|
if account_chevron_button != null:
|
|
_style_account_chevron(account_chevron_button)
|
|
if credits_badge != null:
|
|
credits_badge.refresh_theme_controls()
|
|
if sidebar != null:
|
|
sidebar.add_theme_stylebox_override("panel", _sidebar_box())
|
|
if header_panel != null:
|
|
header_panel.add_theme_stylebox_override("panel", _top_bar_box())
|
|
if header_title != null:
|
|
header_title.add_theme_color_override("font_color", Brand.text_color(is_dark_mode))
|
|
if theme_button != null:
|
|
_style_shell_theme_button(theme_button)
|
|
for nav_key in nav_buttons:
|
|
for button in nav_buttons[nav_key]:
|
|
_style_nav_button(button, nav_key == current_nav_key)
|
|
var selector = screens.get("selection")
|
|
if selector != null and selector.has_method("refresh_theme_controls"):
|
|
selector.refresh_theme_controls()
|
|
var donations = screens.get("donations")
|
|
if donations != null and donations.has_method("refresh_theme_controls"):
|
|
donations.refresh_theme_controls()
|
|
var player = screens.get("player")
|
|
if player != null and player.has_method("refresh_theme_controls"):
|
|
player.refresh_theme_controls()
|
|
|
|
|
|
func _refresh_theme_button() -> void:
|
|
if theme_button == null:
|
|
return
|
|
Brand.set_theme_toggle_icon(theme_button, theme_service.is_dark_mode())
|
|
theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode"
|
|
_style_shell_theme_button(theme_button)
|
|
|
|
|
|
func _logout() -> void:
|
|
_perform_logout("")
|
|
|
|
|
|
func _on_session_expired(_reason: String) -> void:
|
|
_perform_logout("Your session expired. Sign in again.")
|
|
|
|
|
|
func _perform_logout(login_message: String) -> void:
|
|
_close_app_menu()
|
|
if session_expiry_dialog != null and session_expiry_dialog.has_method("dismiss_for_logout"):
|
|
session_expiry_dialog.dismiss_for_logout()
|
|
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
|
BlockSessionService.finish_block()
|
|
elif BlockSessionService.session_kind == "simulation":
|
|
BlockSessionService.block_started = false
|
|
QuestionBankService.clear_questions()
|
|
if AuthState.is_logged_in or not AuthState.access_token.is_empty():
|
|
AuthState.clear_session()
|
|
show_screen("login", login_message)
|
|
|
|
|
|
func _build_app_menu() -> void:
|
|
app_menu = PanelContainer.new()
|
|
app_menu.visible = false
|
|
app_menu.top_level = true
|
|
app_menu.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
app_menu.custom_minimum_size = Vector2(340, 290)
|
|
app_menu.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
|
|
add_child(app_menu)
|
|
|
|
var content := VBoxContainer.new()
|
|
content.add_theme_constant_override("separation", 12)
|
|
app_menu.add_child(content)
|
|
|
|
var title := Label.new()
|
|
title.text = "Paused"
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
Style.set_label(title, 28, Style.TEXT)
|
|
content.add_child(title)
|
|
|
|
var resume := Button.new()
|
|
resume.text = "Resume"
|
|
resume.custom_minimum_size = Vector2(0, 48)
|
|
Style.style_primary_button(resume)
|
|
UISoundService.set_button_sound(resume, "close")
|
|
resume.pressed.connect(_close_app_menu)
|
|
content.add_child(resume)
|
|
|
|
var profile_button := Button.new()
|
|
profile_button.text = "Profile"
|
|
profile_button.custom_minimum_size = Vector2(0, 48)
|
|
Style.style_chip(profile_button, Style.CYAN)
|
|
UISoundService.set_button_sound(profile_button, "open")
|
|
profile_button.pressed.connect(_navigate_from_pause.bind("profile"))
|
|
content.add_child(profile_button)
|
|
|
|
var settings_button := Button.new()
|
|
settings_button.text = "Settings"
|
|
settings_button.custom_minimum_size = Vector2(0, 48)
|
|
Style.style_chip(settings_button, Style.CYAN)
|
|
UISoundService.set_button_sound(settings_button, "open")
|
|
settings_button.pressed.connect(_navigate_from_pause.bind("settings"))
|
|
content.add_child(settings_button)
|
|
|
|
menu_exit_button = Button.new()
|
|
menu_exit_button.text = "Exit Run"
|
|
menu_exit_button.custom_minimum_size = Vector2(0, 48)
|
|
UISoundService.set_button_sound(menu_exit_button, "back")
|
|
menu_exit_button.pressed.connect(exit_current_run)
|
|
content.add_child(menu_exit_button)
|
|
|
|
var logout := Button.new()
|
|
logout.text = "Log Out"
|
|
logout.custom_minimum_size = Vector2(0, 48)
|
|
Style.style_chip(logout, Style.RED)
|
|
UISoundService.set_button_sound(logout, "back")
|
|
logout.pressed.connect(_logout)
|
|
content.add_child(logout)
|
|
|
|
|
|
func _toggle_app_menu() -> void:
|
|
if app_menu == null:
|
|
return
|
|
if app_menu.visible:
|
|
UISoundService.play_sound("close")
|
|
_close_app_menu()
|
|
else:
|
|
UISoundService.play_sound("open")
|
|
app_menu.visible = true
|
|
app_menu.size = app_menu.custom_minimum_size
|
|
app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5
|
|
menu_exit_button.visible = (current_screen_name == "player" and BlockSessionService.block_started) or current_screen_name in ["ccs_player", "flashcards_study"]
|
|
if menu_exit_button.visible:
|
|
menu_exit_button.text = (
|
|
"Abandon CCS Case" if current_screen_name == "ccs_player"
|
|
else "Exit Study Session" if current_screen_name == "flashcards_study"
|
|
else "Leave (timer continues)" if BlockSessionService.session_kind == "simulation"
|
|
else "Exit Run")
|
|
Style.style_chip(menu_exit_button, Style.AMBER)
|
|
if current_screen_name == "player": BlockSessionService.pause_current_question_timer()
|
|
|
|
|
|
func _close_app_menu() -> void:
|
|
if app_menu != null:
|
|
app_menu.visible = false
|
|
BlockSessionService.resume_current_question_timer()
|
|
|
|
|
|
func _navigate_from_pause(target_screen: String) -> void:
|
|
var return_screen := current_screen_name
|
|
show_screen(target_screen, {"return_screen": return_screen})
|
|
if return_screen == "player":
|
|
BlockSessionService.pause_current_question_timer()
|
|
|
|
|
|
func exit_current_run() -> void:
|
|
_close_app_menu()
|
|
if current_screen_name == "ccs_player":
|
|
var ccs_player = screens.get("ccs_player")
|
|
if ccs_player != null and ccs_player.has_method("leave_case"): ccs_player.leave_case()
|
|
return
|
|
if current_screen_name == "flashcards_study":
|
|
show_screen("flashcards_hub")
|
|
return
|
|
var was_core_run := BlockSessionService.session_kind == "core"
|
|
var was_review_run := BlockSessionService.session_kind == "review"
|
|
var was_dynamic_run := BlockSessionService.session_kind == "dynamic"
|
|
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
|
BlockSessionService.finish_block()
|
|
elif BlockSessionService.session_kind == "simulation":
|
|
BlockSessionService.block_started = false
|
|
if was_review_run or was_dynamic_run:
|
|
DashboardService.refresh()
|
|
show_screen("dashboard")
|
|
else:
|
|
show_screen("selection", "exam" if was_core_run else "step3")
|