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);
// 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);

View file

@ -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

View file

@ -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<String>,
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<String>, type_name: &str) {
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 } },
outputType: { fromBinary(arr: Uint8Array): T },
options: PostProtoOptions = {},
hasOpChanges = false,
): Promise<T> {
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<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, {
method: "POST",
headers: {
"Content-Type": "application/binary",
},
headers,
body,
});
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);
}
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,

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 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);
}
}