Move note saving to TS

This commit is contained in:
Abdo 2025-05-25 19:20:11 +03:00
parent 75bc67608a
commit 1809e793e6
4 changed files with 64 additions and 55 deletions

View file

@ -10,6 +10,7 @@ package anki.frontend;
import "anki/scheduler.proto"; import "anki/scheduler.proto";
import "anki/generic.proto"; import "anki/generic.proto";
import "anki/search.proto"; import "anki/search.proto";
import "anki/notes.proto";
service FrontendService { service FrontendService {
// Returns values from the reviewer // Returns values from the reviewer
@ -28,7 +29,9 @@ service FrontendService {
// Warns python that the deck option web view is ready to receive requests. // Warns python that the deck option web view is ready to receive requests.
rpc deckOptionsReady(generic.Empty) returns (generic.Empty); rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
// Editor
rpc editorReady(generic.Empty) returns (generic.Empty); rpc editorReady(generic.Empty) returns (generic.Empty);
rpc editorUpdateNote(notes.UpdateNotesRequest) returns (generic.Empty);
} }
service BackendFrontendService {} service BackendFrontendService {}

View file

@ -402,24 +402,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# focus lost or key/button pressed? # focus lost or key/button pressed?
if cmd.startswith("blur") or cmd.startswith("key"): 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) 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": if type == "blur":
self.currentField = None self.currentField = None
# run any filters # run any filters

View file

@ -608,13 +608,30 @@ def editor_ready() -> bytes:
def handle_on_main() -> None: def handle_on_main() -> None:
window = aqt.mw.app.activeWindow() 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 window.editor._set_ready() # type: ignore
aqt.mw.taskman.run_on_main(handle_on_main) aqt.mw.taskman.run_on_main(handle_on_main)
return b"" 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 = [ post_handler_list = [
congrats_info, congrats_info,
get_deck_configs_for_update, get_deck_configs_for_update,
@ -631,6 +648,7 @@ post_handler_list = [
deck_options_require_close, deck_options_require_close,
deck_options_ready, deck_options_ready,
editor_ready, editor_ready,
editor_update_note,
] ]
@ -647,6 +665,7 @@ exposed_backend_list = [
# NotesService # NotesService
"get_field_names", "get_field_names",
"get_note", "get_note",
"new_note",
# NotetypesService # NotetypesService
"get_notetype", "get_notetype",
"get_notetype_names", "get_notetype_names",

View file

@ -224,14 +224,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`); bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`);
} }
let noteId: bigint | null = null; let note: Note | null = null;
export function setNoteId(ntid: bigint | null): void { 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. // 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 // It should be refactored once we work on our own Undo stack
for (const pi of plainTextInputs) { for (const pi of plainTextInputs) {
pi.api.codeMirror.editor.then((editor) => editor.clearHistory()); pi.api.codeMirror.editor.then((editor) => editor.clearHistory());
} }
noteId = ntid;
} }
let notetypeMeta: NotetypeIdAndModTime; 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; let isImageOcclusion = false;
function setIsImageOcclusion(val: boolean) { function setIsImageOcclusion(val: boolean) {
isImageOcclusion = val; 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 { function transformContentBeforeSave(content: string): string {
return content.replace(/ data-editor-shrink="(true|false)"/g, ""); 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 { function updateField(index: number, content: string): void {
fieldSave.schedule( fieldSave.schedule(() => {
() => bridgeCommand(`key:${index}`);
bridgeCommand( note!.fields[index] = transformContentBeforeSave(content);
`key:${index}:${getNoteId()}:${transformContentBeforeSave( updateCurrentNote();
content, }, 600);
)}`,
),
600,
);
} }
function saveFieldNow(): void { function saveFieldNow(): void {
@ -422,6 +424,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
getNote, getNote,
getNotetype, getNotetype,
encodeIriPaths, encodeIriPaths,
newNote,
editorUpdateNote,
} from "@generated/backend"; } from "@generated/backend";
import { wrapInternal } from "@tslib/wrap"; 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 StickyBadge from "./StickyBadge.svelte";
import ButtonGroupItem from "$lib/components/ButtonGroupItem.svelte"; import ButtonGroupItem from "$lib/components/ButtonGroupItem.svelte";
import PreviewButton from "./PreviewButton.svelte"; import PreviewButton from "./PreviewButton.svelte";
import type { Note } from "@generated/anki/notes_pb";
$: isIOImageLoaded = false; $: isIOImageLoaded = false;
$: ioImageLoadedStore.set(isIOImageLoaded); $: 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({ const notetype = await getNotetype({
ntid: notetypeId, ntid: notetypeId,
}); });
let fieldValues: string[] = notetype.fields.map(() => "");
const fieldNames = ( const fieldNames = (
await getFieldNames({ await getFieldNames({
ntid: notetype.id, 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) => const clozeFields = fieldNames.map((name, index) =>
clozeFieldOrds.includes(index), clozeFieldOrds.includes(index),
); );
let tags: string[] = []; if (mode === "add") {
if (nid) { setNote(await newNote({ ntid: notetype.id }));
const note = await getNote({ } else {
nid, setNote(
}); await getNote({
fieldValues = ( nid,
await Promise.all( }),
note.fields.map((field) => encodeIriPaths({ val: field })), );
)
).map((field) => field.val);
tags = note.tags;
} }
const fieldValues = (
await Promise.all(
note!.fields.map((field) => encodeIriPaths({ val: field })),
)
).map((field) => field.val);
const tags = note!.tags;
saveSession(); saveSession();
setFields(fieldNames, fieldValues); setFields(fieldNames, fieldValues);
setIsImageOcclusion( setIsImageOcclusion(
@ -610,7 +618,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
]), ]),
); );
focusField(focusTo); focusField(focusTo);
setNoteId(nid);
// TODO: lastTextColor/lastHighlightColor profile config // TODO: lastTextColor/lastHighlightColor profile config
// setColorButtons(["#0000ff", "#0000ff"]); // setColorButtons(["#0000ff", "#0000ff"]);
setTags(tags); setTags(tags);
@ -666,8 +673,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setClozeHint, setClozeHint,
saveNow, saveNow,
focusIfField, focusIfField,
getNoteId,
setNoteId,
setNotetypeMeta, setNotetypeMeta,
wrap, wrap,
setMathjaxEnabled, setMathjaxEnabled,
@ -786,11 +791,9 @@ components and functionality for general note editing.
on:focusout={() => { on:focusout={() => {
$focusedField = null; $focusedField = null;
setAddonButtonsDisabled(true); setAddonButtonsDisabled(true);
bridgeCommand( bridgeCommand(`blur:${index}`);
`blur:${index}:${getNoteId()}:${transformContentBeforeSave( note!.fields[index] = transformContentBeforeSave(get(content));
get(content), updateCurrentNote();
)}`,
);
}} }}
on:mouseenter={() => { on:mouseenter={() => {
$hoveredField = fields[index]; $hoveredField = fields[index];