Merge branch 'main' into descriptive_launcher

This commit is contained in:
GithubAnon0000 2025-09-24 14:29:20 +00:00 committed by GitHub
commit c22aa78eb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 217 additions and 52 deletions

View file

@ -1 +1 @@
25.09
25.09.2

View file

@ -1 +1,2 @@
nodeLinker: node-modules
enableScripts: false

View file

@ -49,6 +49,7 @@ Sander Santema <github.com/sandersantema/>
Thomas Brownback <https://github.com/brownbat/>
Andrew Gaul <andrew@gaul.org>
kenden
Emil Hamrin <github.com/e-hamrin>
Nickolay Yudin <kelciour@gmail.com>
neitrinoweb <github.com/neitrinoweb/>
Andreas Reis <github.com/nwwt>

4
Cargo.lock generated
View file

@ -46,9 +46,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.1.1"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
dependencies = [
"cssparser",
"html5ever 0.35.0",

View file

@ -51,7 +51,7 @@ ninja_gen = { "path" = "build/ninja_gen" }
unicase = "=2.6.0" # any changes could invalidate sqlite indexes
# normal
ammonia = "4.1.0"
ammonia = "4.1.2"
anyhow = "1.0.98"
async-compression = { version = "0.4.24", features = ["zstd", "tokio"] }
async-stream = "0.3.6"

View file

@ -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();

View file

@ -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 <jakub.kaczmarzyk@gmail.com>"
ENTRYPOINT ["/opt/anki/venv/bin/anki"]

@ -1 +1 @@
Subproject commit 6552c95a81d162422b2a50126547cc7f1b50c2fd
Subproject commit 480ef0da728c7ea3485c58529ae7ee02be3e5dba

View file

@ -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

@ -1 +1 @@
Subproject commit dad4e2736a2b53dcdb52d79b5703dd464c05d666
Subproject commit fd5f984785ad07a0d3dbd893ee3d7e3671eaebd6

View file

@ -27,6 +27,9 @@ service FrontendService {
rpc deckOptionsRequireClose(generic.Empty) returns (generic.Empty);
// Warns python that the deck option web view is ready to receive requests.
rpc deckOptionsReady(generic.Empty) returns (generic.Empty);
// Save colour picker's custom colour palette
rpc SaveCustomColours(generic.Empty) returns (generic.Empty);
}
service BackendFrontendService {}

View file

@ -18,7 +18,7 @@ from anki._legacy import DeprecatedNamesMixinForModule
TR = anki._fluent.LegacyTranslationEnum
FormatTimeSpan = _pb.FormatTimespanRequest
# When adding new languages here, check lang_to_disk_lang() below
langs = sorted(
[
("Afrikaans", "af_ZA"),
@ -38,6 +38,7 @@ langs = sorted(
("Italiano", "it_IT"),
("lo jbobau", "jbo_EN"),
("Lenga d'òc", "oc_FR"),
("Қазақша", "kk_KZ"),
("Magyar", "hu_HU"),
("Nederlands", "nl_NL"),
("Norsk", "nb_NO"),
@ -64,6 +65,7 @@ langs = sorted(
("Українська мова", "uk_UA"),
("Հայերեն", "hy_AM"),
("עִבְרִית", "he_IL"),
("ייִדיש", "yi"),
("العربية", "ar_SA"),
("فارسی", "fa_IR"),
("ภาษาไทย", "th_TH"),
@ -104,6 +106,7 @@ compatMap = {
"it": "it_IT",
"ja": "ja_JP",
"jbo": "jbo_EN",
"kk": "kk_KZ",
"ko": "ko_KR",
"la": "la_LA",
"mn": "mn_MN",
@ -126,6 +129,7 @@ compatMap = {
"uk": "uk_UA",
"uz": "uz_UZ",
"vi": "vi_VN",
"yi": "yi",
}
@ -233,7 +237,7 @@ def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
def is_rtl(lang: str) -> bool:
return lang in ("he", "ar", "fa", "ug")
return lang in ("he", "ar", "fa", "ug", "yi")
# strip off unicode isolation markers from a translated string

View file

@ -226,6 +226,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Anon_0000",
"Bilolbek Normuminov",
"Sagiv Marzini",
"Zhanibek Rassululy",
)
)

View file

@ -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)

View file

@ -151,6 +151,7 @@ class Editor:
self.add_webview()
self.setupWeb()
self.setupShortcuts()
self.setupColourPalette()
gui_hooks.editor_did_init(self)
# Initial setup
@ -349,6 +350,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
keys, fn, _ = row
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 checkFocus() -> None:
if self.currentField is None:

View file

@ -170,13 +170,42 @@ def favicon() -> Response:
def _mime_for_path(path: str) -> str:
"Mime type for provided path/filename."
if path.endswith(".css"):
# some users may have invalid mime type in the Windows registry
return "text/css"
elif path.endswith(".js") or path.endswith(".mjs"):
return "application/javascript"
_, ext = os.path.splitext(path)
ext = ext.lower()
# Badly-behaved apps on Windows can alter the standard mime types in the registry, which can completely
# break Anki's UI. So we hard-code the most common extensions.
mime_types = {
".css": "text/css",
".js": "application/javascript",
".mjs": "application/javascript",
".html": "text/html",
".htm": "text/html",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".ico": "image/x-icon",
".json": "application/json",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".webm": "video/webm",
".ogg": "audio/ogg",
".pdf": "application/pdf",
".txt": "text/plain",
}
if mime := mime_types.get(ext):
return mime
else:
# autodetect
# fallback to mimetypes, which may consult the registry
mime, _encoding = mimetypes.guess_type(path)
return mime or "application/octet-stream"
@ -599,6 +628,15 @@ def deck_options_ready() -> bytes:
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 = [
congrats_info,
get_deck_configs_for_update,
@ -614,6 +652,7 @@ post_handler_list = [
search_in_browser,
deck_options_require_close,
deck_options_ready,
save_custom_colours,
]

View file

@ -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")

View file

@ -180,7 +180,7 @@ class CustomStyles:
QPushButton {{
margin: 1px;
}}
QPushButton:focus {{
QPushButton:focus, QPushButton:default:hover {{
border: 2px solid {tm.var(colors.BORDER_FOCUS)};
outline: none;
margin: 0px;
@ -199,9 +199,6 @@ class CustomStyles:
)
};
}}
QPushButton:default:hover {{
border-width: 2px;
}}
QPushButton:pressed,
QPushButton:checked,
QSpinBox::up-button:pressed,

View file

@ -134,5 +134,8 @@ pub fn ensure_os_supported() -> Result<()> {
#[cfg(all(unix, not(target_os = "macos")))]
unix::ensure_glibc_supported()?;
#[cfg(target_os = "windows")]
windows::ensure_windows_version_supported()?;
Ok(())
}

View file

@ -38,6 +38,26 @@ fn is_windows_10() -> bool {
}
}
/// Ensures Windows 10 version 1809 or later
pub fn ensure_windows_version_supported() -> Result<()> {
unsafe {
let mut info = OSVERSIONINFOW {
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
..Default::default()
};
if RtlGetVersion(&mut info).is_err() {
anyhow::bail!("Failed to get Windows version information");
}
if info.dwBuildNumber >= 17763 {
return Ok(());
}
anyhow::bail!("Windows 10 version 1809 or later is required.")
}
}
pub fn ensure_terminal_shown() -> Result<()> {
unsafe {
if !GetConsoleWindow().is_invalid() {

View file

@ -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()

View file

@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Shortcut from "$lib/components/Shortcut.svelte";
import { saveCustomColours } from "@generated/backend";
export let keyCombination: string | null = null;
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;
</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}
<Shortcut {keyCombination} on:action={() => inputRef.click()} />

View file

@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ColorPicker from "./ColorPicker.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import WithColorHelper from "./WithColorHelper.svelte";
import { saveCustomColours } from "@generated/backend";
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);
bridgeCommand(`lastHighlightColor:${color}`);
}}
on:change={() => setTextColor()}
on:change={() => {
setTextColor();
saveCustomColours({});
}}
/>
</IconButton>
</WithColorHelper>

View file

@ -22,6 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ColorPicker from "./ColorPicker.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import WithColorHelper from "./WithColorHelper.svelte";
import { saveCustomColours } from "@generated/backend";
export let color: string;
@ -158,6 +159,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setTimeout(() => {
setTextColor();
}, 200);
saveCustomColours({});
}}
/>
</IconButton>

View file

@ -10,6 +10,9 @@ export function allImagesLoaded(): Promise<void[]> {
}
function imageLoaded(img: HTMLImageElement): Promise<void> {
if (!img.getAttribute("decoding")) {
img.decoding = "async";
}
return img.complete
? Promise.resolve()
: new Promise((resolve) => {

View file

@ -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