Files
step3/scripts/ui/circular_progress.gd
hz 70aa425fde Rebuild flashcard study screen from mockup, with real backend-backed stats
Study screen now matches the provided mockup's layout: a "Current
Deck" panel with a circular mastery-progress ring, and a "Spaced
Repetition" panel (New/Due/Learning/Review counts, streak, daily
goal) on either side of the card. Kept the app's real global sidebar
rather than the mockup's own nav, per direction.

Mastery/Streak/Daily Goal weren't tracked anywhere, so this adds real
backend support rather than showing fabricated numbers:
- Mastered = graduated to review state AND FSRS stability >= 21 days
  (FlashcardScheduleStore.MasteryStabilityDaysThreshold), stricter
  than the plain Review-state count
- CurrentStreakDays = consecutive UTC days with a review, computed
  from review-log timestamps in the new pure FlashcardScheduling
  .ComputeStreakDays (unit tested, habit-streak semantics: doesn't
  reset to 0 the instant a new day starts before today's first review)
- DailyGoal/ReviewsToday reuse the account's existing ReviewsPerDay
  setting rather than inventing a new one
Also removes the exit "X" button in favor of the same Escape/pause
menu the question-player flow uses (main.gd's shared app_menu, with
an "Exit Study Session" action), and adds a new circular_progress.gd
reusable ring control plus a real "Options" menu (bury/suspend the
current card) wired to schedule endpoints that had no UI before.
2026-07-21 23:20:14 -05:00

51 lines
1.7 KiB
GDScript

extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
var ring_value := 0.0
var track_color := Color("26324a")
var fill_color := Color("2563eb")
var ring_width := 8.0
var value_label: Label
var caption_label: Label
func _init() -> void:
custom_minimum_size = Vector2(112, 112)
var center := CenterContainer.new()
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(center)
var stack := VBoxContainer.new()
stack.add_theme_constant_override("separation", 0)
center.add_child(stack)
value_label = Label.new()
value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
value_label.add_theme_font_size_override("font_size", 24)
stack.add_child(value_label)
caption_label = Label.new()
caption_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
caption_label.add_theme_font_size_override("font_size", 12)
stack.add_child(caption_label)
func configure(fill_fraction: float, value_text: String, caption_text: String, accent: Color, dark: bool) -> void:
ring_value = clampf(fill_fraction, 0.0, 1.0)
track_color = Color("26324a") if dark else Color("e2e8f0")
fill_color = accent
value_label.text = value_text
value_label.add_theme_color_override("font_color", Brand.text_color(dark))
caption_label.text = caption_text
caption_label.add_theme_color_override("font_color", Brand.muted_color(dark))
queue_redraw()
func _draw() -> void:
var radius := minf(size.x, size.y) / 2.0 - ring_width / 2.0
if radius <= 0.0:
return
var center := size / 2.0
draw_arc(center, radius, 0.0, TAU, 64, track_color, ring_width, true)
if ring_value > 0.0:
draw_arc(center, radius, -PI / 2.0, -PI / 2.0 + TAU * ring_value, 64, fill_color, ring_width, true)