mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Compare commits
51 commits
3307fdaf11
...
de5da69897
Author | SHA1 | Date | |
---|---|---|---|
![]() |
de5da69897 | ||
![]() |
3890e12c9e | ||
![]() |
80cff16250 | ||
![]() |
75d9026be5 | ||
![]() |
6854d13b88 | ||
![]() |
29072654db | ||
![]() |
ec6f09958a | ||
![]() |
c2957746f4 | ||
![]() |
9e415869b8 | ||
![]() |
7e8a1076c1 | ||
![]() |
b97fb45e06 | ||
![]() |
61094d387a | ||
![]() |
90ed4cc115 | ||
![]() |
1e67a773c6 | ||
![]() |
c7fd7a0965 | ||
![]() |
d775efcb06 | ||
![]() |
2e0a75ed83 | ||
![]() |
45793b3b64 | ||
![]() |
a08bca2673 | ||
![]() |
0acc8d14e0 | ||
![]() |
f35b2cf5d2 | ||
![]() |
d0d1c519e6 | ||
![]() |
eac356139c | ||
![]() |
7805b1b426 | ||
![]() |
c64dd6c959 | ||
![]() |
1ec9f4902e | ||
![]() |
9d3451f97b | ||
![]() |
e46d98e2e0 | ||
![]() |
f01e0f8d0b | ||
![]() |
7cb8e62254 | ||
![]() |
d81ec73205 | ||
![]() |
4bf38ec2af | ||
![]() |
fac5d64558 | ||
![]() |
d49f1eb430 | ||
![]() |
860a8b4295 | ||
![]() |
6c540c89f1 | ||
![]() |
a365369562 | ||
![]() |
28402c548d | ||
![]() |
b256e88b1d | ||
![]() |
9dbb7abdbb | ||
![]() |
8a57d1c5e1 | ||
![]() |
992c8ad731 | ||
![]() |
7e92c40169 | ||
![]() |
f4eb7e0ff9 | ||
![]() |
8c0d1d1720 | ||
![]() |
5d536f2f8e | ||
![]() |
7788aa7785 | ||
![]() |
758cfa2693 | ||
![]() |
34c1dfd849 | ||
![]() |
244aade836 | ||
![]() |
6869e9fd36 |
32 changed files with 520 additions and 42 deletions
2
.version
2
.version
|
@ -1 +1 @@
|
||||||
25.09
|
25.09.2
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
enableScripts: false
|
||||||
|
|
|
@ -49,6 +49,7 @@ Sander Santema <github.com/sandersantema/>
|
||||||
Thomas Brownback <https://github.com/brownbat/>
|
Thomas Brownback <https://github.com/brownbat/>
|
||||||
Andrew Gaul <andrew@gaul.org>
|
Andrew Gaul <andrew@gaul.org>
|
||||||
kenden
|
kenden
|
||||||
|
Emil Hamrin <github.com/e-hamrin>
|
||||||
Nickolay Yudin <kelciour@gmail.com>
|
Nickolay Yudin <kelciour@gmail.com>
|
||||||
neitrinoweb <github.com/neitrinoweb/>
|
neitrinoweb <github.com/neitrinoweb/>
|
||||||
Andreas Reis <github.com/nwwt>
|
Andreas Reis <github.com/nwwt>
|
||||||
|
|
|
@ -28,7 +28,11 @@ pub fn setup_yarn(args: YarnArgs) {
|
||||||
.arg("--ignore-scripts"),
|
.arg("--ignore-scripts"),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
run_command(Command::new(&args.yarn_bin).arg("install"));
|
run_command(
|
||||||
|
Command::new(&args.yarn_bin)
|
||||||
|
.arg("install")
|
||||||
|
.arg("--immutable"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::write(args.stamp, b"").unwrap();
|
std::fs::write(args.stamp, b"").unwrap();
|
||||||
|
|
|
@ -1,35 +1,78 @@
|
||||||
# This Dockerfile uses three stages.
|
# This is a user-contributed Dockerfile. No official support is available.
|
||||||
# 1. Compile anki (and dependencies) and build python wheels.
|
|
||||||
# 2. Create a virtual environment containing anki and its dependencies.
|
|
||||||
# 3. Create a final image that only includes anki's virtual environment and required
|
|
||||||
# system packages.
|
|
||||||
|
|
||||||
ARG PYTHON_VERSION="3.9"
|
|
||||||
ARG DEBIAN_FRONTEND="noninteractive"
|
ARG DEBIAN_FRONTEND="noninteractive"
|
||||||
|
|
||||||
# Build anki.
|
FROM ubuntu:24.04 AS build
|
||||||
FROM python:$PYTHON_VERSION AS build
|
|
||||||
RUN curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64 \
|
|
||||||
> /usr/local/bin/bazel \
|
|
||||||
&& chmod +x /usr/local/bin/bazel \
|
|
||||||
# Bazel expects /usr/bin/python
|
|
||||||
&& ln -s /usr/local/bin/python /usr/bin/python
|
|
||||||
WORKDIR /opt/anki
|
WORKDIR /opt/anki
|
||||||
|
ENV PYTHON_VERSION="3.13"
|
||||||
|
|
||||||
|
|
||||||
|
# System deps
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libbz2-dev \
|
||||||
|
libreadline-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
liblzma-dev \
|
||||||
|
ca-certificates \
|
||||||
|
ninja-build \
|
||||||
|
rsync \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libgl1 \
|
||||||
|
libx11-6 \
|
||||||
|
libxext6 \
|
||||||
|
libxrender1 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxkbcommon-x11-0 \
|
||||||
|
libxcb1 \
|
||||||
|
libxcb-render0 \
|
||||||
|
libxcb-shm0 \
|
||||||
|
libxcb-icccm4 \
|
||||||
|
libxcb-image0 \
|
||||||
|
libxcb-keysyms1 \
|
||||||
|
libxcb-randr0 \
|
||||||
|
libxcb-shape0 \
|
||||||
|
libxcb-xfixes0 \
|
||||||
|
libxcb-xinerama0 \
|
||||||
|
libxcb-xinput0 \
|
||||||
|
libsm6 \
|
||||||
|
libice6 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# install rust with rustup
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
|
# Install uv and Python 3.13 with uv
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||||
|
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
|
||||||
|
ENV PATH="/root/.local/bin:${PATH}"
|
||||||
|
|
||||||
|
RUN uv python install ${PYTHON_VERSION} --default
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
# Build python wheels.
|
|
||||||
RUN ./tools/build
|
RUN ./tools/build
|
||||||
|
|
||||||
|
|
||||||
# Install pre-compiled Anki.
|
# Install pre-compiled Anki.
|
||||||
FROM python:${PYTHON_VERSION}-slim as installer
|
FROM python:3.13-slim AS installer
|
||||||
WORKDIR /opt/anki/
|
WORKDIR /opt/anki/
|
||||||
COPY --from=build /opt/anki/wheels/ wheels/
|
COPY --from=build /opt/anki/out/wheels/ wheels/
|
||||||
# Use virtual environment.
|
# Use virtual environment.
|
||||||
RUN python -m venv venv \
|
RUN python -m venv venv \
|
||||||
&& ./venv/bin/python -m pip install --no-cache-dir setuptools wheel \
|
&& ./venv/bin/python -m pip install --no-cache-dir setuptools wheel \
|
||||||
&& ./venv/bin/python -m pip install --no-cache-dir /opt/anki/wheels/*.whl
|
&& ./venv/bin/python -m pip install --no-cache-dir /opt/anki/wheels/*.whl
|
||||||
|
|
||||||
|
|
||||||
# We use another build stage here so we don't include the wheels in the final image.
|
# We use another build stage here so we don't include the wheels in the final image.
|
||||||
FROM python:${PYTHON_VERSION}-slim as final
|
FROM python:3.13-slim AS final
|
||||||
COPY --from=installer /opt/anki/venv /opt/anki/venv
|
COPY --from=installer /opt/anki/venv /opt/anki/venv
|
||||||
ENV PATH=/opt/anki/venv/bin:$PATH
|
ENV PATH=/opt/anki/venv/bin:$PATH
|
||||||
# Install run-time dependencies.
|
# Install run-time dependencies.
|
||||||
|
@ -59,9 +102,9 @@ RUN apt-get update \
|
||||||
libxrender1 \
|
libxrender1 \
|
||||||
libxtst6 \
|
libxtst6 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add non-root user.
|
# Add non-root user.
|
||||||
RUN useradd --create-home anki
|
RUN useradd --create-home anki
|
||||||
USER anki
|
USER anki
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
ENTRYPOINT ["/opt/anki/venv/bin/anki"]
|
ENTRYPOINT ["/opt/anki/venv/bin/anki"]
|
||||||
LABEL maintainer="Jakub Kaczmarzyk <jakub.kaczmarzyk@gmail.com>"
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6552c95a81d162422b2a50126547cc7f1b50c2fd
|
Subproject commit 480ef0da728c7ea3485c58529ae7ee02be3e5dba
|
|
@ -46,6 +46,20 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val }
|
||||||
studying-unbury = Unbury
|
studying-unbury = Unbury
|
||||||
studying-what-would-you-like-to-unbury = What would you like to unbury?
|
studying-what-would-you-like-to-unbury = What would you like to unbury?
|
||||||
studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet.
|
studying-you-havent-recorded-your-voice-yet = You haven't recorded your voice yet.
|
||||||
|
studying-card-studied-in-minute =
|
||||||
|
{ $cards ->
|
||||||
|
[one] { $cards } card
|
||||||
|
*[other] { $cards } cards
|
||||||
|
} studied in
|
||||||
|
{ $minutes ->
|
||||||
|
[one] { $minutes } minute.
|
||||||
|
*[other] { $minutes } minutes.
|
||||||
|
}
|
||||||
|
studying-question-time-elapsed = Question time elapsed
|
||||||
|
studying-answer-time-elapsed = Answer time elapsed
|
||||||
|
|
||||||
|
## OBSOLETE; you do not need to translate this
|
||||||
|
|
||||||
studying-card-studied-in =
|
studying-card-studied-in =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
[one] { $count } card studied in
|
[one] { $count } card studied in
|
||||||
|
@ -56,5 +70,3 @@ studying-minute =
|
||||||
[one] { $count } minute.
|
[one] { $count } minute.
|
||||||
*[other] { $count } minutes.
|
*[other] { $count } minutes.
|
||||||
}
|
}
|
||||||
studying-question-time-elapsed = Question time elapsed
|
|
||||||
studying-answer-time-elapsed = Answer time elapsed
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit dad4e2736a2b53dcdb52d79b5703dd464c05d666
|
Subproject commit fd5f984785ad07a0d3dbd893ee3d7e3671eaebd6
|
|
@ -27,6 +27,9 @@ service FrontendService {
|
||||||
rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty);
|
rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty);
|
||||||
// Warns python that the deck option web view is ready to receive requests.
|
// Warns python that the deck option web view is ready to receive requests.
|
||||||
rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
|
rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
|
||||||
|
|
||||||
|
// Save colour picker's custom colour palette
|
||||||
|
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
service BackendFrontendService {}
|
service BackendFrontendService {}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import aqt.browser
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.collection import Config
|
from anki.collection import Config
|
||||||
from anki.tags import MARKED_TAG
|
from anki.tags import MARKED_TAG
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks, is_mac
|
||||||
from aqt.qt import (
|
from aqt.qt import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
@ -81,10 +81,15 @@ class Previewer(QDialog):
|
||||||
qconnect(self.finished, self._on_finished)
|
qconnect(self.finished, self._on_finished)
|
||||||
self.silentlyClose = True
|
self.silentlyClose = True
|
||||||
self.vbox = QVBoxLayout()
|
self.vbox = QVBoxLayout()
|
||||||
|
spacing = 6
|
||||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.vbox.setSpacing(spacing)
|
||||||
self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
|
self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
|
||||||
self.vbox.addWidget(self._web)
|
self.vbox.addWidget(self._web)
|
||||||
self.bbox = QDialogButtonBox()
|
self.bbox = QDialogButtonBox()
|
||||||
|
self.bbox.setContentsMargins(
|
||||||
|
spacing, spacing if is_mac else 0, spacing, spacing
|
||||||
|
)
|
||||||
self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
|
self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
|
||||||
|
|
||||||
gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)
|
gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)
|
||||||
|
|
|
@ -151,6 +151,7 @@ class Editor:
|
||||||
self.add_webview()
|
self.add_webview()
|
||||||
self.setupWeb()
|
self.setupWeb()
|
||||||
self.setupShortcuts()
|
self.setupShortcuts()
|
||||||
|
self.setupColourPalette()
|
||||||
gui_hooks.editor_did_init(self)
|
gui_hooks.editor_did_init(self)
|
||||||
|
|
||||||
# Initial setup
|
# Initial setup
|
||||||
|
@ -349,6 +350,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
keys, fn, _ = row
|
keys, fn, _ = row
|
||||||
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
||||||
|
|
||||||
|
def setupColourPalette(self) -> None:
|
||||||
|
if not (colors := self.mw.col.get_config("customColorPickerPalette")):
|
||||||
|
return
|
||||||
|
for i, colour in enumerate(colors[: QColorDialog.customCount()]):
|
||||||
|
if not QColor.isValidColorName(colour):
|
||||||
|
continue
|
||||||
|
QColorDialog.setCustomColor(i, QColor.fromString(colour))
|
||||||
|
|
||||||
def _addFocusCheck(self, fn: Callable) -> Callable:
|
def _addFocusCheck(self, fn: Callable) -> Callable:
|
||||||
def checkFocus() -> None:
|
def checkFocus() -> None:
|
||||||
if self.currentField is None:
|
if self.currentField is None:
|
||||||
|
|
|
@ -1075,9 +1075,9 @@ title="{}" {}>{}</button>""".format(
|
||||||
self.overview = Overview(self)
|
self.overview = Overview(self)
|
||||||
|
|
||||||
def setupReviewer(self) -> None:
|
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
|
# Syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -334,6 +334,7 @@ def is_sveltekit_page(path: str) -> bool:
|
||||||
"import-csv",
|
"import-csv",
|
||||||
"import-page",
|
"import-page",
|
||||||
"image-occlusion",
|
"image-occlusion",
|
||||||
|
"reviewer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -599,6 +600,15 @@ def deck_options_ready() -> bytes:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def save_custom_colours() -> bytes:
|
||||||
|
colors = [
|
||||||
|
QColorDialog.customColor(i).name(QColor.NameFormat.HexArgb)
|
||||||
|
for i in range(QColorDialog.customCount())
|
||||||
|
]
|
||||||
|
aqt.mw.col.set_config("customColorPickerPalette", colors)
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
post_handler_list = [
|
post_handler_list = [
|
||||||
congrats_info,
|
congrats_info,
|
||||||
get_deck_configs_for_update,
|
get_deck_configs_for_update,
|
||||||
|
@ -614,6 +624,7 @@ post_handler_list = [
|
||||||
search_in_browser,
|
search_in_browser,
|
||||||
deck_options_require_close,
|
deck_options_require_close,
|
||||||
deck_options_ready,
|
deck_options_ready,
|
||||||
|
save_custom_colours,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import aqt.browser
|
||||||
import aqt.operations
|
import aqt.operations
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
from anki.collection import Config, OpChanges, OpChangesWithCount
|
from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||||
|
from anki.lang import with_collapsed_whitespace
|
||||||
from anki.scheduler.base import ScheduleCardsAsNew
|
from anki.scheduler.base import ScheduleCardsAsNew
|
||||||
from anki.scheduler.v3 import (
|
from anki.scheduler.v3 import (
|
||||||
CardAnswer,
|
CardAnswer,
|
||||||
|
@ -966,11 +967,15 @@ timerStopped = false;
|
||||||
elapsed = self.mw.col.timeboxReached()
|
elapsed = self.mw.col.timeboxReached()
|
||||||
if elapsed:
|
if elapsed:
|
||||||
assert not isinstance(elapsed, bool)
|
assert not isinstance(elapsed, bool)
|
||||||
part1 = tr.studying_card_studied_in(count=elapsed[1])
|
cards_val = elapsed[1]
|
||||||
mins = int(round(elapsed[0] / 60))
|
minutes_val = int(round(elapsed[0] / 60))
|
||||||
part2 = tr.studying_minute(count=mins)
|
message = with_collapsed_whitespace(
|
||||||
|
tr.studying_card_studied_in_minute(
|
||||||
|
cards=cards_val, minutes=str(minutes_val)
|
||||||
|
)
|
||||||
|
)
|
||||||
fin = tr.studying_finish()
|
fin = tr.studying_finish()
|
||||||
diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin])
|
diag = askUserDialog(message, [tr.studying_continue(), fin])
|
||||||
diag.setIcon(QMessageBox.Icon.Information)
|
diag.setIcon(QMessageBox.Icon.Information)
|
||||||
if diag.run() == fin:
|
if diag.run() == fin:
|
||||||
self.mw.moveToState("deckBrowser")
|
self.mw.moveToState("deckBrowser")
|
||||||
|
@ -1228,6 +1233,69 @@ 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 _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
|
# 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
|
||||||
|
|
|
@ -174,7 +174,7 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let health_check_passed = if health_check {
|
let health_check_passed = if health_check && input.train_set.len() > 300 {
|
||||||
let fsrs = FSRS::new(None)?;
|
let fsrs = FSRS::new(None)?;
|
||||||
fsrs.evaluate_with_time_series_splits(input, |_| true)
|
fsrs.evaluate_with_time_series_splits(input, |_| true)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Shortcut from "$lib/components/Shortcut.svelte";
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
||||||
|
import { saveCustomColours } from "@generated/backend";
|
||||||
|
|
||||||
export let keyCombination: string | null = null;
|
export let keyCombination: string | null = null;
|
||||||
export let value: string;
|
export let value: string;
|
||||||
|
@ -11,7 +12,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let inputRef: HTMLInputElement;
|
let inputRef: HTMLInputElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input bind:this={inputRef} tabindex="-1" type="color" bind:value on:input on:change />
|
<input
|
||||||
|
bind:this={inputRef}
|
||||||
|
tabindex="-1"
|
||||||
|
type="color"
|
||||||
|
bind:value
|
||||||
|
on:input
|
||||||
|
on:change
|
||||||
|
on:click={() => saveCustomColours({})}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if keyCombination}
|
{#if keyCombination}
|
||||||
<Shortcut {keyCombination} on:action={() => inputRef.click()} />
|
<Shortcut {keyCombination} on:action={() => inputRef.click()} />
|
||||||
|
|
|
@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import ColorPicker from "./ColorPicker.svelte";
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import WithColorHelper from "./WithColorHelper.svelte";
|
import WithColorHelper from "./WithColorHelper.svelte";
|
||||||
|
import { saveCustomColours } from "@generated/backend";
|
||||||
|
|
||||||
export let color: string;
|
export let color: string;
|
||||||
|
|
||||||
|
@ -134,7 +135,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
color = setColor(event);
|
color = setColor(event);
|
||||||
bridgeCommand(`lastHighlightColor:${color}`);
|
bridgeCommand(`lastHighlightColor:${color}`);
|
||||||
}}
|
}}
|
||||||
on:change={() => setTextColor()}
|
on:change={() => {
|
||||||
|
setTextColor();
|
||||||
|
saveCustomColours({});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</WithColorHelper>
|
</WithColorHelper>
|
||||||
|
|
|
@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import ColorPicker from "./ColorPicker.svelte";
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import WithColorHelper from "./WithColorHelper.svelte";
|
import WithColorHelper from "./WithColorHelper.svelte";
|
||||||
|
import { saveCustomColours } from "@generated/backend";
|
||||||
|
|
||||||
export let color: string;
|
export let color: string;
|
||||||
|
|
||||||
|
@ -158,6 +159,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setTextColor();
|
setTextColor();
|
||||||
}, 200);
|
}, 200);
|
||||||
|
saveCustomColours({});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -10,6 +10,9 @@ export function allImagesLoaded(): Promise<void[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function imageLoaded(img: HTMLImageElement): Promise<void> {
|
function imageLoaded(img: HTMLImageElement): Promise<void> {
|
||||||
|
if (!img.getAttribute("decoding")) {
|
||||||
|
img.decoding = "async";
|
||||||
|
}
|
||||||
return img.complete
|
return img.complete
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: new Promise((resolve) => {
|
: new Promise((resolve) => {
|
||||||
|
|
17
ts/routes/reviewer/+page.svelte
Normal file
17
ts/routes/reviewer/+page.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script>
|
||||||
|
import ReviewerBottomOuter from "./reviewer-bottom/ReviewerBottomOuter.svelte";
|
||||||
|
import ReviewerOuter from "./reviewerOuter.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ReviewerOuter></ReviewerOuter>
|
||||||
|
<ReviewerBottomOuter></ReviewerBottomOuter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
0
ts/routes/reviewer/index.ts
Normal file
0
ts/routes/reviewer/index.ts
Normal file
14
ts/routes/reviewer/reviewer-bottom/AnswerButton.svelte
Normal file
14
ts/routes/reviewer/reviewer-bottom/AnswerButton.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 { bridgeCommand } from "@tslib/bridgecommand";
|
||||||
|
import type { AnswerButtonInfo } from "./types";
|
||||||
|
|
||||||
|
export let info: AnswerButtonInfo;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => bridgeCommand(`ease${info.i}`)}>
|
||||||
|
{info.label}
|
||||||
|
</button>
|
16
ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
Normal file
16
ts/routes/reviewer/reviewer-bottom/RemainingNumber.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!--
|
||||||
|
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;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={cls}>
|
||||||
|
{#if underlined}
|
||||||
|
<u><slot /></u>
|
||||||
|
{:else}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
|
</span>
|
79
ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
Normal file
79
ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<!--
|
||||||
|
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 { Writable } from "svelte/store";
|
||||||
|
import AnswerButton from "./AnswerButton.svelte";
|
||||||
|
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||||
|
import * as tr from "@generated/ftl";
|
||||||
|
import RemainingNumber from "./RemainingNumber.svelte";
|
||||||
|
import type { AnswerButtonInfo } from "./types";
|
||||||
|
|
||||||
|
export let answerButtons: Writable<AnswerButtonInfo[]>;
|
||||||
|
export let remaining: Writable<number[]>;
|
||||||
|
export let remainingIndex: Writable<number>;
|
||||||
|
|
||||||
|
$: console.log($remaining);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="outer" class="fancy">
|
||||||
|
<div id="tableinner">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
title={tr.actionsShortcutKey({ val: "E" })}
|
||||||
|
on:click={() => bridgeCommand("edit")}
|
||||||
|
>
|
||||||
|
{tr.studyingEdit()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="review-buttons">
|
||||||
|
<span>
|
||||||
|
<RemainingNumber cls="new-count" underlined={$remainingIndex === 0}>
|
||||||
|
{$remaining[0]}
|
||||||
|
</RemainingNumber> +
|
||||||
|
<RemainingNumber cls="learn-count" underlined={$remainingIndex === 1}>
|
||||||
|
{$remaining[1]}
|
||||||
|
</RemainingNumber> +
|
||||||
|
<RemainingNumber cls="review-count" underlined={$remainingIndex === 2}>
|
||||||
|
{$remaining[2]}
|
||||||
|
</RemainingNumber>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{#if $answerButtons.length}
|
||||||
|
{#each $answerButtons as answerButton}
|
||||||
|
<AnswerButton info={answerButton}></AnswerButton>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<button on:click={() => bridgeCommand("ans")}>
|
||||||
|
{tr.studyingShowAnswer()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
on:click={() => bridgeCommand("more")}
|
||||||
|
title={tr.actionsShortcutKey({ val: "M" })}
|
||||||
|
>
|
||||||
|
{tr.studyingMore()}↧
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#tableinner {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -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 { onMount } from "svelte";
|
||||||
|
import ReviewerBottom from "./ReviewerBottom.svelte";
|
||||||
|
import "./index.scss";
|
||||||
|
import { setupBottomBar } from "./reviewer-bottom";
|
||||||
|
|
||||||
|
let reviewerInfo: null | ReturnType<typeof setupBottomBar> = null;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
reviewerInfo = setupBottomBar();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if reviewerInfo}
|
||||||
|
<ReviewerBottom {...reviewerInfo}></ReviewerBottom>
|
||||||
|
{/if}
|
|
@ -1,9 +1,10 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
@use "../../../../../ts/lib/sass/root-vars";
|
@use "../../../lib/sass/root-vars";
|
||||||
@use "../../../../../ts/lib/sass/vars" as *;
|
@use "../../../lib/sass/vars" as *;
|
||||||
@use "../../../../../ts/lib/sass/card-counts";
|
@use "../../../lib/sass/card-counts";
|
||||||
|
@use "../../../lib/sass/buttons";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--focus-color: #{palette-of(border-focus)};
|
--focus-color: #{palette-of(border-focus)};
|
||||||
|
@ -16,6 +17,8 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#middle td[align="center"] {
|
#middle td[align="center"] {
|
90
ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts
Normal file
90
ts/routes/reviewer/reviewer-bottom/reviewer-bottom.ts
Normal file
|
@ -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 = `<font color=red>${timeString}</font>`;
|
||||||
|
} else {
|
||||||
|
timeNode.textContent = timeString;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
const answerButtons = writable<AnswerButtonInfo[]>([]);
|
||||||
|
const remaining = writable<[number, number, number]>([0, 0, 0]);
|
||||||
|
const remainingIndex = writable<number>(-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,
|
||||||
|
};
|
||||||
|
}
|
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;
|
||||||
|
}
|
16
ts/routes/reviewer/reviewer.svelte
Normal file
16
ts/routes/reviewer/reviewer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let html: Writable<string>;
|
||||||
|
export let cardClass: Writable<string>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="qa" class={$cardClass}>
|
||||||
|
{@html $html}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#qa {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
18
ts/routes/reviewer/reviewer.ts
Normal file
18
ts/routes/reviewer/reviewer.ts
Normal file
|
@ -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 };
|
||||||
|
}
|
21
ts/routes/reviewer/reviewerOuter.svelte
Normal file
21
ts/routes/reviewer/reviewerOuter.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { setupReviewer } from "./reviewer";
|
||||||
|
import Reviewer from "./reviewer.svelte";
|
||||||
|
|
||||||
|
import "../../reviewer/reviewer.scss";
|
||||||
|
|
||||||
|
let reviewerInfo: null | ReturnType<typeof setupReviewer> = null;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
reviewerInfo = setupReviewer();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if reviewerInfo}
|
||||||
|
<Reviewer {...reviewerInfo}></Reviewer>
|
||||||
|
{/if}
|
|
@ -6939,8 +6939,8 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite@npm:6":
|
"vite@npm:6":
|
||||||
version: 6.3.5
|
version: 6.3.6
|
||||||
resolution: "vite@npm:6.3.5"
|
resolution: "vite@npm:6.3.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: "npm:^0.25.0"
|
esbuild: "npm:^0.25.0"
|
||||||
fdir: "npm:^6.4.4"
|
fdir: "npm:^6.4.4"
|
||||||
|
@ -6989,7 +6989,7 @@ __metadata:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
vite: bin/vite.js
|
vite: bin/vite.js
|
||||||
checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c
|
checksum: 10c0/add701f1e72596c002275782e38d0389ab400c1be330c93a3009804d62db68097a936ca1c53c3301df3aaacfe5e328eab547060f31ef9c49a277ae50df6ad4fb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue