## 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).
## Linked issue (required)
Refs #5134
## Summary / motivation (required)
When running ci, instead of manually compiling cargo-llvm-cov and
cargo-nextest when not cached, this pr pulls them in as prebuilt
binaries instead
## Steps to reproduce (required, use N/A if not applicable)
N/A
## How to test (required)
CI should pass, and running `./check` and `just test --coverage` should
work locally too
### 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
Continuing from
https://github.com/ankitects/anki/issues/5134#issuecomment-4936603431,
we can avoid installing the crates globally when run locally, but in ci
it doesn't really matter so we can use install-action as per normal.
W.r.t nextest's features, all `default-no-update` does is remove its
ability to self-update. Since we're puling in prebuilt binaries during
ci there's no difference
I've left out n2 for now, as without a release workflow on its repo or
on anki's fork, we'd still have to compile it ourselves (caching it can
be unsafe, but it sort of is already cached by setup-rust-toolchain?)
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue (required)
Fixes#4345.
## Summary / motivation (required)
`Notetype::merge_fields` and `merge_templates`
(`rslib/src/notetype/merge.rs`) used the **incoming** notetype's index
directly as a position into **our** own `fields`/`templates`. When the
incoming notetype has more fields than ours — e.g. a corrupt notetype
whose field matches one of ours a second time at an index past the end
of our list — `Vec::swap`/`Vec::insert` received an out-of-bounds index
and panicked:
```
thread '<unnamed>' panicked at rslib/src/notetype/merge.rs:42:33:
index out of bounds: the len is 22 but the index is 22
```
This made the affected deck impossible to import.
The fix clamps the target index to the valid range so the merge degrades
gracefully instead of panicking. All incoming fields/templates are still
merged in; a duplicate match simply collapses onto the existing entry
rather than crashing.
## Steps to reproduce (required, use N/A if not applicable)
1. Import a deck whose notetype has more fields than the local notetype
it maps to, where one incoming field matches an existing field a second
time (reported in the wild on corrupt decks —
https://forums.ankiweb.net/t/im-unable-to-import-any-decks/66497).
2. The import panics with `index out of bounds` in `notetype/merge.rs`.
## 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
Added two regression tests — `merge_longer_other_with_duplicate_field`
and `merge_longer_other_with_duplicate_template`. Verified they **panic
on the unfixed code** and **pass with the fix**, and that the whole
`notetype::merge` module passes:
```
cargo test -p anki --lib notetype::merge::
test result: ok. 8 passed; 0 failed
```
`rustfmt --check` is clean on the changed file.
## Before / after behavior (optional)
Before: importing such a notetype panics and aborts the import. After:
the merge completes, mapping the duplicate onto the existing
field/template.
## Risk / compatibility / migration (optional)
Low — the clamp only changes behavior in the previously-panicking
out-of-bounds case; in-range merges are unaffected.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
<!--
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 / Refs #123 -->
closes#4871
## How to test (required)
Find "Labs" as a new option in the preferences
### 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 -->
I've implemented the "Labs" feature in a web-view as I presume we are
moving away from QT and
Currently these options have no function. Before we can add the
functionality we should try and come up with the pattern that we should
use to register new flags.
## UI evidence (required for visual changes; otherwise N/A)
<img width="746" height="736" alt="image"
src="https://github.com/user-attachments/assets/28c4fd28-a593-4599-b43e-c2c23881251e"
/>
<img width="746" height="736" alt="image"
src="https://github.com/user-attachments/assets/c1cd69f9-3d8d-4866-a784-030b4274b4e7"
/>
## Scope
- [X] This PR is focused on one change (no unrelated edits).
## Linked issue
Reported on
[Discord](https://discord.com/channels/368267295601983490/373169624797282304/1524169568862601287).
## Summary / motivation
We call `QApplication.activeWindow()` in several mediasrv request
handlers to get a parent for Qt dialogs or do asynchronous operations,
but it returns None on some systems, apparently due to race issues.
```
Anki 26.05 (5d51ca02) (ao)
Python 3.13.13 Qt 6.11.0 PyQt 6.11.0
Platform: macOS-26.5.1-arm64-arm-64bit-Mach-O
Traceback (most recent call last):
File "/Users/criticalay/Documents/GitHub/anki/qt/aqt/taskman.py", line 152, in raise_exception
raise exception
File "/Users/criticalay/Documents/GitHub/anki/qt/aqt/taskman.py", line 148, in _on_closures_pending
closure()
~~~~~~~^^
File "/Users/criticalay/Documents/GitHub/anki/qt/aqt/mediasrv.py", line 808, in <lambda>
aqt.mw.taskman.run_on_main(lambda: self.callback(self))
~~~~~~~~~~~~~^^^^^^
File "/Users/criticalay/Documents/GitHub/anki/qt/aqt/mediasrv.py", line 828, in callback
assert window is not None
^^^^^^^^^^^^^^^^^^
AssertionError
===Add-ons (active)===
(add-on provided name [Add-on folder, installed at, version, is config changed])
AnkiWebView Inspector ['31746032', 2023-06-28T00:56, 'None', '']
===IDs of active AnkiWeb add-ons===
31746032
===Add-ons (inactive)===
(add-on provided name [Add-on folder, installed at, version, is config changed])
```
## Steps to reproduce
I failed to reproduce it so far on macOS/Windows.
## 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-68c0cc61cecehttps://github.com/user-attachments/assets/810dfa53-cee8-4d6c-b22f-3b6daf025cda
## Scope
- [X] This PR is focused on one change (no unrelated edits).
## 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).
## Linked issue
Related to fixes submitted upstream in #4765
## Summary / motivation
Bump Briefcase and update the templates for some changes:
- The option to create a Windows desktop shortcut is shown in the page
where the installation path is set instead of its own page.
- The minimum Windows build number is set in pyproject.toml and enforced
both at build time and install time.
- The MSI installer shows required disk space.
## How to test
Run `./tools/ninja installer:package` on Windows and give the installer
a quick test.
## Linked issue
Fixes#5128
## Summary / motivation
Avoid marking the collection as modified if the preferences screen is
closed without making changes to collection settings.
## Steps to reproduce (before)
1. Open the preferences screen and close it without making changes.
2. Notice _Edit > Undo Preferences_ is active.
## How to test (after)
1. Open the preferences screen and close it without making changes.
2. Notice _Edit > Undo_ is inactive.
Eight new browsing-search-* strings for the mobile browse chip search
bar and filter panel
[ankimobile#14](https://github.com/ankitects/ankimobile/issues/14).
I believe these have value for future use outside of mobile
specifically.
| Key | English |
|-----|---------|
| `browsing-search-recent` | Recent |
| `browsing-search-suggestions` | Suggestions |
| `browsing-search-syntax-mode` | Syntax mode |
| `browsing-search-text-match` | Search "{ $query }" |
| `browsing-search-facet-starters` | Filter by |
| `browsing-search-quick` | Quick filters |
| `browsing-search-quick-flagged` | Flagged |
| `browsing-search-quick-leeches` | Leeches |
I did try to do due diligence and could not find any reasonable versions
of these strings in ankimobile or core.
## 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).
Closes#4524.
Strip `fsrs_params_4/5/6` from each deck config when exporting with
`with_scheduling=false`. These weights are persistent on the exporter's
review history and shouldn't be exported.
Tested with new gather-layer unit tests and a round-trip apkg
export-and-reimport test; all 65 existing `import_export` tests pass.
---------
Signed-off-by: Christos Longros <chris.longros@gmail.com>
<!--
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).
## Linked issue
Fixes#5109
## Summary / motivation
This changes the `sync_status()` method so that it reports `NoChanges`
for new empty collections. Currently it reports `FullSync` on a fresh
collection and this status persists even after syncs because the process
works as follows:
1. Anki checks the local collection's modification and schema change
timestamps against the time of the last sync (See
`sync_status_offline()`).
2. If the collection was never synced (`last_sync=0`)
`sync_status_offline()` returns `FullSync` as there's no special
handling for this case.
3. `sync_status_inner()` returns the value returned by
`sync_status_offline()` if it's not `NoChanges`, skipping the AnkiWeb
network request, so the remote collection's modification time is never
compared to the local modification time (they are both 0 on a fresh
collection & account, so `online_sync_status_check()` would have
returned `NoChanges` if not for the early return).
4. When the user clicks the sync button, the action required is
determined solely by `online_sync_status_check()`, which returns
`NoChanges` as noted earlier, so no sync is done and `last_sync` never
gets updated, causing the offline sync status to get stuck on
`FullSync`.
## Steps to reproduce (before)
1. Create a new AnkiWeb account and log in to it in a fresh Anki
profile. Notice the sync button is now red.
2. Click the sync button (without creating notes or making any DB
modifications to the collection) and confirm no prompt to accept the
"fake" full sync.
3. Notice the button is still red, indicating the full sync status
persists and won't be cleared until you make changes to the either the
local or remote collection.
## How to test (after)
Repeat the same process and confirm the sync button is never red for a
fresh collection.
## Linked issue
Closes#5126
## Summary
This prevents translation strings from the
https://github.com/ankitects/anki-desktop-ftl to be used in Rust/TS code
as AnkiMobile does not pull this module.
## Steps to reproduce (before)
Qt strings such as `addons-no-updates-available` were allowed to be used
in Rust: #5122
## How to test (after)
Revert #5122 and confirm you get a compile error with this PR.
## 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).
## Linked issue (required)
Fixes#5108
## Summary / motivation (required)
`/favicon.ico` responds as soon as mediasrv's HTTP thread starts, before
the profile's collection finishes loading, `setupMediaServer()` runs
synchronously in `AnkiQt.__init__`, while `setupProfile()` is deferred
via a `QTimer`. Playwright's `webServer.url` readiness check polled
`/favicon.ico`, so the first e2e test to run in a given CI run could
race the async profile load and time out waiting for `.note-editor`, an
intermittent false negative, not a real editor bug.
This adds `GET /_anki/readyz`, which only returns 200 once `aqt.mw.col`
is open (503 otherwise — Playwright's webServer keeps polling on
non-2xx/3xx responses), and points `playwright.config.ts`'s
`webServer.url` at it instead of `/favicon.ico`.
## Steps to reproduce (required, use N/A if not applicable)
1. Run `just test-e2e` repeatedly on a loaded machine (or add artificial
delay to profile loading).
2. Occasionally, the first test in `context-switching.spec.ts` times out
at `fixtures.ts:37` (`waitForSelector(".note-editor")`) because
Playwright started the test run before the collection had finished
opening.
3. See CI failure:
https://github.com/ankitects/anki/actions/runs/28748635969/job/85243844164
## How to test (required)
- Ran `just test-e2e` locally and confirmed the suite passes.
- Verified `/_anki/readyz` returns 503 immediately after mediasrv starts
and 200 once the profile/collection finishes loading, by curling it
during `qt/tests/launch_anki_for_e2e.py` startup.
- No new test was added — this is a fix to test *infrastructure* (the
readiness gate), not to editor behavior; existing e2e specs are the
regression coverage.
## Before / after behavior (optional)
Before: e2e run could fail intermittently on the first test with a
`.note-editor` timeout, unrelated to the change under test.
After: `webServer` only accepts traffic once the collection is open, so
no test races the profile-load window.
## Risk / compatibility / migration (optional)
Low risk. `/_anki/readyz` is a new, isolated, read-only route used only
by the e2e harness; no existing behavior changes.
## Summary / motivation
Two independent gaps in the `check-linked-issue` workflow caused false
positives on valid PRs:
1. **Cross-repo references not recognised** — the regex only matched
bare `#NNN` but GitHub also accepts `owner/repo#NNN`. Any PR referencing
an issue from another repository (e.g. `Refs ankitects/ankimobile#10`)
was still flagged. Observed in #5111.
2. **Org members should be trusted contributors** — maintainers and core
team members already know the process; requiring them to link an issue
on every PR adds friction with no benefit. The check now calls the
GitHub membership API and skips the requirement for org members
entirely.
## Steps to reproduce
1. Open a PR with `Refs ankitects/ankimobile#10` as the linked issue →
bot flags it as missing.
2. Open a PR as an org member without a linked issue → bot should not
flag it.
## How to test
1. Open a draft PR from an external contributor with `Refs
owner/repo#NNN` → verify **no** `missing-issue` label.
2. Open a draft PR as an org member with no linked issue → verify **no**
label or comment.
3. Open a draft PR as an external contributor with no linked issue →
verify label and comment **are** applied.
## 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).
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.
<!--
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#4791
<!-- Fixes#123 / Closes#123 / Refs #123 -->
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
Previously, using set due date on a new card was broken for FSRS because
new cards didn't have a memory state which was how the set due date
function determined if FSRS is enabled or not. See
https://github.com/ankitects/anki/pull/4035#discussion_r2107488134.
This PR also makes it so that new cards behave as new cards after set
due date is used on them (Only while FSRS is enabled).
## 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. create a new card
2. set it's due date
3. set it's due date again
4. the due date won't change
5. use grade now
6. the card will be scheduled based on the future date due date rather
than now
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
I followed the reproduction steps and instead, after using set due date
the card remains in a new state, and the saved interval is the inputted
number (with a minimum of one to prevent it appearing as a reset)
### 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 -->
## 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
-->
If there are any ways I have not foreseen that an interval can be set to
0, then it may cause problems with this approach.
## 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).
<!--
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 / Refs #123 -->
Refs #4080
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
bump fsrs version
## 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.
-->
N/A
## 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 -->
## 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).
<!--
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#4394
## Summary / motivation (required)
Currently, `card_state_updater` calls `get_config_bool` for
`BoolKey::Fsrs` and `BoolKey::FsrsShortTermWithStepsEnabled` on every
card answer, incurring a SQLite read each time.
With this PR, the flags are read once at queue-build time and then
threaded into `CardQueues` so that `card_state_updater` can read the
cached value instead of hitting the config DB on every answer.
When `card_queues` is `None` (unit tests, direct API calls outside the
reviewer), fall back to `get_config_bool` to preserve existing behavior.
This is the same pattern used for `load_balancer` in #3860.
## Steps to reproduce (required, use N/A if not applicable)
N/A — performance optimization, no user-facing change.
## How to test (required)
Not sure
### 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
<!-- 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).
## 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).
## Linked issue
Refs #5016
## Summary / motivation
Adds follow-up test coverage for audio playback after #5066.
This strengthens the previous basic sound tests by covering additional
edge cases in `anki.sound` and by adding a real mpv playback smoke test.
The goal is to catch regressions where the mpv binary exists and
responds to `--version`, but fails to actually load/decode/play an audio
file in Anki's automated environment.
Coverage added:
- `SoundOrVideoTag.path()` with filenames containing directory
separators
- `hooks.media_file_filter` application in sound tag path resolution
- case-insensitive audio extension detection
- rejection of filenames without extensions
- `_packagedCmd()` preference for `anki_audio` only on macOS/Windows
- generated WAV playback through mpv in headless mode
## Steps to reproduce
1. Have a broken or incompatible mpv/`anki_audio` binary available.
2. Run the sound-related Python tests.
3. Without a playback smoke test, `mpv --version` may pass even if mpv
cannot load/play an audio file.
## How to test
Run:
```sh
just test-py
```
Both passed locally.
The mpv smoke test generates a minimal WAV file and runs mpv with
headless/null audio-video output, so it validates decoding/playback
without requiring audio hardware.
## Before / after behavior
Before: coverage verified that mpv existed and responded to `--version`,
but did not prove it could play/decode an audio file.
After: tests verify that mpv can load and play a generated WAV file in
an automated headless mode.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Summary / motivation
Two related bugs in the `check-linked-issue` workflow caused unnecessary
noise on PRs:
1. **Wrong keywords**: the regex only accepted `closes`, `fixes`, and
`resolves` variants, but the PR template explicitly shows `Refs #123` as
a valid option. Any PR using `Refs #NNN`, `Ref #NNN`, or `References
#NNN` was incorrectly flagged. Observed in #5087.
2. **Stale bot comment**: once the contributor added an issue link and
the `missing-issue` label was removed, the bot's warning comment
remained visible on the PR, creating unnecessary noise.
## Steps to reproduce (required, use N/A if not applicable)
1. Open a PR with `Refs #<open issue>` in the linked issue section
(following the template hint).
2. Bot applies `missing-issue` label and posts a comment despite the
issue being referenced.
3. Edit the PR description to use `Closes #NNN` instead, label is
removed but the bot comment stays.
## How to test (required)
1. Open a draft PR with no issue link → verify label and comment are
applied.
2. Edit the description to add `Refs #NNN` → verify label and comment
are both removed.
3. Repeat with `Closes #NNN`, `Fixes #NNN`, `References #NNN`.
## Risk / compatibility / migration
No Risk, CI workflow only.
Closes#5016
## Summary
- Adds `pylib/tests/test_sound.py` covering `anki/sound.py`:
`SoundOrVideoTag.path()`, `strip_av_refs()`, and `AV_REF_RE` matching
- Adds `qt/tests/test_sound.py` covering `aqt/sound.py`: audio file
extension detection and mpv binary smoke test (skipped on platforms
without a bundled binary)
## How to test
```
just test-py --coverage
```
Feel free to suggest additional tests if this isn't comprehensive
enough.
A follow-up to #5060
This moves the `complexipy` and `complexipy-diff` Ninja actions out of
the base `check` actions so that they are not run on `./ninja check`.
## 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).