Add user progress dashboard

This commit is contained in:
hz
2026-07-18 23:45:38 -05:00
parent 2e52a7ea0a
commit 2dcac51452
26 changed files with 983 additions and 17 deletions
+40
View File
@@ -0,0 +1,40 @@
extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
var days: Array = []
var dark := true
func set_days(next_days: Array, dark_mode: bool) -> void:
days = next_days.duplicate(true)
dark = dark_mode
queue_redraw()
func _draw() -> void:
if days.is_empty():
return
var bottom := size.y - 24.0
var top := 20.0
var slot: float = size.x / float(days.size())
var maximum := 1
for day in days:
maximum = maxi(maximum, int(day.get("studyTimeSeconds", 0)))
for index in range(days.size()):
var day: Dictionary = days[index]
var seconds := int(day.get("studyTimeSeconds", 0))
var bar_height: float = (bottom - top) * float(seconds) / float(maximum)
var width: float = minf(30.0, slot * 0.48)
var rect := Rect2(index * slot + (slot - width) * 0.5, bottom - bar_height, width, maxf(2.0, bar_height))
draw_rect(rect, Brand.ROYAL_BLUE, true)
var label := _weekday(str(day.get("dateUtc", "")))
draw_string(Brand.font, Vector2(index * slot, size.y - 5), label, HORIZONTAL_ALIGNMENT_CENTER, slot, 11, Brand.muted_color(dark))
func _weekday(iso_date: String) -> String:
if iso_date.length() < 10:
return ""
var unix := Time.get_unix_time_from_datetime_string(iso_date.left(10) + "T00:00:00")
var date := Time.get_date_dict_from_unix_time(unix)
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(date.get("weekday", 0))]
@@ -0,0 +1 @@
uid://4yvpcg55sqe
+40
View File
@@ -0,0 +1,40 @@
extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
var values: PackedFloat32Array = []
var dark := true
func set_values(next_values: Array, dark_mode: bool) -> void:
values = PackedFloat32Array(next_values)
dark = dark_mode
queue_redraw()
func _draw() -> void:
var left := 42.0
var top := 12.0
var right := size.x - 12.0
var bottom := size.y - 28.0
var grid := Color("24314b") if dark else Color("dbe3ef")
var muted := Brand.muted_color(dark)
for percentage in [0, 25, 50, 75, 100]:
var y: float = bottom - (bottom - top) * float(percentage) / 100.0
draw_line(Vector2(left, y), Vector2(right, y), grid, 1.0)
draw_string(Brand.font, Vector2(4, y + 4), str(percentage), HORIZONTAL_ALIGNMENT_LEFT, 34, 11, muted)
var target_y: float = bottom - (bottom - top) * 0.70
draw_dashed_line(Vector2(left, target_y), Vector2(right, target_y), Brand.ROYAL_BLUE, 5.0, 2.0)
if values.is_empty():
return
var points := PackedVector2Array()
var count: int = values.size()
for index in range(count):
var x: float = left if count == 1 else lerpf(left, right, float(index) / float(count - 1))
var y: float = bottom - (bottom - top) * clampf(values[index], 0.0, 100.0) / 100.0
points.append(Vector2(x, y))
if points.size() > 1:
draw_polyline(points, Brand.ROYAL_BLUE, 2.5, true)
for point in points:
draw_circle(point, 3.5, Brand.PURPLE)
draw_circle(point, 2.0, Color.WHITE)
+1
View File
@@ -0,0 +1 @@
uid://c11sgvq5oewuj
+475
View File
@@ -0,0 +1,475 @@
extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
const TrendChart := preload("res://scripts/ui/dashboard_chart.gd")
const ActivityChart := preload("res://scripts/ui/dashboard_activity_chart.gd")
var main: Control
var scroll: ScrollContainer
var content: VBoxContainer
var status_label: Label
var metric_grid: GridContainer
var analytics_grid: GridContainer
var lower_grid: GridContainer
var trend_chart: Control
var activity_chart: Control
var system_list: VBoxContainer
var focus_content: VBoxContainer
var review_list: VBoxContainer
var recent_list: VBoxContainer
var weekly_summary: VBoxContainer
var metric_values: Dictionary = {}
var metric_details: Dictionary = {}
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
DashboardService.dashboard_changed.connect(_render)
DashboardService.dashboard_failed.connect(_show_error)
UIThemeService.theme_changed.connect(_rebuild)
resized.connect(_update_responsive)
_build()
func on_show(_payload: Variant = null) -> void:
if DashboardService.data.is_empty():
_show_loading()
else:
_render()
DashboardService.refresh()
func _build() -> void:
for child in get_children():
remove_child(child)
child.free()
metric_values.clear()
metric_details.clear()
scroll = ScrollContainer.new()
scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
content = VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content.add_theme_constant_override("separation", 14)
scroll.add_child(content)
var eyebrow := _label("YOUR PROGRESS", 13, Brand.ROYAL_BLUE)
content.add_child(eyebrow)
var hero_row := HBoxContainer.new()
hero_row.add_theme_constant_override("separation", 16)
content.add_child(hero_row)
var hero_copy := VBoxContainer.new()
hero_copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hero_row.add_child(hero_copy)
var display_name := AuthState.username.strip_edges()
if display_name.is_empty():
display_name = "there"
else:
display_name = display_name.split("@")[0].capitalize()
hero_copy.add_child(_label("Welcome back, %s" % display_name, 28, Brand.text_color(UIThemeService.is_dark_mode()), true))
hero_copy.add_child(_label("Here is how your Step 3 preparation is progressing.", 15, Brand.muted_color(UIThemeService.is_dark_mode())))
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 10)
hero_copy.add_child(actions)
var continue_button := _button("Continue studying", "chevron-right")
continue_button.pressed.connect(func(): main.show_screen("selection", "question_bank"))
actions.add_child(continue_button)
var missed_button := _button("Review missed questions", "clipboard-list", false)
missed_button.pressed.connect(func(): main.show_screen("profile"))
actions.add_child(missed_button)
status_label = _label("", 14, Brand.muted_color(UIThemeService.is_dark_mode()))
status_label.visible = false
content.add_child(status_label)
metric_grid = GridContainer.new()
metric_grid.columns = 4
metric_grid.add_theme_constant_override("h_separation", 0)
metric_grid.add_theme_constant_override("v_separation", 8)
content.add_child(_card(metric_grid, 12))
_add_metric("qbank", "Question Bank Rating", "chart-no-axes-column-increasing", Brand.ROYAL_BLUE)
_add_metric("ccs", "CCS Rating", "stethoscope", Brand.PURPLE)
_add_metric("accuracy", "First-Attempt Accuracy", "activity", Brand.ROYAL_BLUE)
_add_metric("time", "Study Time", "clock-3", Brand.PURPLE)
analytics_grid = GridContainer.new()
analytics_grid.columns = 2
analytics_grid.add_theme_constant_override("h_separation", 12)
analytics_grid.add_theme_constant_override("v_separation", 12)
content.add_child(analytics_grid)
var performance := VBoxContainer.new()
performance.size_flags_horizontal = Control.SIZE_EXPAND_FILL
performance.add_theme_constant_override("separation", 10)
performance.add_child(_section_title("Performance trend"))
trend_chart = TrendChart.new()
trend_chart.custom_minimum_size = Vector2(520, 190)
trend_chart.size_flags_horizontal = Control.SIZE_EXPAND_FILL
performance.add_child(trend_chart)
performance.add_child(HSeparator.new())
var system_header := HBoxContainer.new()
system_header.add_child(_section_title("Performance by system"))
var header_space := Control.new()
header_space.size_flags_horizontal = Control.SIZE_EXPAND_FILL
system_header.add_child(header_space)
var breakdown := Button.new()
breakdown.text = "View full breakdown"
breakdown.flat = true
breakdown.pressed.connect(func(): main.show_screen("profile"))
system_header.add_child(breakdown)
performance.add_child(system_header)
system_list = VBoxContainer.new()
system_list.add_theme_constant_override("separation", 8)
performance.add_child(system_list)
analytics_grid.add_child(_card(performance, 18))
var right_column := VBoxContainer.new()
right_column.add_theme_constant_override("separation", 12)
right_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
analytics_grid.add_child(right_column)
focus_content = VBoxContainer.new()
focus_content.add_theme_constant_override("separation", 10)
right_column.add_child(_card(focus_content, 16))
review_list = VBoxContainer.new()
review_list.add_theme_constant_override("separation", 7)
right_column.add_child(_card(review_list, 16))
lower_grid = GridContainer.new()
lower_grid.columns = 2
lower_grid.add_theme_constant_override("h_separation", 12)
lower_grid.add_theme_constant_override("v_separation", 12)
content.add_child(lower_grid)
var weekly := VBoxContainer.new()
weekly.add_theme_constant_override("separation", 8)
weekly.add_child(_section_title("Weekly activity"))
var weekly_row := HBoxContainer.new()
weekly_row.add_theme_constant_override("separation", 18)
weekly.add_child(weekly_row)
activity_chart = ActivityChart.new()
activity_chart.custom_minimum_size = Vector2(360, 150)
activity_chart.size_flags_horizontal = Control.SIZE_EXPAND_FILL
weekly_row.add_child(activity_chart)
weekly_summary = VBoxContainer.new()
weekly_summary.custom_minimum_size.x = 180
weekly_summary.add_theme_constant_override("separation", 10)
weekly_row.add_child(weekly_summary)
lower_grid.add_child(_card(weekly, 16))
recent_list = VBoxContainer.new()
recent_list.add_theme_constant_override("separation", 6)
lower_grid.add_child(_card(recent_list, 16))
_update_responsive()
if not DashboardService.data.is_empty():
_render()
func _rebuild(_is_dark: bool = UIThemeService.is_dark_mode()) -> void:
_build()
func _render() -> void:
var data: Dictionary = DashboardService.data
status_label.visible = false
_set_metric("qbank", str(int(data.get("questionBankRating", 1000))), "%d rated questions" % int(data.get("ratedQuestionCount", 0)))
_set_metric("ccs", str(int(data.get("ccsRating", 1000))), "%d rated cases" % int(data.get("ratedCcsCaseCount", 0)))
_set_metric("accuracy", "%d%%" % int(data.get("firstAttemptAccuracy", 0)), "Last %d questions" % int(data.get("accuracySampleSize", 0)))
_set_metric("time", _format_duration(int(data.get("studyTimeSecondsThisWeek", 0))), "This week")
var trend_values: Array = []
for point in data.get("performanceTrend", []):
if int(point.get("total", 0)) > 0:
trend_values.append(float(point.get("accuracy", 0)))
trend_chart.set_values(trend_values, UIThemeService.is_dark_mode())
_render_systems(data.get("systems", []))
_render_focus(data.get("systems", []))
_render_review(data)
var weekly: Array = data.get("weeklyActivity", [])
activity_chart.set_days(weekly, UIThemeService.is_dark_mode())
_render_weekly(data, weekly)
_render_recent(data.get("recentActivity", []))
func _render_systems(systems_value: Array) -> void:
_clear(system_list)
var systems := systems_value.duplicate(true)
systems.sort_custom(func(a: Dictionary, b: Dictionary): return int(a.get("total", 0)) > int(b.get("total", 0)))
if systems.is_empty():
system_list.add_child(_label("Answer questions to build your system breakdown.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
return
var displayed: Array = systems.slice(0, mini(5, systems.size()))
var best_accuracy := -1
var worst_accuracy := 101
for item in displayed:
best_accuracy = maxi(best_accuracy, int(item.get("accuracy", 0)))
worst_accuracy = mini(worst_accuracy, int(item.get("accuracy", 0)))
for item in displayed:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var name := _label(str(item.get("system", "General")), 13, Brand.text_color(UIThemeService.is_dark_mode()))
name.custom_minimum_size.x = 190
name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
row.add_child(name)
var bar := ProgressBar.new()
bar.show_percentage = false
bar.value = int(item.get("accuracy", 0))
bar.custom_minimum_size = Vector2(150, 7)
bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_style_progress(bar, Brand.PURPLE if int(item.get("accuracy", 0)) == worst_accuracy else Brand.ROYAL_BLUE)
row.add_child(bar)
var percentage := _label("%d%%" % int(item.get("accuracy", 0)), 14, Brand.ROYAL_BLUE, true)
percentage.custom_minimum_size.x = 48
row.add_child(percentage)
var count := _label("%d / %d" % [int(item.get("correct", 0)), int(item.get("total", 0))], 12, Brand.muted_color(UIThemeService.is_dark_mode()))
count.custom_minimum_size.x = 58
row.add_child(count)
var marker := "Strongest" if int(item.get("accuracy", 0)) == best_accuracy else ("Needs work" if int(item.get("accuracy", 0)) == worst_accuracy else "")
var marker_label := _label(marker, 11, Brand.SUCCESS if marker == "Strongest" else Brand.MAGENTA)
marker_label.custom_minimum_size.x = 72
row.add_child(marker_label)
system_list.add_child(row)
func _render_focus(systems: Array) -> void:
_clear(focus_content)
focus_content.add_child(_section_title("Study focus"))
if systems.is_empty():
focus_content.add_child(_label("Complete a few questions to receive a focused recommendation.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
return
var weakest: Dictionary = systems[0]
for item in systems:
if int(item.get("accuracy", 0)) < int(weakest.get("accuracy", 0)):
weakest = item
focus_content.add_child(_label("Recommended focus", 12, Brand.muted_color(UIThemeService.is_dark_mode())))
focus_content.add_child(_label(str(weakest.get("system", "General")), 23, Brand.PURPLE, true))
focus_content.add_child(_label("%d%% accuracy across %d questions" % [int(weakest.get("accuracy", 0)), int(weakest.get("total", 0))], 13, Brand.text_color(UIThemeService.is_dark_mode())))
var progress := ProgressBar.new()
progress.show_percentage = false
progress.value = int(weakest.get("accuracy", 0))
progress.custom_minimum_size.y = 8
_style_progress(progress, Brand.PURPLE)
focus_content.add_child(progress)
var practice := _button("Practice this system", "chevron-right")
practice.pressed.connect(func(): main.show_screen("selection", "question_bank"))
focus_content.add_child(practice)
func _render_review(data: Dictionary) -> void:
_clear(review_list)
review_list.add_child(_section_title("Review queue"))
review_list.add_child(_review_action("%d missed questions" % int(data.get("missedQuestionCount", 0)), "x", Brand.MAGENTA, "profile"))
review_list.add_child(_review_action("%d bookmarks" % int(data.get("bookmarkCount", 0)), "bookmark", Brand.ROYAL_BLUE, "bookmarks"))
review_list.add_child(_review_action("%d notes" % int(data.get("noteCount", 0)), "notebook-pen", Brand.AMBER, "notes"))
func _render_weekly(data: Dictionary, days: Array) -> void:
_clear(weekly_summary)
var questions := 0
var cases := 0
for day in days:
questions += int(day.get("questions", 0))
cases += int(day.get("ccsCases", 0))
weekly_summary.add_child(_summary_stat(str(questions), "Questions answered", "notebook-pen", Brand.ROYAL_BLUE))
weekly_summary.add_child(_summary_stat(str(cases), "CCS cases completed", "stethoscope", Brand.PURPLE))
weekly_summary.add_child(_summary_stat("%d-day" % int(data.get("currentStreak", 0)), "Current streak", "sun", Brand.AMBER))
func _render_recent(items: Array) -> void:
_clear(recent_list)
recent_list.add_child(_section_title("Recent activity"))
if items.is_empty():
recent_list.add_child(_label("Your completed study activity will appear here.", 14, Brand.muted_color(UIThemeService.is_dark_mode())))
return
for item in items:
var row := HBoxContainer.new()
row.custom_minimum_size.y = 34
row.add_theme_constant_override("separation", 9)
var type := str(item.get("type", "question"))
var icon_name := "stethoscope" if type == "ccs" else ("clipboard-list" if type == "simulation" else "activity")
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.custom_minimum_size = Vector2(20, 20)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
row.add_child(icon)
var title := _label(str(item.get("title", "Study activity")), 13, Brand.text_color(UIThemeService.is_dark_mode()))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
row.add_child(title)
row.add_child(_label(str(item.get("detail", "")), 13, Brand.ROYAL_BLUE, true))
row.add_child(_label(_relative_time(str(item.get("occurredUtc", ""))), 11, Brand.muted_color(UIThemeService.is_dark_mode())))
recent_list.add_child(row)
func _add_metric(key: String, title: String, icon_name: String, accent: Color) -> void:
var cell := HBoxContainer.new()
cell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
cell.custom_minimum_size = Vector2(230, 82)
cell.add_theme_constant_override("separation", 12)
var icon_panel := PanelContainer.new()
icon_panel.custom_minimum_size = Vector2(48, 48)
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(UIThemeService.is_dark_mode(), accent, 24))
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.custom_minimum_size = Vector2(22, 22)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_panel.add_child(icon)
cell.add_child(icon_panel)
var copy := VBoxContainer.new()
copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
copy.add_child(_label(title, 12, Brand.text_color(UIThemeService.is_dark_mode())))
var value := _label("--", 25, Brand.text_color(UIThemeService.is_dark_mode()), true)
copy.add_child(value)
var detail := _label("Loading", 11, Brand.muted_color(UIThemeService.is_dark_mode()))
copy.add_child(detail)
metric_values[key] = value
metric_details[key] = detail
cell.add_child(copy)
metric_grid.add_child(cell)
func _set_metric(key: String, value: String, detail: String) -> void:
(metric_values.get(key) as Label).text = value
(metric_details.get(key) as Label).text = detail
func _review_action(text: String, icon_name: String, accent: Color, route: String) -> Button:
var button := _button(text, icon_name, false)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.add_theme_color_override("icon_normal_color", accent)
button.pressed.connect(func(): main._on_navigation_pressed(route))
return button
func _summary_stat(value: String, caption: String, icon_name: String, accent: Color) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.custom_minimum_size = Vector2(24, 24)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
row.add_child(icon)
var copy := VBoxContainer.new()
copy.add_child(_label(value, 20, accent, true))
copy.add_child(_label(caption, 11, Brand.muted_color(UIThemeService.is_dark_mode())))
row.add_child(copy)
return row
func _card(child: Control, margin: int) -> PanelContainer:
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override("panel", _card_box(margin))
panel.add_child(child)
return panel
func _card_box(margin: int) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
var dark := UIThemeService.is_dark_mode()
box.bg_color = Color("081426") if dark else Color.WHITE
box.border_color = Color("26344f") if dark else Brand.LIGHT_BORDER
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = margin
box.content_margin_right = margin
box.content_margin_top = margin
box.content_margin_bottom = margin
return box
func _button(text: String, icon_name: String = "", primary: bool = true) -> Button:
var button := Button.new()
button.text = text
button.custom_minimum_size = Vector2(0, 42)
button.add_theme_font_override("font", Brand.font)
button.add_theme_font_size_override("font_size", 14)
if not icon_name.is_empty():
button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name)
button.add_theme_constant_override("icon_max_width", 18)
if primary:
var normal := StyleBoxFlat.new()
normal.bg_color = Brand.ROYAL_BLUE
normal.set_corner_radius_all(6)
var hover := normal.duplicate() as StyleBoxFlat
hover.bg_color = Color("3474ee")
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", normal)
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("icon_normal_color", Color.WHITE)
else:
button.add_theme_stylebox_override("normal", Brand.secondary_button_box(UIThemeService.is_dark_mode()))
return button
func _label(text: String, font_size: int, color: Color, bold: bool = false) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
if bold:
label.add_theme_constant_override("outline_size", 0)
return label
func _section_title(text: String) -> Label:
return _label(text, 16, Brand.text_color(UIThemeService.is_dark_mode()), true)
func _style_progress(bar: ProgressBar, accent: Color) -> void:
var background := StyleBoxFlat.new()
background.bg_color = Color("26324a") if UIThemeService.is_dark_mode() else Color("e2e8f0")
background.set_corner_radius_all(4)
var fill := StyleBoxFlat.new()
fill.bg_color = accent
fill.set_corner_radius_all(4)
bar.add_theme_stylebox_override("background", background)
bar.add_theme_stylebox_override("fill", fill)
func _show_loading() -> void:
if status_label != null:
status_label.text = "Loading your progress..."
status_label.visible = true
func _show_error(message: String) -> void:
status_label.text = message
status_label.add_theme_color_override("font_color", Brand.MAGENTA)
status_label.visible = true
func _update_responsive() -> void:
if metric_grid == null:
return
var compact := size.x < 1050.0
metric_grid.columns = 2 if compact else 4
analytics_grid.columns = 1 if compact else 2
lower_grid.columns = 1 if compact else 2
func _clear(container: Container) -> void:
for child in container.get_children():
container.remove_child(child)
child.free()
func _format_duration(total_seconds: int) -> String:
var hours := int(floor(float(total_seconds) / 3600.0))
var minutes := int(floor(float(total_seconds % 3600) / 60.0))
return "%dh %02dm" % [hours, minutes]
func _relative_time(iso_value: String) -> String:
var cleaned := iso_value.trim_suffix("Z")
var occurred := Time.get_unix_time_from_datetime_string(cleaned)
if occurred <= 0:
return ""
var elapsed := maxi(0, int(Time.get_unix_time_from_system() - occurred))
if elapsed < 3600:
return "%dm ago" % maxi(1, int(floor(float(elapsed) / 60.0)))
if elapsed < 86400:
return "%dh ago" % int(floor(float(elapsed) / 3600.0))
return "%dd ago" % int(floor(float(elapsed) / 86400.0))
+1
View File
@@ -0,0 +1 @@
uid://fe058164alja
+1 -1
View File
@@ -662,7 +662,7 @@ func _accept_session(body: PackedByteArray, fallback_username: String) -> void:
password_input.text = ""
_set_busy(false)
UISoundService.play_sound("correct")
main.show_screen("selection", "exam")
main.show_screen("dashboard")
func _create_request(key: String, callback: Callable) -> void:
+37 -7
View File
@@ -6,6 +6,7 @@ const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd")
var screens := {}
var header_panel: PanelContainer
var header_title: Label
var app_menu: PanelContainer
var menu_exit_button: Button
var sound_toggle: CheckButton
@@ -39,7 +40,7 @@ func _ready() -> void:
theme_service.theme_changed.connect(_on_theme_changed)
resized.connect(_update_responsive_layout)
_update_responsive_layout()
show_screen("selection" if AuthState.is_logged_in else "login", "exam")
show_screen("dashboard" if AuthState.is_logged_in else "login")
func _update_responsive_layout() -> void:
@@ -76,6 +77,7 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
return
current_screen_name = screen_name
_update_header_title(screen_name)
_apply_screen_margins(screen_name)
screen.visible = true
if header_panel != null:
@@ -140,6 +142,12 @@ func _build_ui() -> void:
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)
var header_spacer := Control.new()
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(header_spacer)
@@ -208,6 +216,12 @@ func _build_ui() -> void:
screens_container.add_child(login)
screens["login"] = login
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
@@ -355,15 +369,13 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
wordmark.add_child(mind)
container.add_child(HSeparator.new())
_add_nav_button(container, "home", "Home", "house", true)
_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, "exams", "Exams", "clipboard-list", true)
_add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true)
_add_nav_button(container, "performance", "Performance", "chart-no-axes-column-increasing", true)
_add_nav_button(container, "flashcards", "Flashcards", "copy", false)
_add_nav_button(container, "study_plan", "Study Plan", "calendar-days", true)
_add_nav_button(container, "exams", "Exam Simulations", "clipboard-list", true)
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", true)
_add_nav_button(container, "notes", "Notes", "notebook-pen", true)
_add_nav_button(container, "performance", "Profile", "user-round", true)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
@@ -399,7 +411,7 @@ func _add_nav_button(container: VBoxContainer, key: String, label_text: String,
func _on_navigation_pressed(key: String) -> void:
match key:
"home": show_screen("selection", "exam")
"home": show_screen("dashboard")
"question_bank": show_screen("selection", "question_bank")
"exams": show_screen("simulation_history")
"ccs": show_screen("ccs_hub")
@@ -411,6 +423,22 @@ func _on_navigation_pressed(key: String) -> void:
"logout": _logout()
func _update_header_title(screen_name: String) -> void:
if header_title == null:
return
var titles := {
"dashboard": "Dashboard",
"selection": "Question Bank",
"simulation_history": "Exam Simulations",
"ccs_hub": "CCS Cases",
"profile": "Profile",
"settings": "Settings",
"notes": "Notes",
"bookmarks": "Bookmarks"
}
header_title.text = str(titles.get(screen_name, ""))
func _build_account_menu() -> void:
account_menu = PanelContainer.new()
account_menu.visible = false
@@ -691,6 +719,8 @@ func _on_theme_changed(is_dark_mode: bool) -> void:
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:
+3 -1
View File
@@ -3,7 +3,9 @@ extends RefCounted
static func key_for_screen(screen_name: String, payload: Variant = null) -> String:
var key := "home"
if screen_name == "selection":
if screen_name == "dashboard":
key = "home"
elif screen_name == "selection":
key = "question_bank" if str(payload) == "question_bank" else "home"
elif screen_name in ["map", "setup", "review"]:
key = "study_plan" if screen_name == "map" else "question_bank"
+4 -2
View File
@@ -1298,7 +1298,8 @@ func _submit_or_next() -> void:
QuestionBankService.record_first_attempt(
question_id_for_attempt,
attempted_choice,
_presented_choice_ids()
_presented_choice_ids(),
int(floor(BlockSessionService.get_core_question_total_elapsed_seconds()))
)
var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged) if BlockSessionService.session_kind == "core" else BlockSessionService.submit_bookmark_attempt(selected_choice, eliminated, flagged)
if result.is_empty():
@@ -1322,7 +1323,8 @@ func _submit_or_next() -> void:
QuestionBankService.record_first_attempt(
question_id_for_attempt,
selected_choice,
_presented_choice_ids()
_presented_choice_ids(),
int(floor(BlockSessionService.get_current_elapsed_seconds()))
)
current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged)
_show_answer_review()