diff --git a/qt/aqt/main.py b/qt/aqt/main.py index c707d1b2a..3affb3a2e 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1075,9 +1075,9 @@ title="{}" {}>{}""".format( self.overview = Overview(self) def setupReviewer(self) -> None: - from aqt.reviewer import Reviewer + from aqt.reviewer import Reviewer, SvelteReviewer - self.reviewer = Reviewer(self) + self.reviewer = SvelteReviewer(self) if True else Reviewer(self) # Syncing ########################################################################## diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 820e762d9..83f45c933 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -334,6 +334,7 @@ def is_sveltekit_page(path: str) -> bool: "import-csv", "import-page", "image-occlusion", + "reviewer", ] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a8839c598..ccbb43773 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -1228,6 +1228,69 @@ timerStopped = false; 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 _remaining(self) -> str: + if not self.mw.col.conf["dueCounts"]: + return "" + + idx, counts = self._v3.counts() + self.bottom.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.bottom.web.eval("_showQuestion(%s,%d);" % ("", maxTime)) + + def _linkHandler(self, url: str) -> None: + if url == "bottomReady": + self._showQuestion() + self._remaining() + return + super()._linkHandler(url) + + def _initWeb(self) -> None: + self._reps = 0 + # 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();") + # ensure bottom web functions trigger + self.bottom.web = self.web + self.mw.bottomWeb.hide() + + # if the last element is a comment, then the RUN_STATE_MUTATION code # breaks due to the comment wrongly commenting out python code. # To prevent this we put the js code on a separate line diff --git a/ts/routes/reviewer/+page.svelte b/ts/routes/reviewer/+page.svelte new file mode 100644 index 000000000..2baa1b1f6 --- /dev/null +++ b/ts/routes/reviewer/+page.svelte @@ -0,0 +1,17 @@ + + +
+ + +
+ + diff --git a/ts/routes/reviewer/index.ts b/ts/routes/reviewer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte b/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte new file mode 100644 index 000000000..96918be95 --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte b/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte new file mode 100644 index 000000000..4761dfdcd --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte @@ -0,0 +1,16 @@ + + + + + {#if underlined} + + {:else} + + {/if} + diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte new file mode 100644 index 000000000..9dd6160b7 --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -0,0 +1,79 @@ + + + +
+
+
+ +
+
+ + + {$remaining[0]} + + + + {$remaining[1]} + + + + {$remaining[2]} + + +
+ {#if $answerButtons.length} + {#each $answerButtons as answerButton} + + {/each} + {:else} + + {/if} +
+
+
+ +
+
+
+ + diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte new file mode 100644 index 000000000..59747cc5d --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottomOuter.svelte @@ -0,0 +1,20 @@ + + + +{#if reviewerInfo} + +{/if} diff --git a/qt/aqt/data/web/css/reviewer-bottom.scss b/ts/routes/reviewer/reviewer-bottom/index.scss similarity index 87% rename from qt/aqt/data/web/css/reviewer-bottom.scss rename to ts/routes/reviewer/reviewer-bottom/index.scss index 59098a5fb..0f189d6b9 100644 --- a/qt/aqt/data/web/css/reviewer-bottom.scss +++ b/ts/routes/reviewer/reviewer-bottom/index.scss @@ -1,9 +1,10 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -@use "../../../../../ts/lib/sass/root-vars"; -@use "../../../../../ts/lib/sass/vars" as *; -@use "../../../../../ts/lib/sass/card-counts"; +@use "../../../lib/sass/root-vars"; +@use "../../../lib/sass/vars" as *; +@use "../../../lib/sass/card-counts"; +@use "../../../lib/sass/buttons"; :root { --focus-color: #{palette-of(border-focus)}; @@ -16,6 +17,8 @@ body { margin: 0; padding: 0; + font-size: 12px; + height: 72px; } #middle td[align="center"] { diff --git a/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts new file mode 100644 index 000000000..deedbd4bd --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts @@ -0,0 +1,90 @@ +import { bridgeCommand } from "@tslib/bridgecommand"; +import { writable } from "svelte/store"; +import type { AnswerButtonInfo } from "./types"; + +export function setupBottomBar() { + /* + let timerStopped = false; + + let maxTime = 0; + + function updateTime(): void { + const timeNode = document.getElementById("time"); + if (maxTime === 0) { + timeNode.textContent = ""; + return; + } + globalThis.time = Math.min(maxTime, globalThis.time); + const m = Math.floor(globalThis.time / 60); + const s = globalThis.time % 60; + const sStr = String(s).padStart(2, "0"); + const timeString = `${m}:${sStr}`; + + if (maxTime === time) { + timeNode.innerHTML = `${timeString}`; + } else { + timeNode.textContent = timeString; + } + }*/ + + const answerButtons = writable([]); + const remaining = writable<[number, number, number]>([0, 0, 0]); + const remainingIndex = writable(-1); + + let intervalId: number | undefined; + + function _showQuestion(_txt: string, _maxTime_: number): void { + _showAnswer([]); + globalThis.time = 0; + // maxTime = maxTime_; + // updateTime(); + + if (intervalId !== undefined) { + clearInterval(intervalId); + } + + /* + intervalId = setInterval(function() { + if (!timerStopped) { + globalThis.time += 1; + //updateTime(); + } + }, 1000);*/ + } + + function _showAnswer(info: AnswerButtonInfo[], _stopTimer = false): void { + console.log(info); + answerButtons.set(info); + // timerStopped = stopTimer; + } + + function _updateRemaining(counts: [number, number, number], idx: number) { + remaining.set(counts); + remainingIndex.set(idx); + } + + globalThis.showQuestion = _showQuestion; + globalThis.showAnswer = _showAnswer; + globalThis._updateRemaining = _updateRemaining; + + /* + function selectedAnswerButton(): string | undefined { + const node = document.activeElement as HTMLElement; + if (!node) { + return; + } + return node.dataset.ease; + } + */ + + // TODO This should probably be a "ready" command now that it is part of the actual reviewer, + // Currently this depends on this component mounting after the reviewer which it should but seems hacky. + // Maybe use a counter with a counter.subscribe($counter == 2 then call("ready")) + bridgeCommand("bottomReady"); + + return { + answerButtons, + remaining, + remainingIndex, + }; +} diff --git a/ts/routes/reviewer/reviewer-bottom/types.ts b/ts/routes/reviewer/reviewer-bottom/types.ts new file mode 100644 index 000000000..4068ca1f9 --- /dev/null +++ b/ts/routes/reviewer/reviewer-bottom/types.ts @@ -0,0 +1,9 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface AnswerButtonInfo { + "extra": string; + "key": string; + "i": number; + "label": string; + "due": string; +} diff --git a/ts/routes/reviewer/reviewer.svelte b/ts/routes/reviewer/reviewer.svelte new file mode 100644 index 000000000..aee74e922 --- /dev/null +++ b/ts/routes/reviewer/reviewer.svelte @@ -0,0 +1,16 @@ + + +
+ {@html $html} +
+ + diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts new file mode 100644 index 000000000..c2f4ea781 --- /dev/null +++ b/ts/routes/reviewer/reviewer.ts @@ -0,0 +1,18 @@ +import { writable } from "svelte/store"; +import { preloadAnswerImages } from "../../reviewer/images"; + +export function setupReviewer() { + const html = writable(""); + const cardClass = writable(""); + + function showQuestion(q, a, cc) { + html.set(q); + cardClass.set(cc); + preloadAnswerImages(a); + } + + globalThis._showAnswer = html.set; + globalThis._showQuestion = showQuestion; + + return { html, cardClass }; +} diff --git a/ts/routes/reviewer/reviewerOuter.svelte b/ts/routes/reviewer/reviewerOuter.svelte new file mode 100644 index 000000000..c261675f7 --- /dev/null +++ b/ts/routes/reviewer/reviewerOuter.svelte @@ -0,0 +1,21 @@ + + + +{#if reviewerInfo} + +{/if}