Add system (curated) vs. user-uploaded flashcards
System decks/notes are owned by a reserved sentinel user id, reusing all existing per-user store/index plumbing unchanged. Regular users see them under a separate "Official Decks" section, can study them with fully independent per-user FSRS progress (lazily materialized on first visit), but can never edit or archive them - enforced both in the client (no Edit/Archive controls) and server-side (ownership guard on every mutating endpoint). Includes a small idempotent seed command (`dotnet run -- seed-flashcards`) for testing, since there's no in-app authoring UI yet.
This commit is contained in:
@@ -31,6 +31,7 @@ var editor_overlay: Control
|
||||
var editor_panel: PanelContainer
|
||||
var editor_deck_label: Label
|
||||
var editor_note_id := ""
|
||||
var editor_target_deck_id := ""
|
||||
var editor_expected_revision: Variant = null
|
||||
var editor_type_selector: OptionButton
|
||||
var editor_front_input: TextEdit
|
||||
@@ -333,10 +334,23 @@ func _on_decks_changed(loaded_decks: Array) -> void:
|
||||
deck_ids = [""]
|
||||
var total_due := 0
|
||||
var total_new := 0
|
||||
var my_decks := decks.filter(func(s): return not bool(s.get("deck", {}).get("isSystem", false)))
|
||||
var official_decks := decks.filter(func(s): return bool(s.get("deck", {}).get("isSystem", false)))
|
||||
if my_decks.size() > 0:
|
||||
deck_selector.add_separator("My Decks")
|
||||
deck_ids.append("")
|
||||
for summary in my_decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
deck_selector.add_item(str(deck.get("name", "Deck")))
|
||||
deck_ids.append(str(deck.get("id", "")))
|
||||
if official_decks.size() > 0:
|
||||
deck_selector.add_separator("Official Decks")
|
||||
deck_ids.append("")
|
||||
for summary in official_decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
deck_selector.add_item(str(deck.get("name", "Deck")))
|
||||
deck_ids.append(str(deck.get("id", "")))
|
||||
for summary in decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
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:
|
||||
@@ -355,6 +369,16 @@ func _selected_deck_id() -> String:
|
||||
return str(deck_ids[deck_selector.selected])
|
||||
|
||||
|
||||
func _is_system_deck(id: String) -> bool:
|
||||
if id.is_empty():
|
||||
return false
|
||||
for summary in decks:
|
||||
var deck: Dictionary = summary.get("deck", {})
|
||||
if str(deck.get("id", "")) == id:
|
||||
return bool(deck.get("isSystem", false))
|
||||
return false
|
||||
|
||||
|
||||
func _reload_notes() -> void:
|
||||
status_label.text = "Loading flashcards..."
|
||||
var query := {
|
||||
@@ -426,6 +450,13 @@ func _note_row(note: Dictionary) -> Control:
|
||||
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 is_system := bool(note.get("isSystem", false))
|
||||
if is_system:
|
||||
var official_label := Label.new()
|
||||
official_label.text = "Official"
|
||||
official_label.add_theme_font_size_override("font_size", 12)
|
||||
official_label.add_theme_color_override("font_color", Brand.PURPLE)
|
||||
header_row.add_child(official_label)
|
||||
var tags: Array = note.get("tags", [])
|
||||
if tags.size() > 0:
|
||||
var tags_label := Label.new()
|
||||
@@ -446,25 +477,26 @@ func _note_row(note: Dictionary) -> Control:
|
||||
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))
|
||||
actions.add_child(edit_button)
|
||||
if not is_system:
|
||||
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))
|
||||
actions.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
|
||||
_reload_notes())
|
||||
actions.add_child(archive_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
|
||||
_reload_notes())
|
||||
actions.add_child(archive_button)
|
||||
|
||||
return row
|
||||
|
||||
@@ -594,7 +626,11 @@ 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())
|
||||
if note_id.is_empty():
|
||||
editor_target_deck_id = "" if _is_system_deck(_selected_deck_id()) else _selected_deck_id()
|
||||
else:
|
||||
editor_target_deck_id = str(note.get("deckId", ""))
|
||||
editor_deck_label.text = "Saving to: %s" % _deck_name_for_id(editor_target_deck_id)
|
||||
_on_editor_type_changed(editor_type_selector.selected)
|
||||
editor_overlay.visible = true
|
||||
|
||||
@@ -629,7 +665,7 @@ func _save_editor() -> void:
|
||||
if not trimmed.is_empty():
|
||||
tags.append(trimmed)
|
||||
var payload := {
|
||||
"deckId": _selected_deck_id(),
|
||||
"deckId": editor_target_deck_id,
|
||||
"stableId": "",
|
||||
"type": "cloze" if is_cloze else "basic",
|
||||
"prompt": prompt,
|
||||
|
||||
@@ -72,6 +72,7 @@ var compact_layout := false
|
||||
|
||||
var deck_panel: PanelContainer
|
||||
var deck_name_label: Label
|
||||
var deck_system_badge: Label
|
||||
var deck_ring: Control
|
||||
var deck_mastered_label: Label
|
||||
var change_deck_button: Button
|
||||
@@ -371,6 +372,13 @@ func _build_deck_panel() -> void:
|
||||
deck_name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
content.add_child(deck_name_label)
|
||||
|
||||
deck_system_badge = Label.new()
|
||||
deck_system_badge.text = "Official Deck"
|
||||
deck_system_badge.add_theme_font_size_override("font_size", 12)
|
||||
deck_system_badge.add_theme_color_override("font_color", Brand.PURPLE)
|
||||
deck_system_badge.visible = false
|
||||
content.add_child(deck_system_badge)
|
||||
|
||||
var ring_center := CenterContainer.new()
|
||||
content.add_child(ring_center)
|
||||
deck_ring = CircularProgress.new()
|
||||
@@ -597,6 +605,7 @@ func _refresh_deck_panel() -> void:
|
||||
var name := "All decks"
|
||||
var total := 0
|
||||
var mastered := 0
|
||||
var is_system := false
|
||||
if deck_id.is_empty():
|
||||
for summary in decks:
|
||||
total += int(summary.get("total", 0))
|
||||
@@ -608,8 +617,10 @@ func _refresh_deck_panel() -> void:
|
||||
name = str(deck.get("name", "Deck"))
|
||||
total = int(summary.get("total", 0))
|
||||
mastered = int(summary.get("mastered", 0))
|
||||
is_system = bool(deck.get("isSystem", false))
|
||||
break
|
||||
deck_name_label.text = name
|
||||
deck_system_badge.visible = is_system
|
||||
var fraction := float(mastered) / float(total) if total > 0 else 0.0
|
||||
deck_ring.configure(fraction, "%d%%" % roundi(fraction * 100.0), "Mastery", Brand.ROYAL_BLUE, dark)
|
||||
deck_mastered_label.text = "%d / %d cards mastered" % [mastered, total]
|
||||
|
||||
Reference in New Issue
Block a user