Rework flashcards library to match the rest of the app's visual language
Compared side-by-side against Dashboard, the library screen used entirely different visual primitives: a gradient primary button Dashboard doesn't use anywhere, per-row bordered cards with low contrast against the dark background, and a translucent pill badge instead of the icon-circle pattern used everywhere else (stat tiles, review queue, recent activity). - Study/Save/Commit import now use Dashboard's actual primary button style (flat ROYAL_BLUE, not the login/session-dialog gradient) - Added a stat-tile row (Due today / New available / Decks) using Dashboard's exact icon-circle + value + label construction, replacing the due/new counts previously crammed into the deck dropdown's text - The note list now lives in one bordered card with a section title (matching Dashboard's chart/list panels) instead of each row being its own separate low-contrast card - Per-row card-type indicator is now an icon circle (clipboard-list / notebook-pen) matching the Recent Activity list pattern, instead of a translucent color-coded pill badge
This commit is contained in:
+171
-52
@@ -6,6 +6,13 @@ const Cloze := preload("res://scripts/ui/flashcard_cloze.gd")
|
||||
|
||||
var main
|
||||
|
||||
var stats_row: HFlowContainer
|
||||
var stat_due_value: Label
|
||||
var stat_new_value: Label
|
||||
var stat_decks_value: Label
|
||||
var list_card: PanelContainer
|
||||
var list_section_title: Label
|
||||
|
||||
var deck_selector: OptionButton
|
||||
var deck_ids: Array = []
|
||||
var new_deck_button: Button
|
||||
@@ -56,17 +63,28 @@ func _ready() -> void:
|
||||
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_theme_constant_override("separation", 14)
|
||||
_build_dropdown_backdrop()
|
||||
_build_stats()
|
||||
_build_toolbar()
|
||||
_build_filters()
|
||||
status_label = Label.new()
|
||||
status_label.add_theme_font_override("font", Brand.font)
|
||||
add_child(status_label)
|
||||
|
||||
list_card = PanelContainer.new()
|
||||
list_card.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
list_card.add_theme_stylebox_override("panel", _list_card_box(UIThemeService.is_dark_mode()))
|
||||
add_child(list_card)
|
||||
var list_card_content := VBoxContainer.new()
|
||||
list_card_content.add_theme_constant_override("separation", 10)
|
||||
list_card.add_child(list_card_content)
|
||||
list_section_title = _section_title("Your flashcards")
|
||||
list_card_content.add_child(list_section_title)
|
||||
list_scroll = ScrollContainer.new()
|
||||
list_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(list_scroll)
|
||||
list_card_content.add_child(list_scroll)
|
||||
list_container = VBoxContainer.new()
|
||||
list_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
list_container.add_theme_constant_override("separation", 8)
|
||||
list_container.add_theme_constant_override("separation", 14)
|
||||
list_scroll.add_child(list_container)
|
||||
debounce_timer = Timer.new()
|
||||
debounce_timer.one_shot = true
|
||||
@@ -93,6 +111,79 @@ func on_show(_payload: Variant = null) -> void:
|
||||
_reload_notes()
|
||||
|
||||
|
||||
func _build_stats() -> void:
|
||||
var previous_due := stat_due_value.text if stat_due_value != null else "--"
|
||||
var previous_new := stat_new_value.text if stat_new_value != null else "--"
|
||||
var previous_decks := stat_decks_value.text if stat_decks_value != null else "--"
|
||||
if stats_row == null:
|
||||
stats_row = HFlowContainer.new()
|
||||
stats_row.add_theme_constant_override("separation", 24)
|
||||
add_child(stats_row)
|
||||
move_child(stats_row, 0)
|
||||
for child in stats_row.get_children():
|
||||
child.queue_free()
|
||||
stat_due_value = _add_stat(stats_row, "Due today", "clock-3", Brand.AMBER)
|
||||
stat_new_value = _add_stat(stats_row, "New available", "clipboard-plus", Brand.ROYAL_BLUE)
|
||||
stat_decks_value = _add_stat(stats_row, "Decks", "clipboard-list", Brand.PURPLE)
|
||||
stat_due_value.text = previous_due
|
||||
stat_new_value.text = previous_new
|
||||
stat_decks_value.text = previous_decks
|
||||
|
||||
|
||||
func _add_stat(row: HFlowContainer, title: String, icon_name: String, accent: Color) -> Label:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
var cell := HBoxContainer.new()
|
||||
cell.custom_minimum_size = Vector2(200, 56)
|
||||
cell.add_theme_constant_override("separation", 12)
|
||||
row.add_child(cell)
|
||||
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(dark, 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
|
||||
var caption := Label.new()
|
||||
caption.text = title
|
||||
caption.add_theme_font_size_override("font_size", 13)
|
||||
caption.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
copy.add_child(caption)
|
||||
var value := Label.new()
|
||||
value.text = "--"
|
||||
value.add_theme_font_size_override("font_size", 24)
|
||||
value.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
copy.add_child(value)
|
||||
cell.add_child(copy)
|
||||
return value
|
||||
|
||||
|
||||
func _list_card_box(dark: bool) -> StyleBoxFlat:
|
||||
var box := StyleBoxFlat.new()
|
||||
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 = 18
|
||||
box.content_margin_right = 18
|
||||
box.content_margin_top = 16
|
||||
box.content_margin_bottom = 16
|
||||
return box
|
||||
|
||||
|
||||
func _section_title(text: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
label.add_theme_font_size_override("font_size", 16)
|
||||
label.add_theme_color_override("font_color", Brand.text_color(UIThemeService.is_dark_mode()))
|
||||
return label
|
||||
|
||||
|
||||
func _build_toolbar() -> void:
|
||||
var row := HFlowContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
@@ -113,11 +204,7 @@ func _build_toolbar() -> void:
|
||||
study_button = Button.new()
|
||||
study_button.text = "Study"
|
||||
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)
|
||||
_style_primary_button(study_button)
|
||||
study_button.pressed.connect(_start_study)
|
||||
row.add_child(study_button)
|
||||
|
||||
@@ -156,6 +243,19 @@ func _build_toolbar() -> void:
|
||||
])
|
||||
|
||||
|
||||
func _style_primary_button(button: Button) -> void:
|
||||
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("font_hover_color", Color.WHITE)
|
||||
|
||||
|
||||
func _build_dropdown_backdrop() -> void:
|
||||
dropdown_backdrop = Control.new()
|
||||
dropdown_backdrop.visible = false
|
||||
@@ -250,15 +350,22 @@ func _on_decks_changed(loaded_decks: Array) -> void:
|
||||
deck_selector.clear()
|
||||
deck_selector.add_item("All decks")
|
||||
deck_ids = [""]
|
||||
var total_due := 0
|
||||
var total_new := 0
|
||||
for summary in decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
var label := "%s (%d due, %d new)" % [str(deck.get("name", "Deck")), int(summary.get("due", 0)), int(summary.get("new", 0))]
|
||||
deck_selector.add_item(label)
|
||||
deck_selector.add_item(str(deck.get("name", "Deck")))
|
||||
deck_ids.append(str(deck.get("id", "")))
|
||||
total_due += int(summary.get("due", 0))
|
||||
total_new += int(summary.get("new", 0))
|
||||
if previous_selection != null:
|
||||
var restored := deck_ids.find(previous_selection)
|
||||
if restored >= 0:
|
||||
deck_selector.select(restored)
|
||||
if stat_due_value != null:
|
||||
stat_due_value.text = str(total_due)
|
||||
stat_new_value.text = str(total_new)
|
||||
stat_decks_value.text = str(decks.size())
|
||||
|
||||
|
||||
func _selected_deck_id() -> String:
|
||||
@@ -288,35 +395,56 @@ func _on_notes_loaded(response: Dictionary) -> void:
|
||||
func _render_notes() -> void:
|
||||
for child in list_container.get_children():
|
||||
child.queue_free()
|
||||
status_label.text = "" if notes.size() > 0 else "No flashcards match these filters yet."
|
||||
for note in notes:
|
||||
list_container.add_child(_note_row(note))
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
if list_section_title != null:
|
||||
list_section_title.text = "Your flashcards (%d)" % notes.size()
|
||||
status_label.text = ""
|
||||
if notes.is_empty():
|
||||
var empty_label := Label.new()
|
||||
empty_label.text = "No flashcards match these filters yet."
|
||||
empty_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
list_container.add_child(empty_label)
|
||||
return
|
||||
for index in notes.size():
|
||||
if index > 0:
|
||||
list_container.add_child(_row_separator(dark))
|
||||
list_container.add_child(_note_row(notes[index]))
|
||||
|
||||
|
||||
func _note_row(note: Dictionary) -> Control:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
var card := PanelContainer.new()
|
||||
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 type_str := str(note.get("type", "basic"))
|
||||
var accent := Brand.TEAL if type_str == "cloze" else Brand.ROYAL_BLUE
|
||||
|
||||
var row := BoxContainer.new()
|
||||
row.vertical = compact_layout
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
card.add_child(row)
|
||||
row.add_theme_constant_override("separation", 14)
|
||||
|
||||
var icon_panel := PanelContainer.new()
|
||||
icon_panel.custom_minimum_size = Vector2(40, 40)
|
||||
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 20))
|
||||
var icon := TextureRect.new()
|
||||
icon.texture = load("res://assets/icons/lucide/%s.svg" % ("notebook-pen" if type_str == "cloze" else "clipboard-list"))
|
||||
icon.custom_minimum_size = Vector2(18, 18)
|
||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon_panel.add_child(icon)
|
||||
row.add_child(icon_panel)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content.add_theme_constant_override("separation", 6)
|
||||
content.add_theme_constant_override("separation", 4)
|
||||
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 type_label := Label.new()
|
||||
type_label.text = type_str.capitalize()
|
||||
type_label.add_theme_font_size_override("font_size", 12)
|
||||
type_label.add_theme_color_override("font_color", accent)
|
||||
header_row.add_child(type_label)
|
||||
var tags: Array = note.get("tags", [])
|
||||
if tags.size() > 0:
|
||||
var tags_label := Label.new()
|
||||
@@ -333,13 +461,17 @@ func _note_row(note: Dictionary) -> Control:
|
||||
prompt_label.add_theme_font_override("font", Brand.font)
|
||||
content.add_child(prompt_label)
|
||||
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 8)
|
||||
row.add_child(actions)
|
||||
|
||||
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)
|
||||
actions.add_child(edit_button)
|
||||
|
||||
var archive_button := Button.new()
|
||||
archive_button.text = "Archive"
|
||||
@@ -351,30 +483,16 @@ func _note_row(note: Dictionary) -> Control:
|
||||
FlashcardService.archive_notes([str(note.get("id", ""))], true)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
_reload_notes())
|
||||
row.add_child(archive_button)
|
||||
actions.add_child(archive_button)
|
||||
|
||||
return card
|
||||
return row
|
||||
|
||||
|
||||
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
|
||||
func _row_separator(dark: bool) -> ColorRect:
|
||||
var line := ColorRect.new()
|
||||
line.custom_minimum_size = Vector2(0, 1)
|
||||
line.color = Color("1c2b49") if dark else Brand.LIGHT_BORDER
|
||||
return line
|
||||
|
||||
|
||||
# ---------- Note editor ----------
|
||||
@@ -464,11 +582,7 @@ func _build_editor_overlay() -> void:
|
||||
var save_button := Button.new()
|
||||
save_button.text = "Save"
|
||||
save_button.custom_minimum_size = Vector2(0, 46)
|
||||
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)
|
||||
_style_primary_button(save_button)
|
||||
save_button.pressed.connect(_save_editor)
|
||||
content.add_child(save_button)
|
||||
|
||||
@@ -635,7 +749,7 @@ func _build_import_overlay() -> void:
|
||||
import_commit_button.text = "Commit import"
|
||||
import_commit_button.custom_minimum_size = Vector2(0, 46)
|
||||
import_commit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
Style.style_primary_button(import_commit_button)
|
||||
_style_primary_button(import_commit_button)
|
||||
import_commit_button.pressed.connect(_commit_pending_import)
|
||||
actions.add_child(import_commit_button)
|
||||
|
||||
@@ -703,6 +817,11 @@ func _refresh_status() -> void:
|
||||
export_menu.add_theme_stylebox_override("panel", Brand.panel_box(dark))
|
||||
if import_menu != null:
|
||||
import_menu.add_theme_stylebox_override("panel", Brand.panel_box(dark))
|
||||
if list_card != null:
|
||||
list_card.add_theme_stylebox_override("panel", _list_card_box(dark))
|
||||
if list_section_title != null:
|
||||
list_section_title.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
_build_stats()
|
||||
_render_notes()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user