diff --git a/qt/aqt/data/web/js/BUILD.bazel b/qt/aqt/data/web/js/BUILD.bazel index b138bd31b..20db65237 100644 --- a/qt/aqt/data/web/js/BUILD.bazel +++ b/qt/aqt/data/web/js/BUILD.bazel @@ -37,12 +37,21 @@ copy_files_into_group( package = "//ts/editor", ) +copy_files_into_group( + name = "reviewer_extras", + srcs = [ + "reviewer_extras.js", + ], + package = "//ts/reviewer", +) + filegroup( name = "js", srcs = [ "aqt_es5", "editor", "mathjax.js", + "reviewer_extras", "//qt/aqt/data/web/js/vendor", ], visibility = ["//qt:__subpackages__"], diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index ab43c6f98..a5302df77 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -22,6 +22,7 @@ import aqt from anki import hooks from anki.collection import GraphPreferences, OpChanges from anki.decks import UpdateDeckConfigs +from anki.scheduler.v3 import NextStates from anki.utils import devMode, from_json_bytes from aqt.deckoptions import DeckOptionsDialog from aqt.operations.deck import update_deck_configs @@ -307,12 +308,29 @@ def update_deck_configs_request() -> bytes: return b"" +def next_card_states() -> bytes: + if states := aqt.mw.reviewer.get_next_states(): + return states.SerializeToString() + else: + return b"" + + +def set_next_card_states() -> bytes: + key = request.headers.get("key", "") + input = NextStates() + input.ParseFromString(request.data) + aqt.mw.reviewer.set_next_states(key, input) + return b"" + + post_handlers = { "graphData": graph_data, "graphPreferences": graph_preferences, "setGraphPreferences": set_graph_preferences, "deckConfigsForUpdate": deck_configs_for_update, "updateDeckConfigs": update_deck_configs_request, + "nextCardStates": next_card_states, + "setNextCardStates": set_next_card_states, # pylint: disable=unnecessary-lambda "i18nResources": i18n_resources, "congratsInfo": congrats_info, diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 8fc339172..7a948b333 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -6,6 +6,7 @@ from __future__ import annotations import difflib import html import json +import random import re import unicodedata as ucd from dataclasses import dataclass @@ -124,6 +125,7 @@ class Reviewer: self.state: Optional[str] = None self._refresh_needed: Optional[RefreshNeeded] = None self._v3: Optional[V3CardInfo] = None + self._state_mutation_key = str(random.randint(0, 2 ** 64 - 1)) self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -131,6 +133,7 @@ class Reviewer: self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore self.web.set_bridge_command(self._linkHandler, self) self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self)) + self._state_mutation_js = self.mw.col.get_config("cardStateCustomizer") self._reps: int = None self._refresh_needed = RefreshNeeded.QUEUES self.refresh_if_needed() @@ -231,6 +234,25 @@ class Reviewer: self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) self.card.startTimer() + def get_next_states(self) -> Optional[NextStates]: + if v3 := self._v3: + return v3.next_states + else: + return None + + def set_next_states(self, key: str, states: NextStates) -> None: + if key != self._state_mutation_key: + return + + if v3 := self._v3: + v3.next_states = states + + def _run_state_mutation_hook(self) -> None: + if self._v3 and (js := self._state_mutation_js): + self.web.eval( + f"anki.mutateNextCardStates('{self._state_mutation_key}', (states) => {{ {js} }})" + ) + # Audio ########################################################################## @@ -268,6 +290,8 @@ class Reviewer: "js/mathjax.js", "js/vendor/mathjax/tex-chtml.js", "js/reviewer.js", + "js/vendor/protobuf.min.js", + "js/reviewer_extras.js", ], context=self, ) @@ -308,6 +332,7 @@ class Reviewer: # render & update bottom q = self._mungeQA(q) q = gui_hooks.card_will_show(q, c, "reviewQuestion") + self._run_state_mutation_hook() bodyclass = theme_manager.body_classes_for_card_ord(c.ord) diff --git a/ts/lib/postrequest.ts b/ts/lib/postrequest.ts index d36b3cdeb..64abd1d49 100644 --- a/ts/lib/postrequest.ts +++ b/ts/lib/postrequest.ts @@ -3,9 +3,9 @@ export async function postRequest( path: string, - body: string | Uint8Array + body: string | Uint8Array, + headers: Record = {} ): Promise { - const headers = {}; if (body instanceof Uint8Array) { headers["Content-type"] = "application/octet-stream"; } diff --git a/ts/reviewer/BUILD.bazel b/ts/reviewer/BUILD.bazel new file mode 100644 index 000000000..4b2217a48 --- /dev/null +++ b/ts/reviewer/BUILD.bazel @@ -0,0 +1,54 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") +load("//ts/svelte:svelte.bzl", "svelte", "svelte_check") +load("//ts:esbuild.bzl", "esbuild") +load("//ts:compile_sass.bzl", "compile_sass") + +ts_library( + name = "lib", + srcs = glob(["*.ts"]), + deps = [ + "//ts/lib", + "//ts/lib:backend_proto", + ], +) + +esbuild( + name = "reviewer_extras", + srcs = [ + "//ts:protobuf-shim.js", + ], + args = [ + "--inject:$(location //ts:protobuf-shim.js)", + "--resolve-extensions=.mjs,.js", + "--log-level=warning", + ], + entry_point = "index.ts", + external = [ + "protobufjs/light", + ], + visibility = ["//visibility:public"], + deps = [ + ":lib", + "//ts/lib", + "//ts/lib:backend_proto", + ], +) + +# Tests +################ + +prettier_test( + name = "format_check", + srcs = glob([ + "*.ts", + ]), +) + +eslint_test( + name = "eslint", + srcs = glob([ + "*.ts", + ]), +) diff --git a/ts/reviewer/answering.ts b/ts/reviewer/answering.ts new file mode 100644 index 000000000..6f7960ff1 --- /dev/null +++ b/ts/reviewer/answering.ts @@ -0,0 +1,28 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import * as pb from "lib/backend_proto"; +import { postRequest } from "lib/postrequest"; + +async function getNextStates(): Promise { + return pb.BackendProto.NextCardStates.decode( + await postRequest("/_anki/nextCardStates", "") + ); +} + +async function setNextStates( + key: string, + states: pb.BackendProto.NextCardStates +): Promise { + const data: Uint8Array = pb.BackendProto.NextCardStates.encode(states).finish(); + await postRequest("/_anki/setNextCardStates", data, { key }); +} + +export async function mutateNextCardStates( + key: string, + mutator: (states: pb.BackendProto.NextCardStates) => void +): Promise { + const states = await getNextStates(); + mutator(states); + await setNextStates(key, states); +} diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts new file mode 100644 index 000000000..ebe6a96fb --- /dev/null +++ b/ts/reviewer/index.ts @@ -0,0 +1,9 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// This is a temporary extra file we load separately from reviewer.ts. Once +// reviewer.ts has been migrated into ts/, the code here can be merged into +// it. + +import { mutateNextCardStates } from "./answering"; +globalThis.anki = { ...globalThis.anki, mutateNextCardStates };