mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add _raw methods for all methods in the backend (#1594)
* Add _bytes methods for all methods in the backend Expose get_note in qt/aqt/mediasrv.py * Satisfy formatter * Rename _bytes function to _raw and have them bytes as input * Fix backend generation * Use lib/proto/deckOptions in deck-options * Add exposed_backend to qt/aqt/mediasrv.py * Move some more backend methods to exposed_backend_list * Use protobufjs for congrats and i18n * Use protobufjs for completeTag * Use protobufjs services in change-notetype * Reorder post handlers in alphabetical manner * Satisfy tests * Remove unused collection methods * Rename access_backend to raw_backend_request * Use _vendor.stringcase instead of creating a new function * Remove SKIP_UNROLL_OUTPUT * Directly call _run_command in non _raw methods * Remove TranslateString, ChangeNotetype and CompleteTag from SKIP_UNROLL_INPUT * Remove UpdateDeckConfigs from SKIP_UNROLL_INPUT * Remove ChangeNotetype from SKIP_UNROLL_INPUT * Remove SKIP_UNROLL_INPUT * Fix typing issue with translate_string - Adds typing support for Protobuf maps in genbackend.py * Do not emit convenience method for protobuf TranslateString
This commit is contained in:
parent
578ef6b2bc
commit
a8d4774cdb
22 changed files with 256 additions and 344 deletions
|
@ -106,12 +106,22 @@ class RustBackend(RustBackendGenerated):
|
||||||
def translate(
|
def translate(
|
||||||
self, module_index: int, message_index: int, **kwargs: str | int | float
|
self, module_index: int, message_index: int, **kwargs: str | int | float
|
||||||
) -> str:
|
) -> str:
|
||||||
return self.translate_string(
|
args = {
|
||||||
translate_string_in(
|
k: i18n_pb2.TranslateArgValue(str=v)
|
||||||
module_index=module_index, message_index=message_index, **kwargs
|
if isinstance(v, str)
|
||||||
)
|
else i18n_pb2.TranslateArgValue(number=v)
|
||||||
|
for k, v in kwargs.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
input = i18n_pb2.TranslateStringRequest(
|
||||||
|
module_index=module_index,
|
||||||
|
message_index=message_index,
|
||||||
|
args=args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
output_bytes = self.translate_string_raw(input.SerializeToString())
|
||||||
|
return anki.generic_pb2.String.FromString(output_bytes).val
|
||||||
|
|
||||||
def format_time_span(
|
def format_time_span(
|
||||||
self,
|
self,
|
||||||
seconds: Any,
|
seconds: Any,
|
||||||
|
@ -123,31 +133,17 @@ class RustBackend(RustBackendGenerated):
|
||||||
)
|
)
|
||||||
return self.format_timespan(seconds=seconds, context=context)
|
return self.format_timespan(seconds=seconds, context=context)
|
||||||
|
|
||||||
def _run_command(self, service: int, method: int, input: Any) -> bytes:
|
def _run_command(self, service: int, method: int, input: bytes) -> bytes:
|
||||||
input_bytes = input.SerializeToString()
|
|
||||||
try:
|
try:
|
||||||
return self._backend.command(service, method, input_bytes)
|
return self._backend.command(service, method, input)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
err_bytes = bytes(error.args[0])
|
error_bytes = bytes(error.args[0])
|
||||||
|
|
||||||
err = backend_pb2.BackendError()
|
err = backend_pb2.BackendError()
|
||||||
err.ParseFromString(err_bytes)
|
err.ParseFromString(error_bytes)
|
||||||
raise backend_exception_to_pylib(err)
|
raise backend_exception_to_pylib(err)
|
||||||
|
|
||||||
|
|
||||||
def translate_string_in(
|
|
||||||
module_index: int, message_index: int, **kwargs: str | int | float
|
|
||||||
) -> i18n_pb2.TranslateStringRequest:
|
|
||||||
args = {
|
|
||||||
k: i18n_pb2.TranslateArgValue(str=v)
|
|
||||||
if isinstance(v, str)
|
|
||||||
else i18n_pb2.TranslateArgValue(number=v)
|
|
||||||
for k, v in kwargs.items()
|
|
||||||
}
|
|
||||||
return i18n_pb2.TranslateStringRequest(
|
|
||||||
module_index=module_index, message_index=message_index, args=args
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Translations(GeneratedTranslations):
|
class Translations(GeneratedTranslations):
|
||||||
def __init__(self, backend: ref[RustBackend] | None):
|
def __init__(self, backend: ref[RustBackend] | None):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
|
@ -51,24 +51,7 @@ LABEL_OPTIONAL = 1
|
||||||
LABEL_REQUIRED = 2
|
LABEL_REQUIRED = 2
|
||||||
LABEL_REPEATED = 3
|
LABEL_REPEATED = 3
|
||||||
|
|
||||||
# messages we don't want to unroll in codegen
|
RAW_ONLY = {"TranslateString"}
|
||||||
SKIP_UNROLL_INPUT = {
|
|
||||||
"TranslateString",
|
|
||||||
"SetPreferences",
|
|
||||||
"UpdateDeckConfigs",
|
|
||||||
"AnswerCard",
|
|
||||||
"ChangeNotetype",
|
|
||||||
"CompleteTag",
|
|
||||||
}
|
|
||||||
SKIP_UNROLL_OUTPUT = {"GetPreferences"}
|
|
||||||
|
|
||||||
SKIP_DECODE = {
|
|
||||||
"Graphs",
|
|
||||||
"GetGraphPreferences",
|
|
||||||
"GetChangeNotetypeInfo",
|
|
||||||
"CompleteTag",
|
|
||||||
"CardStats",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def python_type(field):
|
def python_type(field):
|
||||||
|
@ -116,62 +99,59 @@ def fix_snakecase(name):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def get_input_args(msg):
|
def get_input_args(input_type):
|
||||||
fields = sorted(msg.fields, key=lambda x: x.number)
|
fields = sorted(input_type.fields, key=lambda x: x.number)
|
||||||
self_star = ["self"]
|
self_star = ["self"]
|
||||||
if len(fields) >= 2:
|
if len(fields) >= 2:
|
||||||
self_star.append("*")
|
self_star.append("*")
|
||||||
return ", ".join(self_star + [f"{f.name}: {python_type(f)}" for f in fields])
|
return ", ".join(self_star + [f"{f.name}: {python_type(f)}" for f in fields])
|
||||||
|
|
||||||
|
|
||||||
def get_input_assign(msg):
|
def get_input_assign(input_type):
|
||||||
fields = sorted(msg.fields, key=lambda x: x.number)
|
fields = sorted(input_type.fields, key=lambda x: x.number)
|
||||||
return ", ".join(f"{f.name}={f.name}" for f in fields)
|
return ", ".join(f"{f.name}={f.name}" for f in fields)
|
||||||
|
|
||||||
|
|
||||||
def render_method(service_idx, method_idx, method):
|
def render_method(service_idx, method_idx, method):
|
||||||
input_name = method.input_type.name
|
|
||||||
if (
|
|
||||||
(input_name.endswith("Request") or len(method.input_type.fields) < 2)
|
|
||||||
and not method.input_type.oneofs
|
|
||||||
and not method.name in SKIP_UNROLL_INPUT
|
|
||||||
):
|
|
||||||
input_args = get_input_args(method.input_type)
|
|
||||||
input_assign = get_input_assign(method.input_type)
|
|
||||||
input_assign_outer = (
|
|
||||||
f"input = {fullname(method.input_type.full_name)}({input_assign})\n "
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
input_args = f"self, input: {fullname(method.input_type.full_name)}"
|
|
||||||
input_assign_outer = ""
|
|
||||||
name = fix_snakecase(stringcase.snakecase(method.name))
|
name = fix_snakecase(stringcase.snakecase(method.name))
|
||||||
|
input_name = method.input_type.name
|
||||||
|
|
||||||
|
if (
|
||||||
|
input_name.endswith("Request") or len(method.input_type.fields) < 2
|
||||||
|
) and not method.input_type.oneofs:
|
||||||
|
input_params = get_input_args(method.input_type)
|
||||||
|
input_assign_full = f"message = {fullname(method.input_type.full_name)}({get_input_assign(method.input_type)})"
|
||||||
|
else:
|
||||||
|
input_params = f"self, message: {fullname(method.input_type.full_name)}"
|
||||||
|
input_assign_full = ""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
len(method.output_type.fields) == 1
|
len(method.output_type.fields) == 1
|
||||||
and method.output_type.fields[0].type != TYPE_ENUM
|
and method.output_type.fields[0].type != TYPE_ENUM
|
||||||
and method.name not in SKIP_UNROLL_OUTPUT
|
|
||||||
):
|
):
|
||||||
# unwrap single return arg
|
# unwrap single return arg
|
||||||
f = method.output_type.fields[0]
|
f = method.output_type.fields[0]
|
||||||
single_field = f".{f.name}"
|
|
||||||
return_type = python_type(f)
|
return_type = python_type(f)
|
||||||
|
single_attribute = f".{f.name}"
|
||||||
else:
|
else:
|
||||||
single_field = ""
|
|
||||||
return_type = fullname(method.output_type.full_name)
|
return_type = fullname(method.output_type.full_name)
|
||||||
|
single_attribute = ""
|
||||||
if method.name in SKIP_DECODE:
|
|
||||||
return_type = "bytes"
|
|
||||||
|
|
||||||
buf = f"""\
|
buf = f"""\
|
||||||
def {name}({input_args}) -> {return_type}:
|
def {name}_raw(self, message: bytes) -> bytes:
|
||||||
{input_assign_outer}"""
|
return self._run_command({service_idx}, {method_idx}, message)
|
||||||
|
|
||||||
if method.name in SKIP_DECODE:
|
|
||||||
buf += f"""return self._run_command({service_idx}, {method_idx}, input)
|
|
||||||
"""
|
"""
|
||||||
else:
|
|
||||||
buf += f"""output = {fullname(method.output_type.full_name)}()
|
if not method.name in RAW_ONLY:
|
||||||
output.ParseFromString(self._run_command({service_idx}, {method_idx}, input))
|
buf += f"""\
|
||||||
return output{single_field}
|
def {name}({input_params}) -> {return_type}:
|
||||||
|
{input_assign_full}
|
||||||
|
raw_bytes = self._run_command({service_idx}, {method_idx}, message.SerializeToString())
|
||||||
|
output = {fullname(method.output_type.full_name)}()
|
||||||
|
output.ParseFromString(raw_bytes)
|
||||||
|
return output{single_attribute}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return buf
|
return buf
|
||||||
|
@ -235,7 +215,7 @@ or removed at any time. Instead, please use the methods on the collection
|
||||||
instead. Eg, don't use col.backend.all_deck_config(), instead use
|
instead. Eg, don't use col.backend.all_deck_config(), instead use
|
||||||
col.decks.all_config()
|
col.decks.all_config()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
|
|
|
@ -804,25 +804,12 @@ class Collection(DeprecatedNamesMixin):
|
||||||
|
|
||||||
return CollectionStats(self)
|
return CollectionStats(self)
|
||||||
|
|
||||||
def card_stats_data(self, card_id: CardId) -> bytes:
|
def card_stats_data(self, card_id: CardId) -> stats_pb2.CardStatsResponse:
|
||||||
return self._backend.card_stats(card_id)
|
return self._backend.card_stats(card_id)
|
||||||
|
|
||||||
def studied_today(self) -> str:
|
def studied_today(self) -> str:
|
||||||
return self._backend.studied_today()
|
return self._backend.studied_today()
|
||||||
|
|
||||||
def graph_data(self, search: str, days: int) -> bytes:
|
|
||||||
return self._backend.graphs(search=search, days=days)
|
|
||||||
|
|
||||||
def get_graph_preferences(self) -> bytes:
|
|
||||||
return self._backend.get_graph_preferences()
|
|
||||||
|
|
||||||
def set_graph_preferences(self, prefs: GraphPreferences) -> None:
|
|
||||||
self._backend.set_graph_preferences(input=prefs)
|
|
||||||
|
|
||||||
def congrats_info(self) -> bytes:
|
|
||||||
"Don't use this, it will likely go away in the future."
|
|
||||||
return self._backend.congrats_info().SerializeToString()
|
|
||||||
|
|
||||||
# Undo
|
# Undo
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
@ -285,7 +285,8 @@ class DeckManager(DeprecatedNamesMixin):
|
||||||
return self.col._backend.get_deck_configs_for_update(deck_id)
|
return self.col._backend.get_deck_configs_for_update(deck_id)
|
||||||
|
|
||||||
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
|
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
|
||||||
return self.col._backend.update_deck_configs(input=input)
|
op_bytes = self.col._backend.update_deck_configs_raw(input.SerializeToString())
|
||||||
|
return OpChanges.FromString(op_bytes)
|
||||||
|
|
||||||
def all_config(self) -> list[DeckConfigDict]:
|
def all_config(self) -> list[DeckConfigDict]:
|
||||||
"A list of all deck config."
|
"A list of all deck config."
|
||||||
|
|
|
@ -370,7 +370,7 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
|
|
||||||
def change_notetype_info(
|
def change_notetype_info(
|
||||||
self, *, old_notetype_id: NotetypeId, new_notetype_id: NotetypeId
|
self, *, old_notetype_id: NotetypeId, new_notetype_id: NotetypeId
|
||||||
) -> bytes:
|
) -> ChangeNotetypeInfo:
|
||||||
return self.col._backend.get_change_notetype_info(
|
return self.col._backend.get_change_notetype_info(
|
||||||
old_notetype_id=old_notetype_id, new_notetype_id=new_notetype_id
|
old_notetype_id=old_notetype_id, new_notetype_id=new_notetype_id
|
||||||
)
|
)
|
||||||
|
@ -388,7 +388,8 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
field/template count. Each value represents the index in the previous
|
field/template count. Each value represents the index in the previous
|
||||||
notetype. -1 indicates the original value will be discarded.
|
notetype. -1 indicates the original value will be discarded.
|
||||||
"""
|
"""
|
||||||
return self.col._backend.change_notetype(input)
|
op_bytes = self.col._backend.change_notetype_raw(input.SerializeToString())
|
||||||
|
return OpChanges.FromString(op_bytes)
|
||||||
|
|
||||||
# legacy API - used by unit tests and add-ons
|
# legacy API - used by unit tests and add-ons
|
||||||
|
|
||||||
|
@ -414,15 +415,13 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
template_map = self._convert_legacy_map(cmap, len(newModel["tmpls"]))
|
template_map = self._convert_legacy_map(cmap, len(newModel["tmpls"]))
|
||||||
|
|
||||||
self.col._backend.change_notetype(
|
self.col._backend.change_notetype(
|
||||||
ChangeNotetypeRequest(
|
note_ids=nids,
|
||||||
note_ids=nids,
|
new_fields=field_map,
|
||||||
new_fields=field_map,
|
new_templates=template_map,
|
||||||
new_templates=template_map,
|
old_notetype_name=notetype["name"],
|
||||||
old_notetype_name=notetype["name"],
|
old_notetype_id=notetype["id"],
|
||||||
old_notetype_id=notetype["id"],
|
new_notetype_id=newModel["id"],
|
||||||
new_notetype_id=newModel["id"],
|
current_schema=self.col.db.scalar("select scm from col"),
|
||||||
current_schema=self.col.db.scalar("select scm from col"),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _convert_legacy_map(
|
def _convert_legacy_map(
|
||||||
|
|
|
@ -86,7 +86,8 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
def answer_card(self, input: CardAnswer) -> OpChanges:
|
def answer_card(self, input: CardAnswer) -> OpChanges:
|
||||||
"Update card to provided state, and remove it from queue."
|
"Update card to provided state, and remove it from queue."
|
||||||
self.reps += 1
|
self.reps += 1
|
||||||
return self.col._backend.answer_card(input=input)
|
op_bytes = self.col._backend.answer_card_raw(input.SerializeToString())
|
||||||
|
return OpChanges.FromString(op_bytes)
|
||||||
|
|
||||||
def state_is_leech(self, new_state: SchedulingState) -> bool:
|
def state_is_leech(self, new_state: SchedulingState) -> bool:
|
||||||
"True if new state marks the card as a leech."
|
"True if new state marks the card as a leech."
|
||||||
|
|
|
@ -26,6 +26,7 @@ from anki.utils import ids2str
|
||||||
|
|
||||||
# public exports
|
# public exports
|
||||||
TagTreeNode = tags_pb2.TagTreeNode
|
TagTreeNode = tags_pb2.TagTreeNode
|
||||||
|
CompleteTagRequest = tags_pb2.CompleteTagRequest
|
||||||
MARKED_TAG = "marked"
|
MARKED_TAG = "marked"
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,11 +69,6 @@ class TagManager(DeprecatedNamesMixin):
|
||||||
"Set browser expansion state for tag, registering the tag if missing."
|
"Set browser expansion state for tag, registering the tag if missing."
|
||||||
return self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
|
return self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
|
||||||
|
|
||||||
def complete_tag(self, input_bytes: bytes) -> bytes:
|
|
||||||
input = tags_pb2.CompleteTagRequest()
|
|
||||||
input.ParseFromString(input_bytes)
|
|
||||||
return self.col._backend.complete_tag(input)
|
|
||||||
|
|
||||||
# Bulk addition/removal from specific notes
|
# Bulk addition/removal from specific notes
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,13 @@ def test_stats():
|
||||||
col.addNote(note)
|
col.addNote(note)
|
||||||
c = note.cards()[0]
|
c = note.cards()[0]
|
||||||
# card stats
|
# card stats
|
||||||
card_stats = CardStats()
|
card_stats = col.card_stats_data(c.id)
|
||||||
card_stats.ParseFromString(col.card_stats_data(c.id))
|
|
||||||
assert card_stats.note_id == note.id
|
assert card_stats.note_id == note.id
|
||||||
col.reset()
|
col.reset()
|
||||||
c = col.sched.getCard()
|
c = col.sched.getCard()
|
||||||
col.sched.answerCard(c, 3)
|
col.sched.answerCard(c, 3)
|
||||||
col.sched.answerCard(c, 2)
|
col.sched.answerCard(c, 2)
|
||||||
card_stats.ParseFromString(col.card_stats_data(c.id))
|
card_stats = col.card_stats_data(c.id)
|
||||||
assert len(card_stats.revlog) == 2
|
assert len(card_stats.revlog) == 2
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import time
|
||||||
import traceback
|
import traceback
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_cors # type: ignore
|
import flask_cors # type: ignore
|
||||||
|
@ -21,15 +22,14 @@ from waitress.server import create_server
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import CardId
|
from anki._vendor import stringcase
|
||||||
from anki.collection import GraphPreferences, OpChanges
|
from anki.collection import OpChanges
|
||||||
from anki.decks import UpdateDeckConfigs
|
from anki.decks import DeckConfigsForUpdate, UpdateDeckConfigs
|
||||||
from anki.models import NotetypeNames
|
|
||||||
from anki.scheduler.v3 import NextStates
|
from anki.scheduler.v3 import NextStates
|
||||||
from anki.utils import dev_mode, from_json_bytes
|
from anki.utils import dev_mode
|
||||||
from aqt.changenotetype import ChangeNotetypeDialog
|
from aqt.changenotetype import ChangeNotetypeDialog
|
||||||
from aqt.deckoptions import DeckOptionsDialog
|
from aqt.deckoptions import DeckOptionsDialog
|
||||||
from aqt.operations.deck import update_deck_configs
|
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
|
||||||
app = flask.Flask(__name__, root_path="/fake")
|
app = flask.Flask(__name__, root_path="/fake")
|
||||||
|
@ -375,40 +375,20 @@ def _extract_request(
|
||||||
return LocalFileRequest(root=aqt.mw.col.media.dir(), path=path)
|
return LocalFileRequest(root=aqt.mw.col.media.dir(), path=path)
|
||||||
|
|
||||||
|
|
||||||
def graph_data() -> bytes:
|
|
||||||
args = from_json_bytes(request.data)
|
|
||||||
return aqt.mw.col.graph_data(search=args["search"], days=args["days"])
|
|
||||||
|
|
||||||
|
|
||||||
def graph_preferences() -> bytes:
|
|
||||||
return aqt.mw.col.get_graph_preferences()
|
|
||||||
|
|
||||||
|
|
||||||
def set_graph_preferences() -> None:
|
|
||||||
prefs = GraphPreferences()
|
|
||||||
prefs.ParseFromString(request.data)
|
|
||||||
aqt.mw.col.set_graph_preferences(prefs)
|
|
||||||
|
|
||||||
|
|
||||||
def congrats_info() -> bytes:
|
def congrats_info() -> bytes:
|
||||||
if not aqt.mw.col.sched._is_finished():
|
if not aqt.mw.col.sched._is_finished():
|
||||||
aqt.mw.taskman.run_on_main(lambda: aqt.mw.moveToState("review"))
|
aqt.mw.taskman.run_on_main(lambda: aqt.mw.moveToState("review"))
|
||||||
return aqt.mw.col.congrats_info()
|
return raw_backend_request("congrats_info")()
|
||||||
|
|
||||||
|
|
||||||
def i18n_resources() -> bytes:
|
def get_deck_configs_for_update() -> bytes:
|
||||||
args = from_json_bytes(request.data)
|
config_bytes = aqt.mw.col._backend.get_deck_configs_for_update_raw(request.data)
|
||||||
return aqt.mw.col.i18n_resources(modules=args["modules"])
|
configs = DeckConfigsForUpdate.FromString(config_bytes)
|
||||||
|
configs.have_addons = aqt.mw.addonManager.dirty
|
||||||
|
return configs.SerializeToString()
|
||||||
|
|
||||||
|
|
||||||
def deck_configs_for_update() -> bytes:
|
def update_deck_configs() -> bytes:
|
||||||
args = from_json_bytes(request.data)
|
|
||||||
msg = aqt.mw.col.decks.get_deck_configs_for_update(deck_id=args["deckId"])
|
|
||||||
msg.have_addons = aqt.mw.addonManager.dirty
|
|
||||||
return msg.SerializeToString()
|
|
||||||
|
|
||||||
|
|
||||||
def update_deck_configs_request() -> bytes:
|
|
||||||
# the regular change tracking machinery expects to be started on the main
|
# the regular change tracking machinery expects to be started on the main
|
||||||
# thread and uses a callback on success, so we need to run this op on
|
# thread and uses a callback on success, so we need to run this op on
|
||||||
# main, and return immediately from the web request
|
# main, and return immediately from the web request
|
||||||
|
@ -421,7 +401,7 @@ def update_deck_configs_request() -> bytes:
|
||||||
window.reject()
|
window.reject()
|
||||||
|
|
||||||
def handle_on_main() -> None:
|
def handle_on_main() -> None:
|
||||||
update_deck_configs(parent=aqt.mw, input=input).success(
|
update_deck_configs_op(parent=aqt.mw, input=input).success(
|
||||||
on_success
|
on_success
|
||||||
).run_in_background()
|
).run_in_background()
|
||||||
|
|
||||||
|
@ -444,18 +424,6 @@ def set_next_card_states() -> bytes:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
def notetype_names() -> bytes:
|
|
||||||
msg = NotetypeNames(entries=aqt.mw.col.models.all_names_and_ids())
|
|
||||||
return msg.SerializeToString()
|
|
||||||
|
|
||||||
|
|
||||||
def change_notetype_info() -> bytes:
|
|
||||||
args = from_json_bytes(request.data)
|
|
||||||
return aqt.mw.col.models.change_notetype_info(
|
|
||||||
old_notetype_id=args["oldNotetypeId"], new_notetype_id=args["newNotetypeId"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def change_notetype() -> bytes:
|
def change_notetype() -> bytes:
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
||||||
|
@ -468,31 +436,49 @@ def change_notetype() -> bytes:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
def complete_tag() -> bytes:
|
post_handler_list = [
|
||||||
return aqt.mw.col.tags.complete_tag(request.data)
|
congrats_info,
|
||||||
|
get_deck_configs_for_update,
|
||||||
|
update_deck_configs,
|
||||||
|
next_card_states,
|
||||||
|
set_next_card_states,
|
||||||
|
change_notetype,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def card_stats() -> bytes:
|
exposed_backend_list = [
|
||||||
args = from_json_bytes(request.data)
|
# I18nService
|
||||||
return aqt.mw.col.card_stats_data(CardId(args["cardId"]))
|
"i18n_resources",
|
||||||
|
# NotesService
|
||||||
|
"get_note",
|
||||||
|
# NotetypesService
|
||||||
|
"get_notetype_names",
|
||||||
|
"get_change_notetype_info",
|
||||||
|
# StatsService
|
||||||
|
"card_stats",
|
||||||
|
"graphs",
|
||||||
|
"get_graph_preferences",
|
||||||
|
"set_graph_preferences",
|
||||||
|
# TagsService
|
||||||
|
"complete_tag",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# these require a collection
|
def raw_backend_request(endpoint: str) -> Callable[[], bytes]:
|
||||||
|
# check for key at startup
|
||||||
|
from anki._backend import RustBackend
|
||||||
|
|
||||||
|
assert hasattr(RustBackend, f"{endpoint}_raw")
|
||||||
|
|
||||||
|
return lambda: getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data)
|
||||||
|
|
||||||
|
|
||||||
|
# all methods in here require a collection
|
||||||
post_handlers = {
|
post_handlers = {
|
||||||
"graphData": graph_data,
|
stringcase.camelcase(handler.__name__): handler for handler in post_handler_list
|
||||||
"graphPreferences": graph_preferences,
|
} | {
|
||||||
"setGraphPreferences": set_graph_preferences,
|
stringcase.camelcase(handler): raw_backend_request(handler)
|
||||||
"deckConfigsForUpdate": deck_configs_for_update,
|
for handler in exposed_backend_list
|
||||||
"updateDeckConfigs": update_deck_configs_request,
|
|
||||||
"nextCardStates": next_card_states,
|
|
||||||
"setNextCardStates": set_next_card_states,
|
|
||||||
"changeNotetypeInfo": change_notetype_info,
|
|
||||||
"notetypeNames": notetype_names,
|
|
||||||
"changeNotetype": change_notetype,
|
|
||||||
"i18nResources": i18n_resources,
|
|
||||||
"congratsInfo": congrats_info,
|
|
||||||
"completeTag": complete_tag,
|
|
||||||
"cardStats": card_stats,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Stats } from "../lib/proto";
|
import type { Stats } from "../lib/proto";
|
||||||
import { getCardStats } from "./lib";
|
import { stats as statsService } from "../lib/proto";
|
||||||
import Container from "../components/Container.svelte";
|
import Container from "../components/Container.svelte";
|
||||||
import Row from "../components/Row.svelte";
|
import Row from "../components/Row.svelte";
|
||||||
import CardStats from "./CardStats.svelte";
|
import CardStats from "./CardStats.svelte";
|
||||||
|
@ -19,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
async function updateStats(cardId: number): Promise<void> {
|
async function updateStats(cardId: number): Promise<void> {
|
||||||
const requestedCardId = cardId;
|
const requestedCardId = cardId;
|
||||||
const cardStats = await getCardStats(requestedCardId);
|
const cardStats = await statsService.cardStats({ cid: requestedCardId });
|
||||||
|
|
||||||
/* Skip if another update has been triggered in the meantime. */
|
/* Skip if another update has been triggered in the meantime. */
|
||||||
if (requestedCardId === cardId) {
|
if (requestedCardId === cardId) {
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
import { Stats } from "../lib/proto";
|
|
||||||
import { postRequest } from "../lib/postrequest";
|
|
||||||
|
|
||||||
export async function getCardStats(cardId: number): Promise<Stats.CardStatsResponse> {
|
|
||||||
return Stats.CardStatsResponse.decode(
|
|
||||||
await postRequest("/_anki/cardStats", JSON.stringify({ cardId })),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,39 +1,35 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
/* eslint
|
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ChangeNotetypeState, getChangeNotetypeInfo, getNotetypeNames } from "./lib";
|
|
||||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||||
|
import { notetypes, empty } from "../lib/proto";
|
||||||
import { checkNightMode } from "../lib/nightmode";
|
import { checkNightMode } from "../lib/nightmode";
|
||||||
|
import { ChangeNotetypeState } from "./lib";
|
||||||
|
|
||||||
import ChangeNotetypePage from "./ChangeNotetypePage.svelte";
|
import ChangeNotetypePage from "./ChangeNotetypePage.svelte";
|
||||||
import "./change-notetype-base.css";
|
import "./change-notetype-base.css";
|
||||||
|
|
||||||
|
const notetypeNames = notetypes.getNotetypeNames(empty);
|
||||||
|
const i18n = setupI18n({
|
||||||
|
modules: [ModuleName.ACTIONS, ModuleName.CHANGE_NOTETYPE, ModuleName.KEYBOARD],
|
||||||
|
});
|
||||||
|
|
||||||
export async function setupChangeNotetypePage(
|
export async function setupChangeNotetypePage(
|
||||||
oldNotetypeId: number,
|
oldNotetypeId: number,
|
||||||
newNotetypeId: number,
|
newNotetypeId: number,
|
||||||
): Promise<ChangeNotetypePage> {
|
): Promise<ChangeNotetypePage> {
|
||||||
const [info, names] = await Promise.all([
|
const changeNotetypeInfo = notetypes.getChangeNotetypeInfo({
|
||||||
getChangeNotetypeInfo(oldNotetypeId, newNotetypeId),
|
oldNotetypeId,
|
||||||
getNotetypeNames(),
|
newNotetypeId,
|
||||||
setupI18n({
|
});
|
||||||
modules: [
|
const [names, info] = await Promise.all([notetypeNames, changeNotetypeInfo, i18n]);
|
||||||
ModuleName.ACTIONS,
|
|
||||||
ModuleName.CHANGE_NOTETYPE,
|
|
||||||
ModuleName.KEYBOARD,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
|
|
||||||
const state = new ChangeNotetypeState(names, info);
|
const state = new ChangeNotetypeState(names, info);
|
||||||
return new ChangeNotetypePage({
|
return new ChangeNotetypePage({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: { state } as any,
|
props: { state },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,37 +2,10 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { Notetypes } from "../lib/proto";
|
import { notetypes, Notetypes } from "../lib/proto";
|
||||||
import { postRequest } from "../lib/postrequest";
|
|
||||||
import { readable, Readable } from "svelte/store";
|
import { readable, Readable } from "svelte/store";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
|
|
||||||
export async function getNotetypeNames(): Promise<Notetypes.NotetypeNames> {
|
|
||||||
return Notetypes.NotetypeNames.decode(
|
|
||||||
await postRequest("/_anki/notetypeNames", ""),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getChangeNotetypeInfo(
|
|
||||||
oldNotetypeId: number,
|
|
||||||
newNotetypeId: number,
|
|
||||||
): Promise<Notetypes.ChangeNotetypeInfo> {
|
|
||||||
return Notetypes.ChangeNotetypeInfo.decode(
|
|
||||||
await postRequest(
|
|
||||||
"/_anki/changeNotetypeInfo",
|
|
||||||
JSON.stringify({ oldNotetypeId, newNotetypeId }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function changeNotetype(
|
|
||||||
input: Notetypes.ChangeNotetypeRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
const data: Uint8Array = Notetypes.ChangeNotetypeRequest.encode(input).finish();
|
|
||||||
await postRequest("/_anki/changeNotetype", data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nullToNegativeOne(list: (number | null)[]): number[] {
|
function nullToNegativeOne(list: (number | null)[]): number[] {
|
||||||
return list.map((val) => val ?? -1);
|
return list.map((val) => val ?? -1);
|
||||||
}
|
}
|
||||||
|
@ -51,11 +24,11 @@ export class ChangeNotetypeInfoWrapper {
|
||||||
|
|
||||||
constructor(info: Notetypes.ChangeNotetypeInfo) {
|
constructor(info: Notetypes.ChangeNotetypeInfo) {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
const templates = info.input!.newTemplates!;
|
const templates = info.input?.newTemplates ?? [];
|
||||||
if (templates.length > 0) {
|
if (templates.length > 0) {
|
||||||
this.templates = negativeOneToNull(templates);
|
this.templates = negativeOneToNull(templates);
|
||||||
}
|
}
|
||||||
this.fields = negativeOneToNull(info.input!.newFields!);
|
this.fields = negativeOneToNull(info.input?.newFields ?? []);
|
||||||
this.oldNotetypeName = info.oldNotetypeName;
|
this.oldNotetypeName = info.oldNotetypeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +114,7 @@ export enum MapContext {
|
||||||
Field,
|
Field,
|
||||||
Template,
|
Template,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChangeNotetypeState {
|
export class ChangeNotetypeState {
|
||||||
readonly info: Readable<ChangeNotetypeInfoWrapper>;
|
readonly info: Readable<ChangeNotetypeInfoWrapper>;
|
||||||
readonly notetypes: Readable<NotetypeListEntry[]>;
|
readonly notetypes: Readable<NotetypeListEntry[]>;
|
||||||
|
@ -168,10 +142,11 @@ export class ChangeNotetypeState {
|
||||||
async setTargetNotetypeIndex(idx: number): Promise<void> {
|
async setTargetNotetypeIndex(idx: number): Promise<void> {
|
||||||
this.info_.input().newNotetypeId = this.notetypeNames.entries[idx].id!;
|
this.info_.input().newNotetypeId = this.notetypeNames.entries[idx].id!;
|
||||||
this.notetypesSetter(this.buildNotetypeList());
|
this.notetypesSetter(this.buildNotetypeList());
|
||||||
const newInfo = await getChangeNotetypeInfo(
|
const { oldNotetypeId, newNotetypeId } = this.info_.input();
|
||||||
this.info_.input().oldNotetypeId,
|
const newInfo = await notetypes.getChangeNotetypeInfo({
|
||||||
this.info_.input().newNotetypeId,
|
oldNotetypeId,
|
||||||
);
|
newNotetypeId,
|
||||||
|
});
|
||||||
|
|
||||||
this.info_ = new ChangeNotetypeInfoWrapper(newInfo);
|
this.info_ = new ChangeNotetypeInfoWrapper(newInfo);
|
||||||
this.info_.unusedItems(MapContext.Field);
|
this.info_.unusedItems(MapContext.Field);
|
||||||
|
@ -197,16 +172,16 @@ export class ChangeNotetypeState {
|
||||||
this.infoSetter(this.info_);
|
this.infoSetter(this.info_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataForSaving(): Notetypes.ChangeNotetypeRequest {
|
||||||
|
return this.info_.intoInput();
|
||||||
|
}
|
||||||
|
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
if (this.info_.unchanged()) {
|
if (this.info_.unchanged()) {
|
||||||
alert("No changes to save");
|
alert("No changes to save");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await changeNotetype(this.dataForSaving());
|
await notetypes.changeNotetype(this.dataForSaving());
|
||||||
}
|
|
||||||
|
|
||||||
dataForSaving(): Notetypes.ChangeNotetypeRequest {
|
|
||||||
return this.info_.intoInput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildNotetypeList(): NotetypeListEntry[] {
|
private buildNotetypeList(): NotetypeListEntry[] {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { getCongratsInfo } from "./lib";
|
import { scheduler, empty } from "../lib/proto";
|
||||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||||
import { checkNightMode } from "../lib/nightmode";
|
import { checkNightMode } from "../lib/nightmode";
|
||||||
|
|
||||||
|
@ -14,16 +14,15 @@ export async function setupCongrats(): Promise<CongratsPage> {
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
await i18n;
|
await i18n;
|
||||||
|
|
||||||
const info = await getCongratsInfo();
|
const info = await scheduler.congratsInfo(empty);
|
||||||
const page = new CongratsPage({
|
const page = new CongratsPage({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: { info },
|
props: { info },
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(async () => {
|
||||||
getCongratsInfo().then((info) => {
|
const info = await scheduler.congratsInfo(empty);
|
||||||
page.$set({ info });
|
page.$set({ info });
|
||||||
});
|
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { Scheduler } from "../lib/proto";
|
import type { Scheduler } from "../lib/proto";
|
||||||
import { postRequest } from "../lib/postrequest";
|
|
||||||
import { naturalUnit, unitAmount, unitName } from "../lib/time";
|
import { naturalUnit, unitAmount, unitName } from "../lib/time";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
|
||||||
export async function getCongratsInfo(): Promise<Scheduler.CongratsInfoResponse> {
|
|
||||||
return Scheduler.CongratsInfoResponse.decode(
|
|
||||||
await postRequest("/_anki/congratsInfo", ""),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildNextLearnMsg(info: Scheduler.CongratsInfoResponse): string {
|
export function buildNextLearnMsg(info: Scheduler.CongratsInfoResponse): string {
|
||||||
const secsUntil = info.secsUntilNextLearn;
|
const secsUntil = info.secsUntilNextLearn;
|
||||||
// next learning card not due today?
|
// next learning card not due today?
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
import "../sveltelib/export-runtime";
|
import "../sveltelib/export-runtime";
|
||||||
|
|
||||||
import { getDeckOptionsInfo, DeckOptionsState } from "./lib";
|
import { DeckOptionsState } from "./lib";
|
||||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||||
|
import { deckConfig } from "../lib/proto";
|
||||||
import { checkNightMode } from "../lib/nightmode";
|
import { checkNightMode } from "../lib/nightmode";
|
||||||
import { touchDeviceKey, modalsKey } from "../components/context-keys";
|
import { touchDeviceKey, modalsKey } from "../components/context-keys";
|
||||||
|
|
||||||
|
@ -24,8 +25,11 @@ const i18n = setupI18n({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function setupDeckOptions(deckId: number): Promise<DeckOptionsPage> {
|
export async function setupDeckOptions(did: number): Promise<DeckOptionsPage> {
|
||||||
const [info] = await Promise.all([getDeckOptionsInfo(deckId), i18n]);
|
const [info] = await Promise.all([
|
||||||
|
deckConfig.getDeckConfigsForUpdate({ did }),
|
||||||
|
i18n,
|
||||||
|
]);
|
||||||
|
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@ export async function setupDeckOptions(deckId: number): Promise<DeckOptionsPage>
|
||||||
context.set(modalsKey, new Map());
|
context.set(modalsKey, new Map());
|
||||||
context.set(touchDeviceKey, "ontouchstart" in document.documentElement);
|
context.set(touchDeviceKey, "ontouchstart" in document.documentElement);
|
||||||
|
|
||||||
const state = new DeckOptionsState(deckId, info);
|
const state = new DeckOptionsState(did, info);
|
||||||
return new DeckOptionsPage({
|
return new DeckOptionsPage({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: { state },
|
props: { state },
|
||||||
|
|
|
@ -244,8 +244,8 @@ test("saving", () => {
|
||||||
expect(out.targetDeckId).toBe(123);
|
expect(out.targetDeckId).toBe(123);
|
||||||
// in no-changes case, currently selected config should
|
// in no-changes case, currently selected config should
|
||||||
// be returned
|
// be returned
|
||||||
expect(out.configs.length).toBe(1);
|
expect(out.configs!.length).toBe(1);
|
||||||
expect(out.configs[0].name).toBe("another one");
|
expect(out.configs![0].name).toBe("another one");
|
||||||
expect(out.applyToChildren).toBe(false);
|
expect(out.applyToChildren).toBe(false);
|
||||||
|
|
||||||
// rename, then change current deck
|
// rename, then change current deck
|
||||||
|
@ -255,7 +255,7 @@ test("saving", () => {
|
||||||
|
|
||||||
// renamed deck should be in changes, with current deck as last element
|
// renamed deck should be in changes, with current deck as last element
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(true);
|
||||||
expect(out.configs.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
|
expect(out.configs!.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
|
||||||
expect(out.applyToChildren).toBe(true);
|
expect(out.applyToChildren).toBe(true);
|
||||||
|
|
||||||
// start again, adding new deck
|
// start again, adding new deck
|
||||||
|
@ -275,7 +275,7 @@ test("saving", () => {
|
||||||
// only contain Default, which is the new current deck
|
// only contain Default, which is the new current deck
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(true);
|
||||||
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
|
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
|
||||||
expect(out.configs.map((c) => c.name)).toStrictEqual(["Default"]);
|
expect(out.configs!.map((c) => c.name)).toStrictEqual(["Default"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("aux data", () => {
|
test("aux data", () => {
|
||||||
|
@ -302,8 +302,8 @@ test("aux data", () => {
|
||||||
|
|
||||||
// ensure changes serialize
|
// ensure changes serialize
|
||||||
const out = state.dataForSaving(true);
|
const out = state.dataForSaving(true);
|
||||||
expect(out.configs.length).toBe(2);
|
expect(out.configs!.length).toBe(2);
|
||||||
const json = out.configs.map(
|
const json = out.configs!.map(
|
||||||
(c) =>
|
(c) =>
|
||||||
JSON.parse(new TextDecoder().decode((c.config as any).other)) as Record<
|
JSON.parse(new TextDecoder().decode((c.config as any).other)) as Record<
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -1,29 +1,12 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { DeckConfig } from "../lib/proto";
|
import { DeckConfig, deckConfig } from "../lib/proto";
|
||||||
import { postRequest } from "../lib/postrequest";
|
|
||||||
import { Writable, writable, get, Readable, readable } from "svelte/store";
|
import { Writable, writable, get, Readable, readable } from "svelte/store";
|
||||||
import { isEqual, cloneDeep } from "lodash-es";
|
import { isEqual, cloneDeep } from "lodash-es";
|
||||||
import { localeCompare } from "../lib/i18n";
|
import { localeCompare } from "../lib/i18n";
|
||||||
import type { DynamicSvelteComponent } from "../sveltelib/dynamicComponent";
|
import type { DynamicSvelteComponent } from "../sveltelib/dynamicComponent";
|
||||||
|
|
||||||
export async function getDeckOptionsInfo(
|
|
||||||
deckId: number,
|
|
||||||
): Promise<DeckConfig.DeckConfigsForUpdate> {
|
|
||||||
return DeckConfig.DeckConfigsForUpdate.decode(
|
|
||||||
await postRequest("/_anki/deckConfigsForUpdate", JSON.stringify({ deckId })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveDeckOptions(
|
|
||||||
input: DeckConfig.UpdateDeckConfigsRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
const data: Uint8Array = DeckConfig.UpdateDeckConfigsRequest.encode(input).finish();
|
|
||||||
await postRequest("/_anki/updateDeckConfigs", data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeckOptionsId = number;
|
export type DeckOptionsId = number;
|
||||||
|
|
||||||
export interface ConfigWithCount {
|
export interface ConfigWithCount {
|
||||||
|
@ -185,7 +168,9 @@ export class DeckOptionsState {
|
||||||
this.updateConfigList();
|
this.updateConfigList();
|
||||||
}
|
}
|
||||||
|
|
||||||
dataForSaving(applyToChildren: boolean): DeckConfig.UpdateDeckConfigsRequest {
|
dataForSaving(
|
||||||
|
applyToChildren: boolean,
|
||||||
|
): NonNullable<DeckConfig.IUpdateDeckConfigsRequest> {
|
||||||
const modifiedConfigsExcludingCurrent = this.configs
|
const modifiedConfigsExcludingCurrent = this.configs
|
||||||
.map((c) => c.config)
|
.map((c) => c.config)
|
||||||
.filter((c, idx) => {
|
.filter((c, idx) => {
|
||||||
|
@ -199,17 +184,17 @@ export class DeckOptionsState {
|
||||||
// current must come last, even if unmodified
|
// current must come last, even if unmodified
|
||||||
this.configs[this.selectedIdx].config,
|
this.configs[this.selectedIdx].config,
|
||||||
];
|
];
|
||||||
return DeckConfig.UpdateDeckConfigsRequest.create({
|
return {
|
||||||
targetDeckId: this.targetDeckId,
|
targetDeckId: this.targetDeckId,
|
||||||
removedConfigIds: this.removedConfigs,
|
removedConfigIds: this.removedConfigs,
|
||||||
configs,
|
configs,
|
||||||
applyToChildren,
|
applyToChildren,
|
||||||
cardStateCustomizer: get(this.cardStateCustomizer),
|
cardStateCustomizer: get(this.cardStateCustomizer),
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(applyToChildren: boolean): Promise<void> {
|
async save(applyToChildren: boolean): Promise<void> {
|
||||||
await saveDeckOptions(this.dataForSaving(applyToChildren));
|
await deckConfig.updateDeckConfigs(this.dataForSaving(applyToChildren));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCurrentConfigChanged(config: ConfigInner): void {
|
private onCurrentConfigChanged(config: ConfigInner): void {
|
||||||
|
|
|
@ -19,8 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
replaceWithUnicodeSeparator,
|
replaceWithUnicodeSeparator,
|
||||||
replaceWithColons,
|
replaceWithColons,
|
||||||
} from "./tags";
|
} from "./tags";
|
||||||
import { Tags } from "../lib/proto";
|
import { tags as tagsService } from "../lib/proto";
|
||||||
import { postRequest } from "../lib/postrequest";
|
|
||||||
import { execCommand } from "./helpers";
|
import { execCommand } from "./helpers";
|
||||||
|
|
||||||
export let size: number;
|
export let size: number;
|
||||||
|
@ -56,14 +55,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let autocompleteDisabled: boolean = false;
|
let autocompleteDisabled: boolean = false;
|
||||||
|
|
||||||
async function fetchSuggestions(input: string): Promise<string[]> {
|
async function fetchSuggestions(input: string): Promise<string[]> {
|
||||||
const data = await postRequest(
|
const { tags } = await tagsService.completeTag({ input, matchLimit: 500 });
|
||||||
"/_anki/completeTag",
|
return tags;
|
||||||
Tags.CompleteTagRequest.encode(
|
|
||||||
Tags.CompleteTagRequest.create({ input, matchLimit: 500 }),
|
|
||||||
).finish(),
|
|
||||||
);
|
|
||||||
const response = Tags.CompleteTagResponse.decode(data);
|
|
||||||
return response.tags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
|
const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
|
||||||
|
|
|
@ -4,49 +4,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { PreferenceRaw, PreferencePayload } from "../sveltelib/preferences";
|
|
||||||
|
|
||||||
import { Stats } from "../lib/proto";
|
import { Stats } from "../lib/proto";
|
||||||
import { postRequest } from "../lib/postrequest";
|
import { stats, empty } from "../lib/proto";
|
||||||
|
import type { PreferenceRaw } from "../sveltelib/preferences";
|
||||||
import useAsync from "../sveltelib/async";
|
import useAsync from "../sveltelib/async";
|
||||||
import useAsyncReactive from "../sveltelib/asyncReactive";
|
import useAsyncReactive from "../sveltelib/asyncReactive";
|
||||||
import { getPreferences } from "../sveltelib/preferences";
|
import { getPreferences } from "../sveltelib/preferences";
|
||||||
|
|
||||||
import { daysToRevlogRange } from "./graph-helpers";
|
import { daysToRevlogRange } from "./graph-helpers";
|
||||||
|
|
||||||
export let search: Writable<string>;
|
export let search: Writable<string>;
|
||||||
export let days: Writable<number>;
|
export let days: Writable<number>;
|
||||||
|
|
||||||
async function getGraphData(
|
|
||||||
search: string,
|
|
||||||
days: number,
|
|
||||||
): Promise<Stats.GraphsResponse> {
|
|
||||||
return Stats.GraphsResponse.decode(
|
|
||||||
await postRequest("/_anki/graphData", JSON.stringify({ search, days })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGraphPreferences(): Promise<Stats.GraphPreferences> {
|
|
||||||
return Stats.GraphPreferences.decode(
|
|
||||||
await postRequest("/_anki/graphPreferences", JSON.stringify({})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setGraphPreferences(
|
|
||||||
prefs: PreferencePayload<Stats.GraphPreferences>,
|
|
||||||
): Promise<void> {
|
|
||||||
await postRequest(
|
|
||||||
"/_anki/setGraphPreferences",
|
|
||||||
Stats.GraphPreferences.encode(prefs).finish(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: graphLoading,
|
loading: graphLoading,
|
||||||
error: graphError,
|
error: graphError,
|
||||||
value: graphValue,
|
value: graphValue,
|
||||||
} = useAsyncReactive(() => getGraphData($search, $days), [search, days]);
|
} = useAsyncReactive(
|
||||||
|
() => stats.graphs({ search: $search, days: $days }),
|
||||||
|
[search, days],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: prefsLoading,
|
loading: prefsLoading,
|
||||||
|
@ -54,8 +30,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
value: prefsValue,
|
value: prefsValue,
|
||||||
} = useAsync(() =>
|
} = useAsync(() =>
|
||||||
getPreferences(
|
getPreferences(
|
||||||
getGraphPreferences,
|
() => stats.getGraphPreferences(empty),
|
||||||
setGraphPreferences,
|
async (input: Stats.IGraphPreferences): Promise<void> => {
|
||||||
|
stats.setGraphPreferences(input);
|
||||||
|
},
|
||||||
Stats.GraphPreferences.toObject.bind(Stats.GraphPreferences) as (
|
Stats.GraphPreferences.toObject.bind(Stats.GraphPreferences) as (
|
||||||
preferences: Stats.GraphPreferences,
|
preferences: Stats.GraphPreferences,
|
||||||
options: { defaults: boolean },
|
options: { defaults: boolean },
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { FluentBundle, FluentResource } from "@fluent/bundle";
|
||||||
|
|
||||||
import { firstLanguage, setBundles } from "./bundles";
|
import { firstLanguage, setBundles } from "./bundles";
|
||||||
import type { ModuleName } from "./modules";
|
import type { ModuleName } from "./modules";
|
||||||
|
import { i18n } from "../proto";
|
||||||
|
|
||||||
export function supportsVerticalText(): boolean {
|
export function supportsVerticalText(): boolean {
|
||||||
const firstLang = firstLanguage();
|
const firstLang = firstLanguage();
|
||||||
|
@ -73,19 +74,13 @@ export function withoutUnicodeIsolation(s: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupI18n(args: { modules: ModuleName[] }): Promise<void> {
|
export async function setupI18n(args: { modules: ModuleName[] }): Promise<void> {
|
||||||
const resp = await fetch("/_anki/i18nResources", {
|
const resources = await i18n.i18nResources(args);
|
||||||
method: "POST",
|
const json = JSON.parse(String.fromCharCode(...resources.json));
|
||||||
body: JSON.stringify(args),
|
|
||||||
});
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw Error(`unexpected reply: ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
const json = await resp.json();
|
|
||||||
|
|
||||||
const newBundles: FluentBundle[] = [];
|
const newBundles: FluentBundle[] = [];
|
||||||
for (const i in json.resources) {
|
for (const res in json.resources) {
|
||||||
const text = json.resources[i];
|
const text = json.resources[res];
|
||||||
const lang = json.langs[i];
|
const lang = json.langs[res];
|
||||||
const bundle = new FluentBundle([lang, "en-US"]);
|
const bundle = new FluentBundle([lang, "en-US"]);
|
||||||
const resource = new FluentResource(text);
|
const resource = new FluentResource(text);
|
||||||
bundle.addResource(resource);
|
bundle.addResource(resource);
|
||||||
|
|
|
@ -1,17 +1,77 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
import { anki } from "./backend_proto";
|
import { anki } from "./backend_proto";
|
||||||
|
import type { RPCImpl, RPCImplCallback, Message, rpc } from "protobufjs";
|
||||||
|
|
||||||
import Cards = anki.cards;
|
import Cards = anki.cards;
|
||||||
|
import Collection = anki.collection;
|
||||||
import DeckConfig = anki.deckconfig;
|
import DeckConfig = anki.deckconfig;
|
||||||
|
import Decks = anki.decks;
|
||||||
import Generic = anki.generic;
|
import Generic = anki.generic;
|
||||||
|
import I18n = anki.i18n;
|
||||||
|
import Notes = anki.notes;
|
||||||
import Notetypes = anki.notetypes;
|
import Notetypes = anki.notetypes;
|
||||||
import Scheduler = anki.scheduler;
|
import Scheduler = anki.scheduler;
|
||||||
import Stats = anki.stats;
|
import Stats = anki.stats;
|
||||||
import Tags = anki.tags;
|
import Tags = anki.tags;
|
||||||
|
|
||||||
export { Stats, Cards, DeckConfig, Notetypes, Scheduler, Tags };
|
export { Cards, Collection, Decks, Generic, Notes };
|
||||||
|
|
||||||
|
export const empty = Generic.Empty.encode(Generic.Empty.create()).finish();
|
||||||
|
|
||||||
|
async function serviceCallback(
|
||||||
|
method: rpc.ServiceMethod<Message<any>, Message<any>>,
|
||||||
|
requestData: Uint8Array,
|
||||||
|
callback: RPCImplCallback,
|
||||||
|
): Promise<void> {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set("Content-type", "application/octet-stream");
|
||||||
|
|
||||||
|
const methodName = method.name[0].toLowerCase() + method.name.substring(1);
|
||||||
|
const path = `/_anki/${methodName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch(path, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: requestData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await result.blob();
|
||||||
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
callback(null, uint8Array);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error caught");
|
||||||
|
callback(error as Error, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DeckConfig };
|
||||||
|
export const deckConfig = DeckConfig.DeckConfigService.create(
|
||||||
|
serviceCallback as RPCImpl,
|
||||||
|
);
|
||||||
|
|
||||||
|
export { I18n };
|
||||||
|
export const i18n = I18n.I18nService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
export { Notetypes };
|
||||||
|
export const notetypes = Notetypes.NotetypesService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
export { Scheduler };
|
||||||
|
export const scheduler = Scheduler.SchedulerService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
export { Stats };
|
||||||
|
export const stats = Stats.StatsService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
export { Tags };
|
||||||
|
export const tags = Tags.TagsService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
export function unwrapOptionalNumber(
|
export function unwrapOptionalNumber(
|
||||||
msg: Generic.IInt64 | Generic.IUInt32 | Generic.IInt32 | null | undefined,
|
msg: Generic.IInt64 | Generic.IUInt32 | Generic.IInt32 | null | undefined,
|
||||||
|
|
Loading…
Reference in a new issue