358 Commits
Author SHA1 Message Date
aarian 7a4db0038e chore: enable strict_optional for aqt modules needing no changes (#5645)
Closes #5644. Part of #3317.

22 `aqt` modules already type-check cleanly under `strict_optional`, so
this opts them in. Config only — no code changes.

Verified clean with `mypy --platform` for `darwin`, `linux` and `win32`.

`aqt.mpv` is left out: it sets `ignore_errors = True`, so a stanza there
would be a no-op.
2026-10-01 06:57:28 +03:00
Louis VicatandAbdo cd096774bc fix: apply video driver settings before the Qt app is created (#5701)
## Linked issue (required)

Fixes #5700

## Summary / motivation (required)

Since #5637, `setupGL()` runs after `AnkiApp` is constructed. Some of
what it sets is only read by Qt at startup (`AA_UseSoftwareOpenGL` on
macOS, `QT_OPENGL` on Windows), so safe mode and the Software driver are
no longer fully applied.

This calls `setupGL()` before creating the app again, where it was
before #5637. Safe mode is decided at that point, from `--safemode` or
from Shift. Since Qt can only detect Shift once the app exists,
`shift_held_before_app()` checks it with platform APIs:
`CGEventSourceFlagsState()` on macOS and `GetAsyncKeyState(VK_SHIFT)` on
Windows.

If Qt still detects Shift after the app is created but the check above
didn't (e.g. on Linux, where it isn't implemented), `setupGL()` is
called again with the Software driver. On Linux the settings it makes
still take effect at that point, so that path keeps working as it does
today. Happy to drop that fallback if you'd prefer.

## Steps to reproduce (required, use N/A if not applicable)

1. On macOS, run `/Applications/Anki.app/Contents/MacOS/anki --safemode`
from a terminal
2. The console shows `Qt warning: Attribute Qt::AA_UseSoftwareOpenGL
must be set before QCoreApplication is created.`

## How to test (required)

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- `qt/tests/test_startup_video_driver.py` runs `_run()` with a stand-in
for `AnkiApp` and checks that `setupGL()` is called with the expected
driver (profile driver, or Software with `--safemode` or Shift held)
before the app is constructed. Fails on main, passes with this change.
- Launched the source tree on macOS with a temporary base folder, with
`--safemode`: the warning above appears on main and is gone with this
change. With Shift held, `shift_held_before_app()` returns True and the
warning doesn't appear either.
- On Windows 11, checked the `GetAsyncKeyState(VK_SHIFT)` call used
here: it returns False before Shift is pressed and True while it's held.
I haven't tested a full Anki launch on Windows.
- ruff check/format pass on the changed files. I haven't been able to
run the full `just check` locally yet, so I'm relying on CI for the
rest.

## Before / after behavior (optional)

Before: `--safemode` and the Software driver don't set
`AA_UseSoftwareOpenGL` (macOS) / `QT_OPENGL` (Windows) in time. After:
they're applied before the app is created.

## Risk / compatibility / migration (optional)

This touches the same startup block as #5686, so whichever lands second
will need a small rebase. I'll take care of it if this one goes second.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-10-01 06:44:51 +03:00
mslxl d1d484c824 fix: stream full sync to disk instead of buffering in memory (#5717)
## Linked issue (required)

Fixes #5716

## Summary / motivation (required)

During a full download, Anki loads the whole collection into memory
before saving it to disk. With a large collection, iOS can kill the app
partway through the sync because of OOM. See #5716 for details.

This PR streams the download straight into the temporary file instead,
so memory usage no longer grows with the collection size.

## Steps to reproduce (required, use N/A if not applicable)

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->

1. Set up a self-hosted `anki-sync-server`, and put a large enough
collection database(My collection is ~3GiB)
2. On AnkiMobile or Amgi, with an empty local collection, point the
client to the self-hosted server
3. Start full sync, and then it get killed because of OOM


## How to test (required)

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->

- Pointed [Amgi](https://github.com/antigluten/amgi)'s `anki-upstream`
submodule at this branch, then ran a full download of a ~3 GiB
collection from a self-hosted sync server on iOS. The app no longer gets
killed for running out of memory, and the sync completes. AnkiMobile is
close-source so I can not test it.
- Built and ran Anki desktop from this branch. It starts and works
normally.
- Ran the `cargo test -p anki --lib sync::`


### Checklist (minimum)

- [X] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

## Before / after behavior (optional)

Before: A full download loads the whole collection into memory before
saving it, so on iOS, syncing a large collection (e.g. ~3 GiB) can get
the app killed for running out of memory.

After: The collection is saved to disk as it downloads

Measured peak memory while downloading a 512 MiB collection from a LAN
server: it dropped from about 534 MB to about 22 MB with Amgi.

## Scope

- [X] This PR is focused on one change (no unrelated edits).
2026-09-29 13:52:29 +01:00
Artem Lytkin 41d7464c9b fix: apply the UI scale before creating the Qt app (#5686)
## Linked issue (required)

Fixes #5676

## Summary / motivation (required)

#5637 moved the `QT_SCALE_FACTOR` assignment to after `AnkiApp` is
created, but Qt only reads it when the app is constructed, so the "User
interface size" setting stopped doing anything. this sets it before the
app is created again; `setupGL()` stays where it is (smaller version
suggested in review)

## Steps to reproduce (required, use N/A if not applicable)

set "User interface size" to 200% in preferences, restart, the UI is
still the default size

## How to test (required)

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

qt/tests/test_startup.py runs `_run()` with a stand-in for AnkiApp and
checks QT_SCALE_FACTOR is already set when it's constructed, fails on
main and passes with this. also checked with the 26.9.3 wheels on
Windows at 150%: device pixel ratio 1.5 before, 3.0 after

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-09-28 14:15:54 +03:00
Zain QureshiandClaude Opus 5.5 bd69a45305 docs: minimum Python for the pylib/aqt packages is 3.10 (#5678)
## Linked issue (required)

Closes #5679

## Summary / motivation (required)

The "pre-built Python wheels" section in both `docs/development.md` and
`docs-site/developers/development.mdx` says you need Python 3.9 or later
and recommends 3.9 because 3.10+ had minimal testing. The packages now
declare `requires-python = ">=3.10"` (`pylib/pyproject.toml`,
`qt/pyproject.toml`), so pip on 3.9 won't install them at all. This
updates both copies to 3.10 or later and drops the outdated 3.9
recommendation.

This is my first PR, so it also adds my name to `CONTRIBUTORS`.

## Steps to reproduce (required, use N/A if not applicable)

N/A (docs).

## How to test (required)

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

Docs-only; compared against `requires-python` in every `pyproject.toml`
in the repo (the root dev environment is `>=3.12`, the published
packages are `>=3.10`).

## Scope

- [x] This PR is focused on one change (no unrelated edits).

AI assistance: found and prepared with Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 11:20:34 -03:00
Athitheya GobinathanandAbdo a5a0e44467 Fix premature closure of cloze when inside MathJax (#4612)
The cloze tokenizer in `rslib/src/cloze.rs` previously treated any
occurrence of `}}` as a cloze terminator, even when it appeared inside
MathJax expressions.

Example:
    {{c1:: \( \frac{1}{ \sqrt{ \pi }} \) }}

The `}}` from `\sqrt{\pi}}` would cause the cloze to terminate early,
resulting in incorrect parsing and rendering.
<img width="725" height="257" alt="Screenshot 2026-03-14 at 11 17 21 AM"
src="https://github.com/user-attachments/assets/76e475ed-b3ab-4bb5-9538-2e65d3a136fe"
/>

<img width="610" height="253" alt="Screenshot 2026-03-14 at 11 17 46 AM"
src="https://github.com/user-attachments/assets/ecefe3ad-960d-4fd8-a2d9-501edef11679"
/>

`normal_text()` now tracks when the scanner is inside a MathJax region
(`\(...\)` or `\[...\]`). While inside MathJax, `}}` is ignored as a
cloze terminator.


Cloze openings are still recognized inside MathJax.

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-09-24 14:41:31 +03:00
wofiporiaandAbdo e6fefb2962 docs: improve manual translation instructions (#5519)
## Linked issue (required)

Fixes #5510

## Summary / motivation (required)

Expands the manual translation instructions as requested in #5510:

- `docs-site/translators/anki/manual.mdx` now contains detailed
instructions for translating the documentation: forking the repository,
working in a language subdirectory (existing or new), keeping Markdown
formatting intact while translating the MDX pages, registering
translated pages in `docs.json` (with an example following the shape of
the existing language entries), and submitting a PR. It also notes that
partial translations are welcome, that documentation changes are
licensed under CC BY-SA 4.0, and where to ask questions.
- The Translations section of `docs-site/README.md` now links to that
page instead of holding the details itself, reversing the previous
direction per the issue.
- The Translations sections of `docs-site/developers/contributing.mdx`
and the cog-generated `docs/contributing.md` briefly mention
documentation translations, which were previously only covered for
interface strings.

Also adds myself to the CONTRIBUTORS file in a separate commit, as
requested for first pull requests.

## Steps to reproduce (required, use N/A if not applicable)

N/A

## How to test (required)

Documentation-only change. Verified that:

- All paths referenced in the new instructions exist in the repository
(e.g. `docs-site/ru/manual/`, `docs-site/manual/`, and the other
language subdirectories).
- The `docs.json` example matches the structure of the existing language
entries under `navigation.languages`.
- Both internal links resolve to pages registered in
`docs-site/docs.json` (`/translators/anki/manual`,
`/translators/anki/developers`).
- No MDX-special characters (`{`, `<`) appear outside fenced code blocks
in the new content, so `mintlify validate` should pass.
- `docs/contributing.md` stays in sync with
`docs-site/developers/contributing.mdx` under the transformation in
`docs/cogdocs.py`.

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
*Not applicable for a documentation-only change; the relevant check is
the Docs Site workflow (`mintlify validate` / `mintlify a11y`), which
runs in CI.*
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
*N/A - documentation-only changes are exempt from tests per
`contributing.md`.*

## Before / after behavior (optional)

N/A

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-09-22 12:06:56 +03:00
praxishandAbdo f5a0b60806 docs: clarify Windows x64 and ARM64 installation requirements (#5582)
## Linked issue

Fixes #5336

## Summary / motivation

The Windows installation guide lists only a generic 64-bit Windows
requirement, even though Anki provides separate x64 and ARM64 installers
with different Windows requirements. Document both configurations and
explain how to match the processor type shown in Windows Settings to the
download label.

## Steps to reproduce

N/A: documentation only.

## How to test

- [x] `RELEASE=1 just check` passed on the focused branch on macOS Apple
Silicon.
- [x] No unit tests added for a documentation-only change.
- Confirmed the requirements and download labels against [Anki's
download page](https://apps.ankiweb.net/): Windows 10+ (x64) and Windows
11 (ARM).
- Confirmed that release 26.08.1 includes separate `win-x64.msi` and
`win-arm64.msi` installers.
- Checked the Settings path against [Microsoft's system-type
instructions](https://support.microsoft.com/en-us/windows/32-bit-and-64-bit-windows-frequently-asked-questions-c6ca9541-8dce-4d48-0415-94a3faa2e13d)
and [Windows ARM64
guidance](https://www.microsoft.com/en-us/software-download/windows11arm64).
- Mintlify 4.2.884: build validation and accessibility checks passed.
The site-wide accessibility scan reports 32 pre-existing parse warnings
on other pages; the edited Windows page is checked successfully.

## Risk / compatibility / migration

Documentation only. Existing guidance for older Windows releases and
32-bit systems is retained. No installer or platform support changes.

## UI evidence

N/A: documentation only.

## Scope

- [x] Focused on Windows requirements and choosing the matching
installer.

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-09-11 20:08:37 +03:00
Caleb_Meadows 0842216571 fix: preserve FSRS unlock timeout across mounts (#5536)
## Linked issue (required)

Fixes #5516

## Summary / motivation (required)

Make the existing FSRS parameter unlock timing API available before the
parameter editor is mounted, and retain the host's chosen timeout when
FSRS is disabled and enabled again. Register the API and store its
timeout in the component's module script; keep click tracking local to
each editor instance.

## Steps to reproduce (required, use N/A if not applicable)

1. Open the deck-options page with FSRS disabled.
2. In that page's JavaScript console, run
`anki.setParameterUnlockClickTimeoutMs(1000)`.
3. Before this fix, the call throws `TypeError:
anki.setParameterUnlockClickTimeoutMs is not a function`.

## How to test (required)

### Checklist (minimum)

- [x] I ran `just check` locally.
- [x] I added a browser regression for the changed behavior.

### Details

- The regression failed on the original implementation at the pre-mount
setter call and passes with the fix.
- It checks configuration before the first mount, retention across
remounts, updates while absent and present, the three-click gate, the
default timeout, and reset after page reload. Playwright's clock
controls the click intervals.
- `just test-ts`: 61 tests passed.
- `just test-e2e`: 27 tests passed, using the repository's disposable
Anki harness.
- `just check`: passed, including 572 Rust tests and the Python,
formatting, lint, and type checks.
- The Qt checks emitted an audio-thread warning (`mw` was `None` in
`qt/aqt/sound.py`); all 93 Qt tests passed and `just check` exited
successfully.

## Before / after behavior (optional)

Before: the API exists only after the editor mounts, and mounting a new
editor resets the timeout to 500 ms.

After: the API exists when the deck-options JavaScript loads, and the
selected timeout lasts for the page's lifetime.

## Risk / compatibility / migration (optional)

Existing API names, the 500 ms default, and the three-click requirement
are preserved. Click counts and pending timers remain local to each
editor. No stored data or scheduling behavior changes.

## UI evidence (required for visual changes; otherwise N/A)

N/A.

## Scope

- [x] This PR is focused on one change.

Developed with assistance from Codex.
2026-09-08 21:42:14 +01:00
jamesliuai 9e32ad8849 fix: refresh reviewer toolbar background after theme changes (#5241)
## Linked issue (required)

Fixes #5240 

## Summary / motivation (required)

During review, the top toolbar copies the main reviewer webview's
computed background so that the card background continues behind the
toolbar. When the theme changes, the webviews update their theme
classes, but the toolbar's copied inline `background` remains unchanged.
This leaves the top of the reviewer using the previous theme's
background.

This PR clears the stale inline background when the theme changes and
schedules it to be copied again after the webviews have queued their
theme-class updates. The additional behavior is limited to the review
screen.

Tests cover refreshing the background during review and ensuring that
the background-copying behavior is not triggered on other screens.

This is narrower than the general live-theme issue addressed by #1471
and #1497. The reviewer toolbar's computed-background behavior was
introduced later as part of #2262.

## Steps to reproduce (required, use N/A if not applicable)

1. Start Anki and open **Preferences**.
2. Set **Theme** to **Light**.
3. Ensure **Minimalist mode** and **Hide top bar during review** are
disabled.
4. Close Preferences and start reviewing a deck.
5. While the question side of a card is displayed, reopen Preferences.
6. Change **Theme** from **Light** to **Dark**.
7. Close Preferences without showing the answer or moving to another
card.
8. Observe that the reviewer changes to the dark theme, but the
background behind the top toolbar remains light.

The issue can also be reproduced in the opposite direction, and when
Anki follows the system theme while the operating system changes between
light and dark appearance.

## How to test (required)

### Checklist (minimum)

- [x] I ran `just check` locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

Automated testing:

- `just fmt`
- `just test-py`
- `just check`

Manual testing:

1. Launched the fixed build with `just run`.
2. Repeated the reproduction steps above.
3. Confirmed that the top toolbar background immediately matches the
rest of the reviewer after switching from light to dark.
4. Repeated the test from dark to light.
5. Confirmed that changing themes outside the reviewer continues to
behave as before.

## Before / after behavior (optional)

Before: the reviewer updates to the new theme, but the background behind
the top toolbar retains the previous theme's color.

After: the top toolbar background is recalculated after the theme
changes and matches the rest of the reviewer.

## Risk / compatibility / migration (optional)

Low risk. The change only adds theme-change handling to `TopWebView`
while Anki is in the review state. It calls the existing base
theme-change handler and reuses the existing background-copying method.

## UI evidence (required for visual changes; otherwise N/A)

### Light to dark

**Before**

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/a04bb77b-5a34-459e-896c-d00a66601b83"
/>

**After**

<img width="400" alt="Screenshot 2026-07-30 at 13 52 49"
src="https://github.com/user-attachments/assets/481871b5-a4bb-43bc-8cbc-8400c447fae3"
/>

### Dark to light

**Before**

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/9a94f926-f4fc-440c-8f63-7a94feae9e71"
/>

**After**

<img width="400" alt="Screenshot 2026-07-30 at 13 52 39"
src="https://github.com/user-attachments/assets/c48a32d3-da0c-4593-b751-cdcd8a0487f6"
/>

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-09-02 17:10:51 +03:00
Asuka Minato b9d69a0abd bump fsrs to 6.6.2 (#5494)
closes #5495
2026-09-02 12:41:04 +03:00
Zaveshaa 0218af9bef Hide FSRS-only sort orders in filtered deck dialog when FSRS is disabled (#5407)
Fixes #5397.

Selecting ascending/descending retrievability (or relative overdueness)
for a filtered deck while FSRS is disabled raises a `SQLITE` error,
because `build_retrievability_query()` returns an empty order fragment
without FSRS (`order by , fnvhash(...)`), and the overdueness expression
reads FSRS memory state regardless.

As suggested in the issue by Luc, this hides those three orders in the
filtered deck dialog when FSRS is off, the same way deck options already
handles review order.

The combo box rows are mapped through the list of actually offered
orders instead of assuming row == enum value, so stored configs stay
valid. If a stored config references one of the hidden orders (e.g. FSRS
was turned off after it was built), the dialog falls back to Random
rather than showing a broken selection. I checked that the mapping
round-trips correctly with FSRS on and off, including the fallback path.
2026-09-01 13:45:01 -03:00
Carlos Mendez 5a58c8516f feat: improved error message for CSV import (#5484)
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.

Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->

## Linked issue (required)

<!-- Fixes #123 / Closes #123 -->
Fixes #4346 

## Summary / motivation (required)

<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
Improves the CSV import error experience for empty or invalid CSV files.

Previously, the import error page exposed an HTTP status code and did
not provide a clear way for the user to dismiss the error and return to
the main Anki window.

Changes:
- displays the import error without exposing the HTTP status code
- adds an "Okay" button to dismiss the error
- use Anki frontend RPC architecture to close the active import dialog
- handles CSV import loader errors with a try/catch and displays them
through the shared ErrorPage component

The solution uses the existing `ImportDialog.reject()` path so the
dialog box performs normal clean up before closing.

This implementation was inspired by the previous discussion and earlier
work in #4383. Thank you @medProgAyat and @josod827. Your discussion and
implementations were a great assest to helping me solve and implement a
solution.

## Steps to reproduce (required, use N/A if not applicable)

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->

1. open Anki and click on import button
2. select a empty csv
3. error page should open

## How to test (required)

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->

### Checklist (minimum)

- [ x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->
Manual testing:
1. Imported an empty CSV file.
2. Confirmed the custom error page is displayed.
3. Confirmed the HTTP status code is not displayed to the user.
4. Confirmed clicking the "Okay" button closes the import dialog and
returns to the main Anki window.
5. Imported a valid CSV file and confirmed the normal CSV import flow
still works.

## Before / after behavior (optional)

<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

Before:
- Empty CSV imports display an error without a clear dismissal action.
- Development error output may include the HTTP status code.

After:
- The error is displayed through the shared error UI.
- The user can click "Okay" to close the import dialog.
- The close action is handled through a frontend RPC.

## Risk / compatibility / migration (optional)

<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

Low risk. The change is limited to CSV import error handling and the
import dialog close path.


## UI evidence (required for visual changes; otherwise N/A)

<!-- Screenshot or short video -->

before:
<img width="800" height="800" alt="image"
src="https://github.com/user-attachments/assets/4fe77689-3ebd-4a98-9bd9-4916214bbb29"
/>

after:
<img width="788" height="818" alt="image"
src="https://github.com/user-attachments/assets/8451f465-894a-4f52-a11b-459fc6840326"
/>


## Scope

- [x ] This PR is focused on one change (no unrelated edits).
2026-09-01 15:43:18 +03:00
趙子賢 ce26e216f1 fix: only attach handler to add-on's root logger (#5497)
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.

Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->

## Linked issue (required)

Fixes #5487.

## Summary / motivation (required)

<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->

When deriving a child logger from an add-on logger, both of them got
`TimedRotatingFileHandler` attached to the loggers handler. This makes
log messages duplicated in log file and cause `PermissionError:
[WinError 32]` when rotating log files on Windows.

To solve this issue, the new `get_logger` only attaches
`TimedRotatingFileHandler` to the add-on logger (`addon.name`), not the
child one (`addon.name.child`). Besides, when a child logger is created
with `logging.getLogger("addon.name.child")`, the add-on logger
(`addon.name`) is created if not existed.
The add-on logger becomes the one and only logger that has
`TimedRotatingFileHandler` handler down the hierarchy from itself.

I also fixes a minor bug of `module` variable. In the previous
implementation, the `module` only contained part of the module name if
the name contained `addon.`. I use `removeprefix` to solve it.

## Steps to reproduce (required, use N/A if not applicable)

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->

`Anki2/addons21/test_logger/__init__.py`:
```python
from aqt.addons import AddonManager

logger = AddonManager.get_logger(__name__)
child_logger = logger.getChild("child")

child_logger.info("This info messages is duplicated in log file.")
```

When the log file is rotated on Windows, it raises `PermissionError:
[WinError 32]`. Please check #5487 for detailed bug report.

## How to test (required)

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->

Just create a test add-on to check this behavior as the "Steps to
reproduce" above.

## Before / after behavior (optional)

<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

### Before
1. When deriving a child logger from an add-on logger, both of them got
`TimedRotatingFileHandler` attached to the loggers handler. The log
messages duplicated in log file and cause `PermissionError: [WinError
32]` when rotating log files on Windows.
2. If the logger name contains `addon.`, characters after `addon.` are
stripped.

### After
1. When an add-on logger is created no matter it is an add-on logger
(`addon.name`) or a child logger (`addon.name.child`), the add-on logger
is created with `TimedRotatingFileHandler` handler, and the child logger
is created as a normal logger.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-09-01 10:51:54 +03:00
Jakson Souza 3a5d285821 fix: play audio immediately when attached in the editor (#5320)
## Linked issue (required)

Closes #5321

## Summary / motivation (required)

In the old Qt-based editor, attaching an audio file (paperclip button or
the mic recording button) played the file immediately, before the card
was saved. This was handled by `Editor.fnameToLink()` in
`qt/aqt/editor.py`, which called `av_player.play_file_with_caller()`
right after building the `[sound:...]` tag.

That method was removed in 4dd402334 ("Remove legacy editor code") once
the editor moved to the Svelte/TS implementation. Its replacement,
`filenameToLink()` in
`ts/routes/editor/rich-text-input/data-transfer.ts`, only builds the
`[sound:...]` string and never carried the playback call forward. So
today, attaching audio inserts the tag silently and you only hear it
after saving and reopening the card (or opening the Cards preview).

This restores the immediate playback, using the same `av_player`
mechanism the rest of the app uses (so it respects the configured audio
backend/volume), by:

- Adding a `PlayFile` RPC to `FrontendService` in `frontend.proto`,
following the same pattern as the existing Python-only
`RecordAudio`/`OpenMedia` RPCs (no Rust implementation needed,
`FrontendService` is filtered out of Rust codegen in
`rslib/rust_interface.rs`).
- Implementing `play_file()` in `qt/aqt/mediasrv.py`. It resolves the
path relative to the media folder the same way
`open_media`/`show_in_media_folder` do, then plays it on the main
thread. When the active window is the editor, it uses
`av_player.play_file_with_caller(path, window.editor.editorMode)`
instead of a plain `play_file()`, the same pattern already used by
`open_cards_dialog`/`open_fields_dialog` in the same file. This ties
playback to `Editor.cleanup()`'s existing
`av_player.stop_and_clear_queue_if_caller(self.editorMode)` call, so the
sound is stopped if the editor closes mid-playback.
- Calling the new `playFile()` binding from `attachPath()` in
`TemplateButtons.svelte` right after the media is inserted, only when
the attached file is audio (added an `isAudio()` helper next to
`filenameToLink()`, reusing the existing `audioSuffixes` list).

## Steps to reproduce (required, use N/A if not applicable)

1. Open the Add or Edit dialog for a note.
2. Click the paperclip (attach) button in the field toolbar.
3. Pick an audio file.
4. Notice nothing plays. The `[sound:...]` tag is inserted silently, and
you only hear the audio after saving/reviewing or opening the Cards
preview.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

Ran `just check` locally, everything passed, including a new vitest
covering `isAudio()` in `data-transfer.test.ts`. Manually verified in
`just run`: opened Add dialog, attached an audio file via the paperclip
button and it plays as soon as it's inserted into the field, before
saving. Also tested the mic recording button (F5), which goes through
the same `attachPath()` function; recording and playback both work as
expected.

## Before / after behavior (optional)

Before: attaching audio via the paperclip inserts the `[sound:...]` tag
silently.
After: the audio plays immediately after being inserted, matching the
old Qt editor's behavior.

## Risk / compatibility / migration (optional)

Low risk. New RPC is additive (no changes to existing `FrontendService`
methods), and playback only triggers for files already classified as
audio/video by the existing suffix list used elsewhere in the same file.

## UI evidence (required for visual changes; otherwise N/A)

N/A, behavior-only change (audio playback), nothing visual to show.

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-08-13 13:50:58 +03:00
Peter Szilvasi 8bf7d91a50 chore: exclude docs contributions from BSD license check (#5267)
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.

Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->

## Linked issue (required)

Fixes #5265
<!-- Fixes #123 / Closes #123 / Refs #123 -->

## Summary / motivation (required)

The document only changes are under the CC BY-SA 4 license, therefore no
need to enforce BSD 3 license contribution agreement.
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->

## Steps to reproduce (required, use N/A if not applicable)

N/A

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->

## How to test (required)

End-to-end local GH action:
1. Install the [act](https://github.com/nektos/act) tool to run GH
action locally
2. Install ubuntu container: `podman pull
ghcr.io/catthehacker/ubuntu:act-24.04`
3. Create a clean working tree from this branch: `git checkout -b
test-docs`
4. Add a doc only change: `echo x >> README.md && git add README.md`
5. Commit the changes: `git -c user.email=nobody@example.com commit -m
"docs only"`
6. Run the minilints step: `act -j minilints --container-daemon-socket
unix:///tmp/podman.sock -P
ubuntu-24.04=ghcr.io/catthehacker/ubuntu:act-24.04`
7. The log should contain the "Run minilints" step.

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->

## Before / after behavior (optional)

Before: The documentation only changes trigger the contribution
agreement check.
After: If the changes are documentation only, it workflow skips the
contribution check.
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

## Risk / compatibility / migration (optional)

N/A
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

## UI evidence (required for visual changes; otherwise N/A)

N/A
<!-- Screenshot or short video -->

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-08-10 21:07:35 +03:00
Andreas 7c13bc4692 fix: Browse Add-ons URL (#5290)
## Linked issue (required)

Fixes #5289

## Summary / motivation (required)

Updates the Browse Add-ons link to use the current `/shared/addons` URL
directly instead of the legacy `/shared/addons/2.1` URL, which redirects
to it.

## Steps to reproduce (required, use N/A if not applicable)

1. Open Tools → Add-ons.
2. Click Get Add-ons…
3. Click Browse Add-ons.
4. Observe that `/shared/addons/2.1` is opened and redirects to
   `/shared/addons`.

## How to test (required)

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

Manually verified that `/shared/addons/2.1` redirects to
`/shared/addons`,
and that `/shared/addons` loads directly. No local build or automated
checks
were run; the change was made using GitHub's web editor.

## Before / after behavior (optional)

Before: Browse Add-ons opens the legacy `/shared/addons/2.1` URL and
relies
on a redirect.

After: Browse Add-ons opens `/shared/addons` directly.

## Risk / compatibility / migration (optional)

N/A. This is a one-line URL change.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-08-07 11:30:12 -03:00
Qaisar Sulaiman 3e85288731 fix: separate add-on entries in Tools menu with separator (#5124)
## Linked issue (required)

Closes #5125

## Summary / motivation (required)

Add a separator at the end of Anki's built-in Tools menu actions, before
any add-on-provided menu entries.

Add-ons commonly append actions directly to `mw.form.menuTools`. This
keeps add-on entries visually separated from Anki's built-in Tools
actions, especially on macOS where Preferences is shown elsewhere and
add-on entries can appear immediately below "Check for Updates".

Because Qt hides trailing menu separators by default, users without
add-on-provided Tools menu entries should not see an extra separator.

## Steps to reproduce (required, use N/A if not applicable)

1. Install one or more add-ons that append entries to the Tools menu.
2. Open the Tools menu.
3. Observe that add-on-provided entries appear directly after the
built-in Tools menu entries.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- Parsed `qt/aqt/forms/main.ui` as XML.
- Ran `git diff --check`.
- No automated test was added because this is a one-line visual menu
grouping change in a Qt Designer file.

## Before / after behavior (optional)

Before: add-on-provided Tools menu entries can appear immediately after
Anki's built-in actions.

After: add-on-provided Tools menu entries are visually separated from
Anki's built-in actions.

## Risk / compatibility / migration (optional)

Low risk. The change adds a Qt menu separator after the built-in Tools
actions. Qt hides trailing menu separators by default, so users without
add-on-provided Tools menu entries should not see an extra divider.

## UI evidence (required for visual changes; otherwise N/A)

<img width="627" height="576" alt="before after"
src="https://github.com/user-attachments/assets/ac1b341a-abdf-4c98-8eeb-4af990bc8118"
/>

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-08-05 13:21:47 +03:00
Aristotelis P. 51b741f591 fix: Guard on_operation_did_execute against deleted web views (#5234)
## Linked issue (required)

Fixes #5233 
## Summary / motivation (required)

Since #4029, every `AnkiWebView` subscribes to
`gui_hooks.operation_did_execute` in `__init__`, and the subscription is
only removed in `cleanup()`. Web views that are destroyed without a
`cleanup()` call, which is common in add-ons that rely on Qt parent
ownership for teardown, leave a dead handler behind, and the next
`CollectionOp` then raises `RuntimeError: wrapped C/C++ object has been
deleted`.

This was latent before #4029: `__init__` only registered on
`theme_did_change` and `body_classes_need_update`, which fire rarely
enough that a missed `cleanup()` had no practical consequences.
`operation_did_execute` fires after every operation, so the same leak
now produces an error dialog within seconds. This isn't specific to one
add-on: QA against 26.08b2 reproduced the identical error with several
popular add-ons (Card Stats, Remaining Time, Cloze Hide All, AJT
Japanese), see the linked issue for details.

#5143 recently fixed the same crash class for the Find Duplicates dialog
(#5140, there via `theme_did_change`) by adding the missing `cleanup()`
call at the dialog's close site. That approach works for Anki's own
dialogs, but there is no equivalent close site to patch for add-on web
views that are destroyed through Qt parent ownership, so this gap needs
to be covered in the handler itself.

This PR guards the handler with `sip.isdeleted(self)` and unsubscribes
the dead handler, mirroring the existing guards for late web events in
`_maybeRunActions` and `_shouldIgnoreWebEvent`. Unsubscribing (rather
than just returning) means leaked web views don't accumulate in the hook
list and their Python wrappers can be collected.

The removal is deferred via `mw.progress.single_shot(0, ...)` rather
than done inline, because the generated hook code iterates `self._hooks`
directly and an inline removal would skip the following handler for that
firing.

## Steps to reproduce (required, use N/A if not applicable)

Tested against 26.08b2, no add-on needed:

1. Open the debug console and run:
   ```python
   from aqt.webview import AnkiWebView
   w = AnkiWebView(parent=mw)
   w.deleteLater()
   ```
2. Close the console and answer a card, or run any other `CollectionOp`
3. An error dialog appears with `RuntimeError: wrapped C/C++ object of
type AnkiWebView has been deleted`

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed. (Qt lifetime behavior, not currently covered by the
test harness - see below, and #5143 for precedent)

### Details

Manual verification via the repro steps above: after the fix, the
operation completes without an error and the dead handler is removed
from the hook (checkable via
`len(gui_hooks.operation_did_execute._hooks)` in the debug console
before and after).

No unit test added: the behavior depends on Qt object deletion timing
and a running event loop, which the Python test suite doesn't currently
cover (the existing `sip.isdeleted` guards in this file are in the same
situation).

## Before / after behavior (optional)

Before: one error dialog per web view that was destroyed without
`cleanup()`, raised on the next operation after the deletion, e.g. when
answering the next card after closing a browser that hosted an add-on
web view.

After: the dead handler is silently dropped and the operation completes
normally.

## Risk / compatibility / migration (optional)

Low risk: a small guard with no behavior change for web views that are
cleaned up properly. Add-ons are the beneficiaries, no add-on facing API
changes.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-30 13:08:35 +03:00
xiaohan484 f13c15aef0 fix: build bundled fcitx5 plugin against the bundled Qt (#5142)
## Linked issue (required)

Fixes #5110

## Summary / motivation (required)

CI builds the fcitx5 plugin against one Qt but ships it inside PyQt6's
Qt — a
version mismatch that crashes Anki on startup for every fcitx5 user.

**Root cause:** the plugin subclasses `QPlatformInputContext`, a Qt
private API
with no ABI guarantee across minor versions. Built against Qt 6.2.4
headers but
run inside Qt 6.11.0, it dereferences a misplaced member and SIGSEGVs on
first
focus.

**Solution:** build the plugin against the same Qt as the bundled
`pyqt6-qt6`
and install it where the packaging step (`qt/tools/build_installer.py`)
looks.
The per-flag reasoning is in the commit message.

## Steps to reproduce (required, use N/A if not applicable)

1. On a Linux desktop configured to use fcitx5 as the input method,
install the
   official Anki 26.05 release.
2. Launch Anki normally.
3. It dies with SIGSEGV on startup, as soon as the main window takes
focus.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

`release.yml` is `workflow_dispatch`-only, so it can't run on the PR. I
reproduced its build step locally: rebuilt the plugin against Qt 6.11.0
(confirmed the `Qt_6.11` ELF tag and a clean `dlopen(RTLD_NOW)`),
dropped it into
an Anki 26.05 bundle, and typed Chinese and Japanese without the crash.

## Risk / compatibility / migration (optional)

1. Fixes `build-linux-x86` only. `build-linux-arm-installer` installs a
prebuilt
`fcitx5-frontend-qt6` from apt, built against Ubuntu's Qt rather than
the
bundled 6.11.0 — the same class of mismatch. I have no arm hardware to
confirm
   or fix it, so it's left for a follow-up.
2. Adds an `aqtinstall` step (~1.5 GB Qt download) to the Linux release
build.
3. **Not validated on CI.** `release.yml` is `workflow_dispatch`-only,
so an
external contributor can't run it; only its build step is reproduced
locally
   (see How to test). **Please confirm it in a real release build.**

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-15 16:19:30 +03:00
Pierre 1c6175daa4 fix: card info not updating on repeat (#5115)
## Linked issue (required)

Fixes #5114 

## Summary / motivation (required)

This fixes an issue I've noticed where if a card repeats, the card info
does not update. This was not optimal, because card info should provide
the most up-to-date info about a card possible, and the previous logic
was not evident to users. This issue could be triggered by answering
"Again" at the end of a study session, at which point a card would
repeat, but the card info would remain the same. Other methods of
triggering included, but were not limited to learning only 1 card at a
time.

## Steps to reproduce (required, use N/A if not applicable)

1. Open card info 
2. Answering "Again" at the end of a study session, at which point a
card would repeat
3. The card info would remain the same, contrary to desire

## How to test (required)

Manual tests

### Checklist (minimum)

- [X] I ran `./ninja check` or an equivalent relevant check locally.
- [X] I added or updated tests when the change is non-trivial or
behavior changed. (N/A)

## Before / after behavior (optional)

Before: On direct card repeats, card info pane would not update.
After: On direct card repeats, card info does repeat

## UI evidence (required for visual changes; otherwise N/A)


https://github.com/user-attachments/assets/bf1409cb-fb27-4c50-9ca2-68c0cc61cece


https://github.com/user-attachments/assets/810dfa53-cee8-4d6c-b22f-3b6daf025cda

## Scope

- [X] This PR is focused on one change (no unrelated edits).
2026-07-13 06:04:46 +08:00
Timothy (Tim) Lee 3c9f2f7dff fix: clean up FindDuplicatesDialog's webview on close to prevent theme-change crash (#5143)
## Linked issue (required)
Fixes #5140

## Summary / motivation (required)
`FindDuplicatesDialog` never called `AnkiWebView.cleanup()` on close,
unlike
the other dialogs that embed a webview (`EmptyCardsDialog`,
`CardInfoDialog`,
`NewDeckStats`), which all do. Without it, the webview's
`on_theme_did_change`
stays registered in `gui_hooks.theme_did_change` after Qt destroys the
underlying C++ widget, so a theme change shortly after close crashes
with
`RuntimeError: wrapped C/C++ object of type FindDupesWebView has been
deleted`.

## Steps to reproduce
1. Open Find Duplicates (Notes menu → Find Duplicates).
2. Close the dialog.
3. Trigger a theme change (toggle system light/dark mode).
4. Anki crashes with the RuntimeError above.

## How to test
Reproduced the crash via the steps above. No automated test included -
this
change just applies the same `.cleanup()` call the three sibling dialogs
already make in their `finished`/`reject` handler. `./ninja check`
passes.

### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed. (same pattern as
EmptyCardsDialog/CardInfoDialog/NewDeckStats - see above)

## Before / after behavior
Before: closing Find Duplicates then triggering a theme change could
crash
Anki. After: the dialog cleans up its webview like its sibling dialogs,
so
the same theme change no longer touches a deleted object.

## Risk / compatibility / migration
None - additive cleanup call, same pattern already used elsewhere in the
codebase.

## UI evidence
N/A - no visual change.

## Scope
- [x] This PR is focused on one change (no unrelated edits).
2026-07-13 00:11:19 +08:00
Yuuki a053f6a3ca Fix note editor jumps html focus (#4687)
## Linked issue (required)
Closes #4234 
<!-- Fixes #123 / Closes #123 / Refs #123 -->

## Summary / motivation (required)
When the HTML editor (</>) is open and focused in the browser's note
editor, switching to a different note and then scrolling and then
clicking (anywhere) causes the view to jump back to the HTML editor's
position. This happens because the `refocus()` triggers both the
browser's native scroll to focused element and CodeMirror's internal
scroll to cursor behavior.

The fix prevents scrolling during refocus in two places:

`PlainTextInput.svelte`: use focus({ preventScroll: true }) instead of
editor.focus() in refocus() to prevent the browser from scrolling the
focused element into view
`code-mirror.ts`: pass { scroll: false } to setSelections() in the focus
handler to prevent CodeMirror from internally scrolling to the restored
cursor position.
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->

## Steps to reproduce (required, use N/A if not applicable)

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->
(Following the original issue instructions)
In the browser:

1. Open HTML editor for any field
2. Click inside, putting focus in the HTML editor input
3. Select a different note
4. Scroll to a different part in the note editor and click anywhere
The view jumps to the position of the HTML editor where the focus was
previously

## How to test (required)
Follow the original issue related to this pull request. There is a video
showing the problem.

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->


<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

## Risk / compatibility / migration (optional)
Changes are scoped to two lines in two files. Only the refocus() code
path is affected. User's focus behavior is unchanged.
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-10 08:42:45 +08:00
Ashish Yadav 57eb9f49af fix: omit empty "Error details:" line from network errors (#5120)
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.

Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->

## Linked issue (required)
- Fixes #5117

## Summary / motivation (required)
`NetworkError::message` always appended the `network-details` line, so
an error with an empty `info` string rendered a dangling "Error
details:" label with nothing after it. Only append the details when
there is something to show.

The message is built in the shared backend, so the empty label showed up
on every client (it was reported on AnkiDroid) is still used unchanged
when details are present.

## Steps to reproduce (required, use N/A if not applicable)
Any network error

## How to test (required)
Added tests

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->

## Before / after behavior (optional)

<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

## Risk / compatibility / migration (optional)

<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

## UI evidence (required for visual changes; otherwise N/A)

<!-- Screenshot or short video -->

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-09 18:57:27 +03:00
Ian b585d6a295 i18n: add core FTL strings for browse card-row status chips (#5111)
## Linked issue (required)

Refs ankitects/ankimobile#10 (pt 1)

## Summary / motivation (required)

Adds four new core FTL strings in `ftl/core/browsing.ftl` for
AnkiMobile's redesigned browse card rows. AnkiMobile pt 1 still
hardcodes a few user-visible and VoiceOver labels (leech chip, compact
"now" due text, tag overflow, leech+lapse a11y). Compact due spans
already reuse `scheduling-answer-button-time-*`; these are the remaining
gaps called out in AnkiMobile PR #24.

All keys are **new additions only** — no existing strings are changed,
and nothing in desktop/core consumes them yet. Intended for AnkiMobile
wiring first, with desktop and AnkiDroid able to reuse later.

| Key | English | Use |
|-----|---------|-----|
| `browsing-leech` | Leech | Status chip label |
| `browsing-due-now` | Now | Compact due text for overdue / due-today |
| `browsing-more-tags` | 1 more tag / {count} more tags | VoiceOver for
tag overflow chip |
| `browsing-leech-lapses` | Leech, {count} lapse(s) | VoiceOver for
leech badge with lapse count |

Out of scope: `browsing-card-suspended-undo` (AnkiMobile pt 2,
mobile-only for now).

## Steps to reproduce (required, use N/A if not applicable)

N/A — new i18n keys only; no behavior change in this repo.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- Ran `./ninja rslib:i18n` to regenerate bindings.
- Verified new keys appear in generated TypeScript/Python/Rust output
(`browsingDueNow`, `browsingLeech`, `browsingMoreTags`,
`browsingLeechLapses`).
- No runtime callers added in this PR; AnkiMobile will wire these up
after core lands.

## Before / after behavior (optional)

N/A — strings are added to the template but unused until consumers adopt
them.

## Risk / compatibility / migration (optional)

Low risk. Additive-only FTL change; no breaking changes or migration
needed.

## UI evidence (required for visual changes; otherwise N/A)

N/A — no UI changes in this repo.

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-07 15:15:22 +03:00
Marcus Lo 3186cc31c4 fix: detect empty cards when template uses special field conditionals (#4807)
## Linked issue (required)

Fixes #4750

## Summary / motivation (required)

`new_cards_required_normal()` builds a set of non-empty fields from the
note's actual fields, but only special-cases `Tags`. Other special
fields like `Deck`, `Subdeck`, `Card`, `CardFlag`, `Type`, and `CardID`
are always populated at render time but were missing from this set. This
caused `{{^Deck}}` conditionals to be incorrectly evaluated as
potentially rendering, making cards appear non-empty to the checker. The
fix adds all always-present special fields to `nonempty_fields`.

## Steps to reproduce (required)

1. Create a Basic (and reversed card) note with content in both fields
2. Change Card 1's front template to `{{^Deck}}{{Back}}{{/Deck}}`
3. Observe the previewer shows a blank card
4. Run Tools → Check → Empty Cards — no empty cards are reported

## How to test (required)

Run `./ninja check:pytest` — a new regression test
`test_empty_cards_special_fields` in `pylib/tests/test_cards.py` covers
this case.

### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details
New test verifies that a card with `{{^Deck}}{{Back}}{{/Deck}}` as its
front template is correctly detected as empty.

## Before / after behavior (optional)

Before: cards using special field conditionals like `{{^Deck}}` were not
detected as empty. After: they are correctly flagged.

## Risk / compatibility / migration (optional)

Low risk. Only affects empty card detection logic, not card rendering or
scheduling.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope
- [x] This PR is focused on one change (no unrelated edits).
2026-07-06 01:44:19 +08:00
helmut carter 9b5346a507 fix(bug): race condition (#4635)
Publish the media sync abort handle before spawning the worker, clear it
with a scope guard, and return `CollectionNotOpen` instead of panicking
if the collection is gone before the worker starts. This avoids a race
where closing the collection during media sync startup could panic.
2026-07-06 00:18:45 +08:00
d 7060797747 fix: follow Anki's language preference in webview and native form controls (#4935)
## Linked issue (required)

Fixes #4823

## Summary / motivation

Webview form controls (e.g. `<input type="date">` picker) were rendered
in the operating system's language instead of the language selected in
Anki's preferences. The webview never receives Anki's chosen language,
so its locale fell back to the system default.

This change calls `QLocale.setDefault` passing Anki's resolved language
in `setupLangAndBackend()` (`qt/aqt/__init__.py`), right after the
language is applied, so locale aware controls follow the user's chosen
language, as suggested in issue body.

`setupLangAndBackend()` runs during Anki startup and is responsible for
setting some language-related Qt state, such as QTranslator, therefore
adding the QLocale default setting there is consistent with the existing
responsibilities of the function.

<img width="2070" height="1574" alt="image"
src="https://github.com/user-attachments/assets/40c4a50d-1d39-48ac-b62b-e5db335e69db"
/>

## Steps to reproduce

1. Set the operating system language to something other than current
Anki lang (my OS is in portuguese)
2. Launch Anki, (e.g. `./run -l en -p teste`, -l is the language
parameter)
3. Open a screen with a web date field (e.g. card type edit screen and
add `<input type="date">`).
4. The picker's date format render in the system language, not in Anki's
language.

## How to test

1. Launch Anki
2. Add > "Cards..." > Add a script like the one bellow to card type
template field
```html
<input type="date">
<pre id="loc"></pre>
<script>
  document.getElementById('loc').textContent =
    "navigator.language = " + navigator.language + "\n" +
    "Intl locale        = " + new Intl.DateTimeFormat().resolvedOptions().locale + "\n" +
    "toLocaleDateString = " + new Date().toLocaleDateString() + "\n" +
    "month name         = " + new Intl.DateTimeFormat(undefined,{month:'long'}).format(new Date());
  </script>
```
3. Check if side preview language matches Anki's language.
 
**You can also check QLocale language in debug console (ctrl+shift+;)**
```py
import platform, anki.lang
from aqt.qt import QLocale, QSysInfo
from anki.utils import version_with_build
print("Anki version  :", version_with_build())
print("OS            :", QSysInfo.prettyProductName(), platform.machine())
print("Anki lang     :", anki.lang.current_lang) 
print("OS language   :", QLocale.system().name())
print("QLocale def   :", QLocale().name()) 
print("QLocale is equal to anki lang?", QLocale().name() == QLocale(anki.lang.current_lang).name())
```

**Debug console script to check if all selectable languages in
pylib/anki/lang.py:22-80 resolve to a valid QLocale**
```py
import anki.lang
from aqt.qt import QLocale
# when the language is not valid, QLocale constructor falls back to QLocale.Language.C https://doc.qt.io/qt-6/qlocale.html
print("fallback=", QLocale("not a valid language").name())
bad = []
for name, code in anki.lang.langs:
    disk = anki.lang.lang_to_disk_lang(code)      
    qt_lang = disk.replace("-", "_")              
    loc = QLocale(qt_lang)
    if loc.language() == QLocale.Language.C:       # fallback
        bad.append((name, code, qt_lang))
    print(f"{code:<8} -> {qt_lang:<7} -> {loc.name():<8} {loc.language().name}")

n = len(anki.lang.langs)
print(f"\ntotal={n}  ok={n - len(bad)}  fell_back_to_C={len(bad)}")
print("FAILURES:", bad if bad else "none — all selectable langs resolve to a valid QLocale")
```
<img width="986" height="667" alt="image"
src="https://github.com/user-attachments/assets/83a3bfee-1024-406e-a395-cdba30c61100"
/>
<img width="726" height="664" alt="image"
src="https://github.com/user-attachments/assets/06ea79fe-a4e6-44e1-96b3-2baff8be1f96"
/>


### Checklist

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior change

This is a trivial change (3 lines) in the Qt layer, and I chose not to
add a test for two reasons:

- Consistency: other Qt layer related stuff in setupLangAndBackend (such
as QTranslator) is not currently tested, and nothing in the existing
suite calls this function directly.
- I could not find a place where a new test would be a natural fit. I
considered adding something to test_i18n.py, but it did not seem aligned
with the other tests

## Before (Anki language: Russian, system language: Brazilian
Portuguese)

<img width="1022" height="408" alt="image"
src="https://github.com/user-attachments/assets/62f0af94-c7aa-461f-9a06-f3e42924d459"
/>

## After (Anki language: Russian, system language: Brazilian Portuguese)

<img width="1027" height="368" alt="image"
src="https://github.com/user-attachments/assets/ac11c152-be16-431e-92f0-c7240a25ddda"
/>

## Risk / compatibility / migration (optional)
Low risk; The fix sets the app-wide default QLocale (via
QLocale.setDefault) once at startup, to the language Anki already
resolves. Language changes already require a restart (there's even a pop
up asking for it), so there is no stale-locale edge.

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-02 19:16:07 +03:00
Tim Gatzke 8127fd2488 fix(fsrs): avoid interval regressions from fuzz (#4826)
## Linked issue (required)

Fixes #4431

## Summary / motivation (required)

Fix an FSRS edge case where fuzz can make a `Good` interval go
backwards, such as `4 -> 3`.

The existing safeguard only handled cases where the newly rounded
interval was already greater than the previous one. This change
preserves the previous interval when it still falls inside the current
fuzz range, and adds a regression test for the reported case.

## Steps to reproduce (required, use N/A if not applicable)

1. Start from an FSRS review state where the previous `Good` interval
was fuzzed up from 3 days to 4 days.
2. Review the card again in a state where FSRS computes a new `Good`
interval that rounds to 3 days but still has a fuzz range that includes
4.
3. Observe that the old logic schedules the next `Good` interval as 3
days.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- Ran `cargo test -p anki scheduler::states::review::test --
--nocapture`
- Added a regression test for the reported `4 -> 3` case
- Ran `./ninja check` successfully

## Before / after behavior (optional)

Before: a card whose previous interval had been fuzzed up to 4 days
could later show a `Good` interval of 3 days.

After: when the previous interval still lies inside the current fuzz
range, the interval is preserved instead of moving backwards.

## Risk / compatibility / migration (optional)

Low risk, as the change is limited to FSRS passing review interval
handling and no migration steps are required.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-07-01 15:27:12 +01:00
Niko Savola 36213cb1a9 fix: set minimum height for deck options (#5062)
## Linked issue (required)

Fixes #5061

## Summary / motivation (required)

This PR resolves a sizing issue with the options view on Hyprland and
possibly other Wayland-based tiling window managers.

### Changes:
**Minimum Height Constraint:** Adds `self.setMinimumHeight(500)` to
`DeckOptionsDialog._setup_ui()`. This ensures that even when the webview
is temporarily hidden on load
(`self.web.hide_while_preserving_layout()`), tiling window managers do
not collapse the dialog to an unusable height.

## Steps to reproduce (required, use N/A if not applicable)

See #5061

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed. (The existing dialog geometry helpers cover this, and
all Pytest/Aqt checks pass).

### Details

Tested manually on a Linux setup running Hyprland:
1. **Minimum Size:** When opening the deck options, the window respects
the minimum size constraints and opens at a readable size.
<img width="1395" height="1375" alt="image"
src="https://github.com/user-attachments/assets/963e9c46-ef7b-42f1-8b54-3a7f9a1473ce"
/>


## Before / after behavior (optional)

* **Before:** Window opened collapsed into a tiny horizontal box on
tiling compositors under Wayland, and user-resized geometries were lost
on Save/Cancel close events.
  
<img width="1403" height="626" alt="image"
src="https://github.com/user-attachments/assets/5cd02070-4563-4d73-9526-b86c8345649e"
/>

* **After:** Window is reliably opened at least 500px high, and user
size adjustments are correctly saved on all close operations.
<img width="1395" height="1375" alt="image"
src="https://github.com/user-attachments/assets/963e9c46-ef7b-42f1-8b54-3a7f9a1473ce"
/>


## Risk / compatibility / migration (optional)

Low risk. A minimum height of 500px easily fits on all standard modern
screens (including small laptops). The number may also be changed if
this seems too large.

## UI evidence (required for visual changes; otherwise N/A)

See screenshots above

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-06-26 16:22:20 +03:00
krMaynardandFernando Lins c881800f77 Fix flaky scheduler test (#5030)
## Linked issue (required)

Fixes #5025

## Summary / motivation (required)

`scheduler::answering::test::new_limited_by_reviews` in
`rslib/src/scheduler/answering/mod.rs` is time-of-day dependent and
fails when
run between roughly 3:50–4:00 GMT. The source carried a
`// FIXME: This fails between 3:50-4:00 GMT` comment documenting the
flakiness.

The test's final `answer_good()` schedules a learning card a 10-minute
step
ahead. Run shortly before the default 4am rollover, that step crosses
into the
next day, so the card is no longer counted as intraday learning and the
final
`assert_counts!(col, 0, 0, 0)` breaks.

This guards the test with the same `if col.timing_today()?.near_cutoff()
{ return Ok(()); }`
early-return that the sibling timing-sensitive tests in this file
(`state_application`, `elapsed_secs`) already use, and replaces the
FIXME with a
comment explaining the cause. `near_cutoff()`'s 25-minute margin safely
covers
the 10-minute step window.

## Steps to reproduce (required, use N/A if not applicable)

1. Set the system clock to a few minutes before the collection's daily
cutoff
(the default 4am rollover — i.e. ~03:50–03:59 in the collection's
timezone).
2. Run `cargo test -p anki --lib
scheduler::answering::test::new_limited_by_reviews`.
3. The final `assert_counts!(col, 0, 0, 0)` fails because the last
learning
   card's 10-minute step lands after the cutoff.

## How to test (required)

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

`cargo test -p anki --lib
scheduler::answering::test::new_limited_by_reviews`
passes, and the full `scheduler::answering` module passes (5/5). The fix
mirrors
the existing `near_cutoff()` guard already used by the two sibling
timing-sensitive tests in the same file.

## Before / after behavior (optional)

Before: the test fails non-deterministically in the ~10-minute window
before the
daily cutoff. After: it early-returns in the near-cutoff window
(25-minute
margin) and runs normally the rest of the day.

## Risk / compatibility / migration (optional)

N/A — test-only change, no production behavior affected.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
2026-06-25 16:38:24 -03:00
roostwp07andFernando Lins f57996576f test(pylib): add hook system tests (closes #4931) (#5023)
Closes #4931

## Summary
- Adds `pylib/tests/test_hooks.py` with full behavioral coverage of
`pylib/anki/hooks.py`
- Coverage increased from 35% → 100% (issue target was 80%)
- Tests cover: hook registration/firing, `runFilter` chaining,
`remHook`, exception propagation, and `wrap` (before/after/around)

## How to test
```
just test-py --coverage
```

---------

Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
2026-06-22 21:08:31 +03:00
RedPine404andAbdo 29a2853f1d fix: Bump rust builder and base image for syncserver (#5036)
## Linked issue (required)

Fixes #5035

## Summary / motivation (required)

Rust builder is outdated an no longer builds the syncserver
successfully.

Bumping the version to match rust-toolchain.toml version 1.92.0 fixes
this issue.

I also bumped the base image to alpine:3.23 as alpine:3.21.0 is over a
year old.

## Steps to reproduce (required, use N/A if not applicable)

docker buildx build -f <Dockerfile> --no-cache --build-arg
ANKI_VERSION=<version> -t anki-sync-server .

## How to test (required)

docker buildx build -f <Dockerfile> --no-cache --build-arg
ANKI_VERSION=<version> -t anki-sync-server .

### Checklist (minimum)

- [x] I ran `docker buildx build -f Dockerfile --no-cache --build-arg
ANKI_VERSION=26.05 -t anki-sync-server .`
- [x] I ran `docker buildx build -f Dockerfile.distroless --no-cache
--build-arg ANKI_VERSION=26.05 -t anki-sync-server .`

### Details

Docker image compiled as expected following the updated versions.

## Scope

- [x] This PR is focused addressing the syncserver docker image.

Co-authored-by: Abdo <abdo@abdnh.net>
2026-06-19 18:10:39 +03:00
Konstantin Tutsch daad13f01f fix: failing syncserver Docker build (#5021)
## Summary / motivation (required)

This PR fixes Docker Image builds for syncserver failing due to a Rust
version being used in both available Dockerfiles that is no longer
supported by the two dependencies `time@0.3.47` and `time-core@0.1.8`.

## Issue

Fixes #5022 

## Steps to reproduce (required, use N/A if not applicable)

1. `cd docs/syncserver`
2. `docker build -f Dockerfile --no-cache --build-arg ANKI_VERSION=26.05
-t anki-sync-server .`
3. Error (see issue)

## How to test (required)

1. `cd docs/syncserver`
2. `docker build -f Dockerfile --no-cache --build-arg ANKI_VERSION=26.05
-t anki-sync-server .`
3. run Docker image

## Scope

- [x] This PR is focused on one change (no unrelated edits).

Signed-off-by: Konstantin Tutsch <mail@konstantintutsch.com>
2026-06-19 18:06:47 +03:00
Tim Arnold 9c72a8b828 feat: render sync server messages as rich text (#4560)
## Summary
- Pass `type="rich"` to `showText()` when displaying the sync server
message, enabling HTML rendering via `QTextBrowser.setHtml()`
- Plain-text messages continue to render identically — `setHtml()`
handles plain strings the same as `setPlainText()`
- Allows self-hosted sync servers to send formatted post-sync messages
(e.g. styled statistics, notices with links, tables)

## Context

The `/sync/meta` response includes a `msg` field that is displayed to
the user after sync. The display widget is already a `QTextBrowser`
(which supports Qt rich text/HTML), but `showText()` is called with the
default `type="text"`, routing through `setPlainText()`.

The `showText` helper already has full HTML support — it just needs
`type="rich"`:

```python
# qt/aqt/utils.py
text = QTextBrowser()
text.setOpenExternalLinks(True)
if type == "text":
    text.setPlainText(txt)    # current path
else:
    text.setHtml(txt)         # proposed path
```

**Security:** `QTextBrowser` does not execute JavaScript — only static
HTML/CSS. The sync server is explicitly configured and trusted by the
user.
2026-05-26 21:59:23 +03:00
David Sauerwein e13092ef74 Replace print() logging in pylib with Python logging (#4666)
Use Python logging facilities instead of printing directly to stdout
with print(). This prevents library consumers from being spammed with
unwanted debug logs and stack traces.

Closes #4665

---

My approach to the log levels is `warning` for any deprecation warning
and `debug` for the "blocked main thread" messages.

The pylib is used in the qt parts of the anki code. To the best of my
understanding, logging is already correctly set up there and no
adjustments are needed.

The [issue I created](https://github.com/ankitects/anki/issues/4665) for
this has a reproducer. With this change applied, the messages are gone
when configuring the anki logger accordingly.

```python3
from anki.collection import Collection
import logging

logging.basicConfig(level=logging.DEBUG)
logging.getLogger("anki").setLevel(logging.INFO)

col = Collection("collection.anki2")
auth = col.sync_login(
    username=r"<username>",
    password=r"<password>",
    endpoint="https://sync.ankiweb.net/",
)
col.close()
```

setting the anki logger back to `DEBUG` makes the message appear again.
Note that the format is only slightly different then previously.

```txt
DEBUG:anki._backend:blocked main thread for 309ms
Stack (most recent call last):
  File "/home/david/coding_stuff/anki/repro.py", line 8, in <module>
    auth = col.sync_login(
  File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/collection.py", line 1141, in sync_login
    return self._backend.sync_login(
  File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/_backend_generated.py", line 83, in sync_login
    raw_bytes = self._run_command(1, 3, message.SerializeToString())
  File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/_backend.py", line 168, in _run_command
    logger.debug(
```
2026-05-25 10:55:54 +03:00
Tim GatzkeandAbdo 82eeda9d4f fix(export): preserve browser focus after note export (#4845)
## Linked issue (required)

Fixes #4833

## Summary / motivation (required)

Keep the Browser window focused after exporting selected notes.

The export dialog was opened from the Browser, but the background export
progress and completion tooltip were still parented to the main window.
That could leave the main screen focused after the export completed.
This change carries the dialog parent through the export options and
uses it for progress, errors, and the completion tooltip.

## Steps to reproduce (required, use N/A if not applicable)

1. Open the Browser.
2. Right-click a note and choose Notes > Export Notes.
3. Click Export, choose a save location, and save.
4. Observe that the main screen receives focus after export completion.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- Ran `./ninja check` successfully.
- Tested behavior on macOS, however I currently don't have a Windows or
Linux environment set up to test on right now as I'm on vacation. The
change is limited to dialog parenting, so I expect it to work the same
across platforms. Would appreciate testing on other platforms if
possible.

## Before / after behavior (optional)

Before: exporting selected notes from the Browser could focus the main
window after completion.

After: export progress and completion UI remain parented to the Browser,
so focus is returned to the Browser.

## Risk / compatibility / migration (optional)

Low risk. The change is limited to export UI parenting and falls back to
the main window when no dialog parent is available.

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-05-18 05:30:57 +03:00
Jeremy FleischmanandAbdo 1482fb0937 fix: atomic_rename now works with single component relative paths (#4773)
## Linked issue (required)

[[<!-- Fixes #123 / Closes #123 / Refs #123
-->](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)

## Summary / motivation (required)

See discussion about this on [the

forum](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)

## Steps to reproduce (required, use N/A if not applicable)

`demo.py`:

```python
import tempfile
from pathlib import Path

import anki.collection

with tempfile.TemporaryDirectory() as tempdir:
    tempdir = Path(tempdir)
    col = anki.collection.Collection(str(tempdir / "temp.anki2"))
    col.export_anki_package(
        out_path="output.apkg",
        options=anki.collection.ExportAnkiPackageOptions(),
        limit=None,
    )
```

```console
$ python demo.py
Traceback (most recent call last):
  [... some noise elided here ...]
  File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend_generated.py", line 2037, in export_anki_package
    raw_bytes = self._run_command(37, 4, message.SerializeToString())
  File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend.py", line 171, in _run_command
    raise backend_exception_to_pylib(err)
anki.errors.BackendIOError: Failed to open '': No such file or directory (os error 2)
```

## How to test (required)

I added a unit test. It passes.

### Checklist (minimum)

- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

<!-- Commands, manual steps, edge cases, and what you observed -->

## Before / after behavior (optional)

<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

## Risk / compatibility / migration (optional)

<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

## UI evidence (required for visual changes; otherwise N/A)

<!-- Screenshot or short video -->

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-05-15 16:53:17 +03:00
chiragjaggaandAbdo a2262809c4 fix: regex error fix (#4837)
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.

Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->

## Linked issue (required)

Fixes #4835 

## Summary / motivation (required)

<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->

Fixes the issue #4835 with a minor change in InvalidRegex error.

## Steps to reproduce (required, use N/A if not applicable)

<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
 - Mainly for bugfixes;
    - For bugs: numbered steps before the fix. For non-bugs: write N/A.
 - use N/A for features, refactors, docs, chore, etc.
-->

1. Open the browser and do a find & replace.
2. Put b[ in “Find”.
3. Enable the regex option.
4. Confirm.

## How to test (required)

<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
Reproduce the fix and verify the regex error is not within the `<pre>`
tag.

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

Regex error was return with `<pre>` tags which are removed now so that
the text in the error box is displayed correctly.

## Before / after behavior (optional)
<img width="372" height="211" alt="image"
src="https://github.com/user-attachments/assets/91f53745-301b-4679-b1a5-53fafd628de7"
/>

<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->

## Risk / compatibility / migration (optional)

<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->

## UI evidence (required for visual changes; otherwise N/A)

<img width="360" height="226" alt="image"
src="https://github.com/user-attachments/assets/3b7b3f23-9f35-423e-9b10-62834e0a0dd6"
/>

<!-- Screenshot or short video -->

## Scope

- [x] This PR is focused on one change (no unrelated edits).

---------

Co-authored-by: Abdo <abdo@abdnh.net>
2026-05-15 16:45:41 +03:00
barisayyildiz cfc6dc015e Fix FSRS desired retention not updating on deck preset change (#4469) (#4610)
## Fix FSRS desired retention not updating on deck preset change (#4469)
### Description
This PR fixes issue #4469, where the "Desired Retention" (DR) setting in
the deck options UI failed to update and could be accidentally
overwritten when switching between configuration presets.

**The Cause:**
Previously, an `onPresetChange` method was being bound in
`DeckOptionsPage.svelte` and passed down to `FsrsOptionsOuter.svelte`.
However, this method was not actually implemented within
`FsrsOptionsOuter.svelte`

**Changes Made:**
* Removed the unimplemented `onPresetChange` prop binding from
`FsrsOptionsOuter.svelte`.
* Implemented the `onPresetChange` logic directly inside
`FsrsOptions.svelte` so that the component internally handles updating
its own FSRS values (including Desired Retention) whenever a new preset
is selected.

**Testing:**
* Additionally, I have tested this change in the simulator component and
didn't observe any side effects.

I would like to get your feedback on this.

Fixes #4469
2026-04-29 17:56:01 +03:00
Jeremy Fleischman 2ec326585c fix: use correct type annotation on Collection.add_notes (#4758)
## Linked issue (required)

This is so minor I opted not to create an issue first. Hope that's ok!

## Summary / motivation (required)

`add_notes` loops over the given `requests` 3 times. The previous type
annotation (`Iterable`) allows for things like generators, which can
only be iterated over one time.

## Steps to reproduce (required, use N/A if not applicable)

N/A

## How to test (required)

Advice appreciated here!

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

N/A

## Before / after behavior (optional)

N/A

## Risk / compatibility / migration (optional)

N/A

## UI evidence (required for visual changes; otherwise N/A)

N/A

## Scope

- [x] This PR is focused on one change (no unrelated edits).
2026-04-28 15:05:39 +03:00
maskady d051be6290 fix: contains_cloze_replacement now checks nested conditionals (#4581) (#4651)
Fixes #4581

`contains_cloze_replacement()` was doing a shallow check, missing cloze
fields nested inside conditionals. Delegating to
`all_referenced_cloze_field_names()` fixes the inconsistency with
`contains_field_replacement()`
2026-04-14 21:37:49 +03:00
Abdo 81eefc5f71 Render GH Markdown alerts in Sphinx docs (#4663)
See
https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts

We started using this syntax recently: #4466 #4662

## How to test

See preview:
https://anki-core--4663.org.readthedocs.build/en/4663/windows.html
2026-04-08 19:10:17 +03:00
cypher 65f45ea87d Update docker build command for syncserver (#4562)
The command for building the image does not include the` --platform`
flag which prevents the image from running across all architectures. For
example, if I build the image on an ARM system and then try running on
x86, it won’t work.

This issue can be fixed by using `docker buildx` and adding the flag to
include all of the platforms. I have tested this by building the image
with the `linux/arm64` and `linux/amd64` platform flags on an ARM system
and then running a container with that image on an x86 system.

This would be useful in scenarios where the syncserver runs on devices
that cannot do builds.

The correct command would be 
```bash
# Builds for all existing platforms supported by Docker
docker buildx build -f <Dockerfile> --platform linux/amd64,linux/arm64,windows/amd64 --no-cache --build-arg ANKI_VERSION=<version> -t anki-sync-server .
```
Reference: https://docs.docker.com/build/building/multi-platform/
2026-04-07 16:51:28 +03:00
Christos Longros 64386cab63 fix: clipboard image paste produces [sound:] tag instead of <img> (#4668)
## Summary
- Fix regression from #4474 where pasting clipboard images into the
editor produces `[sound:filename.png]` instead of `<img
src="filename.png">`
- The `pics` tuple in `editor.py` had comma-joined strings like
`"png,PNG"` instead of separate entries `"png", "PNG"`, causing `ext in
pics` to return `False` for png, gif, svg, and ico extensions

## Test plan
- [x] Copy an image to clipboard, paste into editor field — confirms
`<img>` tag is produced instead of `[sound:]`
- [x] Verified build succeeds with `./ninja wheels`

---------

Signed-off-by: Christos Longros <chris.longros@gmail.com>
2026-04-06 14:30:13 +03:00
Yuuki e412ea76d0 fix html editor collapses after toggling sticky fields (#4566)
Problem:
When the HTML editor is active on a field in the Add Cards dialog,
toggling the pin/sticky on any field and then adding a note (Ctrl+Enter)
causes all active HTML editor fields to collapse unexpectedly.
The editor saves the UI state inside a session object. When the sticky
field is toggled, `update_notetype_legacy()` is called and the
notetype's modTime is changed. When we add the new card we call the
`loadNote()` function that does the following:
1. `saveSession()` saves the current state with the modTime.
2. `setNotetypeMeta() `sees the new modTime and detect a mismatch,
deleting the entire session.
3. `setPlainTexts()` finds no saved sessions and returns the defaults.

Solution:
My solution revolves around changing how we check if the session is
changed or not.
I removed the modTime comparison in `setNotetypeMeta()`. The check not
only causes the fallback of the html after toggling the sticky but also
when changing fields names or font changes.
I added a field count check inside `setPlainTexts()` so now the saved
session is discarded only if the number of fields changes.

So now the modTime is used for the syncing in the backend but not for
the frontend UI.
I also thought about maybe a solution while still using the modTime as
updating the saved modTime in `saveSession()` instead of removing the
check, but this would cause a call from the backend instead of using the
cached value, adding complexity to the code.

Fix: #4468
2026-04-02 21:10:11 +03:00
Daniela Silvaanduser1823 3ecde15621 Add Windows quick start guide for new contributors (#4466)
This pull request adds a beginner-friendly quick start guide for
building Anki on Windows.

The guide is intended to help first-time contributors set up their
development environment more easily, without replacing or modifying the
existing official documentation.

No existing files were changed; this contribution only adds a
complementary Markdown document.

---------

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
2026-04-02 20:44:34 +03:00
Oli f8669fc4be Add punctuation, convert raw URLs to markdown links in READMEs (#4570) 2026-04-01 10:23:23 -03:00
Miguel Gazquez 831ca06bc6 Accept images with capitalized extensions. (#4474)
When using the "Attach pictures/audio/video" button in the "Browse"
window, files with a capitalized extension (e.g. `image.JPG`) aren't
selectable.

Add a variable `pics_uppercase` with the extensions in upper case to be
used when selecting a media.

I modified only the places where pics (the list of supported extensions)
was used with the funciton "getFile": on the other places, the text is
converted to lowercase before comparing.

I did only handle the pictures, because I don't think I ever saw a audio
file with a capitalized extension, while for pictures it is very common
(1/3 of the pictures I downloaded for my deck where ".JPG").
2026-03-31 07:46:24 +03:00
Daedalus Indigo 1340a12622 Wrap previewer type field syntax replacement in separate method (#4563)
It is currently difficult to modify how the previewer handles
type-in-the-answer fields because the substitution is done as a very
tiny line within the large `_render_scheduled` method. This PR adds a
`type_ans_preview_filter` method that can easily be patched without
affecting the entire renderer.
2026-03-31 05:56:36 +03:00