Add synced question bookmarks

This commit is contained in:
hz
2026-07-18 23:07:22 -05:00
parent 819a108451
commit 2e52a7ea0a
20 changed files with 1387 additions and 18 deletions
+102 -13
View File
@@ -84,6 +84,7 @@ var clear_highlight_button: Button
var flag_button: Button
var report_button: Button
var notes_button: Button
var bookmark_button: Button
var app_feedback_button: Button
var theme_button: Button
var question_toolbar: HFlowContainer
@@ -161,6 +162,12 @@ func _ready() -> void:
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
if not ExamSimulationService.request_failed.is_connected(_on_simulation_request_failed):
ExamSimulationService.request_failed.connect(_on_simulation_request_failed)
if not BookmarkService.status_changed.is_connected(_on_bookmark_status_changed):
BookmarkService.status_changed.connect(_on_bookmark_status_changed)
if not BookmarkService.question_loaded.is_connected(_on_bookmark_question_loaded):
BookmarkService.question_loaded.connect(_on_bookmark_question_loaded)
if not BookmarkService.question_failed.is_connected(_on_bookmark_question_failed):
BookmarkService.question_failed.connect(_on_bookmark_question_failed)
question_card = PanelContainer.new()
question_card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -267,6 +274,18 @@ func _ready() -> void:
)
question_toolbar.add_child(flag_button)
bookmark_button = Button.new()
bookmark_button.tooltip_text = "Save question"
bookmark_button.icon = preload("res://assets/icons/lucide/bookmark.svg")
bookmark_button.custom_minimum_size = Vector2(44, 44)
bookmark_button.add_theme_constant_override("icon_max_width", 22)
bookmark_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
bookmark_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
bookmark_button.focus_mode = Control.FOCUS_NONE
UISoundService.set_button_sound(bookmark_button, "toggle")
bookmark_button.pressed.connect(_toggle_bookmark_current_question)
question_toolbar.add_child(bookmark_button)
report_button = Button.new()
report_button.tooltip_text = "Report question"
report_button.custom_minimum_size = Vector2(44, 44)
@@ -840,10 +859,10 @@ func _apply_question_theme() -> void:
var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label
if navigator_heading != null:
navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark))
for control in [text_smaller_button, text_larger_button, flag_button, notes_button, app_feedback_button]:
for control in [text_smaller_button, text_larger_button, flag_button, bookmark_button, notes_button, app_feedback_button]:
if control == null:
continue
var active: bool = control == flag_button and flagged
var active: bool = (control == flag_button and flagged) or (control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", ""))))
control.add_theme_stylebox_override("normal", _toolbar_box(dark, active))
control.add_theme_stylebox_override("hover", _toolbar_box(dark, true))
control.add_theme_stylebox_override("pressed", _toolbar_box(dark, true))
@@ -1018,15 +1037,17 @@ func _load_question() -> void:
next_button.disabled = true
skip_button.disabled = false
skip_button.visible = true
skip_button.text = "Skip"
report_button.visible = BlockSessionService.session_kind != "simulation"
var question := BlockSessionService.get_current_question()
if question.is_empty():
main.show_screen("selection", "exam" if BlockSessionService.session_kind == "core" else "step3")
main.show_screen("bookmarks" if BlockSessionService.session_kind == "bookmark" else "selection", "exam" if BlockSessionService.session_kind == "core" else "step3")
return
BookmarkService.observe_question(question)
progress_button.text = _question_progress_text()
progress_button.disabled = BlockSessionService.session_kind == "core"
progress_button.tooltip_text = "" if BlockSessionService.session_kind == "core" else "Open question navigator"
progress_button.disabled = BlockSessionService.session_kind in ["core", "bookmark"]
progress_button.tooltip_text = "" if BlockSessionService.session_kind in ["core", "bookmark"] else "Open question navigator"
var show_timer := _is_session_timer_visible()
timer_label.visible = show_timer
timer_bar.visible = show_timer
@@ -1150,6 +1171,16 @@ func _should_clear_selection_from_click(mouse_event: InputEventMouseButton) -> b
func _skip_current_question() -> void:
if BlockSessionService.session_kind == "bookmark":
if not current_answer.is_empty():
_finish()
return
current_answer = BlockSessionService.skip_bookmark_question(eliminated, flagged)
if current_answer.is_empty():
return
selected_choice = ""
_show_answer_review()
return
if pending_correct_advance or (not current_answer.is_empty() and BlockSessionService.session_kind != "simulation"):
return
if BlockSessionService.session_kind != "core":
@@ -1260,15 +1291,16 @@ func _submit_or_next() -> void:
if selected_choice == "":
return
var question_id_for_attempt := str(BlockSessionService.get_current_question().get("id", ""))
if BlockSessionService.session_kind == "core":
if BlockSessionService.session_kind in ["core", "bookmark"]:
_collapse_resolved_followups()
var attempted_choice := selected_choice
QuestionBankService.record_first_attempt(
question_id_for_attempt,
attempted_choice,
_presented_choice_ids()
)
var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged)
if BlockSessionService.session_kind == "core":
QuestionBankService.record_first_attempt(
question_id_for_attempt,
attempted_choice,
_presented_choice_ids()
)
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():
return
var answer: Dictionary = result.get("answer", {})
@@ -1297,6 +1329,14 @@ func _submit_or_next() -> void:
else:
if BlockSessionService.session_kind == "core":
_request_next_core_question()
elif BlockSessionService.session_kind == "bookmark":
if BookmarkService.has_next_practice():
awaiting_next_question = true
next_button.text = "Loading..."
next_button.disabled = true
BookmarkService.next_practice()
else:
_finish()
else:
_advance_or_finish()
@@ -1345,9 +1385,14 @@ func _show_answer_review() -> void:
_set_answer_review_timer_visibility()
next_button.text = "Next"
next_button.disabled = false
if BlockSessionService.session_kind == "bookmark" and not BookmarkService.has_next_practice():
next_button.text = "End of list"
next_button.disabled = true
if skip_button != null:
skip_button.disabled = true
skip_button.visible = false
skip_button.visible = BlockSessionService.session_kind == "bookmark"
skip_button.text = "Back to Bookmarks" if BlockSessionService.session_kind == "bookmark" else "Skip"
skip_button.disabled = false if BlockSessionService.session_kind == "bookmark" else true
if question_scroll != null:
question_scroll.call_deferred("ensure_control_visible", explanation_label)
@@ -1653,6 +1698,10 @@ func _advance_or_finish() -> void:
func _finish() -> void:
if BlockSessionService.session_kind == "bookmark":
BlockSessionService.finish_block()
main.show_screen("bookmarks")
return
if BlockSessionService.session_kind == "core":
BlockSessionService.finish_block()
main.show_screen("selection", "exam")
@@ -1662,6 +1711,10 @@ func _finish() -> void:
func _exit_run() -> void:
if BlockSessionService.session_kind == "bookmark":
BlockSessionService.finish_block()
main.show_screen("bookmarks")
return
var was_core_run := BlockSessionService.session_kind == "core"
BlockSessionService.resume_current_question_timer()
BlockSessionService.finish_block()
@@ -1879,9 +1932,45 @@ func _refresh_toolbar_state() -> void:
report_button.tooltip_text = "Report submitted; report another issue" if reported_question_ids.has(question_id) else "Report question"
if notes_button != null:
notes_button.visible = BlockSessionService.session_kind != "simulation"
if bookmark_button != null:
bookmark_button.visible = BlockSessionService.session_kind != "simulation"
var bookmark_question_id := str(BlockSessionService.get_current_question().get("id", ""))
var bookmark_value := BookmarkService.is_bookmarked(bookmark_question_id)
var bookmark_pending := BookmarkService.is_pending(bookmark_question_id)
bookmark_button.tooltip_text = ("Saved; waiting to sync" if bookmark_pending else "Remove bookmark") if bookmark_value else ("Removal waiting to sync" if bookmark_pending else "Save question")
_apply_question_theme()
func _toggle_bookmark_current_question() -> void:
var question := BlockSessionService.get_current_question()
if not question.is_empty():
BookmarkService.toggle_question(question)
func _on_bookmark_status_changed(question_id: String, value: bool, pending: bool) -> void:
if question_id != str(BlockSessionService.get_current_question().get("id", "")) or bookmark_button == null:
return
bookmark_button.tooltip_text = ("Saved; waiting to sync" if pending else "Remove bookmark") if value else ("Removal waiting to sync" if pending else "Save question")
_apply_question_theme()
func _on_bookmark_question_loaded(question: Dictionary) -> void:
if not visible or BlockSessionService.session_kind != "bookmark" or not awaiting_next_question:
return
awaiting_next_question = false
if BlockSessionService.replace_bookmark_question(question):
_load_question()
func _on_bookmark_question_failed(message: String) -> void:
if not visible or BlockSessionService.session_kind != "bookmark":
return
awaiting_next_question = false
next_button.text = "Retry"
next_button.disabled = false
next_button.tooltip_text = message
func refresh_theme_controls() -> void:
if media_viewer_background != null:
media_viewer_background.color = Style.BACKGROUND