Redesign flashcards for visual hierarchy and button consistency
Study card: label the answer and "key takeaway" as distinct sections
with a real visible divider (previously an all-but-invisible
HSeparator) instead of one undifferentiated block of text. Also fixes
a real gap where a cloze card's extra field was silently dropped,
since only basic notes combined answer+extra into one label. Bigger
card, gradient Show-Answer button matching the app's signature CTA
style instead of a duller flat teal, and the Undo button now hides
itself when there's nothing to undo instead of sitting there disabled
looking identical to a real action.
Library: colored type badge per card (Basic/Cloze) instead of plain
gray text, more breathing room inside each row, Study promoted to the
same gradient CTA style, and Edit/Archive given a real secondary
button treatment instead of default theme buttons. Note editor fields
renamed ("Back / Extra takeaway" + a separate "Extra" field was
genuinely confusing) and now show which deck a card is being saved to.
This commit is contained in:
@@ -23,6 +23,7 @@ var debounce_timer: Timer
|
||||
|
||||
var editor_overlay: Control
|
||||
var editor_panel: PanelContainer
|
||||
var editor_deck_label: Label
|
||||
var editor_note_id := ""
|
||||
var editor_expected_revision: Variant = null
|
||||
var editor_type_selector: OptionButton
|
||||
@@ -111,8 +112,12 @@ func _build_toolbar() -> void:
|
||||
|
||||
study_button = Button.new()
|
||||
study_button.text = "Study"
|
||||
study_button.custom_minimum_size = Vector2(0, 44)
|
||||
Style.style_primary_button(study_button)
|
||||
study_button.custom_minimum_size = Vector2(110, 44)
|
||||
study_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
study_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
study_button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
|
||||
study_button.add_theme_color_override("font_color", Color.WHITE)
|
||||
study_button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
study_button.pressed.connect(_start_study)
|
||||
row.add_child(study_button)
|
||||
|
||||
@@ -291,7 +296,12 @@ func _render_notes() -> void:
|
||||
func _note_row(note: 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.ROYAL_BLUE))
|
||||
var card_box := Brand.content_card_box(dark, false, Brand.ROYAL_BLUE)
|
||||
card_box.content_margin_left = 16
|
||||
card_box.content_margin_right = 16
|
||||
card_box.content_margin_top = 14
|
||||
card_box.content_margin_bottom = 14
|
||||
card.add_theme_stylebox_override("panel", card_box)
|
||||
var row := BoxContainer.new()
|
||||
row.vertical = compact_layout
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
@@ -299,8 +309,22 @@ func _note_row(note: Dictionary) -> Control:
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content.add_theme_constant_override("separation", 6)
|
||||
row.add_child(content)
|
||||
var type_str := str(note.get("type", "basic"))
|
||||
|
||||
var header_row := HBoxContainer.new()
|
||||
header_row.add_theme_constant_override("separation", 8)
|
||||
content.add_child(header_row)
|
||||
header_row.add_child(_type_badge(type_str, dark))
|
||||
var tags: Array = note.get("tags", [])
|
||||
if tags.size() > 0:
|
||||
var tags_label := Label.new()
|
||||
tags_label.text = ", ".join(PackedStringArray(tags))
|
||||
tags_label.add_theme_font_size_override("font_size", 12)
|
||||
tags_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
header_row.add_child(tags_label)
|
||||
|
||||
var prompt_text := str(note.get("prompt", ""))
|
||||
var preview := Cloze.render_back(prompt_text) if type_str == "cloze" else prompt_text
|
||||
var prompt_label := Label.new()
|
||||
@@ -308,22 +332,21 @@ func _note_row(note: Dictionary) -> Control:
|
||||
prompt_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
prompt_label.add_theme_font_override("font", Brand.font)
|
||||
content.add_child(prompt_label)
|
||||
var meta := Label.new()
|
||||
var tags: Array = note.get("tags", [])
|
||||
meta.text = "%s%s" % [type_str.capitalize(), (" | " + ", ".join(PackedStringArray(tags))) if tags.size() > 0 else ""]
|
||||
meta.add_theme_font_size_override("font_size", 12)
|
||||
meta.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
content.add_child(meta)
|
||||
|
||||
var edit_button := Button.new()
|
||||
edit_button.text = "Edit"
|
||||
edit_button.custom_minimum_size = Vector2(70, 40)
|
||||
edit_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
|
||||
edit_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
|
||||
edit_button.pressed.connect(_open_editor.bind(str(note.get("id", "")), note))
|
||||
row.add_child(edit_button)
|
||||
|
||||
var archive_button := Button.new()
|
||||
archive_button.text = "Archive"
|
||||
archive_button.custom_minimum_size = Vector2(85, 40)
|
||||
archive_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
|
||||
archive_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
|
||||
archive_button.add_theme_color_override("font_color", Color("ff5f76") if dark else Color("be123c"))
|
||||
archive_button.pressed.connect(func():
|
||||
FlashcardService.archive_notes([str(note.get("id", ""))], true)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
@@ -333,6 +356,27 @@ func _note_row(note: Dictionary) -> Control:
|
||||
return card
|
||||
|
||||
|
||||
func _type_badge(type_str: String, dark: bool) -> PanelContainer:
|
||||
var accent := Brand.TEAL if type_str == "cloze" else Brand.ROYAL_BLUE
|
||||
var badge := PanelContainer.new()
|
||||
var box := StyleBoxFlat.new()
|
||||
box.bg_color = Color(accent, 0.16 if dark else 0.12)
|
||||
box.border_color = accent
|
||||
box.set_border_width_all(1)
|
||||
box.set_corner_radius_all(8)
|
||||
box.content_margin_left = 8
|
||||
box.content_margin_right = 8
|
||||
box.content_margin_top = 2
|
||||
box.content_margin_bottom = 2
|
||||
badge.add_theme_stylebox_override("panel", box)
|
||||
var label := Label.new()
|
||||
label.text = type_str.capitalize()
|
||||
label.add_theme_font_size_override("font_size", 11)
|
||||
label.add_theme_color_override("font_color", accent)
|
||||
badge.add_child(label)
|
||||
return badge
|
||||
|
||||
|
||||
# ---------- Note editor ----------
|
||||
|
||||
func _build_editor_overlay() -> void:
|
||||
@@ -373,6 +417,11 @@ func _build_editor_overlay() -> void:
|
||||
close_button.pressed.connect(_close_editor)
|
||||
header.add_child(close_button)
|
||||
|
||||
editor_deck_label = Label.new()
|
||||
editor_deck_label.add_theme_font_size_override("font_size", 13)
|
||||
editor_deck_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
||||
content.add_child(editor_deck_label)
|
||||
|
||||
editor_type_selector = OptionButton.new()
|
||||
editor_type_selector.add_item("Basic")
|
||||
editor_type_selector.add_item("Cloze")
|
||||
@@ -391,13 +440,13 @@ func _build_editor_overlay() -> void:
|
||||
editor_front_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||
content.add_child(editor_front_input)
|
||||
|
||||
content.add_child(_field_label("Back / Extra takeaway"))
|
||||
content.add_child(_field_label("Answer"))
|
||||
editor_back_input = TextEdit.new()
|
||||
editor_back_input.custom_minimum_size = Vector2(0, 80)
|
||||
editor_back_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||
content.add_child(editor_back_input)
|
||||
|
||||
content.add_child(_field_label("Extra"))
|
||||
content.add_child(_field_label("Extra (optional teaching pearl)"))
|
||||
editor_extra_input = TextEdit.new()
|
||||
editor_extra_input.custom_minimum_size = Vector2(0, 60)
|
||||
editor_extra_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||
@@ -415,7 +464,11 @@ func _build_editor_overlay() -> void:
|
||||
var save_button := Button.new()
|
||||
save_button.text = "Save"
|
||||
save_button.custom_minimum_size = Vector2(0, 46)
|
||||
Style.style_primary_button(save_button)
|
||||
save_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
save_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
save_button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
|
||||
save_button.add_theme_color_override("font_color", Color.WHITE)
|
||||
save_button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
save_button.pressed.connect(_save_editor)
|
||||
content.add_child(save_button)
|
||||
|
||||
@@ -446,10 +499,20 @@ func _open_editor(note_id: String, note: Dictionary) -> void:
|
||||
editor_extra_input.text = str(note.get("extra", ""))
|
||||
editor_tags_input.text = ", ".join(PackedStringArray(note.get("tags", [])))
|
||||
editor_error_label.visible = false
|
||||
editor_deck_label.text = "Saving to: %s" % _deck_name_for_id(str(note.get("deckId", "")) if not note_id.is_empty() else _selected_deck_id())
|
||||
_on_editor_type_changed(editor_type_selector.selected)
|
||||
editor_overlay.visible = true
|
||||
|
||||
|
||||
func _deck_name_for_id(id: String) -> String:
|
||||
if not id.is_empty():
|
||||
for summary in decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
if str(deck.get("id", "")) == id:
|
||||
return str(deck.get("name", "Default"))
|
||||
return "Default"
|
||||
|
||||
|
||||
func _close_editor() -> void:
|
||||
editor_overlay.visible = false
|
||||
|
||||
@@ -632,6 +695,8 @@ func _refresh_status() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
if editor_panel != null:
|
||||
editor_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
|
||||
if editor_deck_label != null:
|
||||
editor_deck_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
if import_panel != null:
|
||||
import_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
|
||||
if export_menu != null:
|
||||
|
||||
@@ -26,8 +26,11 @@ var exit_button: Button
|
||||
var paused_banner: Label
|
||||
var card_panel: PanelContainer
|
||||
var front_label: Label
|
||||
var divider: HSeparator
|
||||
var divider: ColorRect
|
||||
var answer_caption: Label
|
||||
var back_label: Label
|
||||
var extra_caption: Label
|
||||
var extra_label: Label
|
||||
var reveal_button: Button
|
||||
var grade_row: HBoxContainer
|
||||
var grade_buttons: Dictionary = {}
|
||||
@@ -57,6 +60,9 @@ func on_show(payload: Variant = null) -> void:
|
||||
current_note = {}
|
||||
current_card = {}
|
||||
revealed = false
|
||||
last_client_review_id = ""
|
||||
last_card_id = ""
|
||||
undo_button.visible = false
|
||||
_render_loading()
|
||||
_load_next()
|
||||
|
||||
@@ -100,11 +106,11 @@ func _build() -> void:
|
||||
root.add_child(center)
|
||||
|
||||
card_panel = PanelContainer.new()
|
||||
card_panel.custom_minimum_size = Vector2(640, 360)
|
||||
card_panel.custom_minimum_size = Vector2(720, 420)
|
||||
card_panel.add_theme_stylebox_override("panel", Brand.content_card_box(UIThemeService.is_dark_mode(), true, Brand.ROYAL_BLUE))
|
||||
center.add_child(card_panel)
|
||||
var card_content := VBoxContainer.new()
|
||||
card_content.add_theme_constant_override("separation", 18)
|
||||
card_content.add_theme_constant_override("separation", 20)
|
||||
card_panel.add_child(card_content)
|
||||
|
||||
empty_label = Label.new()
|
||||
@@ -116,24 +122,42 @@ func _build() -> void:
|
||||
front_label = Label.new()
|
||||
front_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
front_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
front_label.add_theme_font_size_override("font_size", 22)
|
||||
front_label.add_theme_font_size_override("font_size", 25)
|
||||
card_content.add_child(front_label)
|
||||
|
||||
divider = HSeparator.new()
|
||||
divider = ColorRect.new()
|
||||
divider.custom_minimum_size = Vector2(0, 2)
|
||||
divider.visible = false
|
||||
card_content.add_child(divider)
|
||||
|
||||
var answer_block := VBoxContainer.new()
|
||||
answer_block.add_theme_constant_override("separation", 4)
|
||||
card_content.add_child(answer_block)
|
||||
answer_caption = _caption_label("ANSWER")
|
||||
answer_block.add_child(answer_caption)
|
||||
back_label = Label.new()
|
||||
back_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
back_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
back_label.add_theme_font_size_override("font_size", 18)
|
||||
back_label.add_theme_font_size_override("font_size", 19)
|
||||
back_label.visible = false
|
||||
card_content.add_child(back_label)
|
||||
answer_block.add_child(back_label)
|
||||
|
||||
var extra_block := VBoxContainer.new()
|
||||
extra_block.add_theme_constant_override("separation", 4)
|
||||
card_content.add_child(extra_block)
|
||||
extra_caption = _caption_label("KEY TAKEAWAY")
|
||||
extra_block.add_child(extra_caption)
|
||||
extra_label = Label.new()
|
||||
extra_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
extra_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
extra_label.add_theme_font_size_override("font_size", 15)
|
||||
extra_label.visible = false
|
||||
extra_block.add_child(extra_label)
|
||||
|
||||
reveal_button = Button.new()
|
||||
reveal_button.text = "Show Answer (Space)"
|
||||
reveal_button.custom_minimum_size = Vector2(0, 52)
|
||||
Style.style_primary_button(reveal_button)
|
||||
_style_gradient_button(reveal_button)
|
||||
reveal_button.pressed.connect(_reveal)
|
||||
root.add_child(reveal_button)
|
||||
|
||||
@@ -152,10 +176,38 @@ func _build() -> void:
|
||||
|
||||
undo_button = Button.new()
|
||||
undo_button.text = "Undo last review"
|
||||
undo_button.custom_minimum_size = Vector2(0, 42)
|
||||
undo_button.disabled = true
|
||||
undo_button.custom_minimum_size = Vector2(0, 36)
|
||||
undo_button.visible = false
|
||||
undo_button.pressed.connect(_undo)
|
||||
root.add_child(undo_button)
|
||||
_style_ghost_button(undo_button, UIThemeService.is_dark_mode())
|
||||
|
||||
|
||||
func _caption_label(text: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", 12)
|
||||
label.visible = false
|
||||
return label
|
||||
|
||||
|
||||
func _style_gradient_button(button: Button) -> void:
|
||||
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
||||
button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
|
||||
|
||||
func _style_ghost_button(button: Button, dark: bool) -> 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_stylebox_override("focus", empty)
|
||||
button.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
button.add_theme_color_override("font_hover_color", Brand.text_color(dark))
|
||||
|
||||
|
||||
func refresh_theme_controls() -> void:
|
||||
@@ -164,6 +216,11 @@ func refresh_theme_controls() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
card_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||||
empty_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
divider.color = Brand.ROYAL_BLUE if dark else Brand.PURPLE
|
||||
answer_caption.add_theme_color_override("font_color", Brand.ROYAL_BLUE if dark else Brand.PURPLE)
|
||||
extra_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
extra_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
_style_ghost_button(undo_button, dark)
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
@@ -171,8 +228,8 @@ func _update_responsive_layout() -> void:
|
||||
return
|
||||
compact_layout = size.x < 720.0
|
||||
var available := maxf(size.x - 48.0, 240.0)
|
||||
card_panel.custom_minimum_size.x = minf(640.0, available)
|
||||
card_panel.custom_minimum_size.y = 300.0 if compact_layout else 360.0
|
||||
card_panel.custom_minimum_size.x = minf(720.0, available)
|
||||
card_panel.custom_minimum_size.y = 340.0 if compact_layout else 420.0
|
||||
|
||||
|
||||
func _load_next() -> void:
|
||||
@@ -208,8 +265,7 @@ func _render_loading() -> void:
|
||||
empty_label.visible = false
|
||||
front_label.visible = true
|
||||
front_label.text = "Loading..."
|
||||
back_label.visible = false
|
||||
divider.visible = false
|
||||
_hide_answer()
|
||||
reveal_button.visible = false
|
||||
grade_row.visible = false
|
||||
|
||||
@@ -217,8 +273,7 @@ func _render_loading() -> void:
|
||||
func _render_empty(card: Dictionary) -> void:
|
||||
empty_label.visible = true
|
||||
front_label.visible = false
|
||||
back_label.visible = false
|
||||
divider.visible = false
|
||||
_hide_answer()
|
||||
reveal_button.visible = false
|
||||
grade_row.visible = false
|
||||
progress_label.text = "0 due"
|
||||
@@ -226,6 +281,14 @@ func _render_empty(card: Dictionary) -> void:
|
||||
progress_label.text = "%d due, %d new available" % [int(card.get("dueRemaining", 0)), int(card.get("newRemaining", 0))]
|
||||
|
||||
|
||||
func _hide_answer() -> void:
|
||||
divider.visible = false
|
||||
answer_caption.visible = false
|
||||
back_label.visible = false
|
||||
extra_caption.visible = false
|
||||
extra_label.visible = false
|
||||
|
||||
|
||||
func _render_card() -> void:
|
||||
empty_label.visible = false
|
||||
var note_type := str(current_note.get("type", "basic"))
|
||||
@@ -235,8 +298,7 @@ func _render_card() -> void:
|
||||
else:
|
||||
front_label.text = str(current_note.get("prompt", ""))
|
||||
front_label.visible = true
|
||||
back_label.visible = false
|
||||
divider.visible = false
|
||||
_hide_answer()
|
||||
reveal_button.visible = true
|
||||
grade_row.visible = false
|
||||
progress_label.text = "%d due, %d new available" % [due_remaining, new_remaining]
|
||||
@@ -251,11 +313,14 @@ func _reveal() -> void:
|
||||
if note_type == "cloze":
|
||||
back_label.text = Cloze.render_back(str(current_note.get("prompt", "")))
|
||||
else:
|
||||
var answer := str(current_note.get("answer", ""))
|
||||
var extra := str(current_note.get("extra", ""))
|
||||
back_label.text = "%s\n\n%s" % [answer, extra] if not extra.is_empty() else answer
|
||||
back_label.visible = true
|
||||
back_label.text = str(current_note.get("answer", ""))
|
||||
var extra := str(current_note.get("extra", ""))
|
||||
divider.visible = true
|
||||
answer_caption.visible = true
|
||||
back_label.visible = true
|
||||
extra_caption.visible = not extra.is_empty()
|
||||
extra_label.visible = not extra.is_empty()
|
||||
extra_label.text = extra
|
||||
reveal_button.visible = false
|
||||
grade_row.visible = true
|
||||
for rating in grade_buttons:
|
||||
@@ -278,6 +343,7 @@ func _grade(rating: String) -> void:
|
||||
var duration_seconds := int(float(Time.get_ticks_msec() - loaded_started_ms) / 1000.0)
|
||||
last_client_review_id = FlashcardService.submit_review(card_id, rating, expected_revision, duration_seconds, study_day_start_utc)
|
||||
last_card_id = card_id
|
||||
undo_button.visible = true
|
||||
undo_button.disabled = false
|
||||
if not seen_card_ids.has(card_id):
|
||||
seen_card_ids.append(card_id)
|
||||
@@ -295,6 +361,7 @@ func _undo() -> void:
|
||||
undo_button.disabled = true
|
||||
FlashcardService.undo_last_review(last_client_review_id, func(_restored: Dictionary):
|
||||
last_client_review_id = ""
|
||||
undo_button.visible = false
|
||||
seen_card_ids.erase(last_card_id)
|
||||
_load_next())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user