From f10d48db47e2a271a5eda36e13723e0e2817dfeb Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 13 Oct 2025 13:09:49 +0300 Subject: [PATCH] Handle undo for all RPCs that return OpChanges --- proto/anki/frontend.proto | 3 - qt/aqt/mediasrv.py | 94 ++++++++--------------------- rslib/proto/typescript.rs | 21 ++++++- ts/lib/generated/post.ts | 13 ++-- ts/routes/editor/NoteEditor.svelte | 12 ++-- ts/routes/editor/StickyBadge.svelte | 4 +- 6 files changed, 62 insertions(+), 85 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index df92d368b..b57709836 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -32,9 +32,6 @@ service FrontendService { rpc deckOptionsReady(generic.Empty) returns (generic.Empty); // Editor - rpc UpdateEditorNote(notes.UpdateNotesRequest) returns (generic.Empty); - rpc UpdateEditorNotetype(notetypes.Notetype) returns (generic.Empty); - rpc AddEditorNote(notes.AddNoteRequest) returns (notes.AddNoteResponse); rpc ConvertPastedImage(ConvertPastedImageRequest) returns (ConvertPastedImageResponse); rpc OpenFilePicker(openFilePickerRequest) returns (generic.String); diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index eb857b25c..47470c0f3 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -562,36 +562,6 @@ def import_done() -> bytes: return b"" -def import_request(endpoint: str) -> bytes: - output = raw_backend_request(endpoint)() - response = OpChangesOnly() - response.ParseFromString(output) - - def handle_on_main() -> None: - window = aqt.mw.app.activeModalWidget() - on_op_finished(aqt.mw, response, window) - - aqt.mw.taskman.run_on_main(handle_on_main) - - return output - - -def import_csv() -> bytes: - return import_request("import_csv") - - -def import_anki_package() -> bytes: - return import_request("import_anki_package") - - -def import_json_file() -> bytes: - return import_request("import_json_file") - - -def import_json_string() -> bytes: - return import_request("import_json_string") - - def search_in_browser() -> bytes: node = SearchNode() node.ParseFromString(request.data) @@ -638,36 +608,6 @@ def deck_options_ready() -> bytes: return b"" -def editor_op_changes_request(endpoint: str) -> bytes: - output = raw_backend_request(endpoint)() - response = OpChanges() - response.ParseFromString(output) - - def handle_on_main() -> None: - from aqt.editor import NewEditor - - handler = aqt.mw.app.activeWindow() - if handler and isinstance(getattr(handler, "editor", None), NewEditor): - handler = handler.editor # type: ignore - on_op_finished(aqt.mw, response, handler) - - aqt.mw.taskman.run_on_main(handle_on_main) - - return output - - -def update_editor_note() -> bytes: - return editor_op_changes_request("update_notes") - - -def update_editor_notetype() -> bytes: - return editor_op_changes_request("update_notetype") - - -def add_editor_note() -> bytes: - return editor_op_changes_request("add_note") - - def get_setting_json(getter: Callable[[str], Any]) -> bytes: req = generic_pb2.String() req.ParseFromString(request.data) @@ -953,16 +893,9 @@ post_handler_list = [ set_scheduling_states, change_notetype, import_done, - import_csv, - import_anki_package, - import_json_file, - import_json_string, search_in_browser, deck_options_require_close, deck_options_ready, - update_editor_note, - update_editor_notetype, - add_editor_note, get_profile_config_json, set_profile_config_json, get_meta_json, @@ -995,6 +928,10 @@ exposed_backend_list = [ # ImportExportService "get_csv_metadata", "get_import_anki_package_presets", + "import_csv", + "import_anki_package", + "import_json_file", + "import_json_string", # NotesService "get_field_names", "get_note", @@ -1002,6 +939,9 @@ exposed_backend_list = [ "note_fields_check", "defaults_for_adding", "default_deck_for_notetype", + "add_note", + "update_notes", + "update_notetype", # NotetypesService "get_notetype", "get_notetype_names", @@ -1056,7 +996,25 @@ def raw_backend_request(endpoint: str) -> Callable[[], bytes]: assert hasattr(RustBackend, f"{endpoint}_raw") - return lambda: getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data) + def wrapped() -> bytes: + output = getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data) + if "Has-Op-Changes" in request.headers: + response = OpChangesOnly() + response.ParseFromString(output) + + def handle_on_main() -> None: + from aqt.editor import NewEditor + + handler = aqt.mw.app.activeModalWidget() + if handler and isinstance(getattr(handler, "editor", None), NewEditor): + handler = handler.editor # type: ignore + on_op_finished(aqt.mw, response, handler) + + aqt.mw.taskman.run_on_main(handle_on_main) + + return output + + return wrapped # all methods in here require a collection diff --git a/rslib/proto/typescript.rs b/rslib/proto/typescript.rs index 4e941a0ca..61ba27bb8 100644 --- a/rslib/proto/typescript.rs +++ b/rslib/proto/typescript.rs @@ -12,6 +12,7 @@ use anki_proto_gen::Method; use anyhow::Result; use inflections::Inflect; use itertools::Itertools; +use prost_reflect::MessageDescriptor; pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> { let root = Path::new("../../out/ts/lib/generated"); @@ -73,6 +74,7 @@ fn write_ts_method( input_type, output_type, comments, + has_op_changes, }: &MethodDetails, out: &mut String, ) { @@ -80,7 +82,7 @@ fn write_ts_method( writeln!( out, r#"{comments}export async function {method_name}(input: PlainMessage<{input_type}>, options?: PostProtoOptions): Promise<{output_type}> {{ - return await postProto("{method_name}", new {input_type}(input), {output_type}, options); + return await postProto("{method_name}", new {input_type}(input), {output_type}, options, {has_op_changes}); }}"# ).unwrap() } @@ -97,6 +99,7 @@ struct MethodDetails { input_type: String, output_type: String, comments: Option, + has_op_changes: bool, } impl MethodDetails { @@ -105,15 +108,31 @@ impl MethodDetails { let input_type = full_name_to_imported_reference(method.proto.input().full_name()); let output_type = full_name_to_imported_reference(method.proto.output().full_name()); let comments = method.comments.clone(); + let has_op_changes = has_op_changes(&method.proto.output()); Self { method_name: name, input_type, output_type, comments, + has_op_changes, } } } +fn has_op_changes(message: &MessageDescriptor) -> bool { + if message.full_name() == "anki.collection.OpChanges" { + true + } else if let Some(field) = message.get_field(1) { + if let Some(field_message) = field.kind().as_message() { + has_op_changes(field_message) + } else { + false + } + } else { + false + } +} + fn record_referenced_type(referenced_packages: &mut HashSet, type_name: &str) { referenced_packages.insert(type_name.split('.').next().unwrap().to_string()); } diff --git a/ts/lib/generated/post.ts b/ts/lib/generated/post.ts index 90e372520..1c9f23f3a 100644 --- a/ts/lib/generated/post.ts +++ b/ts/lib/generated/post.ts @@ -11,11 +11,12 @@ export async function postProto( input: { toBinary(): Uint8Array; getType(): { typeName: string } }, outputType: { fromBinary(arr: Uint8Array): T }, options: PostProtoOptions = {}, + hasOpChanges = false, ): Promise { try { const inputBytes = input.toBinary(); const path = `/_anki/${method}`; - const outputBytes = await postProtoInner(path, inputBytes); + const outputBytes = await postProtoInner(path, inputBytes, hasOpChanges); return outputType.fromBinary(outputBytes); } catch (err) { const { alertOnError = true } = options; @@ -26,12 +27,14 @@ export async function postProto( } } -async function postProtoInner(url: string, body: Uint8Array): Promise { +async function postProtoInner(url: string, body: Uint8Array, hasOpChanges: boolean): Promise { + const headers = { "Content-Type": "application/binary" }; + if (hasOpChanges) { + headers["Has-Op-Changes"] = "1"; + } const result = await fetch(url, { method: "POST", - headers: { - "Content-Type": "application/binary", - }, + headers, body, }); if (!result.ok) { diff --git a/ts/routes/editor/NoteEditor.svelte b/ts/routes/editor/NoteEditor.svelte index 8438ad1ce..d7e9021ce 100644 --- a/ts/routes/editor/NoteEditor.svelte +++ b/ts/routes/editor/NoteEditor.svelte @@ -247,7 +247,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } values.push(field.config!.sticky); } - await updateEditorNotetype(notetype); + await updateNotetype(notetype); setSticky(values); } } @@ -405,7 +405,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html async function updateCurrentNote() { if (mode !== "add") { try { - await updateEditorNote( + await updateNotes( { notes: [note!], skipUndoEntry: false, @@ -586,7 +586,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html if (!(await noteCanBeAdded())) { return; } - const response = await addEditorNote({ + const response = await addNote({ note: note!, deckId, }); @@ -723,12 +723,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html getNotetype, encodeIriPaths, newNote, - updateEditorNote, + updateNotes, decodeIriPaths, noteFieldsCheck, - addEditorNote, + addNote, addMediaFromPath, - updateEditorNotetype, + updateNotetype, closeAddCards as closeAddCardsBackend, closeEditCurrent as closeEditCurrentBackend, htmlToTextLine, diff --git a/ts/routes/editor/StickyBadge.svelte b/ts/routes/editor/StickyBadge.svelte index 2eb4fb84a..308bd136c 100644 --- a/ts/routes/editor/StickyBadge.svelte +++ b/ts/routes/editor/StickyBadge.svelte @@ -15,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { context as editorFieldContext } from "./EditorField.svelte"; import type { Note } from "@generated/anki/notes_pb"; - import { getNotetype, updateEditorNotetype } from "@generated/backend"; + import { getNotetype, updateNotetype } from "@generated/backend"; import { bridgeCommand } from "@tslib/bridgecommand"; const animated = !document.body.classList.contains("reduce-motion"); @@ -39,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html active = !active; const notetype = await getNotetype({ ntid: note.notetypeId }); notetype.fields[index].config!.sticky = active; - await updateEditorNotetype(notetype); + await updateNotetype(notetype); } }