mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 20:57:13 -05:00
Merge 9011cf87ff into dac26ce671
This commit is contained in:
commit
94289b2480
37 changed files with 1032 additions and 18 deletions
|
|
@ -228,6 +228,17 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
|
||||||
":sveltekit"
|
":sveltekit"
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
build_page(
|
||||||
|
"reviewer-inner",
|
||||||
|
true,
|
||||||
|
inputs![
|
||||||
|
//
|
||||||
|
":ts:lib",
|
||||||
|
":ts:components",
|
||||||
|
":sass",
|
||||||
|
":sveltekit"
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ preferences-on-next-sync-force-changes-in = On next sync, force changes in one d
|
||||||
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
|
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
|
||||||
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
|
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
|
||||||
preferences-generate-latex-images-automatically = Generate LaTeX images (security risk)
|
preferences-generate-latex-images-automatically = Generate LaTeX images (security risk)
|
||||||
|
preferences-use-new-reviewer = Use new reviewer
|
||||||
preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences.
|
preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences.
|
||||||
preferences-periodically-sync-media = Periodically sync media
|
preferences-periodically-sync-media = Periodically sync media
|
||||||
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
|
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ message ConfigKey {
|
||||||
LOAD_BALANCER_ENABLED = 26;
|
LOAD_BALANCER_ENABLED = 26;
|
||||||
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
|
FSRS_SHORT_TERM_WITH_STEPS_ENABLED = 27;
|
||||||
FSRS_LEGACY_EVALUATE = 28;
|
FSRS_LEGACY_EVALUATE = 28;
|
||||||
|
NEW_REVIEWER = 29;
|
||||||
}
|
}
|
||||||
enum String {
|
enum String {
|
||||||
SET_DUE_BROWSER = 0;
|
SET_DUE_BROWSER = 0;
|
||||||
|
|
@ -120,6 +121,7 @@ message Preferences {
|
||||||
uint32 time_limit_secs = 5;
|
uint32 time_limit_secs = 5;
|
||||||
bool load_balancer_enabled = 6;
|
bool load_balancer_enabled = 6;
|
||||||
bool fsrs_short_term_with_steps_enabled = 7;
|
bool fsrs_short_term_with_steps_enabled = 7;
|
||||||
|
bool new_reviewer = 8;
|
||||||
}
|
}
|
||||||
message Editing {
|
message Editing {
|
||||||
bool adding_defaults_to_current_deck = 1;
|
bool adding_defaults_to_current_deck = 1;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package anki.frontend;
|
||||||
import "anki/scheduler.proto";
|
import "anki/scheduler.proto";
|
||||||
import "anki/generic.proto";
|
import "anki/generic.proto";
|
||||||
import "anki/search.proto";
|
import "anki/search.proto";
|
||||||
|
import "anki/card_rendering.proto";
|
||||||
|
|
||||||
service FrontendService {
|
service FrontendService {
|
||||||
// Returns values from the reviewer
|
// Returns values from the reviewer
|
||||||
|
|
@ -30,6 +31,9 @@ service FrontendService {
|
||||||
|
|
||||||
// Save colour picker's custom colour palette
|
// Save colour picker's custom colour palette
|
||||||
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
|
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
|
||||||
|
|
||||||
|
// Plays the listed AV tags
|
||||||
|
rpc PlayAVTags(PlayAVTagsRequest) returns (generic.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
service BackendFrontendService {}
|
service BackendFrontendService {}
|
||||||
|
|
@ -43,3 +47,7 @@ message SetSchedulingStatesRequest {
|
||||||
string key = 1;
|
string key = 1;
|
||||||
scheduler.SchedulingStates states = 2;
|
scheduler.SchedulingStates states = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message PlayAVTagsRequest {
|
||||||
|
repeated card_rendering.AVTag tags = 1;
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,12 @@ import "anki/decks.proto";
|
||||||
import "anki/collection.proto";
|
import "anki/collection.proto";
|
||||||
import "anki/config.proto";
|
import "anki/config.proto";
|
||||||
import "anki/deck_config.proto";
|
import "anki/deck_config.proto";
|
||||||
|
import "anki/card_rendering.proto";
|
||||||
|
|
||||||
service SchedulerService {
|
service SchedulerService {
|
||||||
rpc GetQueuedCards(GetQueuedCardsRequest) returns (QueuedCards);
|
rpc GetQueuedCards(GetQueuedCardsRequest) returns (QueuedCards);
|
||||||
rpc AnswerCard(CardAnswer) returns (collection.OpChanges);
|
rpc AnswerCard(CardAnswer) returns (collection.OpChanges);
|
||||||
|
rpc NextCardData(NextCardDataRequest) returns (NextCardDataResponse);
|
||||||
rpc SchedTimingToday(generic.Empty) returns (SchedTimingTodayResponse);
|
rpc SchedTimingToday(generic.Empty) returns (SchedTimingTodayResponse);
|
||||||
rpc StudiedToday(generic.Empty) returns (generic.String);
|
rpc StudiedToday(generic.Empty) returns (generic.String);
|
||||||
rpc StudiedTodayMessage(StudiedTodayMessageRequest) returns (generic.String);
|
rpc StudiedTodayMessage(StudiedTodayMessageRequest) returns (generic.String);
|
||||||
|
|
@ -285,6 +287,43 @@ message CardAnswer {
|
||||||
uint32 milliseconds_taken = 6;
|
uint32 milliseconds_taken = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message NextCardDataRequest {
|
||||||
|
optional CardAnswer answer = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NextCardDataResponse {
|
||||||
|
message AnswerButton {
|
||||||
|
CardAnswer.Rating rating = 1;
|
||||||
|
string due = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NextCardData {
|
||||||
|
QueuedCards queue = 1;
|
||||||
|
repeated AnswerButton answer_buttons = 2;
|
||||||
|
|
||||||
|
string front = 3;
|
||||||
|
string back = 4;
|
||||||
|
string css = 5;
|
||||||
|
string body_class = 6;
|
||||||
|
bool autoplay = 7;
|
||||||
|
optional string typed_answer = 12;
|
||||||
|
optional string typed_answer_args = 13;
|
||||||
|
|
||||||
|
repeated card_rendering.AVTag question_av_tags = 8;
|
||||||
|
repeated card_rendering.AVTag answer_av_tags = 9;
|
||||||
|
|
||||||
|
// TODO: We can probably make this a little faster by using oneof and
|
||||||
|
// preventing the partial_front and back being sent to svelte where it isn't
|
||||||
|
// used. Alternatively we can use a completely different message for both
|
||||||
|
// Rust -> Python and the Python -> Svelte though this would be more
|
||||||
|
// complicated to implement.
|
||||||
|
repeated card_rendering.RenderedTemplateNode partial_front = 10;
|
||||||
|
repeated card_rendering.RenderedTemplateNode partial_back = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional NextCardData next_card = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message CustomStudyRequest {
|
message CustomStudyRequest {
|
||||||
message Cram {
|
message Cram {
|
||||||
enum CramKind {
|
enum CramKind {
|
||||||
|
|
|
||||||
|
|
@ -81,4 +81,4 @@ button {
|
||||||
#outer {
|
#outer {
|
||||||
border-top-color: color(border-subtle);
|
border-top-color: color(border-subtle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -451,6 +451,19 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="new_reviewer">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>preferences_use_new_reviewer</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="url_schemes">
|
<widget class="QPushButton" name="url_schemes">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
|
|
||||||
|
|
@ -255,13 +255,11 @@ class AnkiQt(QMainWindow):
|
||||||
# screens
|
# screens
|
||||||
self.setupDeckBrowser()
|
self.setupDeckBrowser()
|
||||||
self.setupOverview()
|
self.setupOverview()
|
||||||
self.setupReviewer()
|
# self.setupReviewer()
|
||||||
|
|
||||||
def finish_ui_setup(self) -> None:
|
def finish_ui_setup(self) -> None:
|
||||||
"Actions that are deferred until after add-on loading."
|
"Actions that are deferred until after add-on loading."
|
||||||
self.toolbar.draw()
|
self.toolbar.draw()
|
||||||
# add-ons are only available here after setupAddons
|
|
||||||
gui_hooks.reviewer_did_init(self.reviewer)
|
|
||||||
|
|
||||||
def setupProfileAfterWebviewsLoaded(self) -> None:
|
def setupProfileAfterWebviewsLoaded(self) -> None:
|
||||||
for w in (self.web, self.bottomWeb):
|
for w in (self.web, self.bottomWeb):
|
||||||
|
|
@ -679,6 +677,8 @@ class AnkiQt(QMainWindow):
|
||||||
# dump error to stderr so it gets picked up by errors.py
|
# dump error to stderr so it gets picked up by errors.py
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
self.setupReviewer(self.backend.get_config_bool(Config.Bool.NEW_REVIEWER))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _loadCollection(self) -> None:
|
def _loadCollection(self) -> None:
|
||||||
|
|
@ -1074,10 +1074,13 @@ title="{}" {}>{}</button>""".format(
|
||||||
|
|
||||||
self.overview = Overview(self)
|
self.overview = Overview(self)
|
||||||
|
|
||||||
def setupReviewer(self) -> None:
|
def setupReviewer(self, new: bool) -> None:
|
||||||
from aqt.reviewer import Reviewer
|
from aqt.reviewer import Reviewer, SvelteReviewer
|
||||||
|
|
||||||
self.reviewer = Reviewer(self)
|
self.reviewer = SvelteReviewer(self) if new else Reviewer(self)
|
||||||
|
|
||||||
|
# add-ons are only available here after setupAddons
|
||||||
|
gui_hooks.reviewer_did_init(self.reviewer)
|
||||||
|
|
||||||
# Syncing
|
# Syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,18 @@ import aqt
|
||||||
import aqt.main
|
import aqt.main
|
||||||
import aqt.operations
|
import aqt.operations
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
from anki.cards import Card
|
||||||
from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode
|
from anki.collection import OpChanges, OpChangesOnly, Progress, SearchNode
|
||||||
from anki.decks import UpdateDeckConfigs
|
from anki.decks import UpdateDeckConfigs
|
||||||
|
from anki.frontend_pb2 import PlayAVTagsRequest
|
||||||
from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest
|
from anki.scheduler.v3 import SchedulingStatesWithContext, SetSchedulingStatesRequest
|
||||||
|
from anki.scheduler_pb2 import NextCardDataResponse
|
||||||
|
from anki.template import (
|
||||||
|
PartiallyRenderedCard,
|
||||||
|
TemplateRenderContext,
|
||||||
|
apply_custom_filters,
|
||||||
|
av_tags_to_native,
|
||||||
|
)
|
||||||
from anki.utils import dev_mode
|
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
|
||||||
|
|
@ -38,6 +47,8 @@ from aqt.operations import on_op_finished
|
||||||
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
||||||
from aqt.progress import ProgressUpdate
|
from aqt.progress import ProgressUpdate
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.sound import play_tags
|
||||||
|
from aqt.theme import ThemeManager
|
||||||
from aqt.utils import aqt_data_path, show_warning, tr
|
from aqt.utils import aqt_data_path, show_warning, tr
|
||||||
|
|
||||||
# https://forums.ankiweb.net/t/anki-crash-when-using-a-specific-deck/22266
|
# https://forums.ankiweb.net/t/anki-crash-when-using-a-specific-deck/22266
|
||||||
|
|
@ -363,6 +374,7 @@ def is_sveltekit_page(path: str) -> bool:
|
||||||
"import-csv",
|
"import-csv",
|
||||||
"import-page",
|
"import-page",
|
||||||
"image-occlusion",
|
"image-occlusion",
|
||||||
|
"reviewer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -637,6 +649,55 @@ def save_custom_colours() -> bytes:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
theme_manager = ThemeManager()
|
||||||
|
|
||||||
|
|
||||||
|
def next_card_data() -> bytes:
|
||||||
|
raw = aqt.mw.col._backend.next_card_data_raw(request.data)
|
||||||
|
data = NextCardDataResponse.FromString(raw)
|
||||||
|
backend_card = data.next_card.queue.cards[0].card
|
||||||
|
card = Card(aqt.mw.col, backend_card=backend_card)
|
||||||
|
|
||||||
|
ctx = TemplateRenderContext.from_existing_card(card, False)
|
||||||
|
|
||||||
|
qside = apply_custom_filters(
|
||||||
|
PartiallyRenderedCard.nodes_from_proto(data.next_card.partial_front),
|
||||||
|
ctx,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
aside = apply_custom_filters(
|
||||||
|
PartiallyRenderedCard.nodes_from_proto(data.next_card.partial_back),
|
||||||
|
ctx,
|
||||||
|
qside,
|
||||||
|
)
|
||||||
|
|
||||||
|
q_avtags = ctx.col()._backend.extract_av_tags(text=qside, question_side=True)
|
||||||
|
a_avtags = ctx.col()._backend.extract_av_tags(text=aside, question_side=False)
|
||||||
|
|
||||||
|
# Assumes the av tags are empty in the original response
|
||||||
|
data.next_card.question_av_tags.extend(q_avtags.av_tags)
|
||||||
|
data.next_card.answer_av_tags.extend(a_avtags.av_tags)
|
||||||
|
|
||||||
|
qside = q_avtags.text
|
||||||
|
aside = a_avtags.text
|
||||||
|
|
||||||
|
qside = aqt.mw.prepare_card_text_for_display(qside)
|
||||||
|
aside = aqt.mw.prepare_card_text_for_display(aside)
|
||||||
|
|
||||||
|
data.next_card.front = qside
|
||||||
|
data.next_card.back = aside
|
||||||
|
# Night mode is handled by the frontend so that it works with the browsers theme if used outside of anki.
|
||||||
|
# Perhaps the OS class should be handled this way too?
|
||||||
|
data.next_card.body_class = theme_manager.body_classes_for_card_ord(card.ord, False)
|
||||||
|
|
||||||
|
return data.SerializeToString()
|
||||||
|
|
||||||
|
|
||||||
|
def play_avtags():
|
||||||
|
req = PlayAVTagsRequest.FromString(request.data)
|
||||||
|
play_tags(av_tags_to_native(req.tags))
|
||||||
|
|
||||||
|
|
||||||
post_handler_list = [
|
post_handler_list = [
|
||||||
congrats_info,
|
congrats_info,
|
||||||
get_deck_configs_for_update,
|
get_deck_configs_for_update,
|
||||||
|
|
@ -653,6 +714,8 @@ post_handler_list = [
|
||||||
deck_options_require_close,
|
deck_options_require_close,
|
||||||
deck_options_ready,
|
deck_options_ready,
|
||||||
save_custom_colours,
|
save_custom_colours,
|
||||||
|
next_card_data,
|
||||||
|
play_avtags,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -698,6 +761,9 @@ exposed_backend_list = [
|
||||||
# DeckConfigService
|
# DeckConfigService
|
||||||
"get_ignored_before_count",
|
"get_ignored_before_count",
|
||||||
"get_retention_workload",
|
"get_retention_workload",
|
||||||
|
# CardsService
|
||||||
|
"set_flag",
|
||||||
|
"compare_answer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ class Preferences(QDialog):
|
||||||
form.showProgress.setChecked(reviewing.show_remaining_due_counts)
|
form.showProgress.setChecked(reviewing.show_remaining_due_counts)
|
||||||
form.showPlayButtons.setChecked(not reviewing.hide_audio_play_buttons)
|
form.showPlayButtons.setChecked(not reviewing.hide_audio_play_buttons)
|
||||||
form.interrupt_audio.setChecked(reviewing.interrupt_audio_when_answering)
|
form.interrupt_audio.setChecked(reviewing.interrupt_audio_when_answering)
|
||||||
|
form.new_reviewer.setChecked(reviewing.new_reviewer)
|
||||||
|
|
||||||
editing = self.prefs.editing
|
editing = self.prefs.editing
|
||||||
form.useCurrent.setCurrentIndex(
|
form.useCurrent.setCurrentIndex(
|
||||||
|
|
@ -173,6 +174,8 @@ class Preferences(QDialog):
|
||||||
reviewing.time_limit_secs = form.timeLimit.value() * 60
|
reviewing.time_limit_secs = form.timeLimit.value() * 60
|
||||||
reviewing.hide_audio_play_buttons = not self.form.showPlayButtons.isChecked()
|
reviewing.hide_audio_play_buttons = not self.form.showPlayButtons.isChecked()
|
||||||
reviewing.interrupt_audio_when_answering = self.form.interrupt_audio.isChecked()
|
reviewing.interrupt_audio_when_answering = self.form.interrupt_audio.isChecked()
|
||||||
|
reviewing.new_reviewer = form.new_reviewer.isChecked()
|
||||||
|
aqt.mw.setupReviewer(reviewing.new_reviewer)
|
||||||
|
|
||||||
editing = self.prefs.editing
|
editing = self.prefs.editing
|
||||||
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
|
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
|
||||||
|
|
|
||||||
|
|
@ -1233,6 +1233,79 @@ timerStopped = false;
|
||||||
setFlag = set_flag_on_current_card
|
setFlag = set_flag_on_current_card
|
||||||
|
|
||||||
|
|
||||||
|
class SvelteReviewer(Reviewer):
|
||||||
|
def _answerButtons(self) -> str:
|
||||||
|
default = self._defaultEase()
|
||||||
|
|
||||||
|
assert isinstance(self.mw.col.sched, V3Scheduler)
|
||||||
|
labels = self.mw.col.sched.describe_next_states(self._v3.states)
|
||||||
|
|
||||||
|
def but(i: int, label: str):
|
||||||
|
if i == default:
|
||||||
|
id = "defease"
|
||||||
|
else:
|
||||||
|
id = ""
|
||||||
|
due = self._buttonTime(i, v3_labels=labels)
|
||||||
|
key = (
|
||||||
|
tr.actions_shortcut_key(val=aqt.mw.pm.get_answer_key(i))
|
||||||
|
if aqt.mw.pm.get_answer_key(i)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": id,
|
||||||
|
"key": key,
|
||||||
|
"i": i,
|
||||||
|
"label": label,
|
||||||
|
"due": due,
|
||||||
|
}
|
||||||
|
|
||||||
|
return [but(ease, label) for ease, label in self._answerButtonList()] # type: ignore
|
||||||
|
|
||||||
|
def refresh_if_needed(self):
|
||||||
|
if self._refresh_needed:
|
||||||
|
self.mw.fade_in_webview()
|
||||||
|
self.web.eval("if (anki) {anki.changeReceived()}")
|
||||||
|
self._refresh_needed = None
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
self._initWeb()
|
||||||
|
|
||||||
|
def _remaining(self) -> str:
|
||||||
|
if not self.mw.col.conf["dueCounts"]:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
idx, counts = self._v3.counts()
|
||||||
|
self.web.eval(f"_updateRemaining({json.dumps(counts)},{idx})")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _showAnswerButton(self) -> None:
|
||||||
|
if self.card.should_show_timer():
|
||||||
|
maxTime = self.card.time_limit() / 1000
|
||||||
|
else:
|
||||||
|
maxTime = 0
|
||||||
|
self._remaining()
|
||||||
|
self.web.eval('showQuestion("",%d);' % (maxTime))
|
||||||
|
|
||||||
|
def _buttonTime(self, i: int, v3_labels: Sequence[str]) -> str:
|
||||||
|
return v3_labels[i - 1] if self.mw.col.conf["estTimes"] else ""
|
||||||
|
|
||||||
|
def _linkHandler(self, url: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _initWeb(self) -> None:
|
||||||
|
self._reps = 0
|
||||||
|
# hide the bottom bar
|
||||||
|
self.bottom.web.setHtml("<style>body {margin:0;} html {height:0;}</style>")
|
||||||
|
# main window
|
||||||
|
self.web.load_sveltekit_page("reviewer")
|
||||||
|
# block default drag & drop behavior while allowing drop events to be received by JS handlers
|
||||||
|
self.web.allow_drops = True
|
||||||
|
self.web.eval("_blockDefaultDragDropBehavior();")
|
||||||
|
|
||||||
|
def _shortcutKeys(self) -> Sequence[tuple[str, Callable] | tuple[Qt.Key, Callable]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
# if the last element is a comment, then the RUN_STATE_MUTATION code
|
# if the last element is a comment, then the RUN_STATE_MUTATION code
|
||||||
# breaks due to the comment wrongly commenting out python code.
|
# breaks due to the comment wrongly commenting out python code.
|
||||||
# To prevent this we put the js code on a separate line
|
# To prevent this we put the js code on a separate line
|
||||||
|
|
|
||||||
|
|
@ -925,13 +925,12 @@ def play_clicked_audio(pycmd: str, card: Card) -> None:
|
||||||
"""eg. if pycmd is 'play:q:0', play the first audio on the question side."""
|
"""eg. if pycmd is 'play:q:0', play the first audio on the question side."""
|
||||||
play, context, str_idx = pycmd.split(":")
|
play, context, str_idx = pycmd.split(":")
|
||||||
idx = int(str_idx)
|
idx = int(str_idx)
|
||||||
if context == "q":
|
tags = card.question_av_tags() if context == "q" else card.answer_av_tags()
|
||||||
tags = card.question_av_tags()
|
play_tags([tags[idx]])
|
||||||
else:
|
|
||||||
tags = card.answer_av_tags()
|
|
||||||
av_player.play_tags([tags[idx]])
|
|
||||||
|
|
||||||
|
|
||||||
|
play_tags = av_player.play_tags
|
||||||
|
|
||||||
# Init defaults
|
# Init defaults
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,11 @@ class BottomWebView(ToolbarWebView):
|
||||||
def animate_height(self, height: int) -> None:
|
def animate_height(self, height: int) -> None:
|
||||||
self.web_height = height
|
self.web_height = height
|
||||||
|
|
||||||
if self.mw.pm.reduce_motion() or height == self.height():
|
if (
|
||||||
|
self.mw.pm.reduce_motion()
|
||||||
|
or self.mw.col.conf.get("newReviewer")
|
||||||
|
or height == self.height()
|
||||||
|
):
|
||||||
self.setFixedHeight(height)
|
self.setFixedHeight(height)
|
||||||
else:
|
else:
|
||||||
# Collapse/Expand animation
|
# Collapse/Expand animation
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
AnkiWebViewKind.IMPORT_ANKI_PACKAGE,
|
AnkiWebViewKind.IMPORT_ANKI_PACKAGE,
|
||||||
AnkiWebViewKind.IMPORT_CSV,
|
AnkiWebViewKind.IMPORT_CSV,
|
||||||
AnkiWebViewKind.IMPORT_LOG,
|
AnkiWebViewKind.IMPORT_LOG,
|
||||||
|
AnkiWebViewKind.MAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
global _profile_with_api_access, _profile_without_api_access
|
global _profile_with_api_access, _profile_without_api_access
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ impl From<BoolKeyProto> for BoolKey {
|
||||||
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
|
BoolKeyProto::LoadBalancerEnabled => BoolKey::LoadBalancerEnabled,
|
||||||
BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled,
|
BoolKeyProto::FsrsShortTermWithStepsEnabled => BoolKey::FsrsShortTermWithStepsEnabled,
|
||||||
BoolKeyProto::FsrsLegacyEvaluate => BoolKey::FsrsLegacyEvaluate,
|
BoolKeyProto::FsrsLegacyEvaluate => BoolKey::FsrsLegacyEvaluate,
|
||||||
|
BoolKeyProto::NewReviewer => BoolKey::NewReviewer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ impl crate::services::CardRenderingService for Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rendered_nodes_to_proto(
|
pub(crate) fn rendered_nodes_to_proto(
|
||||||
nodes: Vec<RenderedNode>,
|
nodes: Vec<RenderedNode>,
|
||||||
) -> Vec<anki_proto::card_rendering::RenderedTemplateNode> {
|
) -> Vec<anki_proto::card_rendering::RenderedTemplateNode> {
|
||||||
nodes
|
nodes
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ pub enum BoolKey {
|
||||||
FsrsLegacyEvaluate,
|
FsrsLegacyEvaluate,
|
||||||
LoadBalancerEnabled,
|
LoadBalancerEnabled,
|
||||||
FsrsShortTermWithStepsEnabled,
|
FsrsShortTermWithStepsEnabled,
|
||||||
|
NewReviewer,
|
||||||
#[strum(to_string = "normalize_note_text")]
|
#[strum(to_string = "normalize_note_text")]
|
||||||
NormalizeNoteText,
|
NormalizeNoteText,
|
||||||
#[strum(to_string = "dayLearnFirst")]
|
#[strum(to_string = "dayLearnFirst")]
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ impl Collection {
|
||||||
load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled),
|
load_balancer_enabled: self.get_config_bool(BoolKey::LoadBalancerEnabled),
|
||||||
fsrs_short_term_with_steps_enabled: self
|
fsrs_short_term_with_steps_enabled: self
|
||||||
.get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled),
|
.get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled),
|
||||||
|
new_reviewer: self.get_config_bool(BoolKey::NewReviewer),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ impl Collection {
|
||||||
BoolKey::FsrsShortTermWithStepsEnabled,
|
BoolKey::FsrsShortTermWithStepsEnabled,
|
||||||
s.fsrs_short_term_with_steps_enabled,
|
s.fsrs_short_term_with_steps_enabled,
|
||||||
)?;
|
)?;
|
||||||
|
self.set_config_bool_inner(BoolKey::NewReviewer, settings.new_reviewer)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,13 @@
|
||||||
mod answering;
|
mod answering;
|
||||||
mod states;
|
mod states;
|
||||||
|
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anki_proto::cards;
|
use anki_proto::cards;
|
||||||
use anki_proto::generic;
|
use anki_proto::generic;
|
||||||
use anki_proto::scheduler;
|
use anki_proto::scheduler;
|
||||||
|
use anki_proto::scheduler::next_card_data_response::AnswerButton;
|
||||||
|
use anki_proto::scheduler::next_card_data_response::NextCardData;
|
||||||
use anki_proto::scheduler::ComputeFsrsParamsResponse;
|
use anki_proto::scheduler::ComputeFsrsParamsResponse;
|
||||||
use anki_proto::scheduler::ComputeMemoryStateResponse;
|
use anki_proto::scheduler::ComputeMemoryStateResponse;
|
||||||
use anki_proto::scheduler::ComputeOptimalRetentionResponse;
|
use anki_proto::scheduler::ComputeOptimalRetentionResponse;
|
||||||
|
|
@ -14,6 +18,8 @@ use anki_proto::scheduler::FsrsBenchmarkResponse;
|
||||||
use anki_proto::scheduler::FuzzDeltaRequest;
|
use anki_proto::scheduler::FuzzDeltaRequest;
|
||||||
use anki_proto::scheduler::FuzzDeltaResponse;
|
use anki_proto::scheduler::FuzzDeltaResponse;
|
||||||
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
||||||
|
use anki_proto::scheduler::NextCardDataRequest;
|
||||||
|
use anki_proto::scheduler::NextCardDataResponse;
|
||||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||||
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
||||||
use anki_proto::scheduler::SimulateFsrsWorkloadResponse;
|
use anki_proto::scheduler::SimulateFsrsWorkloadResponse;
|
||||||
|
|
@ -21,15 +27,19 @@ use fsrs::ComputeParametersInput;
|
||||||
use fsrs::FSRSItem;
|
use fsrs::FSRSItem;
|
||||||
use fsrs::FSRSReview;
|
use fsrs::FSRSReview;
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::backend::Backend;
|
use crate::backend::Backend;
|
||||||
|
use crate::card_rendering::service::rendered_nodes_to_proto;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::scheduler::fsrs::params::ComputeParamsRequest;
|
use crate::scheduler::fsrs::params::ComputeParamsRequest;
|
||||||
use crate::scheduler::new::NewCardDueOrder;
|
use crate::scheduler::new::NewCardDueOrder;
|
||||||
use crate::scheduler::states::CardState;
|
use crate::scheduler::states::CardState;
|
||||||
use crate::scheduler::states::SchedulingStates;
|
use crate::scheduler::states::SchedulingStates;
|
||||||
use crate::search::SortMode;
|
use crate::search::SortMode;
|
||||||
|
use crate::services::NotesService;
|
||||||
use crate::stats::studied_today;
|
use crate::stats::studied_today;
|
||||||
|
use crate::template::RenderedNode;
|
||||||
|
|
||||||
impl crate::services::SchedulerService for Collection {
|
impl crate::services::SchedulerService for Collection {
|
||||||
/// This behaves like _updateCutoff() in older code - it also unburies at
|
/// This behaves like _updateCutoff() in older code - it also unburies at
|
||||||
|
|
@ -382,6 +392,89 @@ impl crate::services::SchedulerService for Collection {
|
||||||
delta_days: self.get_fuzz_delta(input.card_id.into(), input.interval)?,
|
delta_days: self.get_fuzz_delta(input.card_id.into(), input.interval)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_card_data(&mut self, req: NextCardDataRequest) -> Result<NextCardDataResponse> {
|
||||||
|
if let Some(answer) = req.answer {
|
||||||
|
self.answer_card(&mut answer.into())?;
|
||||||
|
}
|
||||||
|
let queue = self.get_queued_cards(1, false)?;
|
||||||
|
let next_card = queue.cards.first();
|
||||||
|
if let Some(next_card) = next_card {
|
||||||
|
let cid = next_card.card.id;
|
||||||
|
|
||||||
|
let render = self.render_existing_card(cid, false, true)?;
|
||||||
|
|
||||||
|
let answer_buttons = self
|
||||||
|
.describe_next_states(&next_card.states)?
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, due)| AnswerButton {
|
||||||
|
rating: i as i32,
|
||||||
|
due,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config = self.deck_config_for_card(&next_card.card)?;
|
||||||
|
|
||||||
|
// Typed answer replacements
|
||||||
|
static ANSWER_REGEX: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"\[\[type:(.+?:)?(.+?)\]\]").unwrap());
|
||||||
|
|
||||||
|
const ANSWER_HTML: &str = "<center>
|
||||||
|
<input type=text id=typeans onkeypress=\"_typeAnsPress();\"
|
||||||
|
style=\"font-family: '{self.typeFont}'; font-size: {self.typeSize}px;\">
|
||||||
|
</center>";
|
||||||
|
|
||||||
|
let mut q_nodes = render.qnodes;
|
||||||
|
let typed_answer_parent_node = q_nodes.iter_mut().find_map(|node| {
|
||||||
|
if let RenderedNode::Text { text } = node {
|
||||||
|
let mut out = None;
|
||||||
|
*text = ANSWER_REGEX
|
||||||
|
.replace(text, |cap: ®ex::Captures<'_>| {
|
||||||
|
out = Some((
|
||||||
|
cap.get(1).map(|g| g.as_str().to_string()),
|
||||||
|
cap[2].to_string(),
|
||||||
|
));
|
||||||
|
ANSWER_HTML
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let typed_answer = typed_answer_parent_node.as_ref().map(|field| {
|
||||||
|
let note = self.get_note(next_card.card.note_id.into()).unwrap();
|
||||||
|
let notetype = self.get_notetype(note.notetype_id.into()).unwrap().unwrap();
|
||||||
|
note.fields[notetype.get_field_ord(&field.1).unwrap()].clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(NextCardDataResponse {
|
||||||
|
next_card: Some(NextCardData {
|
||||||
|
queue: Some(queue.into()),
|
||||||
|
|
||||||
|
css: render.css.clone(),
|
||||||
|
partial_front: rendered_nodes_to_proto(q_nodes),
|
||||||
|
partial_back: rendered_nodes_to_proto(render.anodes),
|
||||||
|
|
||||||
|
answer_buttons,
|
||||||
|
autoplay: !config.inner.disable_autoplay,
|
||||||
|
typed_answer,
|
||||||
|
typed_answer_args: typed_answer_parent_node.and_then(|v| v.0),
|
||||||
|
|
||||||
|
// Filled by python
|
||||||
|
front: "".to_string(),
|
||||||
|
back: "".to_string(),
|
||||||
|
body_class: "".to_string(),
|
||||||
|
question_av_tags: vec![],
|
||||||
|
answer_av_tags: vec![],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(NextCardDataResponse::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::services::BackendSchedulerService for Backend {
|
impl crate::services::BackendSchedulerService for Backend {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ async function setInnerHTML(element: Element, html: string): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderError = (type: string) => (error: unknown): string => {
|
export const renderError = (type: string) => (error: unknown): string => {
|
||||||
const errorMessage = String(error).substring(0, 2000);
|
const errorMessage = String(error).substring(0, 2000);
|
||||||
let errorStack: string;
|
let errorStack: string;
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@import "$lib/sass/base";
|
@import "../lib/sass/base";
|
||||||
|
|
||||||
// override Bootstrap transition duration
|
// override Bootstrap transition duration
|
||||||
$carousel-transition: var(--transition);
|
$carousel-transition: var(--transition);
|
||||||
|
|
@ -11,8 +11,8 @@ $carousel-transition: var(--transition);
|
||||||
@import "bootstrap/scss/close";
|
@import "bootstrap/scss/close";
|
||||||
@import "bootstrap/scss/alert";
|
@import "bootstrap/scss/alert";
|
||||||
@import "bootstrap/scss/badge";
|
@import "bootstrap/scss/badge";
|
||||||
@import "$lib/sass/bootstrap-forms";
|
@import "../lib/sass/bootstrap-forms";
|
||||||
@import "$lib/sass/bootstrap-tooltip";
|
@import "../lib/sass/bootstrap-tooltip";
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="date"],
|
input[type="date"],
|
||||||
|
|
|
||||||
96
ts/routes/reviewer-inner/index.ts
Normal file
96
ts/routes/reviewer-inner/index.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import "../base.scss";
|
||||||
|
import "../../reviewer/reviewer.scss";
|
||||||
|
import "mathjax/es5/tex-chtml-full.js";
|
||||||
|
import { renderError } from "../../reviewer";
|
||||||
|
import { enableNightMode } from "../reviewer/reviewer";
|
||||||
|
import type { ReviewerRequest } from "../reviewer/reviewerRequest";
|
||||||
|
import type { InnerReviewerRequest } from "./innerReviewerRequest";
|
||||||
|
|
||||||
|
function postParentMessage(message: ReviewerRequest) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
message,
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const MathJax: any;
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
addEventListener("message", async (e: MessageEvent<InnerReviewerRequest>) => {
|
||||||
|
switch (e.data.type) {
|
||||||
|
case "html": {
|
||||||
|
document.body.innerHTML = e.data.value;
|
||||||
|
if (e.data.css) {
|
||||||
|
style.innerHTML = e.data.css;
|
||||||
|
}
|
||||||
|
if (e.data.bodyclass) {
|
||||||
|
document.body.className = e.data.bodyclass;
|
||||||
|
const theme = urlParams.get("nightMode");
|
||||||
|
if (theme !== null) {
|
||||||
|
enableNightMode();
|
||||||
|
document.body.classList.add("night_mode");
|
||||||
|
document.body.classList.add("nightMode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for mathjax to ready
|
||||||
|
await MathJax.startup.promise
|
||||||
|
.then(() => {
|
||||||
|
// clear MathJax buffers from previous typesets
|
||||||
|
MathJax.typesetClear();
|
||||||
|
|
||||||
|
return MathJax.typesetPromise([document.body]);
|
||||||
|
})
|
||||||
|
.catch(renderError("MathJax"));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.warn(`Unknown message type: ${e.data.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
postParentMessage({ type: "keypress", key: " " });
|
||||||
|
} else if (
|
||||||
|
e.key.length == 1 && "1234 ".includes(e.key)
|
||||||
|
&& !document.activeElement?.matches("input[type=text], input[type=number], textarea")
|
||||||
|
) {
|
||||||
|
postParentMessage({ type: "keypress", key: e.key });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const base = document.createElement("base");
|
||||||
|
base.href = "/";
|
||||||
|
document.head.appendChild(base);
|
||||||
|
|
||||||
|
function pycmd(cmd: string) {
|
||||||
|
const match = cmd.match(/play:(q|a):(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
const [_, context, index] = match;
|
||||||
|
postParentMessage({
|
||||||
|
type: "audio",
|
||||||
|
answerSide: context === "a",
|
||||||
|
index: parseInt(index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.pycmd = pycmd;
|
||||||
|
|
||||||
|
function _typeAnsPress() {
|
||||||
|
const elem = document.getElementById("typeans")! as HTMLInputElement;
|
||||||
|
let key = (window.event as KeyboardEvent).key;
|
||||||
|
key = key.length == 1 ? key : "";
|
||||||
|
postParentMessage(
|
||||||
|
{ type: "typed", value: elem.value + key },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
globalThis._typeAnsPress = _typeAnsPress;
|
||||||
10
ts/routes/reviewer-inner/innerReviewerRequest.ts
Normal file
10
ts/routes/reviewer-inner/innerReviewerRequest.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
interface HtmlMessage {
|
||||||
|
type: "html";
|
||||||
|
value: string;
|
||||||
|
css?: string;
|
||||||
|
bodyclass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InnerReviewerRequest = HtmlMessage;
|
||||||
30
ts/routes/reviewer/+page.svelte
Normal file
30
ts/routes/reviewer/+page.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { ReviewerState, updateNightMode } from "./reviewer";
|
||||||
|
import ReviewerBottom from "./reviewer-bottom/ReviewerBottom.svelte";
|
||||||
|
import Reviewer from "./Reviewer.svelte";
|
||||||
|
|
||||||
|
const state = new ReviewerState();
|
||||||
|
onMount(() => {
|
||||||
|
updateNightMode();
|
||||||
|
globalThis.anki ??= {};
|
||||||
|
globalThis.anki.changeReceived = () => state.showQuestion(null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Reviewer {state}></Reviewer>
|
||||||
|
<ReviewerBottom {state}></ReviewerBottom>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
ts/routes/reviewer/Reviewer.svelte
Normal file
37
ts/routes/reviewer/Reviewer.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { isNightMode, type ReviewerState } from "./reviewer";
|
||||||
|
|
||||||
|
let iframe: HTMLIFrameElement;
|
||||||
|
export let state: ReviewerState;
|
||||||
|
|
||||||
|
$: if (iframe) {
|
||||||
|
state.registerIFrame(iframe);
|
||||||
|
state.registerShortcuts();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="qa">
|
||||||
|
<iframe
|
||||||
|
src={"/_anki/pages/reviewer-inner.html" + (isNightMode() ? "?nightMode" : "")}
|
||||||
|
bind:this={iframe}
|
||||||
|
title="card"
|
||||||
|
frameborder="0"
|
||||||
|
sandbox="allow-scripts"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#qa {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
0
ts/routes/reviewer/index.ts
Normal file
0
ts/routes/reviewer/index.ts
Normal file
37
ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte
Normal file
37
ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type { NextCardDataResponse_AnswerButton } from "@generated/anki/scheduler_pb";
|
||||||
|
import * as tr from "@generated/ftl";
|
||||||
|
import type { ReviewerState } from "../reviewer";
|
||||||
|
|
||||||
|
export let info: NextCardDataResponse_AnswerButton;
|
||||||
|
export let state: ReviewerState;
|
||||||
|
|
||||||
|
const labels = [
|
||||||
|
tr.studyingAgain(),
|
||||||
|
tr.studyingHard(),
|
||||||
|
tr.studyingGood(),
|
||||||
|
tr.studyingEasy(),
|
||||||
|
];
|
||||||
|
$: label = labels[info.rating];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{#if info.due}
|
||||||
|
{info.due}
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<button on:click={() => state.easeButtonPressed(info.rating)}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
75
ts/routes/reviewer/reviewer-bottom/More.svelte
Normal file
75
ts/routes/reviewer/reviewer-bottom/More.svelte
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import DropdownItem from "$lib/components/DropdownItem.svelte";
|
||||||
|
import * as tr from "@generated/ftl";
|
||||||
|
import MoreSubmenu from "./MoreSubmenu.svelte";
|
||||||
|
import MoreItem from "./MoreItem.svelte";
|
||||||
|
import { setFlag } from "@generated/backend";
|
||||||
|
import type { ReviewerState } from "../reviewer";
|
||||||
|
|
||||||
|
let showFloating = false;
|
||||||
|
let showFlags = false;
|
||||||
|
export let state: ReviewerState;
|
||||||
|
|
||||||
|
const flags = [
|
||||||
|
{ colour: tr.actionsFlagRed(), shortcut: "Ctrl+1" },
|
||||||
|
{ colour: tr.actionsFlagOrange(), shortcut: "Ctrl+2" },
|
||||||
|
{ colour: tr.actionsFlagGreen(), shortcut: "Ctrl+3" },
|
||||||
|
{ colour: tr.actionsFlagBlue(), shortcut: "Ctrl+4" },
|
||||||
|
{ colour: tr.actionsFlagPink(), shortcut: "Ctrl+5" },
|
||||||
|
{ colour: tr.actionsFlagTurquoise(), shortcut: "Ctrl+6" },
|
||||||
|
{ colour: tr.actionsFlagPurple(), shortcut: "Ctrl+7" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function changeFlag(index: number) {
|
||||||
|
setFlag({ cardIds: [state.currentCard!.card!.id], flag: index });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MoreSubmenu bind:showFloating>
|
||||||
|
<button
|
||||||
|
slot="button"
|
||||||
|
on:click={() => {
|
||||||
|
showFloating = !showFloating;
|
||||||
|
}}
|
||||||
|
title={tr.actionsShortcutKey({ val: "M" })}
|
||||||
|
>
|
||||||
|
{tr.studyingMore()}{"▾"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div slot="items">
|
||||||
|
<MoreSubmenu bind:showFloating={showFlags}>
|
||||||
|
<DropdownItem
|
||||||
|
slot="button"
|
||||||
|
on:click={() => {
|
||||||
|
showFlags = !showFlags;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tr.studyingFlagCard()}
|
||||||
|
</DropdownItem>
|
||||||
|
<div slot="items">
|
||||||
|
{#each flags as flag, i}
|
||||||
|
<MoreItem
|
||||||
|
shortcut={flag.shortcut}
|
||||||
|
onClick={() => changeFlag(i + 1)}
|
||||||
|
>
|
||||||
|
{flag.colour}
|
||||||
|
</MoreItem>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</MoreSubmenu>
|
||||||
|
</div>
|
||||||
|
</MoreSubmenu>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div :global(button) {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
ts/routes/reviewer/reviewer-bottom/MoreItem.svelte
Normal file
14
ts/routes/reviewer/reviewer-bottom/MoreItem.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import DropdownItem from "$lib/components/DropdownItem.svelte";
|
||||||
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
||||||
|
|
||||||
|
export let shortcut: string = "";
|
||||||
|
export let onClick = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Shortcut keyCombination={shortcut} on:keydown={onClick}></Shortcut>
|
||||||
|
<DropdownItem on:click={onClick}><slot /></DropdownItem>
|
||||||
20
ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte
Normal file
20
ts/routes/reviewer/reviewer-bottom/MoreSubmenu.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Popover from "$lib/components/Popover.svelte";
|
||||||
|
import WithFloating from "$lib/components/WithFloating.svelte";
|
||||||
|
|
||||||
|
export let showFloating = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<WithFloating show={showFloating} inline on:close={() => (showFloating = false)}>
|
||||||
|
<slot slot="reference" name="button"></slot>
|
||||||
|
|
||||||
|
<Popover slot="floating">
|
||||||
|
<slot name="items" />
|
||||||
|
</Popover>
|
||||||
|
</WithFloating>
|
||||||
|
</div>
|
||||||
40
ts/routes/reviewer/reviewer-bottom/Remaining.svelte
Normal file
40
ts/routes/reviewer/reviewer-bottom/Remaining.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { QueuedCards_Queue } from "@generated/anki/scheduler_pb";
|
||||||
|
import type { ReviewerState } from "../reviewer";
|
||||||
|
import RemainingNumber from "./RemainingNumber.svelte";
|
||||||
|
|
||||||
|
export let state: ReviewerState;
|
||||||
|
const cardData = state.cardData;
|
||||||
|
$: queue = $cardData?.queue;
|
||||||
|
$: underlined = queue?.cards[0].queue;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<RemainingNumber
|
||||||
|
cls="new-count"
|
||||||
|
underlined={underlined === QueuedCards_Queue.NEW}
|
||||||
|
count={queue?.newCount}
|
||||||
|
></RemainingNumber>
|
||||||
|
{"+"}
|
||||||
|
<RemainingNumber
|
||||||
|
cls="learn-count"
|
||||||
|
underlined={underlined === QueuedCards_Queue.LEARNING}
|
||||||
|
count={queue?.learningCount}
|
||||||
|
></RemainingNumber>
|
||||||
|
{"+"}
|
||||||
|
<RemainingNumber
|
||||||
|
cls="review-count"
|
||||||
|
underlined={underlined === QueuedCards_Queue.REVIEW}
|
||||||
|
count={queue?.reviewCount}
|
||||||
|
></RemainingNumber>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
Normal file
19
ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
export let underlined: boolean;
|
||||||
|
export let cls: string;
|
||||||
|
export let count: number | undefined;
|
||||||
|
|
||||||
|
$: displayCount = count?.toFixed(0) ?? "?";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={cls}>
|
||||||
|
{#if underlined}
|
||||||
|
<u>{displayCount}</u>
|
||||||
|
{:else}
|
||||||
|
{displayCount}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
86
ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
Normal file
86
ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
import AnswerButton from "./AnswerButton.svelte";
|
||||||
|
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||||
|
import * as tr from "@generated/ftl";
|
||||||
|
import type { ReviewerState } from "../reviewer";
|
||||||
|
import Remaining from "./Remaining.svelte";
|
||||||
|
import More from "./More.svelte";
|
||||||
|
|
||||||
|
export let state: ReviewerState;
|
||||||
|
|
||||||
|
const answerButtons = state.answerButtons;
|
||||||
|
const answerShown = state.answerShown;
|
||||||
|
|
||||||
|
$: button_count = $answerShown ? $answerButtons.length : 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="outer" class="fancy">
|
||||||
|
<div id="tableinner" style="--answer-button-count: {button_count}">
|
||||||
|
<span class="disappearing"></span>
|
||||||
|
<div class="disappearing edit">
|
||||||
|
<button
|
||||||
|
title={tr.actionsShortcutKey({ val: "E" })}
|
||||||
|
on:click={() => bridgeCommand("edit")}
|
||||||
|
>
|
||||||
|
{tr.studyingEdit()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if $answerShown}
|
||||||
|
{#each $answerButtons as answerButton}
|
||||||
|
<AnswerButton {state} info={answerButton}></AnswerButton>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Remaining {state}></Remaining>
|
||||||
|
<button on:click={() => state.showAnswer()}>
|
||||||
|
{tr.studyingShowAnswer()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<span class="disappearing"></span>
|
||||||
|
<div class="disappearing more">
|
||||||
|
<More {state}></More>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#tableinner {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr repeat(var(--answer-button-count, 1), auto) 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#outer {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more,
|
||||||
|
.edit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 583px) {
|
||||||
|
.disappearing {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tableinner {
|
||||||
|
grid-template-columns: repeat(var(--answer-button-count, 1), auto);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
ts/routes/reviewer/reviewer-bottom/index.scss
Normal file
15
ts/routes/reviewer/reviewer-bottom/index.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
|
@use "../../../../qt/aqt/data/web/css/reviewer-bottom.scss";
|
||||||
|
@use "../../../../qt/aqt/data/web/css/toolbar.scss";
|
||||||
|
@use "../../../lib/sass/buttons";
|
||||||
|
|
||||||
|
html body {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 72px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
ts/routes/reviewer/reviewer-bottom/types.ts
Normal file
9
ts/routes/reviewer/reviewer-bottom/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface AnswerButtonInfo {
|
||||||
|
"extra": string;
|
||||||
|
"key": string;
|
||||||
|
"i": number;
|
||||||
|
"label": string;
|
||||||
|
"due": string;
|
||||||
|
}
|
||||||
187
ts/routes/reviewer/reviewer.ts
Normal file
187
ts/routes/reviewer/reviewer.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb";
|
||||||
|
import { compareAnswer, nextCardData, playAvtags } from "@generated/backend";
|
||||||
|
import { derived, get, writable } from "svelte/store";
|
||||||
|
import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest";
|
||||||
|
import type { ReviewerRequest } from "./reviewerRequest";
|
||||||
|
|
||||||
|
export function isNightMode() {
|
||||||
|
// https://stackoverflow.com/a/57795518
|
||||||
|
// This will be true in browsers if darkmode but also false in the reviewer if darkmode
|
||||||
|
// If in the reviewer then this will need to be set by the python instead
|
||||||
|
return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
|| document.documentElement.classList.contains("night-mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableNightMode() {
|
||||||
|
document.documentElement.classList.add("night-mode");
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", "dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNightMode() {
|
||||||
|
if (isNightMode()) {
|
||||||
|
enableNightMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedAnswerRegex = /\[\[type:(.+?:)?(.+?)\]\]/m;
|
||||||
|
|
||||||
|
export class ReviewerState {
|
||||||
|
answerHtml = "";
|
||||||
|
currentTypedAnswer = "";
|
||||||
|
_cardData: NextCardDataResponse_NextCardData | undefined = undefined;
|
||||||
|
beginAnsweringMs = Date.now();
|
||||||
|
readonly cardClass = writable("");
|
||||||
|
readonly answerShown = writable(false);
|
||||||
|
readonly cardData = writable<NextCardDataResponse_NextCardData | undefined>(undefined);
|
||||||
|
readonly answerButtons = derived(this.cardData, ($cardData) => $cardData?.answerButtons ?? []);
|
||||||
|
|
||||||
|
iframe: HTMLIFrameElement | undefined = undefined;
|
||||||
|
|
||||||
|
onReady() {
|
||||||
|
this.iframe!.style.visibility = "visible";
|
||||||
|
this.showQuestion(null);
|
||||||
|
addEventListener("message", this.onMessage.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(e: MessageEvent<ReviewerRequest>) {
|
||||||
|
switch (e.data.type) {
|
||||||
|
case "audio": {
|
||||||
|
const tags = get(this.answerShown) ? this._cardData!.answerAvTags : this._cardData!.questionAvTags;
|
||||||
|
playAvtags({ tags: [tags[e.data.index]] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "typed": {
|
||||||
|
this.currentTypedAnswer = e.data.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "keypress": {
|
||||||
|
this.handleKeyPress(e.data.key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerIFrame(iframe: HTMLIFrameElement) {
|
||||||
|
this.iframe = iframe;
|
||||||
|
iframe.addEventListener("load", this.onReady.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress(key: string) {
|
||||||
|
switch (key) {
|
||||||
|
case "1": {
|
||||||
|
this.easeButtonPressed(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "2": {
|
||||||
|
this.easeButtonPressed(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "3": {
|
||||||
|
this.easeButtonPressed(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "4": {
|
||||||
|
this.easeButtonPressed(3);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case " ": {
|
||||||
|
if (!get(this.answerShown)) {
|
||||||
|
this.showAnswer();
|
||||||
|
} else {
|
||||||
|
this.easeButtonPressed(2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e: KeyboardEvent) {
|
||||||
|
this.handleKeyPress(e.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerShortcuts() {
|
||||||
|
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInnerRequest(message: InnerReviewerRequest) {
|
||||||
|
this.iframe?.contentWindow?.postMessage(message, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHtml(htmlString: string, css?: string, bodyclass?: string) {
|
||||||
|
this.sendInnerRequest({ type: "html", value: htmlString, css, bodyclass });
|
||||||
|
}
|
||||||
|
|
||||||
|
async showQuestion(answer: CardAnswer | null) {
|
||||||
|
const resp = await nextCardData({
|
||||||
|
answer: answer || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: "Congratulation screen" logic
|
||||||
|
this._cardData = resp.nextCard;
|
||||||
|
this.cardData.set(this._cardData);
|
||||||
|
this.answerShown.set(false);
|
||||||
|
|
||||||
|
const question = resp.nextCard?.front || "";
|
||||||
|
this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass);
|
||||||
|
if (this._cardData?.autoplay) {
|
||||||
|
playAvtags({ tags: this._cardData!.questionAvTags });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.beginAnsweringMs = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentCard() {
|
||||||
|
return this._cardData?.queue?.cards[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async showTypedAnswer(html: string) {
|
||||||
|
if (!this._cardData?.typedAnswer || !this._cardData.typedAnswerArgs) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
const compareAnswerResp = await compareAnswer({
|
||||||
|
expected: this._cardData?.typedAnswer,
|
||||||
|
provided: this.currentTypedAnswer,
|
||||||
|
combining: !this._cardData.typedAnswerArgs.includes("nc"),
|
||||||
|
});
|
||||||
|
const display = compareAnswerResp.val;
|
||||||
|
|
||||||
|
console.log({ typedAnswerRegex, html, display });
|
||||||
|
return html.replace(typedAnswerRegex, display);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showAnswer() {
|
||||||
|
this.answerShown.set(true);
|
||||||
|
if (this._cardData?.autoplay) {
|
||||||
|
playAvtags({ tags: this._cardData!.answerAvTags });
|
||||||
|
}
|
||||||
|
this.updateHtml(await this.showTypedAnswer(this._cardData?.back || ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public easeButtonPressed(rating: number) {
|
||||||
|
if (!get(this.answerShown)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const states = this.currentCard!.states!;
|
||||||
|
|
||||||
|
const newState = [
|
||||||
|
states.again!,
|
||||||
|
states.hard!,
|
||||||
|
states.good!,
|
||||||
|
states.easy!,
|
||||||
|
][rating]!;
|
||||||
|
|
||||||
|
this.showQuestion(
|
||||||
|
new CardAnswer({
|
||||||
|
rating: rating,
|
||||||
|
currentState: states!.current!,
|
||||||
|
newState,
|
||||||
|
cardId: this.currentCard?.card?.id,
|
||||||
|
answeredAtMillis: BigInt(Date.now()),
|
||||||
|
millisecondsTaken: Date.now() - this.beginAnsweringMs,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
ts/routes/reviewer/reviewerRequest.ts
Normal file
19
ts/routes/reviewer/reviewerRequest.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
interface AudioMessage {
|
||||||
|
type: "audio";
|
||||||
|
answerSide: boolean;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateTypedAnswerMessage {
|
||||||
|
type: "typed";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyPressMessage {
|
||||||
|
type: "keypress";
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReviewerRequest = AudioMessage | UpdateTypedAnswerMessage | KeyPressMessage;
|
||||||
Loading…
Reference in a new issue