diff --git a/.yarnrc.yml b/.yarnrc.yml
index 3186f3f07..94f5c254e 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -1 +1,2 @@
nodeLinker: node-modules
+enableScripts: false
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index b03108e16..7064c6885 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -49,6 +49,7 @@ Sander Santema
Thomas Brownback
Andrew Gaul
kenden
+Emil Hamrin
Nickolay Yudin
neitrinoweb
Andreas Reis
diff --git a/build/runner/src/yarn.rs b/build/runner/src/yarn.rs
index 9e1bd5b58..7724ed04a 100644
--- a/build/runner/src/yarn.rs
+++ b/build/runner/src/yarn.rs
@@ -28,7 +28,11 @@ pub fn setup_yarn(args: YarnArgs) {
.arg("--ignore-scripts"),
);
} 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();
diff --git a/docs/docker/Dockerfile b/docs/docker/Dockerfile
index 6682f70f6..381d27d1c 100644
--- a/docs/docker/Dockerfile
+++ b/docs/docker/Dockerfile
@@ -1,35 +1,78 @@
-# This Dockerfile uses three stages.
-# 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.
+# This is a user-contributed Dockerfile. No official support is available.
-ARG PYTHON_VERSION="3.9"
ARG DEBIAN_FRONTEND="noninteractive"
-# Build anki.
-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
+FROM ubuntu:24.04 AS build
WORKDIR /opt/anki
-COPY . .
-# Build python wheels.
+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 . .
+
RUN ./tools/build
+
# Install pre-compiled Anki.
-FROM python:${PYTHON_VERSION}-slim as installer
+FROM python:3.13-slim AS installer
WORKDIR /opt/anki/
-COPY --from=build /opt/anki/wheels/ wheels/
+COPY --from=build /opt/anki/out/wheels/ wheels/
# Use virtual environment.
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 /opt/anki/wheels/*.whl
+
# 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
ENV PATH=/opt/anki/venv/bin:$PATH
# Install run-time dependencies.
@@ -59,9 +102,9 @@ RUN apt-get update \
libxrender1 \
libxtst6 \
&& rm -rf /var/lib/apt/lists/*
+
# Add non-root user.
RUN useradd --create-home anki
USER anki
WORKDIR /work
-ENTRYPOINT ["/opt/anki/venv/bin/anki"]
-LABEL maintainer="Jakub Kaczmarzyk "
+ENTRYPOINT ["/opt/anki/venv/bin/anki"]
\ No newline at end of file
diff --git a/ftl/core-repo b/ftl/core-repo
index 6552c95a8..60bd4d483 160000
--- a/ftl/core-repo
+++ b/ftl/core-repo
@@ -1 +1 @@
-Subproject commit 6552c95a81d162422b2a50126547cc7f1b50c2fd
+Subproject commit 60bd4d4834b57b85915f52eb978935ff6b8c1425
diff --git a/ftl/core/studying.ftl b/ftl/core/studying.ftl
index ed3f8eb30..a317a68ba 100644
--- a/ftl/core/studying.ftl
+++ b/ftl/core/studying.ftl
@@ -46,6 +46,20 @@ studying-type-answer-unknown-field = Type answer: unknown field { $val }
studying-unbury = 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-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 =
{ $count ->
[one] { $count } card studied in
@@ -56,5 +70,3 @@ studying-minute =
[one] { $count } minute.
*[other] { $count } minutes.
}
-studying-question-time-elapsed = Question time elapsed
-studying-answer-time-elapsed = Answer time elapsed
diff --git a/ftl/qt-repo b/ftl/qt-repo
index dad4e2736..fd5f98478 160000
--- a/ftl/qt-repo
+++ b/ftl/qt-repo
@@ -1 +1 @@
-Subproject commit dad4e2736a2b53dcdb52d79b5703dd464c05d666
+Subproject commit fd5f984785ad07a0d3dbd893ee3d7e3671eaebd6
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index a8839c598..6d68f9e3a 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -17,6 +17,7 @@ import aqt.browser
import aqt.operations
from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount
+from anki.lang import with_collapsed_whitespace
from anki.scheduler.base import ScheduleCardsAsNew
from anki.scheduler.v3 import (
CardAnswer,
@@ -966,11 +967,15 @@ timerStopped = false;
elapsed = self.mw.col.timeboxReached()
if elapsed:
assert not isinstance(elapsed, bool)
- part1 = tr.studying_card_studied_in(count=elapsed[1])
- mins = int(round(elapsed[0] / 60))
- part2 = tr.studying_minute(count=mins)
+ cards_val = elapsed[1]
+ minutes_val = int(round(elapsed[0] / 60))
+ message = with_collapsed_whitespace(
+ tr.studying_card_studied_in_minute(
+ cards=cards_val, minutes=str(minutes_val)
+ )
+ )
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)
if diag.run() == fin:
self.mw.moveToState("deckBrowser")
diff --git a/rslib/src/scheduler/fsrs/params.rs b/rslib/src/scheduler/fsrs/params.rs
index 726870fe1..d7bb56f5b 100644
--- a/rslib/src/scheduler/fsrs/params.rs
+++ b/rslib/src/scheduler/fsrs/params.rs
@@ -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)?;
fsrs.evaluate_with_time_series_splits(input, |_| true)
.ok()
diff --git a/yarn.lock b/yarn.lock
index 84bd46e31..761f20972 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6939,8 +6939,8 @@ __metadata:
linkType: hard
"vite@npm:6":
- version: 6.3.5
- resolution: "vite@npm:6.3.5"
+ version: 6.3.6
+ resolution: "vite@npm:6.3.6"
dependencies:
esbuild: "npm:^0.25.0"
fdir: "npm:^6.4.4"
@@ -6989,7 +6989,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
- checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c
+ checksum: 10c0/add701f1e72596c002275782e38d0389ab400c1be330c93a3009804d62db68097a936ca1c53c3301df3aaacfe5e328eab547060f31ef9c49a277ae50df6ad4fb
languageName: node
linkType: hard