From 1809e793e6c310f9491d7c9006314acef3c1da54 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sun, 25 May 2025 19:20:11 +0300 Subject: [PATCH] Move note saving to TS --- proto/anki/frontend.proto | 3 ++ qt/aqt/editor.py | 18 +------ qt/aqt/mediasrv.py | 21 +++++++- ts/routes/editor/NoteEditor.svelte | 77 ++++++++++++++++-------------- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 9bf50d8e2..57e6b9fc4 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -10,6 +10,7 @@ package anki.frontend; import "anki/scheduler.proto"; import "anki/generic.proto"; import "anki/search.proto"; +import "anki/notes.proto"; service FrontendService { // Returns values from the reviewer @@ -28,7 +29,9 @@ service FrontendService { // Warns python that the deck option web view is ready to receive requests. rpc deckOptionsReady(generic.Empty) returns (generic.Empty); + // Editor rpc editorReady(generic.Empty) returns (generic.Empty); + rpc editorUpdateNote(notes.UpdateNotesRequest) returns (generic.Empty); } service BackendFrontendService {} diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 32430a010..1f7f2b5e9 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -402,24 +402,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too # focus lost or key/button pressed? if cmd.startswith("blur") or cmd.startswith("key"): - (type, ord_str, nid_str, txt) = cmd.split(":", 3) + (type, ord_str) = cmd.split(":", 1) ord = int(ord_str) - try: - nid = int(nid_str) - except ValueError: - nid = 0 - if nid != self.note.id: - print("ignored late blur") - return - - try: - self.note.fields[ord] = self.mungeHTML(txt) - except IndexError: - print("ignored late blur after notetype change") - return - - if not self.addMode: - self._save_current_note() if type == "blur": self.currentField = None # run any filters diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index ecff5feaf..d0bb3b75b 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -608,13 +608,30 @@ def editor_ready() -> bytes: def handle_on_main() -> None: window = aqt.mw.app.activeWindow() - if isinstance(getattr(window, "editor"), Editor): + if window and isinstance(getattr(window, "editor"), Editor): window.editor._set_ready() # type: ignore aqt.mw.taskman.run_on_main(handle_on_main) return b"" +def editor_update_note() -> bytes: + from aqt.editor import Editor + + output = raw_backend_request("update_notes")() + response = OpChanges() + response.ParseFromString(output) + + def handle_on_main() -> None: + window = aqt.mw.app.activeWindow() + if window and isinstance(getattr(window, "editor"), Editor): + on_op_finished(aqt.mw, response, window.editor) # type: ignore + + aqt.mw.taskman.run_on_main(handle_on_main) + + return output + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -631,6 +648,7 @@ post_handler_list = [ deck_options_require_close, deck_options_ready, editor_ready, + editor_update_note, ] @@ -647,6 +665,7 @@ exposed_backend_list = [ # NotesService "get_field_names", "get_note", + "new_note", # NotetypesService "get_notetype", "get_notetype_names", diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index e5b260d5e..4000a0d93 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -224,14 +224,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`); } - let noteId: bigint | null = null; - export function setNoteId(ntid: bigint | null): void { + let note: Note | null = null; + export function setNote(n: Note): void { + note = n; // TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput. // It should be refactored once we work on our own Undo stack for (const pi of plainTextInputs) { pi.api.codeMirror.editor.then((editor) => editor.clearHistory()); } - noteId = ntid; } let notetypeMeta: NotetypeIdAndModTime; @@ -251,10 +251,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } - function getNoteId(): string | null { - return noteId?.toString() ?? null; - } - let isImageOcclusion = false; function setIsImageOcclusion(val: boolean) { isImageOcclusion = val; @@ -294,18 +290,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html function transformContentBeforeSave(content: string): string { return content.replace(/ data-editor-shrink="(true|false)"/g, ""); + // TODO: mungeHTML() + } + + async function updateCurrentNote() { + if (mode !== "add") { + await editorUpdateNote({ + notes: [note!], + skipUndoEntry: false, + }); + } } function updateField(index: number, content: string): void { - fieldSave.schedule( - () => - bridgeCommand( - `key:${index}:${getNoteId()}:${transformContentBeforeSave( - content, - )}`, - ), - 600, - ); + fieldSave.schedule(() => { + bridgeCommand(`key:${index}`); + note!.fields[index] = transformContentBeforeSave(content); + updateCurrentNote(); + }, 600); } function saveFieldNow(): void { @@ -422,6 +424,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html getNote, getNotetype, encodeIriPaths, + newNote, + editorUpdateNote, } from "@generated/backend"; import { wrapInternal } from "@tslib/wrap"; @@ -442,6 +446,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import StickyBadge from "./StickyBadge.svelte"; import ButtonGroupItem from "$lib/components/ButtonGroupItem.svelte"; import PreviewButton from "./PreviewButton.svelte"; + import type { Note } from "@generated/anki/notes_pb"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); @@ -562,11 +567,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); } - async function loadNote(nid: bigint | null, notetypeId: bigint, focusTo: number) { + async function loadNote(nid: bigint, notetypeId: bigint, focusTo: number) { const notetype = await getNotetype({ ntid: notetypeId, }); - let fieldValues: string[] = notetype.fields.map(() => ""); const fieldNames = ( await getFieldNames({ ntid: notetype.id, @@ -576,18 +580,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const clozeFields = fieldNames.map((name, index) => clozeFieldOrds.includes(index), ); - let tags: string[] = []; - if (nid) { - const note = await getNote({ - nid, - }); - fieldValues = ( - await Promise.all( - note.fields.map((field) => encodeIriPaths({ val: field })), - ) - ).map((field) => field.val); - tags = note.tags; + if (mode === "add") { + setNote(await newNote({ ntid: notetype.id })); + } else { + setNote( + await getNote({ + nid, + }), + ); } + const fieldValues = ( + await Promise.all( + note!.fields.map((field) => encodeIriPaths({ val: field })), + ) + ).map((field) => field.val); + const tags = note!.tags; + saveSession(); setFields(fieldNames, fieldValues); setIsImageOcclusion( @@ -610,7 +618,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ]), ); focusField(focusTo); - setNoteId(nid); // TODO: lastTextColor/lastHighlightColor profile config // setColorButtons(["#0000ff", "#0000ff"]); setTags(tags); @@ -666,8 +673,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html setClozeHint, saveNow, focusIfField, - getNoteId, - setNoteId, setNotetypeMeta, wrap, setMathjaxEnabled, @@ -786,11 +791,9 @@ components and functionality for general note editing. on:focusout={() => { $focusedField = null; setAddonButtonsDisabled(true); - bridgeCommand( - `blur:${index}:${getNoteId()}:${transformContentBeforeSave( - get(content), - )}`, - ); + bridgeCommand(`blur:${index}`); + note!.fields[index] = transformContentBeforeSave(get(content)); + updateCurrentNote(); }} on:mouseenter={() => { $hoveredField = fields[index];