Files
step3/scripts/ui/bookmarks_screen.gd

227 lines
7.8 KiB
GDScript

extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
var main
var search_input: LineEdit
var system_filter: OptionButton
var discipline_filter: OptionButton
var sort_filter: OptionButton
var status_label: Label
var retry_button: Button
var list_container: VBoxContainer
var load_more_button: Button
var debounce_timer: Timer
var list_scroll: ScrollContainer
var saved_scroll_position := 0
var pending_open_question_id := ""
func _ready() -> void:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 16)
_build_filters()
status_label = Label.new()
status_label.add_theme_font_override("font", Brand.font)
status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
add_child(status_label)
retry_button = Button.new()
retry_button.text = "Retry"
retry_button.visible = false
retry_button.custom_minimum_size = Vector2(100, 42)
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
retry_button.pressed.connect(_retry)
add_child(retry_button)
list_scroll = ScrollContainer.new()
list_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(list_scroll)
list_container = VBoxContainer.new()
list_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
list_container.add_theme_constant_override("separation", 10)
list_scroll.add_child(list_container)
load_more_button = Button.new()
load_more_button.text = "Load more"
load_more_button.custom_minimum_size = Vector2(0, 44)
load_more_button.pressed.connect(func(): BookmarkService.load_bookmarks(false))
list_container.add_child(load_more_button)
debounce_timer = Timer.new()
debounce_timer.one_shot = true
debounce_timer.wait_time = 0.35
debounce_timer.timeout.connect(_reload)
add_child(debounce_timer)
BookmarkService.list_changed.connect(_render)
BookmarkService.list_failed.connect(_on_list_failed)
BookmarkService.question_loaded.connect(_on_question_loaded)
BookmarkService.question_failed.connect(_on_question_load_failed)
UIThemeService.theme_changed.connect(func(_dark: bool): _render())
func _build_filters() -> void:
var row := HFlowContainer.new()
row.add_theme_constant_override("separation", 10)
add_child(row)
search_input = LineEdit.new()
search_input.placeholder_text = "Search saved questions"
search_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
search_input.custom_minimum_size = Vector2(260, 44)
search_input.text_changed.connect(func(_value: String): debounce_timer.start())
row.add_child(search_input)
system_filter = _add_filter(row, "All systems")
discipline_filter = _add_filter(row, "All disciplines")
sort_filter = _add_filter(row, "Newest", ["Oldest"])
func _add_filter(parent: Container, first: String, extras: Array = []) -> OptionButton:
var control := OptionButton.new()
control.add_item(first)
for text in extras:
control.add_item(str(text))
control.custom_minimum_size = Vector2(150, 44)
control.item_selected.connect(func(_index: int): _reload())
parent.add_child(control)
return control
func on_show(_payload: Variant = null) -> void:
_populate_categories()
if not BookmarkService.remote_list_loaded:
BookmarkService.load_bookmarks(true, _filters())
_render()
func on_hide() -> void:
if list_scroll != null:
saved_scroll_position = int(list_scroll.scroll_vertical)
func _populate_categories() -> void:
_populate_filter(system_filter, "All systems", BookmarkService.available_systems)
_populate_filter(discipline_filter, "All disciplines", BookmarkService.available_disciplines)
func _populate_filter(control: OptionButton, first: String, values: Array) -> void:
var selected_text := control.get_item_text(control.selected) if control.item_count > 0 else first
control.clear()
control.add_item(first)
for value in values:
if str(value) != "" and str(value) != "All Systems":
control.add_item(str(value))
for index in range(control.item_count):
if control.get_item_text(index) == selected_text:
control.select(index)
break
func _filters() -> Dictionary:
return {
"query": search_input.text,
"system": "" if system_filter.selected == 0 else system_filter.get_item_text(system_filter.selected),
"discipline": "" if discipline_filter.selected == 0 else discipline_filter.get_item_text(discipline_filter.selected),
"sort": "oldest" if sort_filter.selected == 1 else "newest"
}
func _reload() -> void:
BookmarkService.load_bookmarks(true, _filters())
_render()
func _render() -> void:
if list_container == null:
return
if not visible:
return
_populate_categories()
for child in list_container.get_children():
if child != load_more_button:
child.queue_free()
status_label.text = "Loading bookmarks..." if BookmarkService.list_loading else BookmarkService.list_error
retry_button.visible = BookmarkService.list_error != ""
if not BookmarkService.list_loading and BookmarkService.items.is_empty() and BookmarkService.list_error == "":
status_label.text = "No saved questions match these filters."
var insert_index := 0
for item in BookmarkService.items:
var card := _bookmark_card(item)
list_container.add_child(card)
list_container.move_child(card, insert_index)
insert_index += 1
load_more_button.visible = BookmarkService.has_more
load_more_button.disabled = BookmarkService.list_loading
if list_scroll != null:
list_scroll.set_deferred("scroll_vertical", saved_scroll_position)
func _bookmark_card(item: Dictionary) -> Control:
var dark := UIThemeService.is_dark_mode()
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false, Brand.TEAL))
var row := HFlowContainer.new()
row.add_theme_constant_override("separation", 14)
card.add_child(row)
var content := VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(content)
var stem := Label.new()
stem.text = str(item.get("stem", ""))
stem.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
stem.add_theme_font_override("font", Brand.font)
stem.add_theme_font_size_override("font_size", 16)
content.add_child(stem)
var metadata := Label.new()
metadata.text = "%s | %s | Difficulty %d%s" % [
str(item.get("system", "")), str(item.get("discipline", "")),
int(item.get("difficulty", 1)), " | Image" if bool(item.get("has_media", false)) else ""]
metadata.add_theme_font_override("font", Brand.font)
metadata.add_theme_font_size_override("font_size", 13)
metadata.add_theme_color_override("font_color", Brand.muted_color(dark))
content.add_child(metadata)
var open := Button.new()
open.text = "Practice"
open.custom_minimum_size = Vector2(105, 42)
open.pressed.connect(_open_question.bind(str(item.get("question_id", ""))))
row.add_child(open)
var remove := Button.new()
remove.icon = preload("res://assets/icons/lucide/bookmark.svg")
remove.tooltip_text = "Remove bookmark"
remove.custom_minimum_size = Vector2(44, 42)
remove.pressed.connect(BookmarkService.set_bookmarked.bind(str(item.get("question_id", "")), false))
row.add_child(remove)
return card
func _open_question(question_id: String) -> void:
pending_open_question_id = question_id
status_label.text = "Loading question..."
retry_button.visible = false
BookmarkService.begin_practice(question_id)
func _on_question_loaded(question: Dictionary) -> void:
if not visible:
return
pending_open_question_id = ""
if BlockSessionService.start_bookmark_question(question):
main.show_screen("player")
func _on_list_failed(message: String) -> void:
status_label.text = message
retry_button.visible = true
func _on_question_load_failed(message: String) -> void:
if not visible:
return
status_label.text = message
retry_button.visible = true
func _retry() -> void:
retry_button.visible = false
if pending_open_question_id != "":
status_label.text = "Loading question..."
BookmarkService.begin_practice(pending_open_question_id)
else:
_reload()