From 7e8a1076c167b72ab2d2822878f94b9f44598fe8 Mon Sep 17 00:00:00 2001
From: Emil Hamrin <68200971+e-hamrin@users.noreply.github.com>
Date: Wed, 17 Sep 2025 06:02:09 +0200
Subject: [PATCH 1/4] Updated Dockerfile to use Ninja build system (#4321)
* Updated Dockerfile to support ninja build
* Install python using uv
* Bumped python version
* Add disclaimer (dae)
---
CONTRIBUTORS | 1 +
docs/docker/Dockerfile | 83 ++++++++++++++++++++++++++++++++----------
2 files changed, 64 insertions(+), 20 deletions(-)
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/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
From 9e415869b883589c67021dabaf65ee688139a9d7 Mon Sep 17 00:00:00 2001
From: Luc Mcgrady
Date: Wed, 17 Sep 2025 05:04:27 +0100
Subject: [PATCH 2/4] Fix/Add lower review limit to health check. (#4334)
---
rslib/src/scheduler/fsrs/params.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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()
From c2957746f4c4eb14598da3977f1193d45588ecda Mon Sep 17 00:00:00 2001
From: snowtimeglass
Date: Wed, 17 Sep 2025 14:13:59 +0900
Subject: [PATCH 3/4] Make timebox message translatable with flexible variable
order (#4338)
* Make timebox message translatable with flexible variable order
Currently, the timebox dialog message is built from two separate strings,
each containing one variable:
"{ $count } cards studied in" + "{ $count } minutes."
As a result, translators cannot freely reorder the variables in their translations.
This change introduces a single string with both variables, allowing translators
to adjust the order for more natural expressions in their languages.
* Preserve old string for now
* Ensure message doesn't display over two lines
---------
Co-authored-by: Damien Elmes
---
ftl/core/studying.ftl | 16 ++++++++++++++--
qt/aqt/reviewer.py | 13 +++++++++----
2 files changed, 23 insertions(+), 6 deletions(-)
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/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")
From ec6f09958a12d7f29e5f2c5b37589564e8ea4ced Mon Sep 17 00:00:00 2001
From: jcznk <60730312+jcznk@users.noreply.github.com>
Date: Wed, 17 Sep 2025 07:30:22 +0200
Subject: [PATCH 4/4] (UI polish) Improved margins in Card Browser's
"Previewer" (#4337)
* Improved margins in Card Browser's "Preview" pane
* Alternate approach that looks good on Mac too
---------
Co-authored-by: Damien Elmes
Co-authored-by: Damien Elmes
---
qt/aqt/browser/previewer.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py
index 4c9a97fb8..61096b5b3 100644
--- a/qt/aqt/browser/previewer.py
+++ b/qt/aqt/browser/previewer.py
@@ -13,7 +13,7 @@ import aqt.browser
from anki.cards import Card
from anki.collection import Config
from anki.tags import MARKED_TAG
-from aqt import AnkiQt, gui_hooks
+from aqt import AnkiQt, gui_hooks, is_mac
from aqt.qt import (
QCheckBox,
QDialog,
@@ -81,10 +81,15 @@ class Previewer(QDialog):
qconnect(self.finished, self._on_finished)
self.silentlyClose = True
self.vbox = QVBoxLayout()
+ spacing = 6
self.vbox.setContentsMargins(0, 0, 0, 0)
+ self.vbox.setSpacing(spacing)
self._web: AnkiWebView | None = AnkiWebView(kind=AnkiWebViewKind.PREVIEWER)
self.vbox.addWidget(self._web)
self.bbox = QDialogButtonBox()
+ self.bbox.setContentsMargins(
+ spacing, spacing if is_mac else 0, spacing, spacing
+ )
self.bbox.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
gui_hooks.card_review_webview_did_init(self._web, AnkiWebViewKind.PREVIEWER)