Add synced user notes
This commit is contained in:
+20
-1
@@ -12,6 +12,7 @@ var sound_toggle: CheckButton
|
||||
var theme_button: Button
|
||||
var feedback_button: Button
|
||||
var feedback_dialog: Control
|
||||
var notes_dialog: Control
|
||||
var session_expiry_dialog: Control
|
||||
var theme_service
|
||||
var current_screen_name := ""
|
||||
@@ -283,9 +284,18 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(settings)
|
||||
screens["settings"] = settings
|
||||
|
||||
var notes := preload("res://scripts/ui/notes_screen.gd").new()
|
||||
notes.name = "NotesScreen"
|
||||
notes.main = self
|
||||
screens_container.add_child(notes)
|
||||
screens["notes"] = notes
|
||||
|
||||
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
|
||||
feedback_dialog.name = "AppFeedbackDialog"
|
||||
add_child(feedback_dialog)
|
||||
notes_dialog = preload("res://scripts/ui/notes_dialog.gd").new()
|
||||
notes_dialog.name = "NotesDialog"
|
||||
add_child(notes_dialog)
|
||||
session_expiry_dialog = preload("res://scripts/ui/session_expiry_dialog.gd").new()
|
||||
session_expiry_dialog.name = "SessionExpiryDialog"
|
||||
session_expiry_dialog.logout_requested.connect(_logout)
|
||||
@@ -348,7 +358,7 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
|
||||
_add_nav_button(container, "flashcards", "Flashcards", "copy", false)
|
||||
_add_nav_button(container, "study_plan", "Study Plan", "calendar-days", true)
|
||||
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", false)
|
||||
_add_nav_button(container, "notes", "Notes", "notebook-pen", false)
|
||||
_add_nav_button(container, "notes", "Notes", "notebook-pen", true)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
@@ -391,6 +401,7 @@ func _on_navigation_pressed(key: String) -> void:
|
||||
"performance": show_screen("profile")
|
||||
"study_plan": show_screen("map")
|
||||
"settings": show_screen("settings")
|
||||
"notes": show_screen("notes")
|
||||
"logout": _logout()
|
||||
|
||||
|
||||
@@ -648,6 +659,14 @@ func open_feedback_dialog() -> void:
|
||||
feedback_dialog.show_dialog(FeedbackService.build_context(current_screen_name, get_viewport_rect().size))
|
||||
|
||||
|
||||
func open_notes_dialog() -> void:
|
||||
if not AuthState.is_logged_in or notes_dialog == null:
|
||||
return
|
||||
if current_screen_name != "player" or BlockSessionService.session_kind == "simulation":
|
||||
return
|
||||
notes_dialog.show_dialog()
|
||||
|
||||
|
||||
func _on_theme_changed(is_dark_mode: bool) -> void:
|
||||
Style.refresh_tree(self, not is_dark_mode)
|
||||
_refresh_theme_button()
|
||||
|
||||
@@ -17,4 +17,6 @@ static func key_for_screen(screen_name: String, payload: Variant = null) -> Stri
|
||||
key = "performance"
|
||||
elif screen_name == "settings":
|
||||
key = "settings"
|
||||
elif screen_name == "notes":
|
||||
key = "notes"
|
||||
return key
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
extends Control
|
||||
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
const NotesEditor := preload("res://scripts/ui/notes_editor.gd")
|
||||
|
||||
signal closed
|
||||
|
||||
var panel: PanelContainer
|
||||
var notes_editor: VBoxContainer
|
||||
var theme_service: Node
|
||||
var block_session_service: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
theme_service = get_node("/root/UIThemeService")
|
||||
block_session_service = get_node("/root/BlockSessionService")
|
||||
visible = false
|
||||
top_level = true
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0, 0, 0, 0.72)
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dim.gui_input.connect(_on_dim_input)
|
||||
add_child(dim)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(center)
|
||||
panel = PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(760, 650)
|
||||
center.add_child(panel)
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 12)
|
||||
panel.add_child(content)
|
||||
var header := HBoxContainer.new()
|
||||
content.add_child(header)
|
||||
var title := Label.new()
|
||||
title.text = "Notes"
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title.add_theme_font_override("font", Brand.font)
|
||||
title.add_theme_font_size_override("font_size", 26)
|
||||
header.add_child(title)
|
||||
var close_button := Button.new()
|
||||
close_button.icon = preload("res://assets/icons/lucide/x.svg")
|
||||
close_button.tooltip_text = "Close notes"
|
||||
close_button.custom_minimum_size = Vector2(42, 42)
|
||||
close_button.add_theme_constant_override("icon_max_width", 20)
|
||||
close_button.pressed.connect(hide_dialog)
|
||||
header.add_child(close_button)
|
||||
notes_editor = NotesEditor.new()
|
||||
content.add_child(notes_editor)
|
||||
theme_service.theme_changed.connect(_on_theme_changed)
|
||||
resized.connect(_fit_panel)
|
||||
_apply_theme()
|
||||
_fit_panel()
|
||||
|
||||
|
||||
func show_dialog() -> void:
|
||||
notes_editor.activate()
|
||||
visible = true
|
||||
move_to_front()
|
||||
block_session_service.pause_current_question_timer()
|
||||
call_deferred("_focus_editor")
|
||||
|
||||
|
||||
func hide_dialog() -> void:
|
||||
if not visible:
|
||||
return
|
||||
visible = false
|
||||
get_node("/root/NotesService").flush()
|
||||
block_session_service.resume_current_question_timer()
|
||||
closed.emit()
|
||||
|
||||
|
||||
func _focus_editor() -> void:
|
||||
notes_editor.focus_editor()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if visible and event.is_action_pressed("ui_cancel"):
|
||||
hide_dialog()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_dim_input(event: InputEvent) -> void:
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event != null and mouse_event.pressed and mouse_event.button_index == MOUSE_BUTTON_LEFT:
|
||||
hide_dialog()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_theme_changed(_dark: bool) -> void:
|
||||
_apply_theme()
|
||||
|
||||
|
||||
func _apply_theme() -> void:
|
||||
panel.add_theme_stylebox_override("panel", Brand.panel_box(theme_service.is_dark_mode()))
|
||||
|
||||
|
||||
func _fit_panel() -> void:
|
||||
if panel == null:
|
||||
return
|
||||
var available := get_viewport_rect().size - Vector2(32, 32)
|
||||
var target_size := Vector2(
|
||||
minf(760.0, maxf(300.0, available.x)),
|
||||
minf(650.0, maxf(420.0, available.y)))
|
||||
if panel.custom_minimum_size != target_size:
|
||||
panel.custom_minimum_size = target_size
|
||||
@@ -0,0 +1 @@
|
||||
uid://c20n7rxtocujy
|
||||
@@ -0,0 +1,142 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
|
||||
var editor: TextEdit
|
||||
var status_label: Label
|
||||
var count_label: Label
|
||||
var conflict_panel: PanelContainer
|
||||
var applying_content := false
|
||||
var notes_service: Node
|
||||
var theme_service: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
notes_service = get_node("/root/NotesService")
|
||||
theme_service = get_node("/root/UIThemeService")
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_theme_constant_override("separation", 10)
|
||||
|
||||
var metadata_row := HBoxContainer.new()
|
||||
metadata_row.add_theme_constant_override("separation", 12)
|
||||
add_child(metadata_row)
|
||||
status_label = Label.new()
|
||||
status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
Brand.apply_font(status_label)
|
||||
metadata_row.add_child(status_label)
|
||||
count_label = Label.new()
|
||||
Brand.apply_font(count_label)
|
||||
metadata_row.add_child(count_label)
|
||||
|
||||
editor = TextEdit.new()
|
||||
editor.placeholder_text = "Write notes..."
|
||||
editor.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||
editor.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
editor.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
editor.custom_minimum_size = Vector2(0, 260)
|
||||
editor.add_theme_font_override("font", Brand.font)
|
||||
editor.add_theme_font_size_override("font_size", 17)
|
||||
editor.text_changed.connect(_on_text_changed)
|
||||
add_child(editor)
|
||||
|
||||
conflict_panel = PanelContainer.new()
|
||||
conflict_panel.visible = false
|
||||
add_child(conflict_panel)
|
||||
var conflict_content := VBoxContainer.new()
|
||||
conflict_content.add_theme_constant_override("separation", 8)
|
||||
conflict_panel.add_child(conflict_content)
|
||||
var conflict_message := Label.new()
|
||||
conflict_message.text = "This note changed on another device. Choose which version to keep."
|
||||
conflict_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Brand.apply_font(conflict_message)
|
||||
conflict_content.add_child(conflict_message)
|
||||
var conflict_actions := HBoxContainer.new()
|
||||
conflict_actions.add_theme_constant_override("separation", 8)
|
||||
conflict_content.add_child(conflict_actions)
|
||||
var keep_draft := Button.new()
|
||||
keep_draft.text = "Keep my draft"
|
||||
keep_draft.pressed.connect(notes_service.resolve_conflict_keep_draft)
|
||||
conflict_actions.add_child(keep_draft)
|
||||
var use_synced := Button.new()
|
||||
use_synced.text = "Use synced version"
|
||||
use_synced.pressed.connect(notes_service.resolve_conflict_use_synced)
|
||||
conflict_actions.add_child(use_synced)
|
||||
|
||||
notes_service.content_changed.connect(_on_service_content_changed)
|
||||
notes_service.state_changed.connect(_on_service_state_changed)
|
||||
theme_service.theme_changed.connect(_on_theme_changed)
|
||||
_refresh_from_service()
|
||||
_apply_theme()
|
||||
|
||||
|
||||
func activate() -> void:
|
||||
notes_service.ensure_loaded()
|
||||
_refresh_from_service()
|
||||
|
||||
|
||||
func focus_editor() -> void:
|
||||
editor.grab_focus()
|
||||
|
||||
|
||||
func _on_text_changed() -> void:
|
||||
if applying_content:
|
||||
return
|
||||
if editor.text.length() > notes_service.MAX_CONTENT_LENGTH:
|
||||
var caret_line := editor.get_caret_line()
|
||||
var caret_column := editor.get_caret_column()
|
||||
applying_content = true
|
||||
editor.text = editor.text.left(NotesService.MAX_CONTENT_LENGTH)
|
||||
editor.set_caret_line(mini(caret_line, editor.get_line_count() - 1))
|
||||
editor.set_caret_column(caret_column)
|
||||
applying_content = false
|
||||
notes_service.set_content(editor.text)
|
||||
_refresh_metadata()
|
||||
|
||||
|
||||
func _on_service_content_changed(value: String) -> void:
|
||||
if editor.text != value:
|
||||
applying_content = true
|
||||
editor.text = value
|
||||
applying_content = false
|
||||
_refresh_metadata()
|
||||
|
||||
|
||||
func _on_service_state_changed(_new_state: String) -> void:
|
||||
_refresh_metadata()
|
||||
|
||||
|
||||
func _refresh_from_service() -> void:
|
||||
_on_service_content_changed(notes_service.content)
|
||||
|
||||
|
||||
func _refresh_metadata() -> void:
|
||||
status_label.text = notes_service.status_text()
|
||||
count_label.text = "%s / %s" % [
|
||||
_format_count(editor.text.length()),
|
||||
_format_count(notes_service.MAX_CONTENT_LENGTH)
|
||||
]
|
||||
conflict_panel.visible = notes_service.conflict_active
|
||||
status_label.add_theme_color_override(
|
||||
"font_color",
|
||||
Brand.AMBER if notes_service.state in ["dirty", "offline", "conflict"] else Brand.muted_color(theme_service.is_dark_mode()))
|
||||
|
||||
|
||||
func _format_count(value: int) -> String:
|
||||
return "%d,%03d" % [int(floor(float(value) / 1000.0)), value % 1000] if value >= 1000 else str(value)
|
||||
|
||||
|
||||
func _on_theme_changed(_dark: bool) -> void:
|
||||
_apply_theme()
|
||||
|
||||
|
||||
func _apply_theme() -> void:
|
||||
var dark: bool = theme_service.is_dark_mode()
|
||||
editor.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
editor.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark))
|
||||
editor.add_theme_color_override("caret_color", Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE)
|
||||
editor.add_theme_color_override("selection_color", Color(Brand.ROYAL_BLUE, 0.32))
|
||||
editor.add_theme_stylebox_override("normal", Brand.field_box(dark))
|
||||
editor.add_theme_stylebox_override("focus", Brand.field_box(dark, true))
|
||||
conflict_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.AMBER))
|
||||
_refresh_metadata()
|
||||
@@ -0,0 +1 @@
|
||||
uid://du8xhjfdkdakr
|
||||
@@ -0,0 +1,34 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
const NotesEditor := preload("res://scripts/ui/notes_editor.gd")
|
||||
|
||||
var main
|
||||
var notes_editor: VBoxContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_theme_constant_override("separation", 16)
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "STUDY TOOLS"
|
||||
eyebrow.add_theme_font_override("font", Brand.font)
|
||||
eyebrow.add_theme_font_size_override("font_size", 13)
|
||||
eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE)
|
||||
add_child(eyebrow)
|
||||
var title := Label.new()
|
||||
title.text = "Notes"
|
||||
title.add_theme_font_override("font", Brand.font)
|
||||
title.add_theme_font_size_override("font_size", 32)
|
||||
add_child(title)
|
||||
notes_editor = NotesEditor.new()
|
||||
add_child(notes_editor)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
notes_editor.activate()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
get_node("/root/NotesService").flush()
|
||||
@@ -0,0 +1 @@
|
||||
uid://e7os1ufiuo37
|
||||
@@ -83,6 +83,7 @@ var labs_button: Button
|
||||
var clear_highlight_button: Button
|
||||
var flag_button: Button
|
||||
var report_button: Button
|
||||
var notes_button: Button
|
||||
var app_feedback_button: Button
|
||||
var theme_button: Button
|
||||
var question_toolbar: HFlowContainer
|
||||
@@ -273,6 +274,18 @@ func _ready() -> void:
|
||||
report_button.pressed.connect(_toggle_report_current_question)
|
||||
question_toolbar.add_child(report_button)
|
||||
|
||||
notes_button = Button.new()
|
||||
notes_button.tooltip_text = "Open notes"
|
||||
notes_button.icon = preload("res://assets/icons/lucide/notebook-pen.svg")
|
||||
notes_button.custom_minimum_size = Vector2(44, 44)
|
||||
notes_button.add_theme_constant_override("icon_max_width", 22)
|
||||
notes_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
notes_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
notes_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(notes_button, "open")
|
||||
notes_button.pressed.connect(func(): main.open_notes_dialog())
|
||||
question_toolbar.add_child(notes_button)
|
||||
|
||||
app_feedback_button = Button.new()
|
||||
app_feedback_button.text = "Feedback"
|
||||
app_feedback_button.tooltip_text = "Send general feedback or report a bug"
|
||||
@@ -827,7 +840,7 @@ 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, app_feedback_button]:
|
||||
for control in [text_smaller_button, text_larger_button, flag_button, notes_button, app_feedback_button]:
|
||||
if control == null:
|
||||
continue
|
||||
var active: bool = control == flag_button and flagged
|
||||
@@ -1864,6 +1877,8 @@ func _refresh_toolbar_state() -> void:
|
||||
if report_button != null:
|
||||
var question_id := str(BlockSessionService.get_current_question().get("id", ""))
|
||||
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"
|
||||
_apply_question_theme()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user