Handle undo for all RPCs that return OpChanges

This commit is contained in:
Abdo 2025-10-13 13:09:49 +03:00
parent 6165567238
commit f10d48db47
6 changed files with 62 additions and 85 deletions

View file

@ -32,9 +32,6 @@ service FrontendService {
rpc deckOptionsReady(generic.Empty) returns (generic.Empty); rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
// Editor // 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) rpc ConvertPastedImage(ConvertPastedImageRequest)
returns (ConvertPastedImageResponse); returns (ConvertPastedImageResponse);
rpc OpenFilePicker(openFilePickerRequest) returns (generic.String); rpc OpenFilePicker(openFilePickerRequest) returns (generic.String);

View file

@ -562,36 +562,6 @@ def import_done() -> bytes:
return b"" 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: def search_in_browser() -> bytes:
node = SearchNode() node = SearchNode()
node.ParseFromString(request.data) node.ParseFromString(request.data)
@ -638,36 +608,6 @@ def deck_options_ready() -> bytes:
return b"" 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: def get_setting_json(getter: Callable[[str], Any]) -> bytes:
req = generic_pb2.String() req = generic_pb2.String()
req.ParseFromString(request.data) req.ParseFromString(request.data)
@ -953,16 +893,9 @@ post_handler_list = [
set_scheduling_states, set_scheduling_states,
change_notetype, change_notetype,
import_done, import_done,
import_csv,
import_anki_package,
import_json_file,
import_json_string,
search_in_browser, search_in_browser,
deck_options_require_close, deck_options_require_close,
deck_options_ready, deck_options_ready,
update_editor_note,
update_editor_notetype,
add_editor_note,
get_profile_config_json, get_profile_config_json,
set_profile_config_json, set_profile_config_json,
get_meta_json, get_meta_json,
@ -995,6 +928,10 @@ exposed_backend_list = [
# ImportExportService # ImportExportService
"get_csv_metadata", "get_csv_metadata",
"get_import_anki_package_presets", "get_import_anki_package_presets",
"import_csv",
"import_anki_package",
"import_json_file",
"import_json_string",
# NotesService # NotesService
"get_field_names", "get_field_names",
"get_note", "get_note",
@ -1002,6 +939,9 @@ exposed_backend_list = [
"note_fields_check", "note_fields_check",
"defaults_for_adding", "defaults_for_adding",
"default_deck_for_notetype", "default_deck_for_notetype",
"add_note",
"update_notes",
"update_notetype",
# NotetypesService # NotetypesService
"get_notetype", "get_notetype",
"get_notetype_names", "get_notetype_names",
@ -1056,7 +996,25 @@ def raw_backend_request(endpoint: str) -> Callable[[], bytes]:
assert hasattr(RustBackend, f"{endpoint}_raw") 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 # all methods in here require a collection

View file

@ -12,6 +12,7 @@ use anki_proto_gen::Method;
use anyhow::Result; use anyhow::Result;
use inflections::Inflect; use inflections::Inflect;
use itertools::Itertools; use itertools::Itertools;
use prost_reflect::MessageDescriptor;
pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> { pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> {
let root = Path::new("../../out/ts/lib/generated"); let root = Path::new("../../out/ts/lib/generated");
@ -73,6 +74,7 @@ fn write_ts_method(
input_type, input_type,
output_type, output_type,
comments, comments,
has_op_changes,
}: &MethodDetails, }: &MethodDetails,
out: &mut String, out: &mut String,
) { ) {
@ -80,7 +82,7 @@ fn write_ts_method(
writeln!( writeln!(
out, out,
r#"{comments}export async function {method_name}(input: PlainMessage<{input_type}>, options?: PostProtoOptions): Promise<{output_type}> {{ 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() ).unwrap()
} }
@ -97,6 +99,7 @@ struct MethodDetails {
input_type: String, input_type: String,
output_type: String, output_type: String,
comments: Option<String>, comments: Option<String>,
has_op_changes: bool,
} }
impl MethodDetails { impl MethodDetails {
@ -105,15 +108,31 @@ impl MethodDetails {
let input_type = full_name_to_imported_reference(method.proto.input().full_name()); 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 output_type = full_name_to_imported_reference(method.proto.output().full_name());
let comments = method.comments.clone(); let comments = method.comments.clone();
let has_op_changes = has_op_changes(&method.proto.output());
Self { Self {
method_name: name, method_name: name,
input_type, input_type,
output_type, output_type,
comments, 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<String>, type_name: &str) { fn record_referenced_type(referenced_packages: &mut HashSet<String>, type_name: &str) {
referenced_packages.insert(type_name.split('.').next().unwrap().to_string()); referenced_packages.insert(type_name.split('.').next().unwrap().to_string());
} }

View file

@ -11,11 +11,12 @@ export async function postProto<T>(
input: { toBinary(): Uint8Array; getType(): { typeName: string } }, input: { toBinary(): Uint8Array; getType(): { typeName: string } },
outputType: { fromBinary(arr: Uint8Array): T }, outputType: { fromBinary(arr: Uint8Array): T },
options: PostProtoOptions = {}, options: PostProtoOptions = {},
hasOpChanges = false,
): Promise<T> { ): Promise<T> {
try { try {
const inputBytes = input.toBinary(); const inputBytes = input.toBinary();
const path = `/_anki/${method}`; const path = `/_anki/${method}`;
const outputBytes = await postProtoInner(path, inputBytes); const outputBytes = await postProtoInner(path, inputBytes, hasOpChanges);
return outputType.fromBinary(outputBytes); return outputType.fromBinary(outputBytes);
} catch (err) { } catch (err) {
const { alertOnError = true } = options; const { alertOnError = true } = options;
@ -26,12 +27,14 @@ export async function postProto<T>(
} }
} }
async function postProtoInner(url: string, body: Uint8Array): Promise<Uint8Array> { async function postProtoInner(url: string, body: Uint8Array, hasOpChanges: boolean): Promise<Uint8Array> {
const headers = { "Content-Type": "application/binary" };
if (hasOpChanges) {
headers["Has-Op-Changes"] = "1";
}
const result = await fetch(url, { const result = await fetch(url, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/binary",
},
body, body,
}); });
if (!result.ok) { if (!result.ok) {

View file

@ -247,7 +247,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
values.push(field.config!.sticky); values.push(field.config!.sticky);
} }
await updateEditorNotetype(notetype); await updateNotetype(notetype);
setSticky(values); setSticky(values);
} }
} }
@ -405,7 +405,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function updateCurrentNote() { async function updateCurrentNote() {
if (mode !== "add") { if (mode !== "add") {
try { try {
await updateEditorNote( await updateNotes(
{ {
notes: [note!], notes: [note!],
skipUndoEntry: false, skipUndoEntry: false,
@ -586,7 +586,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (!(await noteCanBeAdded())) { if (!(await noteCanBeAdded())) {
return; return;
} }
const response = await addEditorNote({ const response = await addNote({
note: note!, note: note!,
deckId, deckId,
}); });
@ -723,12 +723,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
getNotetype, getNotetype,
encodeIriPaths, encodeIriPaths,
newNote, newNote,
updateEditorNote, updateNotes,
decodeIriPaths, decodeIriPaths,
noteFieldsCheck, noteFieldsCheck,
addEditorNote, addNote,
addMediaFromPath, addMediaFromPath,
updateEditorNotetype, updateNotetype,
closeAddCards as closeAddCardsBackend, closeAddCards as closeAddCardsBackend,
closeEditCurrent as closeEditCurrentBackend, closeEditCurrent as closeEditCurrentBackend,
htmlToTextLine, htmlToTextLine,

View file

@ -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 { context as editorFieldContext } from "./EditorField.svelte";
import type { Note } from "@generated/anki/notes_pb"; 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"; import { bridgeCommand } from "@tslib/bridgecommand";
const animated = !document.body.classList.contains("reduce-motion"); 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; active = !active;
const notetype = await getNotetype({ ntid: note.notetypeId }); const notetype = await getNotetype({ ntid: note.notetypeId });
notetype.fields[index].config!.sticky = active; notetype.fields[index].config!.sticky = active;
await updateEditorNotetype(notetype); await updateNotetype(notetype);
} }
} }