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(
|
||||
self, module_index: int, message_index: int, **kwargs: str | int | float
|
||||
) -> str:
|
||||
return self.translate_string(
|
||||
translate_string_in(
|
||||
module_index=module_index, message_index=message_index, **kwargs
|
||||
)
|
||||
args = {
|
||||
k: i18n_pb2.TranslateArgValue(str=v)
|
||||
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(
|
||||
self,
|
||||
seconds: Any,
|
||||
|
@ -123,31 +133,17 @@ class RustBackend(RustBackendGenerated):
|
|||
)
|
||||
return self.format_timespan(seconds=seconds, context=context)
|
||||
|
||||
def _run_command(self, service: int, method: int, input: Any) -> bytes:
|
||||
input_bytes = input.SerializeToString()
|
||||
def _run_command(self, service: int, method: int, input: bytes) -> bytes:
|
||||
try:
|
||||
return self._backend.command(service, method, input_bytes)
|
||||
return self._backend.command(service, method, input)
|
||||
except Exception as error:
|
||||
err_bytes = bytes(error.args[0])
|
||||
error_bytes = bytes(error.args[0])
|
||||
|
||||
err = backend_pb2.BackendError()
|
||||
err.ParseFromString(err_bytes)
|
||||
err.ParseFromString(error_bytes)
|
||||
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):
|
||||
def __init__(self, backend: ref[RustBackend] | None):
|
||||
self.backend = backend
|
||||
|
|
|
@ -51,24 +51,7 @@ LABEL_OPTIONAL = 1
|
|||
LABEL_REQUIRED = 2
|
||||
LABEL_REPEATED = 3
|
||||
|
||||
# messages we don't want to unroll in codegen
|
||||
SKIP_UNROLL_INPUT = {
|
||||
"TranslateString",
|
||||
"SetPreferences",
|
||||
"UpdateDeckConfigs",
|
||||
"AnswerCard",
|
||||
"ChangeNotetype",
|
||||
"CompleteTag",
|
||||
}
|
||||
SKIP_UNROLL_OUTPUT = {"GetPreferences"}
|
||||
|
||||
SKIP_DECODE = {
|
||||
"Graphs",
|
||||
"GetGraphPreferences",
|
||||
"GetChangeNotetypeInfo",
|
||||
"CompleteTag",
|
||||
"CardStats",
|
||||
}
|
||||
RAW_ONLY = {"TranslateString"}
|
||||
|
||||
|
||||
def python_type(field):
|
||||
|
@ -116,62 +99,59 @@ def fix_snakecase(name):
|
|||
return name
|
||||
|
||||
|
||||
def get_input_args(msg):
|
||||
fields = sorted(msg.fields, key=lambda x: x.number)
|
||||
def get_input_args(input_type):
|
||||
fields = sorted(input_type.fields, key=lambda x: x.number)
|
||||
self_star = ["self"]
|
||||
if len(fields) >= 2:
|
||||
self_star.append("*")
|
||||
return ", ".join(self_star + [f"{f.name}: {python_type(f)}" for f in fields])
|
||||
|
||||
|
||||
def get_input_assign(msg):
|
||||
fields = sorted(msg.fields, key=lambda x: x.number)
|
||||
def get_input_assign(input_type):
|
||||
fields = sorted(input_type.fields, key=lambda x: x.number)
|
||||
return ", ".join(f"{f.name}={f.name}" for f in fields)
|
||||
|
||||
|
||||
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))
|
||||
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 (
|
||||
len(method.output_type.fields) == 1
|
||||
and method.output_type.fields[0].type != TYPE_ENUM
|
||||
and method.name not in SKIP_UNROLL_OUTPUT
|
||||
):
|
||||
# unwrap single return arg
|
||||
f = method.output_type.fields[0]
|
||||
single_field = f".{f.name}"
|
||||
return_type = python_type(f)
|
||||
single_attribute = f".{f.name}"
|
||||
else:
|
||||
single_field = ""
|
||||
return_type = fullname(method.output_type.full_name)
|
||||
|
||||
if method.name in SKIP_DECODE:
|
||||
return_type = "bytes"
|
||||
single_attribute = ""
|
||||
|
||||
buf = f"""\
|
||||
def {name}({input_args}) -> {return_type}:
|
||||
{input_assign_outer}"""
|
||||
def {name}_raw(self, message: bytes) -> bytes:
|
||||
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)}()
|
||||
output.ParseFromString(self._run_command({service_idx}, {method_idx}, input))
|
||||
return output{single_field}
|
||||
|
||||
if not method.name in RAW_ONLY:
|
||||
buf += f"""\
|
||||
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
|
||||
|
|
|
@ -804,25 +804,12 @@ class Collection(DeprecatedNamesMixin):
|
|||
|
||||
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)
|
||||
|
||||
def studied_today(self) -> str:
|
||||
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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -285,7 +285,8 @@ class DeckManager(DeprecatedNamesMixin):
|
|||
return self.col._backend.get_deck_configs_for_update(deck_id)
|
||||
|
||||
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]:
|
||||
"A list of all deck config."
|
||||
|
|
|
@ -370,7 +370,7 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
|
||||
def change_notetype_info(
|
||||
self, *, old_notetype_id: NotetypeId, new_notetype_id: NotetypeId
|
||||
) -> bytes:
|
||||
) -> ChangeNotetypeInfo:
|
||||
return self.col._backend.get_change_notetype_info(
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -414,15 +415,13 @@ and notes.mid = ? and cards.ord = ?""",
|
|||
template_map = self._convert_legacy_map(cmap, len(newModel["tmpls"]))
|
||||
|
||||
self.col._backend.change_notetype(
|
||||
ChangeNotetypeRequest(
|
||||
note_ids=nids,
|
||||
new_fields=field_map,
|
||||
new_templates=template_map,
|
||||
old_notetype_name=notetype["name"],
|
||||
old_notetype_id=notetype["id"],
|
||||
new_notetype_id=newModel["id"],
|
||||
current_schema=self.col.db.scalar("select scm from col"),
|
||||
)
|
||||
note_ids=nids,
|
||||
new_fields=field_map,
|
||||
new_templates=template_map,
|
||||
old_notetype_name=notetype["name"],
|
||||
old_notetype_id=notetype["id"],
|
||||
new_notetype_id=newModel["id"],
|
||||
current_schema=self.col.db.scalar("select scm from col"),
|
||||
)
|
||||
|
||||
def _convert_legacy_map(
|
||||
|
|
|
@ -86,7 +86,8 @@ class Scheduler(SchedulerBaseWithLegacy):
|
|||
def answer_card(self, input: CardAnswer) -> OpChanges:
|
||||
"Update card to provided state, and remove it from queue."
|
||||
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:
|
||||
"True if new state marks the card as a leech."
|
||||
|
|
|
@ -26,6 +26,7 @@ from anki.utils import ids2str
|
|||
|
||||
# public exports
|
||||
TagTreeNode = tags_pb2.TagTreeNode
|
||||
CompleteTagRequest = tags_pb2.CompleteTagRequest
|
||||
MARKED_TAG = "marked"
|
||||
|
||||
|
||||
|
@ -68,11 +69,6 @@ class TagManager(DeprecatedNamesMixin):
|
|||
"Set browser expansion state for tag, registering the tag if missing."
|
||||
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
|
||||
#############################################################
|
||||
|
||||
|
|
|
@ -15,14 +15,13 @@ def test_stats():
|
|||
col.addNote(note)
|
||||
c = note.cards()[0]
|
||||
# card stats
|
||||
card_stats = CardStats()
|
||||
card_stats.ParseFromString(col.card_stats_data(c.id))
|
||||
card_stats = col.card_stats_data(c.id)
|
||||
assert card_stats.note_id == note.id
|
||||
col.reset()
|
||||
c = col.sched.getCard()
|
||||
col.sched.answerCard(c, 3)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import time
|
|||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import flask
|
||||
import flask_cors # type: ignore
|
||||
|
@ -21,15 +22,14 @@ from waitress.server import create_server
|
|||
|
||||
import aqt
|
||||
from anki import hooks
|
||||
from anki.cards import CardId
|
||||
from anki.collection import GraphPreferences, OpChanges
|
||||
from anki.decks import UpdateDeckConfigs
|
||||
from anki.models import NotetypeNames
|
||||
from anki._vendor import stringcase
|
||||
from anki.collection import OpChanges
|
||||
from anki.decks import DeckConfigsForUpdate, UpdateDeckConfigs
|
||||
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.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 *
|
||||
|
||||
app = flask.Flask(__name__, root_path="/fake")
|
||||
|
@ -375,40 +375,20 @@ def _extract_request(
|
|||
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:
|
||||
if not aqt.mw.col.sched._is_finished():
|
||||
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:
|
||||
args = from_json_bytes(request.data)
|
||||
return aqt.mw.col.i18n_resources(modules=args["modules"])
|
||||
def get_deck_configs_for_update() -> bytes:
|
||||
config_bytes = aqt.mw.col._backend.get_deck_configs_for_update_raw(request.data)
|
||||
configs = DeckConfigsForUpdate.FromString(config_bytes)
|
||||
configs.have_addons = aqt.mw.addonManager.dirty
|
||||
return configs.SerializeToString()
|
||||
|
||||
|
||||
def deck_configs_for_update() -> 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:
|
||||
def update_deck_configs() -> bytes:
|
||||
# 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
|
||||
# main, and return immediately from the web request
|
||||
|
@ -421,7 +401,7 @@ def update_deck_configs_request() -> bytes:
|
|||
window.reject()
|
||||
|
||||
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
|
||||
).run_in_background()
|
||||
|
||||
|
@ -444,18 +424,6 @@ def set_next_card_states() -> bytes:
|
|||
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:
|
||||
data = request.data
|
||||
|
||||
|
@ -468,31 +436,49 @@ def change_notetype() -> bytes:
|
|||
return b""
|
||||
|
||||
|
||||
def complete_tag() -> bytes:
|
||||
return aqt.mw.col.tags.complete_tag(request.data)
|
||||
post_handler_list = [
|
||||
congrats_info,
|
||||
get_deck_configs_for_update,
|
||||
update_deck_configs,
|
||||
next_card_states,
|
||||
set_next_card_states,
|
||||
change_notetype,
|
||||
]
|
||||
|
||||
|
||||
def card_stats() -> bytes:
|
||||
args = from_json_bytes(request.data)
|
||||
return aqt.mw.col.card_stats_data(CardId(args["cardId"]))
|
||||
exposed_backend_list = [
|
||||
# I18nService
|
||||
"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 = {
|
||||
"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,
|
||||
"changeNotetypeInfo": change_notetype_info,
|
||||
"notetypeNames": notetype_names,
|
||||
"changeNotetype": change_notetype,
|
||||
"i18nResources": i18n_resources,
|
||||
"congratsInfo": congrats_info,
|
||||
"completeTag": complete_tag,
|
||||
"cardStats": card_stats,
|
||||
stringcase.camelcase(handler.__name__): handler for handler in post_handler_list
|
||||
} | {
|
||||
stringcase.camelcase(handler): raw_backend_request(handler)
|
||||
for handler in exposed_backend_list
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import type { Stats } from "../lib/proto";
|
||||
import { getCardStats } from "./lib";
|
||||
import { stats as statsService } from "../lib/proto";
|
||||
import Container from "../components/Container.svelte";
|
||||
import Row from "../components/Row.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> {
|
||||
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. */
|
||||
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
|
||||
// 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 { notetypes, empty } from "../lib/proto";
|
||||
import { checkNightMode } from "../lib/nightmode";
|
||||
import { ChangeNotetypeState } from "./lib";
|
||||
|
||||
import ChangeNotetypePage from "./ChangeNotetypePage.svelte";
|
||||
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(
|
||||
oldNotetypeId: number,
|
||||
newNotetypeId: number,
|
||||
): Promise<ChangeNotetypePage> {
|
||||
const [info, names] = await Promise.all([
|
||||
getChangeNotetypeInfo(oldNotetypeId, newNotetypeId),
|
||||
getNotetypeNames(),
|
||||
setupI18n({
|
||||
modules: [
|
||||
ModuleName.ACTIONS,
|
||||
ModuleName.CHANGE_NOTETYPE,
|
||||
ModuleName.KEYBOARD,
|
||||
],
|
||||
}),
|
||||
]);
|
||||
const changeNotetypeInfo = notetypes.getChangeNotetypeInfo({
|
||||
oldNotetypeId,
|
||||
newNotetypeId,
|
||||
});
|
||||
const [names, info] = await Promise.all([notetypeNames, changeNotetypeInfo, i18n]);
|
||||
|
||||
checkNightMode();
|
||||
|
||||
const state = new ChangeNotetypeState(names, info);
|
||||
return new ChangeNotetypePage({
|
||||
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
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import { Notetypes } from "../lib/proto";
|
||||
import { postRequest } from "../lib/postrequest";
|
||||
import { notetypes, Notetypes } from "../lib/proto";
|
||||
import { readable, Readable } from "svelte/store";
|
||||
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[] {
|
||||
return list.map((val) => val ?? -1);
|
||||
}
|
||||
|
@ -51,11 +24,11 @@ export class ChangeNotetypeInfoWrapper {
|
|||
|
||||
constructor(info: Notetypes.ChangeNotetypeInfo) {
|
||||
this.info = info;
|
||||
const templates = info.input!.newTemplates!;
|
||||
const templates = info.input?.newTemplates ?? [];
|
||||
if (templates.length > 0) {
|
||||
this.templates = negativeOneToNull(templates);
|
||||
}
|
||||
this.fields = negativeOneToNull(info.input!.newFields!);
|
||||
this.fields = negativeOneToNull(info.input?.newFields ?? []);
|
||||
this.oldNotetypeName = info.oldNotetypeName;
|
||||
}
|
||||
|
||||
|
@ -141,6 +114,7 @@ export enum MapContext {
|
|||
Field,
|
||||
Template,
|
||||
}
|
||||
|
||||
export class ChangeNotetypeState {
|
||||
readonly info: Readable<ChangeNotetypeInfoWrapper>;
|
||||
readonly notetypes: Readable<NotetypeListEntry[]>;
|
||||
|
@ -168,10 +142,11 @@ export class ChangeNotetypeState {
|
|||
async setTargetNotetypeIndex(idx: number): Promise<void> {
|
||||
this.info_.input().newNotetypeId = this.notetypeNames.entries[idx].id!;
|
||||
this.notetypesSetter(this.buildNotetypeList());
|
||||
const newInfo = await getChangeNotetypeInfo(
|
||||
this.info_.input().oldNotetypeId,
|
||||
this.info_.input().newNotetypeId,
|
||||
);
|
||||
const { oldNotetypeId, newNotetypeId } = this.info_.input();
|
||||
const newInfo = await notetypes.getChangeNotetypeInfo({
|
||||
oldNotetypeId,
|
||||
newNotetypeId,
|
||||
});
|
||||
|
||||
this.info_ = new ChangeNotetypeInfoWrapper(newInfo);
|
||||
this.info_.unusedItems(MapContext.Field);
|
||||
|
@ -197,16 +172,16 @@ export class ChangeNotetypeState {
|
|||
this.infoSetter(this.info_);
|
||||
}
|
||||
|
||||
dataForSaving(): Notetypes.ChangeNotetypeRequest {
|
||||
return this.info_.intoInput();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
if (this.info_.unchanged()) {
|
||||
alert("No changes to save");
|
||||
return;
|
||||
}
|
||||
await changeNotetype(this.dataForSaving());
|
||||
}
|
||||
|
||||
dataForSaving(): Notetypes.ChangeNotetypeRequest {
|
||||
return this.info_.intoInput();
|
||||
await notetypes.changeNotetype(this.dataForSaving());
|
||||
}
|
||||
|
||||
private buildNotetypeList(): NotetypeListEntry[] {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { checkNightMode } from "../lib/nightmode";
|
||||
|
||||
|
@ -14,16 +14,15 @@ export async function setupCongrats(): Promise<CongratsPage> {
|
|||
checkNightMode();
|
||||
await i18n;
|
||||
|
||||
const info = await getCongratsInfo();
|
||||
const info = await scheduler.congratsInfo(empty);
|
||||
const page = new CongratsPage({
|
||||
target: document.body,
|
||||
props: { info },
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
getCongratsInfo().then((info) => {
|
||||
page.$set({ info });
|
||||
});
|
||||
setInterval(async () => {
|
||||
const info = await scheduler.congratsInfo(empty);
|
||||
page.$set({ info });
|
||||
}, 60000);
|
||||
|
||||
return page;
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { Scheduler } from "../lib/proto";
|
||||
import { postRequest } from "../lib/postrequest";
|
||||
import type { Scheduler } from "../lib/proto";
|
||||
import { naturalUnit, unitAmount, unitName } from "../lib/time";
|
||||
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 {
|
||||
const secsUntil = info.secsUntilNextLearn;
|
||||
// next learning card not due today?
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
import "../sveltelib/export-runtime";
|
||||
|
||||
import { getDeckOptionsInfo, DeckOptionsState } from "./lib";
|
||||
import { DeckOptionsState } from "./lib";
|
||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||
import { deckConfig } from "../lib/proto";
|
||||
import { checkNightMode } from "../lib/nightmode";
|
||||
import { touchDeviceKey, modalsKey } from "../components/context-keys";
|
||||
|
||||
|
@ -24,8 +25,11 @@ const i18n = setupI18n({
|
|||
],
|
||||
});
|
||||
|
||||
export async function setupDeckOptions(deckId: number): Promise<DeckOptionsPage> {
|
||||
const [info] = await Promise.all([getDeckOptionsInfo(deckId), i18n]);
|
||||
export async function setupDeckOptions(did: number): Promise<DeckOptionsPage> {
|
||||
const [info] = await Promise.all([
|
||||
deckConfig.getDeckConfigsForUpdate({ did }),
|
||||
i18n,
|
||||
]);
|
||||
|
||||
checkNightMode();
|
||||
|
||||
|
@ -33,7 +37,7 @@ export async function setupDeckOptions(deckId: number): Promise<DeckOptionsPage>
|
|||
context.set(modalsKey, new Map());
|
||||
context.set(touchDeviceKey, "ontouchstart" in document.documentElement);
|
||||
|
||||
const state = new DeckOptionsState(deckId, info);
|
||||
const state = new DeckOptionsState(did, info);
|
||||
return new DeckOptionsPage({
|
||||
target: document.body,
|
||||
props: { state },
|
||||
|
|
|
@ -244,8 +244,8 @@ test("saving", () => {
|
|||
expect(out.targetDeckId).toBe(123);
|
||||
// in no-changes case, currently selected config should
|
||||
// be returned
|
||||
expect(out.configs.length).toBe(1);
|
||||
expect(out.configs[0].name).toBe("another one");
|
||||
expect(out.configs!.length).toBe(1);
|
||||
expect(out.configs![0].name).toBe("another one");
|
||||
expect(out.applyToChildren).toBe(false);
|
||||
|
||||
// rename, then change current deck
|
||||
|
@ -255,7 +255,7 @@ test("saving", () => {
|
|||
|
||||
// renamed deck should be in changes, with current deck as last element
|
||||
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);
|
||||
|
||||
// start again, adding new deck
|
||||
|
@ -275,7 +275,7 @@ test("saving", () => {
|
|||
// only contain Default, which is the new current deck
|
||||
out = state.dataForSaving(true);
|
||||
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", () => {
|
||||
|
@ -302,8 +302,8 @@ test("aux data", () => {
|
|||
|
||||
// ensure changes serialize
|
||||
const out = state.dataForSaving(true);
|
||||
expect(out.configs.length).toBe(2);
|
||||
const json = out.configs.map(
|
||||
expect(out.configs!.length).toBe(2);
|
||||
const json = out.configs!.map(
|
||||
(c) =>
|
||||
JSON.parse(new TextDecoder().decode((c.config as any).other)) as Record<
|
||||
string,
|
||||
|
|
|
@ -1,29 +1,12 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { DeckConfig } from "../lib/proto";
|
||||
import { postRequest } from "../lib/postrequest";
|
||||
import { DeckConfig, deckConfig } from "../lib/proto";
|
||||
import { Writable, writable, get, Readable, readable } from "svelte/store";
|
||||
import { isEqual, cloneDeep } from "lodash-es";
|
||||
import { localeCompare } from "../lib/i18n";
|
||||
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 interface ConfigWithCount {
|
||||
|
@ -185,7 +168,9 @@ export class DeckOptionsState {
|
|||
this.updateConfigList();
|
||||
}
|
||||
|
||||
dataForSaving(applyToChildren: boolean): DeckConfig.UpdateDeckConfigsRequest {
|
||||
dataForSaving(
|
||||
applyToChildren: boolean,
|
||||
): NonNullable<DeckConfig.IUpdateDeckConfigsRequest> {
|
||||
const modifiedConfigsExcludingCurrent = this.configs
|
||||
.map((c) => c.config)
|
||||
.filter((c, idx) => {
|
||||
|
@ -199,17 +184,17 @@ export class DeckOptionsState {
|
|||
// current must come last, even if unmodified
|
||||
this.configs[this.selectedIdx].config,
|
||||
];
|
||||
return DeckConfig.UpdateDeckConfigsRequest.create({
|
||||
return {
|
||||
targetDeckId: this.targetDeckId,
|
||||
removedConfigIds: this.removedConfigs,
|
||||
configs,
|
||||
applyToChildren,
|
||||
cardStateCustomizer: get(this.cardStateCustomizer),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async save(applyToChildren: boolean): Promise<void> {
|
||||
await saveDeckOptions(this.dataForSaving(applyToChildren));
|
||||
await deckConfig.updateDeckConfigs(this.dataForSaving(applyToChildren));
|
||||
}
|
||||
|
||||
private onCurrentConfigChanged(config: ConfigInner): void {
|
||||
|
|
|
@ -19,8 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
replaceWithUnicodeSeparator,
|
||||
replaceWithColons,
|
||||
} from "./tags";
|
||||
import { Tags } from "../lib/proto";
|
||||
import { postRequest } from "../lib/postrequest";
|
||||
import { tags as tagsService } from "../lib/proto";
|
||||
import { execCommand } from "./helpers";
|
||||
|
||||
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;
|
||||
|
||||
async function fetchSuggestions(input: string): Promise<string[]> {
|
||||
const data = await postRequest(
|
||||
"/_anki/completeTag",
|
||||
Tags.CompleteTagRequest.encode(
|
||||
Tags.CompleteTagRequest.create({ input, matchLimit: 500 }),
|
||||
).finish(),
|
||||
);
|
||||
const response = Tags.CompleteTagResponse.decode(data);
|
||||
return response.tags;
|
||||
const { tags } = await tagsService.completeTag({ input, matchLimit: 500 });
|
||||
return tags;
|
||||
}
|
||||
|
||||
const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
|
||||
|
|
|
@ -4,49 +4,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { PreferenceRaw, PreferencePayload } from "../sveltelib/preferences";
|
||||
|
||||
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 useAsyncReactive from "../sveltelib/asyncReactive";
|
||||
import { getPreferences } from "../sveltelib/preferences";
|
||||
|
||||
import { daysToRevlogRange } from "./graph-helpers";
|
||||
|
||||
export let search: Writable<string>;
|
||||
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 {
|
||||
loading: graphLoading,
|
||||
error: graphError,
|
||||
value: graphValue,
|
||||
} = useAsyncReactive(() => getGraphData($search, $days), [search, days]);
|
||||
} = useAsyncReactive(
|
||||
() => stats.graphs({ search: $search, days: $days }),
|
||||
[search, days],
|
||||
);
|
||||
|
||||
const {
|
||||
loading: prefsLoading,
|
||||
|
@ -54,8 +30,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
value: prefsValue,
|
||||
} = useAsync(() =>
|
||||
getPreferences(
|
||||
getGraphPreferences,
|
||||
setGraphPreferences,
|
||||
() => stats.getGraphPreferences(empty),
|
||||
async (input: Stats.IGraphPreferences): Promise<void> => {
|
||||
stats.setGraphPreferences(input);
|
||||
},
|
||||
Stats.GraphPreferences.toObject.bind(Stats.GraphPreferences) as (
|
||||
preferences: Stats.GraphPreferences,
|
||||
options: { defaults: boolean },
|
||||
|
|
|
@ -6,6 +6,7 @@ import { FluentBundle, FluentResource } from "@fluent/bundle";
|
|||
|
||||
import { firstLanguage, setBundles } from "./bundles";
|
||||
import type { ModuleName } from "./modules";
|
||||
import { i18n } from "../proto";
|
||||
|
||||
export function supportsVerticalText(): boolean {
|
||||
const firstLang = firstLanguage();
|
||||
|
@ -73,19 +74,13 @@ export function withoutUnicodeIsolation(s: string): string {
|
|||
}
|
||||
|
||||
export async function setupI18n(args: { modules: ModuleName[] }): Promise<void> {
|
||||
const resp = await fetch("/_anki/i18nResources", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw Error(`unexpected reply: ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
const resources = await i18n.i18nResources(args);
|
||||
const json = JSON.parse(String.fromCharCode(...resources.json));
|
||||
|
||||
const newBundles: FluentBundle[] = [];
|
||||
for (const i in json.resources) {
|
||||
const text = json.resources[i];
|
||||
const lang = json.langs[i];
|
||||
for (const res in json.resources) {
|
||||
const text = json.resources[res];
|
||||
const lang = json.langs[res];
|
||||
const bundle = new FluentBundle([lang, "en-US"]);
|
||||
const resource = new FluentResource(text);
|
||||
bundle.addResource(resource);
|
||||
|
|
|
@ -1,17 +1,77 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 type { RPCImpl, RPCImplCallback, Message, rpc } from "protobufjs";
|
||||
|
||||
import Cards = anki.cards;
|
||||
import Collection = anki.collection;
|
||||
import DeckConfig = anki.deckconfig;
|
||||
import Decks = anki.decks;
|
||||
import Generic = anki.generic;
|
||||
import I18n = anki.i18n;
|
||||
import Notes = anki.notes;
|
||||
import Notetypes = anki.notetypes;
|
||||
import Scheduler = anki.scheduler;
|
||||
import Stats = anki.stats;
|
||||
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(
|
||||
msg: Generic.IInt64 | Generic.IUInt32 | Generic.IInt32 | null | undefined,
|
||||
|
|
Loading…
Reference in a new issue