mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
Merge branch 'main' into svelte5
This commit is contained in:
commit
add1248cd8
156 changed files with 813 additions and 731 deletions
|
@ -22,7 +22,7 @@ echo "--- Ensure libs importable"
|
||||||
SKIP_RUN=1 ./run
|
SKIP_RUN=1 ./run
|
||||||
|
|
||||||
echo "--- Check Rust libs"
|
echo "--- Check Rust libs"
|
||||||
cargo install cargo-deny --version 0.14.12
|
cargo install cargo-deny --version 0.14.24
|
||||||
cargo deny check
|
cargo deny check
|
||||||
|
|
||||||
echo "--- Cleanup"
|
echo "--- Cleanup"
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
db-path = "~/.cargo/advisory-db"
|
db-path = "~/.cargo/advisory-db"
|
||||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||||
ignore = [
|
ignore = [
|
||||||
# safemem only used by makeapp
|
# gix only used by burn-train
|
||||||
"RUSTSEC-2023-0081",
|
"RUSTSEC-2024-0350",
|
||||||
]
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
|
@ -23,6 +23,7 @@ allow = [
|
||||||
"CC0-1.0",
|
"CC0-1.0",
|
||||||
"Unlicense",
|
"Unlicense",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
|
"Unicode-3.0",
|
||||||
]
|
]
|
||||||
confidence-threshold = 0.8
|
confidence-threshold = 0.8
|
||||||
# eg { allow = ["Zlib"], name = "adler32", version = "*" },
|
# eg { allow = ["Zlib"], name = "adler32", version = "*" },
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
[settings]
|
[settings]
|
||||||
ensure_newline_before_comments=true
|
py_version=39
|
||||||
force_grid_wrap=0
|
|
||||||
include_trailing_comma=True
|
|
||||||
known_first_party=anki,aqt,tests
|
known_first_party=anki,aqt,tests
|
||||||
line_length=88
|
|
||||||
multi_line_output=3
|
|
||||||
profile=black
|
profile=black
|
||||||
skip=
|
extend_skip=qt/bundle
|
||||||
use_parentheses=True
|
|
||||||
|
|
2
.ruff.toml
Normal file
2
.ruff.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target-version = "py39"
|
||||||
|
extend-exclude = ["qt/bundle"]
|
10
CONTRIBUTORS
10
CONTRIBUTORS
|
@ -148,6 +148,7 @@ user1823 <92206575+user1823@users.noreply.github.com>
|
||||||
Gustaf Carefall <https://github.com/Gustaf-C>
|
Gustaf Carefall <https://github.com/Gustaf-C>
|
||||||
virinci <github.com/virinci>
|
virinci <github.com/virinci>
|
||||||
snowtimeglass <snowtimeglass@gmail.com>
|
snowtimeglass <snowtimeglass@gmail.com>
|
||||||
|
brishtibheja <sorata225yume@gmail.com>
|
||||||
Ben Olson <github.com/grepgrok>
|
Ben Olson <github.com/grepgrok>
|
||||||
Akash Reddy <akashreddy2003@gmail.com>
|
Akash Reddy <akashreddy2003@gmail.com>
|
||||||
Lucio Sauer <watermanpaint@posteo.net>
|
Lucio Sauer <watermanpaint@posteo.net>
|
||||||
|
@ -177,7 +178,14 @@ RRomeroJr <117.rromero@gmail.com>
|
||||||
Xidorn Quan <me@upsuper.org>
|
Xidorn Quan <me@upsuper.org>
|
||||||
Alexander Bocken <alexander@bocken.org>
|
Alexander Bocken <alexander@bocken.org>
|
||||||
James Elmore <email@jameselmore.org>
|
James Elmore <email@jameselmore.org>
|
||||||
|
Ian Samir Yep Manzano <https://github.com/isym444>
|
||||||
|
David Culley <6276049+davidculley@users.noreply.github.com>
|
||||||
|
Rastislav Kish <rastislav.kish@protonmail.com>
|
||||||
|
Expertium <https://github.com/Expertium>
|
||||||
|
Christian Donat <https://github.com/cdonat2>
|
||||||
|
Asuka Minato <https://asukaminato.eu.org>
|
||||||
|
Dillon Baldwin <https://github.com/DillBal>
|
||||||
|
Voczi <https://github.com/voczi>
|
||||||
********************
|
********************
|
||||||
|
|
||||||
The text of the 3 clause BSD license follows:
|
The text of the 3 clause BSD license follows:
|
||||||
|
|
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -1901,9 +1901,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fsrs"
|
name = "fsrs"
|
||||||
version = "0.6.4"
|
version = "1.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70cec685337af48789e58cea6ef59ee9f01289d1083428b03fe14e76b98c817c"
|
checksum = "285d9b275f7d5a276f17006e9d92ea67fa9991187ae88760fa96705fba1f97aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"burn",
|
"burn",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
|
@ -3859,9 +3859,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.64"
|
version = "0.10.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -3891,9 +3891,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.102"
|
version = "0.9.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
@ -35,7 +35,7 @@ git = "https://github.com/ankitects/linkcheck.git"
|
||||||
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||||
|
|
||||||
[workspace.dependencies.fsrs]
|
[workspace.dependencies.fsrs]
|
||||||
version = "0.6.4"
|
version = "1.1.4"
|
||||||
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||||
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
|
# rev = "58ca25ed2bc4bb1dc376208bbcaed7f5a501b941"
|
||||||
# path = "../open-spaced-repetition/fsrs-rs"
|
# path = "../open-spaced-repetition/fsrs-rs"
|
||||||
|
|
|
@ -368,8 +368,8 @@ pub trait FilesHandle {
|
||||||
/// different variables. This is a shortcut for calling .expand_inputs()
|
/// different variables. This is a shortcut for calling .expand_inputs()
|
||||||
/// and then .add_inputs_vec()
|
/// and then .add_inputs_vec()
|
||||||
/// - If the variable name is non-empty, a variable of the same name will be
|
/// - If the variable name is non-empty, a variable of the same name will be
|
||||||
/// created so the file list can be accessed in the command. By convention,
|
/// created so the file list can be accessed in the command. By
|
||||||
/// this is often `in`.
|
/// convention, this is often `in`.
|
||||||
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
|
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
|
||||||
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>);
|
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>);
|
||||||
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
|
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
|
||||||
|
@ -394,10 +394,10 @@ pub trait FilesHandle {
|
||||||
/// - Each output automatically has $builddir/ prefixed to it if it does not
|
/// - Each output automatically has $builddir/ prefixed to it if it does not
|
||||||
/// already start with it.
|
/// already start with it.
|
||||||
/// - If the variable name is non-empty, a variable of the same name will be
|
/// - If the variable name is non-empty, a variable of the same name will be
|
||||||
/// created so the file list can be accessed in the command. By convention,
|
/// created so the file list can be accessed in the command. By
|
||||||
/// this is often `out`.
|
/// convention, this is often `out`.
|
||||||
/// - If subgroup is true, the files are also placed in a subgroup. Eg
|
/// - If subgroup is true, the files are also placed in a subgroup. Eg if a
|
||||||
/// if a rule `foo` exists and subgroup `bar` is provided, the files are
|
/// rule `foo` exists and subgroup `bar` is provided, the files are
|
||||||
/// accessible via `:foo:bar`. The variable name must not be empty, or
|
/// accessible via `:foo:bar`. The variable name must not be empty, or
|
||||||
/// called `out`.
|
/// called `out`.
|
||||||
fn add_outputs_ext(
|
fn add_outputs_ext(
|
||||||
|
|
|
@ -223,18 +223,6 @@ impl BuildAction for SvelteCheck {
|
||||||
build.add_inputs("yarn", inputs![":yarn:bin"]);
|
build.add_inputs("yarn", inputs![":yarn:bin"]);
|
||||||
build.add_inputs("", &self.inputs);
|
build.add_inputs("", &self.inputs);
|
||||||
build.add_inputs("", inputs!["yarn.lock"]);
|
build.add_inputs("", inputs!["yarn.lock"]);
|
||||||
build.add_variable(
|
|
||||||
"compiler_warnings",
|
|
||||||
[
|
|
||||||
"a11y-click-events-have-key-events",
|
|
||||||
"a11y-no-noninteractive-tabindex",
|
|
||||||
"a11y-no-static-element-interactions",
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.map(|warning| format!("{}$:ignore", warning))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(","),
|
|
||||||
);
|
|
||||||
let hash = simple_hash(&self.tsconfig);
|
let hash = simple_hash(&self.tsconfig);
|
||||||
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
|
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,7 +251,7 @@ impl BuildAction for PythonTest {
|
||||||
build.add_variable("folder", self.folder);
|
build.add_variable("folder", self.folder);
|
||||||
build.add_variable(
|
build.add_variable(
|
||||||
"pythonpath",
|
"pythonpath",
|
||||||
&self.python_path.join(if cfg!(windows) { ";" } else { ":" }),
|
self.python_path.join(if cfg!(windows) { ";" } else { ":" }),
|
||||||
);
|
);
|
||||||
build.add_env_var("PYTHONPATH", "$pythonpath");
|
build.add_env_var("PYTHONPATH", "$pythonpath");
|
||||||
build.add_env_var("ANKI_TEST_MODE", "1");
|
build.add_env_var("ANKI_TEST_MODE", "1");
|
||||||
|
|
|
@ -1252,7 +1252,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fsrs",
|
"name": "fsrs",
|
||||||
"version": "0.6.4",
|
"version": "1.1.4",
|
||||||
"authors": "Open Spaced Repetition",
|
"authors": "Open Spaced Repetition",
|
||||||
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
"repository": "https://github.com/open-spaced-repetition/fsrs-rs",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
@ -2521,7 +2521,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openssl",
|
"name": "openssl",
|
||||||
"version": "0.10.64",
|
"version": "0.10.66",
|
||||||
"authors": "Steven Fackler <sfackler@gmail.com>",
|
"authors": "Steven Fackler <sfackler@gmail.com>",
|
||||||
"repository": "https://github.com/sfackler/rust-openssl",
|
"repository": "https://github.com/sfackler/rust-openssl",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
@ -2548,7 +2548,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openssl-sys",
|
"name": "openssl-sys",
|
||||||
"version": "0.9.102",
|
"version": "0.9.103",
|
||||||
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
|
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
|
||||||
"repository": "https://github.com/sfackler/rust-openssl",
|
"repository": "https://github.com/sfackler/rust-openssl",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -3,32 +3,29 @@
|
||||||
For info on contributing things other than code, such as translations, decks
|
For info on contributing things other than code, such as translations, decks
|
||||||
and add-ons, please see https://docs.ankiweb.net/contrib
|
and add-ons, please see https://docs.ankiweb.net/contrib
|
||||||
|
|
||||||
With most users now on 2.1, the past 2 years have been focused on paying down some
|
|
||||||
of the technical debt that Anki's codebase has built up over the years, and making
|
|
||||||
changes that will make future maintenance and refactoring easier. A lot of Anki's
|
|
||||||
"business logic" has been migrated to Rust, which AnkiMobile and AnkiDroid
|
|
||||||
can also take advantage of - previously a lot of effort was wasted writing the same
|
|
||||||
code for each platform, and then debugging differences in the implementations.
|
|
||||||
Considerable effort has also been put into improving the Python side of things,
|
|
||||||
with type hints added to the majority of the codebase, automatic linting/formatting,
|
|
||||||
and refactoring of parts of the code.
|
|
||||||
|
|
||||||
The import/export code remains to be done, and this will likely
|
|
||||||
take a number of months to work through. Until that is complete, new features
|
|
||||||
will not be the top priority, unless they are easy wins as part of the refactoring
|
|
||||||
process.
|
|
||||||
|
|
||||||
If you are planning to contribute any non-trivial changes, please reach out
|
|
||||||
on the support site before you begin work. Some areas (primarily pylib/) are
|
|
||||||
likely to change/conflict with other work, and larger changes will likely need
|
|
||||||
to wait until the refactoring process is done.
|
|
||||||
|
|
||||||
## Help wanted
|
## Help wanted
|
||||||
|
|
||||||
If you'd like to contribute but don't know what to work on, please take a look
|
If you'd like to contribute but don't know what to work on, please take a look
|
||||||
at the [issues tab](https://github.com/ankitects/anki/issues) of the Anki repo
|
at the [issues tab](https://github.com/ankitects/anki/issues) of the Anki repo
|
||||||
on GitHub.
|
on GitHub.
|
||||||
|
|
||||||
|
## Larger changes
|
||||||
|
|
||||||
|
Before starting work on larger changes, especially ones that aren't listed on the
|
||||||
|
issue tracker, please reach out on the forums before you begin work, so we can let
|
||||||
|
you know whether they're likely to be accepted or not. When you spent a bunch of time
|
||||||
|
on a PR that ends up getting rejected, it's no fun for either you or us.
|
||||||
|
|
||||||
|
## Refactoring
|
||||||
|
|
||||||
|
Please avoid PRs that focus on refactoring. Every PR has a cost to review, and a chance
|
||||||
|
of introducing accidental regressions, and often these costs are not worth it for
|
||||||
|
slightly more elegant code.
|
||||||
|
|
||||||
|
That's not to say there's no value in refactoring. But such changes are usually better done
|
||||||
|
in a PR that happens to be working in the same area - for example, making small changes
|
||||||
|
to the code as part of fixing a bug, or a larger refactor when introducing a new feature.
|
||||||
|
|
||||||
## Type hints
|
## Type hints
|
||||||
|
|
||||||
Most of Anki's Python code now has type hints, which improve code completion,
|
Most of Anki's Python code now has type hints, which improve code completion,
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
FROM rust:1.76-alpine3.19 AS builder
|
FROM rust:1.79-alpine3.20 AS builder
|
||||||
|
|
||||||
ARG ANKI_VERSION
|
ARG ANKI_VERSION
|
||||||
|
|
||||||
RUN apk update && apk add --no-cache build-base protobuf && rm -rf /var/cache/apk/*
|
RUN apk update && apk add --no-cache build-base protobuf && rm -rf /var/cache/apk/*
|
||||||
RUN cargo install --git https://github.com/ankitects/anki.git \
|
RUN cargo install --git https://github.com/ankitects/anki.git \
|
||||||
--tag ${ANKI_VERSION} \
|
--tag ${ANKI_VERSION} \
|
||||||
--root /anki-server \
|
--root /anki-server \
|
||||||
anki-sync-server
|
anki-sync-server
|
||||||
|
|
||||||
FROM alpine:3.19.1
|
FROM alpine:3.20
|
||||||
|
|
||||||
RUN adduser -D -h /home/anki anki
|
RUN adduser -D -h /home/anki anki
|
||||||
|
|
||||||
|
@ -25,8 +25,9 @@ EXPOSE ${SYNC_PORT}
|
||||||
|
|
||||||
CMD ["anki-sync-server"]
|
CMD ["anki-sync-server"]
|
||||||
|
|
||||||
# TODO - consider exposing endpoint /health to check on health cause currently it will return 404 error
|
# This health check will work for Anki versions 24.06.3 and newer.
|
||||||
# HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
# For older versions, it may incorrectly report an unhealthy status, which should not be the case.
|
||||||
# CMD wget -qO- http://localhost:${SYNC_PORT} || exit 1
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:${SYNC_PORT}/health || exit 1
|
||||||
|
|
||||||
LABEL maintainer="Jean Khawand <jk@jeankhawand.com>"
|
LABEL maintainer="Jean Khawand <jk@jeankhawand.com>"
|
|
@ -1 +1 @@
|
||||||
Subproject commit 173f043875bb38607d7d31bfcb0eb9874b8e4f3a
|
Subproject commit ababc05c46db93309ac50e7f80c10d6559314aa1
|
|
@ -61,7 +61,7 @@ deck-config-today-only = Today only
|
||||||
|
|
||||||
deck-config-learning-steps = Learning steps
|
deck-config-learning-steps = Learning steps
|
||||||
# Please don't translate `1m`, `2d`
|
# Please don't translate `1m`, `2d`
|
||||||
-deck-config-delay-hint = Delays are typically minutes (eg `1m`) or days (eg `2d`), but hours (eg `1h`) and seconds (eg `30s`) are also supported.
|
-deck-config-delay-hint = Delays are typically minutes (e.g. `1m`) or days (e.g. `2d`), but hours (e.g. `1h`) and seconds (e.g. `30s`) are also supported.
|
||||||
deck-config-learning-steps-tooltip =
|
deck-config-learning-steps-tooltip =
|
||||||
One or more delays, separated by spaces. The first delay will be used
|
One or more delays, separated by spaces. The first delay will be used
|
||||||
when you press the `Again` button on a new card, and is 1 minute by default.
|
when you press the `Again` button on a new card, and is 1 minute by default.
|
||||||
|
@ -100,7 +100,7 @@ deck-config-leech-threshold-tooltip =
|
||||||
think of a mnemonic to help you remember it.
|
think of a mnemonic to help you remember it.
|
||||||
# See actions-suspend-card and scheduling-tag-only for the wording
|
# See actions-suspend-card and scheduling-tag-only for the wording
|
||||||
deck-config-leech-action-tooltip =
|
deck-config-leech-action-tooltip =
|
||||||
`Tag Only`: Add a "leech" tag to the note, and display a pop-up.
|
`Tag Only`: Add a 'leech' tag to the note, and display a pop-up.
|
||||||
|
|
||||||
`Suspend Card`: In addition to tagging the note, hide the card until it is
|
`Suspend Card`: In addition to tagging the note, hide the card until it is
|
||||||
manually unsuspended.
|
manually unsuspended.
|
||||||
|
@ -148,8 +148,8 @@ deck-config-new-gather-priority-tooltip-2 =
|
||||||
the latest-added first.
|
the latest-added first.
|
||||||
|
|
||||||
`Random notes`: gathers cards of randomly selected notes. When sibling burying is
|
`Random notes`: gathers cards of randomly selected notes. When sibling burying is
|
||||||
disabled, this allows all cards of a note to be seen in a session (eg. both a front->back
|
disabled, this allows all cards of a note to be seen in a session (e.g. both a front→back
|
||||||
and back->front card)
|
and back→front card).
|
||||||
|
|
||||||
`Random cards`: gathers cards completely randomly.
|
`Random cards`: gathers cards completely randomly.
|
||||||
deck-config-new-gather-priority-deck = Deck
|
deck-config-new-gather-priority-deck = Deck
|
||||||
|
@ -234,7 +234,7 @@ deck-config-stop-timer-on-answer-tooltip =
|
||||||
## Auto Advance section
|
## Auto Advance section
|
||||||
|
|
||||||
deck-config-seconds-to-show-question = Seconds to show question for
|
deck-config-seconds-to-show-question = Seconds to show question for
|
||||||
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
|
deck-config-seconds-to-show-question-tooltip-3 = When auto advance is activated, the number of seconds to wait before applying the question action. Set to 0 to disable.
|
||||||
deck-config-seconds-to-show-answer = Seconds to show answer for
|
deck-config-seconds-to-show-answer = Seconds to show answer for
|
||||||
deck-config-seconds-to-show-answer-tooltip-2 = When auto advance is activated, the number of seconds to wait before applying the answer action. Set to 0 to disable.
|
deck-config-seconds-to-show-answer-tooltip-2 = When auto advance is activated, the number of seconds to wait before applying the answer action. Set to 0 to disable.
|
||||||
deck-config-question-action-show-answer = Show Answer
|
deck-config-question-action-show-answer = Show Answer
|
||||||
|
@ -242,8 +242,8 @@ deck-config-question-action-show-reminder = Show Reminder
|
||||||
deck-config-question-action = Question action
|
deck-config-question-action = Question action
|
||||||
deck-config-question-action-tool-tip = The action to perform after the question is shown, and time has elapsed.
|
deck-config-question-action-tool-tip = The action to perform after the question is shown, and time has elapsed.
|
||||||
deck-config-answer-action = Answer action
|
deck-config-answer-action = Answer action
|
||||||
deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one.
|
deck-config-answer-action-tooltip-2 = The action to perform after the answer is shown, and time has elapsed.
|
||||||
deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question.
|
deck-config-wait-for-audio-tooltip-2 = Wait for audio to finish before automatically applying the question action or answer action.
|
||||||
|
|
||||||
## Audio section
|
## Audio section
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ deck-config-audio-title = Audio
|
||||||
deck-config-disable-autoplay = Don't play audio automatically
|
deck-config-disable-autoplay = Don't play audio automatically
|
||||||
deck-config-disable-autoplay-tooltip =
|
deck-config-disable-autoplay-tooltip =
|
||||||
When enabled, Anki will not play audio automatically.
|
When enabled, Anki will not play audio automatically.
|
||||||
It can be played manually by clicking/tapping on an audio icon, or by using the replay audio action.
|
It can be played manually by clicking/tapping on an audio icon, or by using the Replay action.
|
||||||
deck-config-skip-question-when-replaying = Skip question when replaying answer
|
deck-config-skip-question-when-replaying = Skip question when replaying answer
|
||||||
deck-config-always-include-question-audio-tooltip =
|
deck-config-always-include-question-audio-tooltip =
|
||||||
Whether the question audio should be included when the Replay action is
|
Whether the question audio should be included when the Replay action is
|
||||||
|
@ -350,7 +350,7 @@ deck-config-compute-optimal-weights = Optimize FSRS parameters
|
||||||
deck-config-compute-minimum-recommended-retention = Minimum recommended retention
|
deck-config-compute-minimum-recommended-retention = Minimum recommended retention
|
||||||
deck-config-optimize-button = Optimize
|
deck-config-optimize-button = Optimize
|
||||||
deck-config-compute-button = Compute
|
deck-config-compute-button = Compute
|
||||||
deck-config-ignore-before = Ignore reviews before
|
deck-config-ignore-before = Ignore cards reviewed before
|
||||||
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
|
deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save".
|
||||||
deck-config-evaluate-button = Evaluate
|
deck-config-evaluate-button = Evaluate
|
||||||
deck-config-desired-retention = Desired retention
|
deck-config-desired-retention = Desired retention
|
||||||
|
@ -368,7 +368,7 @@ deck-config-reschedule-cards-on-change = Reschedule cards on change
|
||||||
deck-config-fsrs-tooltip =
|
deck-config-fsrs-tooltip =
|
||||||
Affects the entire collection.
|
Affects the entire collection.
|
||||||
|
|
||||||
The Free Spaced Repetition Scheduler (FSRS) is an alternative to Anki's legacy SuperMemo 2 (SM2) scheduler.
|
The Free Spaced Repetition Scheduler (FSRS) is an alternative to Anki's legacy SuperMemo 2 (SM-2) scheduler.
|
||||||
By more accurately determining when you are likely to forget, it can help you remember
|
By more accurately determining when you are likely to forget, it can help you remember
|
||||||
more material in the same amount of time. This setting is shared by all deck presets.
|
more material in the same amount of time. This setting is shared by all deck presets.
|
||||||
|
|
||||||
|
@ -388,11 +388,11 @@ deck-config-historical-retention-tooltip =
|
||||||
the missing reviews.
|
the missing reviews.
|
||||||
|
|
||||||
Your review history may be incomplete for two reasons:
|
Your review history may be incomplete for two reasons:
|
||||||
1. Because you've used the 'ignore reviews before' option.
|
1. Because you're using the 'ignore cards reviewed before' option.
|
||||||
2. Because you previously deleted review logs to free up space, or imported material from a different
|
2. Because you previously deleted review logs to free up space, or imported material from a different
|
||||||
SRS program.
|
SRS program.
|
||||||
|
|
||||||
The latter is quite rare, so unless you've used the former option, you probably don't need to adjust
|
The latter is quite rare, so unless you're using the former option, you probably don't need to adjust
|
||||||
this setting.
|
this setting.
|
||||||
deck-config-weights-tooltip2 =
|
deck-config-weights-tooltip2 =
|
||||||
FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use
|
FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use
|
||||||
|
@ -406,12 +406,12 @@ deck-config-reschedule-cards-on-change-tooltip =
|
||||||
will be changed.
|
will be changed.
|
||||||
deck-config-reschedule-cards-warning =
|
deck-config-reschedule-cards-warning =
|
||||||
Depending on your desired retention, this can result in a large number of cards becoming
|
Depending on your desired retention, this can result in a large number of cards becoming
|
||||||
due, so is not recommended when first switching from SM2.
|
due, so is not recommended when first switching from SM-2.
|
||||||
|
|
||||||
Use this option sparingly, as it will add a review entry to each of your cards, and
|
Use this option sparingly, as it will add a review entry to each of your cards, and
|
||||||
increase the size of your collection.
|
increase the size of your collection.
|
||||||
deck-config-ignore-before-tooltip =
|
deck-config-ignore-before-tooltip-2 =
|
||||||
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters.
|
If set, cards reviewed before the provided date will be ignored when optimizing FSRS parameters.
|
||||||
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
|
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
|
||||||
deck-config-compute-optimal-weights-tooltip2 =
|
deck-config-compute-optimal-weights-tooltip2 =
|
||||||
When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are
|
When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are
|
||||||
|
@ -469,6 +469,12 @@ deck-config-bury-tooltip =
|
||||||
|
|
||||||
When using the V3 scheduler, interday learning cards can also be buried. Interday
|
When using the V3 scheduler, interday learning cards can also be buried. Interday
|
||||||
learning cards are cards with a current learning step of one or more days.
|
learning cards are cards with a current learning step of one or more days.
|
||||||
|
deck-config-seconds-to-show-question-tooltip = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
|
||||||
|
deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one.
|
||||||
|
deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question.
|
||||||
|
deck-config-ignore-before-tooltip =
|
||||||
|
If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters.
|
||||||
|
This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons.
|
||||||
deck-config-compute-optimal-retention-tooltip =
|
deck-config-compute-optimal-retention-tooltip =
|
||||||
This tool assumes you're starting with 0 cards, and will attempt to calculate the amount of material you'll
|
This tool assumes you're starting with 0 cards, and will attempt to calculate the amount of material you'll
|
||||||
be able to retain in the given time frame. The estimated retention will greatly depend on your inputs, and
|
be able to retain in the given time frame. The estimated retention will greatly depend on your inputs, and
|
||||||
|
@ -505,3 +511,4 @@ deck-config-compute-optimal-retention-tooltip3 =
|
||||||
set your desired retention to. You may wish to choose a higher desired retention, if you’re willing to trade more study
|
set your desired retention to. You may wish to choose a higher desired retention, if you’re willing to trade more study
|
||||||
time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will
|
time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will
|
||||||
lead to a higher workload, because of the high forgetting rate.
|
lead to a higher workload, because of the high forgetting rate.
|
||||||
|
deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable.
|
||||||
|
|
|
@ -41,6 +41,7 @@ preferences-theme-follow-system = Follow System
|
||||||
preferences-theme-light = Light
|
preferences-theme-light = Light
|
||||||
preferences-theme-dark = Dark
|
preferences-theme-dark = Dark
|
||||||
preferences-v3-scheduler = V3 scheduler
|
preferences-v3-scheduler = V3 scheduler
|
||||||
|
preferences-check-for-updates = Check for program updates
|
||||||
preferences-ignore-accents-in-search = Ignore accents in search (slower)
|
preferences-ignore-accents-in-search = Ignore accents in search (slower)
|
||||||
preferences-backup-explanation =
|
preferences-backup-explanation =
|
||||||
Anki periodically backs up your collection. After backups are more than 2 days old,
|
Anki periodically backs up your collection. After backups are more than 2 days old,
|
||||||
|
|
|
@ -152,7 +152,7 @@ statistics-cards-due =
|
||||||
}
|
}
|
||||||
statistics-backlog-checkbox = Backlog
|
statistics-backlog-checkbox = Backlog
|
||||||
statistics-intervals-title = Review Intervals
|
statistics-intervals-title = Review Intervals
|
||||||
statistics-intervals-subtitle = Delays until reviews are shown again.
|
statistics-intervals-subtitle = Delays until review cards are shown again.
|
||||||
statistics-intervals-day-range =
|
statistics-intervals-day-range =
|
||||||
{ $cards ->
|
{ $cards ->
|
||||||
[one] { $cards } card with a { $daysStart }~{ $daysEnd } day interval
|
[one] { $cards } card with a { $daysStart }~{ $daysEnd } day interval
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1e8db8f33c0da127e67312aeb77f1c3aa901af77
|
Subproject commit 8cc8e3447cca79a7a8720e118388f6383c830eee
|
|
@ -9,8 +9,8 @@
|
||||||
"dev": "cd ts && vite dev",
|
"dev": "cd ts && vite dev",
|
||||||
"build": "cd ts && vite build",
|
"build": "cd ts && vite build",
|
||||||
"preview": "cd ts && vite preview",
|
"preview": "cd ts && vite preview",
|
||||||
"svelte-check:once": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning --compiler-warnings a11y-click-events-have-key-events:ignore,a11y-no-noninteractive-tabindex:ignore,a11y-no-static-element-interactions:ignore",
|
"svelte-check:once": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
|
||||||
"svelte-check": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch --compiler-warnings a11y-click-events-have-key-events:ignore,a11y-no-noninteractive-tabindex:ignore,a11y-no-static-element-interactions:ignore",
|
"svelte-check": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"vitest:once": "cd ts && vitest run",
|
"vitest:once": "cd ts && vitest run",
|
||||||
"vitest": "cd ts && vitest"
|
"vitest": "cd ts && vitest"
|
||||||
},
|
},
|
||||||
|
|
|
@ -402,27 +402,26 @@ message ComputeOptimalRetentionResponse {
|
||||||
float optimal_retention = 1;
|
float optimal_retention = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message OptimalRetentionParameters {
|
|
||||||
double recall_secs_hard = 1;
|
|
||||||
double recall_secs_good = 2;
|
|
||||||
double recall_secs_easy = 3;
|
|
||||||
double forget_secs = 4;
|
|
||||||
double learn_secs = 5;
|
|
||||||
double first_rating_probability_again = 6;
|
|
||||||
double first_rating_probability_hard = 7;
|
|
||||||
double first_rating_probability_good = 8;
|
|
||||||
double first_rating_probability_easy = 9;
|
|
||||||
double review_rating_probability_hard = 10;
|
|
||||||
double review_rating_probability_good = 11;
|
|
||||||
double review_rating_probability_easy = 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetOptimalRetentionParametersRequest {
|
message GetOptimalRetentionParametersRequest {
|
||||||
string search = 1;
|
string search = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetOptimalRetentionParametersResponse {
|
message GetOptimalRetentionParametersResponse {
|
||||||
OptimalRetentionParameters params = 1;
|
uint32 deck_size = 1;
|
||||||
|
uint32 learn_span = 2;
|
||||||
|
float max_cost_perday = 3;
|
||||||
|
float max_ivl = 4;
|
||||||
|
repeated float learn_costs = 5;
|
||||||
|
repeated float review_costs = 6;
|
||||||
|
repeated float first_rating_prob = 7;
|
||||||
|
repeated float review_rating_prob = 8;
|
||||||
|
repeated float first_rating_offsets = 9;
|
||||||
|
repeated float first_session_lens = 10;
|
||||||
|
float forget_rating_offset = 11;
|
||||||
|
float forget_session_len = 12;
|
||||||
|
float loss_aversion = 13;
|
||||||
|
uint32 learn_limit = 14;
|
||||||
|
uint32 review_limit = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvaluateWeightsRequest {
|
message EvaluateWeightsRequest {
|
||||||
|
|
|
@ -6,8 +6,9 @@ from __future__ import annotations
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
from threading import current_thread, main_thread
|
from threading import current_thread, main_thread
|
||||||
from typing import TYPE_CHECKING, Any, Iterable, Sequence
|
from typing import TYPE_CHECKING, Any
|
||||||
from weakref import ref
|
from weakref import ref
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
|
@ -8,7 +8,8 @@ import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Union
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any, Union
|
||||||
|
|
||||||
from anki._vendor import stringcase # type: ignore
|
from anki._vendor import stringcase # type: ignore
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
class Backend:
|
class Backend:
|
||||||
@classmethod
|
@classmethod
|
||||||
def command(cls, service: int, method: int, data: bytes) -> bytes: ...
|
def command(cls, service: int, method: int, data: bytes) -> bytes: ...
|
||||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
import time
|
import time
|
||||||
|
from typing import NewType
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast
|
from collections.abc import Generator, Iterable, Sequence
|
||||||
|
from typing import Any, Literal, Union, cast
|
||||||
|
|
||||||
from anki import (
|
from anki import (
|
||||||
ankiweb_pb2,
|
ankiweb_pb2,
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from re import Match
|
from re import Match
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence, Union
|
from typing import TYPE_CHECKING, Any, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import anki._backend
|
import anki._backend
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
|
from typing import TYPE_CHECKING, Any, NewType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import anki
|
import anki
|
||||||
|
|
|
@ -13,8 +13,9 @@ import threading
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from collections.abc import Sequence
|
||||||
from io import BufferedWriter
|
from io import BufferedWriter
|
||||||
from typing import Any, Optional, Sequence
|
from typing import Any
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -26,16 +27,16 @@ from anki.utils import ids2str, namedtmp, split_fields, strip_html
|
||||||
|
|
||||||
class Exporter:
|
class Exporter:
|
||||||
includeHTML: bool | None = None
|
includeHTML: bool | None = None
|
||||||
ext: Optional[str] = None
|
ext: str | None = None
|
||||||
includeTags: Optional[bool] = None
|
includeTags: bool | None = None
|
||||||
includeSched: Optional[bool] = None
|
includeSched: bool | None = None
|
||||||
includeMedia: Optional[bool] = None
|
includeMedia: bool | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
col: Collection,
|
col: Collection,
|
||||||
did: Optional[DeckId] = None,
|
did: DeckId | None = None,
|
||||||
cids: Optional[list[CardId]] = None,
|
cids: list[CardId] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.did = did
|
self.did = did
|
||||||
|
|
|
@ -94,8 +94,8 @@ class ForeignCard:
|
||||||
class ForeignNote:
|
class ForeignNote:
|
||||||
fields: list[str] = field(default_factory=list)
|
fields: list[str] = field(default_factory=list)
|
||||||
tags: list[str] = field(default_factory=list)
|
tags: list[str] = field(default_factory=list)
|
||||||
notetype: Union[str, NotetypeId] = ""
|
notetype: str | NotetypeId = ""
|
||||||
deck: Union[str, DeckId] = ""
|
deck: str | DeckId = ""
|
||||||
cards: list[ForeignCard] = field(default_factory=list)
|
cards: list[ForeignCard] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ class ForeignNote:
|
||||||
class ForeignData:
|
class ForeignData:
|
||||||
notes: list[ForeignNote] = field(default_factory=list)
|
notes: list[ForeignNote] = field(default_factory=list)
|
||||||
notetypes: list[ForeignNotetype] = field(default_factory=list)
|
notetypes: list[ForeignNotetype] = field(default_factory=list)
|
||||||
default_deck: Union[str, DeckId] = ""
|
default_deck: str | DeckId = ""
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
return json.dumps(self, cls=ForeignDataEncoder, separators=(",", ":"))
|
return json.dumps(self, cls=ForeignDataEncoder, separators=(",", ":"))
|
||||||
|
|
|
@ -17,7 +17,6 @@ Notetype | Card Type
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Tuple, Type
|
|
||||||
|
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
from anki.decks import DeckId
|
from anki.decks import DeckId
|
||||||
|
@ -38,7 +37,7 @@ def serialize(db_path: str, deck_id: DeckId) -> str:
|
||||||
def gather_data(db: DB, deck_id: DeckId) -> ForeignData:
|
def gather_data(db: DB, deck_id: DeckId) -> ForeignData:
|
||||||
facts = gather_facts(db)
|
facts = gather_facts(db)
|
||||||
gather_cards_into_facts(db, facts)
|
gather_cards_into_facts(db, facts)
|
||||||
used_fact_views: dict[Type[MnemoFactView], bool] = {}
|
used_fact_views: dict[type[MnemoFactView], bool] = {}
|
||||||
notes = [fact.foreign_note(used_fact_views) for fact in facts.values()]
|
notes = [fact.foreign_note(used_fact_views) for fact in facts.values()]
|
||||||
notetypes = [fact_view.foreign_notetype() for fact_view in used_fact_views]
|
notetypes = [fact_view.foreign_notetype() for fact_view in used_fact_views]
|
||||||
return ForeignData(notes, notetypes, deck_id)
|
return ForeignData(notes, notetypes, deck_id)
|
||||||
|
@ -54,7 +53,7 @@ def open_mnemosyne_db(db_path: str) -> DB:
|
||||||
|
|
||||||
class MnemoFactView(ABC):
|
class MnemoFactView(ABC):
|
||||||
notetype: str
|
notetype: str
|
||||||
field_keys: Tuple[str, ...]
|
field_keys: tuple[str, ...]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -162,7 +161,7 @@ class MnemoFact:
|
||||||
cards: list[MnemoCard] = field(default_factory=list)
|
cards: list[MnemoCard] = field(default_factory=list)
|
||||||
|
|
||||||
def foreign_note(
|
def foreign_note(
|
||||||
self, used_fact_views: dict[Type[MnemoFactView], bool]
|
self, used_fact_views: dict[type[MnemoFactView], bool]
|
||||||
) -> ForeignNote:
|
) -> ForeignNote:
|
||||||
fact_view = self.fact_view()
|
fact_view = self.fact_view()
|
||||||
used_fact_views[fact_view] = True
|
used_fact_views[fact_view] = True
|
||||||
|
@ -173,7 +172,7 @@ class MnemoFact:
|
||||||
cards=self.foreign_cards(),
|
cards=self.foreign_cards(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def fact_view(self) -> Type[MnemoFactView]:
|
def fact_view(self) -> type[MnemoFactView]:
|
||||||
try:
|
try:
|
||||||
fact_view = self.cards[0].fact_view_id
|
fact_view = self.cards[0].fact_view_id
|
||||||
except IndexError as err:
|
except IndexError as err:
|
||||||
|
@ -190,7 +189,7 @@ class MnemoFact:
|
||||||
|
|
||||||
raise Exception(f"Fact {id} has unknown fact view: {fact_view}")
|
raise Exception(f"Fact {id} has unknown fact view: {fact_view}")
|
||||||
|
|
||||||
def anki_fields(self, fact_view: Type[MnemoFactView]) -> list[str]:
|
def anki_fields(self, fact_view: type[MnemoFactView]) -> list[str]:
|
||||||
return [munge_field(self.fields.get(k, "")) for k in fact_view.field_keys]
|
return [munge_field(self.fields.get(k, "")) for k in fact_view.field_keys]
|
||||||
|
|
||||||
def anki_tags(self) -> list[str]:
|
def anki_tags(self) -> list[str]:
|
||||||
|
|
|
@ -14,6 +14,9 @@ modifying it.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import decorator
|
import decorator
|
||||||
|
|
||||||
# You can find the definitions in ../tools/genhooks.py
|
# You can find the definitions in ../tools/genhooks.py
|
||||||
|
@ -32,7 +35,7 @@ def runHook(hook: str, *args: Any) -> None:
|
||||||
for func in hookFuncs:
|
for func in hookFuncs:
|
||||||
try:
|
try:
|
||||||
func(*args)
|
func(*args)
|
||||||
except:
|
except Exception:
|
||||||
hookFuncs.remove(func)
|
hookFuncs.remove(func)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@ def runFilter(hook: str, arg: Any, *args: Any) -> Any:
|
||||||
for func in hookFuncs:
|
for func in hookFuncs:
|
||||||
try:
|
try:
|
||||||
arg = func(arg, *args)
|
arg = func(arg, *args)
|
||||||
except:
|
except Exception:
|
||||||
hookFuncs.remove(func)
|
hookFuncs.remove(func)
|
||||||
raise
|
raise
|
||||||
return arg
|
return arg
|
||||||
|
|
|
@ -9,7 +9,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
from typing import Any, Callable, Sequence, Type, Union
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any, Type, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Optional
|
from typing import Any
|
||||||
|
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
@ -31,7 +32,7 @@ class MediaMapInvalid(Exception):
|
||||||
|
|
||||||
class Anki2Importer(Importer):
|
class Anki2Importer(Importer):
|
||||||
needMapper = False
|
needMapper = False
|
||||||
deckPrefix: Optional[str] = None
|
deckPrefix: str | None = None
|
||||||
allowUpdate = True
|
allowUpdate = True
|
||||||
src: Collection
|
src: Collection
|
||||||
dst: Collection
|
dst: Collection
|
||||||
|
@ -409,7 +410,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""",
|
||||||
if fname.startswith("_") and not self.dst.media.have(fname):
|
if fname.startswith("_") and not self.dst.media.have(fname):
|
||||||
self._writeDstMedia(fname, self._srcMediaData(fname))
|
self._writeDstMedia(fname, self._srcMediaData(fname))
|
||||||
|
|
||||||
def _mediaData(self, fname: str, dir: Optional[str] = None) -> bytes:
|
def _mediaData(self, fname: str, dir: str | None = None) -> bytes:
|
||||||
if not dir:
|
if not dir:
|
||||||
dir = self.src.media.dir()
|
dir = self.src.media.dir()
|
||||||
path = os.path.join(dir, fname)
|
path = os.path.join(dir, fname)
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from anki.importing.anki2 import Anki2Importer, MediaMapInvalid
|
from anki.importing.anki2 import Anki2Importer, MediaMapInvalid
|
||||||
from anki.utils import tmpfile
|
from anki.utils import tmpfile
|
||||||
|
@ -15,7 +16,7 @@ from anki.utils import tmpfile
|
||||||
|
|
||||||
class AnkiPackageImporter(Anki2Importer):
|
class AnkiPackageImporter(Anki2Importer):
|
||||||
nameToNum: dict[str, str]
|
nameToNum: dict[str, str]
|
||||||
zip: Optional[zipfile.ZipFile]
|
zip: zipfile.ZipFile | None
|
||||||
|
|
||||||
def run(self) -> None: # type: ignore
|
def run(self) -> None: # type: ignore
|
||||||
# extract the deck from the zip file
|
# extract the deck from the zip file
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.utils import max_id
|
from anki.utils import max_id
|
||||||
|
@ -15,7 +16,7 @@ from anki.utils import max_id
|
||||||
class Importer:
|
class Importer:
|
||||||
needMapper = False
|
needMapper = False
|
||||||
needDelimiter = False
|
needDelimiter = False
|
||||||
dst: Optional[Collection]
|
dst: Collection | None
|
||||||
|
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
def __init__(self, col: Collection, file: str) -> None:
|
||||||
self.file = file
|
self.file = file
|
||||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import re
|
import re
|
||||||
from typing import Any, Optional, TextIO
|
from typing import Any, TextIO
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.importing.noteimp import ForeignNote, NoteImporter
|
from anki.importing.noteimp import ForeignNote, NoteImporter
|
||||||
|
@ -20,12 +20,12 @@ class TextImporter(NoteImporter):
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
def __init__(self, col: Collection, file: str) -> None:
|
||||||
NoteImporter.__init__(self, col, file)
|
NoteImporter.__init__(self, col, file)
|
||||||
self.lines = None
|
self.lines = None
|
||||||
self.fileobj: Optional[TextIO] = None
|
self.fileobj: TextIO | None = None
|
||||||
self.delimiter: Optional[str] = None
|
self.delimiter: str | None = None
|
||||||
self.tagsToAdd: list[str] = []
|
self.tagsToAdd: list[str] = []
|
||||||
self.numFields = 0
|
self.numFields = 0
|
||||||
self.dialect: Optional[Any]
|
self.dialect: Any | None
|
||||||
self.data: Optional[str | list[str]]
|
self.data: str | list[str] | None
|
||||||
|
|
||||||
def foreignNotes(self) -> list[ForeignNote]:
|
def foreignNotes(self) -> list[ForeignNote]:
|
||||||
self.open()
|
self.open()
|
||||||
|
@ -99,15 +99,15 @@ class TextImporter(NoteImporter):
|
||||||
if not self.delimiter:
|
if not self.delimiter:
|
||||||
try:
|
try:
|
||||||
self.dialect = sniffer.sniff("\n".join(self.data[:10]), self.patterns)
|
self.dialect = sniffer.sniff("\n".join(self.data[:10]), self.patterns)
|
||||||
except:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
self.dialect = sniffer.sniff(self.data[0], self.patterns)
|
self.dialect = sniffer.sniff(self.data[0], self.patterns)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if self.dialect:
|
if self.dialect:
|
||||||
try:
|
try:
|
||||||
reader = csv.reader(self.data, self.dialect, doublequote=True)
|
reader = csv.reader(self.data, self.dialect, doublequote=True)
|
||||||
except:
|
except Exception:
|
||||||
err()
|
err()
|
||||||
else:
|
else:
|
||||||
if not self.delimiter:
|
if not self.delimiter:
|
||||||
|
@ -126,7 +126,7 @@ class TextImporter(NoteImporter):
|
||||||
if row:
|
if row:
|
||||||
self.numFields = len(row)
|
self.numFields = len(row)
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
err()
|
err()
|
||||||
self.initMapping()
|
self.initMapping()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.config import Config
|
from anki.config import Config
|
||||||
|
@ -76,8 +76,8 @@ class NoteImporter(Importer):
|
||||||
needDelimiter = False
|
needDelimiter = False
|
||||||
allowHTML = False
|
allowHTML = False
|
||||||
importMode = UPDATE_MODE
|
importMode = UPDATE_MODE
|
||||||
mapping: Optional[list[str]]
|
mapping: list[str] | None
|
||||||
tagModified: Optional[str]
|
tagModified: str | None
|
||||||
|
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
def __init__(self, col: Collection, file: str) -> None:
|
||||||
Importer.__init__(self, col, file)
|
Importer.__init__(self, col, file)
|
||||||
|
@ -268,7 +268,7 @@ class NoteImporter(Importer):
|
||||||
|
|
||||||
def updateData(
|
def updateData(
|
||||||
self, n: ForeignNote, id: NoteId, sflds: list[str]
|
self, n: ForeignNote, id: NoteId, sflds: list[str]
|
||||||
) -> Optional[Updates]:
|
) -> Updates | None:
|
||||||
self._ids.append(id)
|
self._ids.append(id)
|
||||||
self.processFields(n, sflds)
|
self.processFields(n, sflds)
|
||||||
if self._tagsMapped:
|
if self._tagsMapped:
|
||||||
|
@ -316,9 +316,7 @@ where id = ? and flds != ?""",
|
||||||
changes2 = self.col.db.scalar("select total_changes()")
|
changes2 = self.col.db.scalar("select total_changes()")
|
||||||
self.updateCount = changes2 - changes
|
self.updateCount = changes2 - changes
|
||||||
|
|
||||||
def processFields(
|
def processFields(self, note: ForeignNote, fields: list[str] | None = None) -> None:
|
||||||
self, note: ForeignNote, fields: Optional[list[str]] = None
|
|
||||||
) -> None:
|
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = [""] * len(self.model["flds"])
|
fields = [""] * len(self.model["flds"])
|
||||||
for c, f in enumerate(self.mapping):
|
for c, f in enumerate(self.mapping):
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
# pytype: disable=attribute-error
|
# pytype: disable=attribute-error
|
||||||
# type: ignore
|
# type: ignore
|
||||||
# pylint: disable=C
|
# pylint: disable=C
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from string import capwords
|
from string import capwords
|
||||||
from typing import Optional, Union
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
from xml.dom.minidom import Element, Text
|
from xml.dom.minidom import Element, Text
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
self.logger("Load done.")
|
self.logger("Load done.")
|
||||||
|
|
||||||
# PARSE
|
# PARSE
|
||||||
def parse(self, node: Optional[Union[Text, Element]] = None) -> None:
|
def parse(self, node: Text | Element | None = None) -> None:
|
||||||
"Parse method - parses document elements"
|
"Parse method - parses document elements"
|
||||||
|
|
||||||
if node is None and self.xmldoc is not None:
|
if node is None and self.xmldoc is not None:
|
||||||
|
|
|
@ -180,9 +180,16 @@ def set_lang(lang: str) -> None:
|
||||||
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
tr_legacyglobal.backend = weakref.ref(current_i18n)
|
||||||
|
|
||||||
|
|
||||||
def get_def_lang(lang: str | None = None) -> tuple[int, str]:
|
def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
|
||||||
"""Return lang converted to name used on disk and its index, defaulting to system language
|
"""Return user_lang converted to name used on disk and its index, defaulting to system language
|
||||||
or English if not available."""
|
or English if not available."""
|
||||||
|
|
||||||
|
def get_index_of_language(wanted_locale: str) -> int | None:
|
||||||
|
for i, (_, locale_) in enumerate(langs):
|
||||||
|
if locale_ == wanted_locale:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805
|
# getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
@ -192,28 +199,30 @@ def get_def_lang(lang: str | None = None) -> tuple[int, str]:
|
||||||
# this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US
|
# this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US
|
||||||
# further below
|
# further below
|
||||||
(sys_lang, enc) = locale.getlocale()
|
(sys_lang, enc) = locale.getlocale()
|
||||||
except:
|
except Exception:
|
||||||
# fails on osx
|
# fails on osx
|
||||||
sys_lang = "en_US"
|
sys_lang = "en_US"
|
||||||
user_lang = lang
|
|
||||||
if user_lang in compatMap:
|
if user_lang in compatMap:
|
||||||
user_lang = compatMap[user_lang]
|
user_lang = compatMap[user_lang]
|
||||||
|
|
||||||
idx = None
|
idx = None
|
||||||
lang = None
|
lang = None
|
||||||
en_idx = None
|
|
||||||
for preferred_lang in (user_lang, sys_lang):
|
for preferred_lang in (user_lang, sys_lang):
|
||||||
for lang_idx, (name, code) in enumerate(langs):
|
idx = get_index_of_language(preferred_lang)
|
||||||
if code == "en_US":
|
is_language_supported = idx is not None
|
||||||
en_idx = lang_idx
|
if is_language_supported:
|
||||||
if code == preferred_lang:
|
assert preferred_lang is not None
|
||||||
idx = lang_idx
|
|
||||||
lang = preferred_lang
|
lang = preferred_lang
|
||||||
if idx is not None:
|
|
||||||
break
|
break
|
||||||
# if the specified language and the system language aren't available, revert to english
|
# if the specified language and the system language aren't available, revert to english
|
||||||
if idx is None:
|
is_preferred_language_supported = idx is not None
|
||||||
idx = en_idx
|
if not is_preferred_language_supported:
|
||||||
lang = "en_US"
|
lang = "en_US"
|
||||||
|
idx = get_index_of_language(lang)
|
||||||
|
is_english_supported = idx is not None
|
||||||
|
if not is_english_supported:
|
||||||
|
raise AssertionError("English is supposed to be a supported language.")
|
||||||
|
assert idx is not None and lang is not None
|
||||||
return (idx, lang)
|
return (idx, lang)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -170,7 +170,7 @@ def _err_msg(col: anki.collection.Collection, type: str, texpath: str) -> str:
|
||||||
if not log:
|
if not log:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
msg += f"<small><pre>{html.escape(log)}</pre></small>"
|
msg += f"<small><pre>{html.escape(log)}</pre></small>"
|
||||||
except:
|
except Exception:
|
||||||
msg += col.tr.media_have_you_installed_latex_and_dvipngdvisvgm()
|
msg += col.tr.media_have_you_installed_latex_and_dvipngdvisvgm()
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pprint
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
|
||||||
from anki import media_pb2
|
from anki import media_pb2
|
||||||
from anki._legacy import DeprecatedNamesMixin, deprecated_keywords
|
from anki._legacy import DeprecatedNamesMixin, deprecated_keywords
|
||||||
|
|
|
@ -7,7 +7,8 @@ import copy
|
||||||
import pprint
|
import pprint
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Any, NewType, Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
from typing import Any, NewType, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
@ -312,7 +313,7 @@ class ModelManager(DeprecatedNamesMixin):
|
||||||
def rename_field(
|
def rename_field(
|
||||||
self, notetype: NotetypeDict, field: FieldDict, new_name: str
|
self, notetype: NotetypeDict, field: FieldDict, new_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
if not field in notetype["flds"]:
|
if field not in notetype["flds"]:
|
||||||
raise Exception("invalid field")
|
raise Exception("invalid field")
|
||||||
field["name"] = new_name
|
field["name"] = new_name
|
||||||
|
|
||||||
|
@ -388,8 +389,8 @@ and notes.mid = ? and cards.ord = ?""",
|
||||||
|
|
||||||
To get defaults, use
|
To get defaults, use
|
||||||
|
|
||||||
input = ChangeNotetypeRequest()
|
info = col.models.change_notetype_info(...)
|
||||||
input.ParseFromString(col.models.change_notetype_info(...))
|
input = info.input
|
||||||
input.note_ids.extend([...])
|
input.note_ids.extend([...])
|
||||||
|
|
||||||
The new_fields and new_templates lists are relative to the new notetype's
|
The new_fields and new_templates lists are relative to the new notetype's
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import NewType, Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import NewType
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.cards
|
import anki.cards
|
||||||
|
@ -94,21 +95,28 @@ class Note(DeprecatedNamesMixin):
|
||||||
self,
|
self,
|
||||||
ord: int = 0,
|
ord: int = 0,
|
||||||
*,
|
*,
|
||||||
custom_note_type: NotetypeDict = None,
|
custom_note_type: NotetypeDict | None = None,
|
||||||
custom_template: TemplateDict = None,
|
custom_template: TemplateDict | None = None,
|
||||||
fill_empty: bool = False,
|
fill_empty: bool = False,
|
||||||
) -> anki.cards.Card:
|
) -> anki.cards.Card:
|
||||||
card = anki.cards.Card(self.col)
|
card = anki.cards.Card(self.col)
|
||||||
card.ord = ord
|
card.ord = ord
|
||||||
card.did = anki.decks.DEFAULT_DECK_ID
|
card.did = anki.decks.DEFAULT_DECK_ID
|
||||||
|
|
||||||
model = custom_note_type or self.note_type()
|
if custom_note_type is None:
|
||||||
template = copy.copy(
|
model = self.note_type()
|
||||||
custom_template
|
else:
|
||||||
or (
|
model = custom_note_type
|
||||||
model["tmpls"][ord] if model["type"] == MODEL_STD else model["tmpls"][0]
|
if model is None:
|
||||||
)
|
raise NotImplementedError
|
||||||
)
|
|
||||||
|
if custom_template is not None:
|
||||||
|
template = custom_template
|
||||||
|
elif model["type"] == MODEL_STD:
|
||||||
|
template = model["tmpls"][ord]
|
||||||
|
else:
|
||||||
|
template = model["tmpls"][0]
|
||||||
|
template = copy.copy(template)
|
||||||
# may differ in cloze case
|
# may differ in cloze case
|
||||||
template["ord"] = card.ord
|
template["ord"] = card.ord
|
||||||
|
|
||||||
|
@ -171,10 +179,7 @@ class Note(DeprecatedNamesMixin):
|
||||||
return self.col.tags.in_list(tag, self.tags)
|
return self.col.tags.in_list(tag, self.tags)
|
||||||
|
|
||||||
def remove_tag(self, tag: str) -> None:
|
def remove_tag(self, tag: str) -> None:
|
||||||
rem = []
|
rem = [tag_ for tag_ in self.tags if tag_.lower() == tag.lower()]
|
||||||
for tag_ in self.tags:
|
|
||||||
if tag_.lower() == tag.lower():
|
|
||||||
rem.append(tag_)
|
|
||||||
for tag_ in rem:
|
for tag_ in rem:
|
||||||
self.tags.remove(tag_)
|
self.tags.remove(tag_)
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@ ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
|
||||||
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
|
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
|
||||||
RepositionDefaults = scheduler_pb2.RepositionDefaultsResponse
|
RepositionDefaults = scheduler_pb2.RepositionDefaultsResponse
|
||||||
|
|
||||||
from typing import Sequence, overload
|
from collections.abc import Sequence
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
from anki import config_pb2
|
from anki import config_pb2
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from anki._legacy import deprecated
|
from anki._legacy import deprecated
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
from anki.consts import (
|
from anki.consts import (
|
||||||
|
@ -54,7 +52,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
|
||||||
print("_nextDueMsg() is obsolete")
|
print("_nextDueMsg() is obsolete")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def rebuildDyn(self, did: Optional[DeckId] = None) -> Optional[int]:
|
def rebuildDyn(self, did: DeckId | None = None) -> int | None:
|
||||||
did = did or self.col.decks.selected()
|
did = did or self.col.decks.selected()
|
||||||
count = self.rebuild_filtered_deck(did).count or None
|
count = self.rebuild_filtered_deck(did).count or None
|
||||||
if not count:
|
if not count:
|
||||||
|
@ -63,7 +61,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
|
||||||
self.col.decks.select(did)
|
self.col.decks.select(did)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def emptyDyn(self, did: Optional[DeckId], lim: Optional[str] = None) -> None:
|
def emptyDyn(self, did: DeckId | None, lim: str | None = None) -> None:
|
||||||
if lim is None:
|
if lim is None:
|
||||||
self.empty_filtered_deck(did)
|
self.empty_filtered_deck(did)
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,7 +14,8 @@ as '2' internally.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Literal, Optional, Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
from anki import frontend_pb2, scheduler_pb2
|
from anki import frontend_pb2, scheduler_pb2
|
||||||
from anki._legacy import deprecated
|
from anki._legacy import deprecated
|
||||||
|
@ -109,7 +110,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
# backend automatically resets queues as operations are performed
|
# backend automatically resets queues as operations are performed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def getCard(self) -> Optional[Card]:
|
def getCard(self) -> Card | None:
|
||||||
"""Fetch the next card from the queue. None if finished."""
|
"""Fetch the next card from the queue. None if finished."""
|
||||||
try:
|
try:
|
||||||
queued_card = self.get_queued_cards().cards[0]
|
queued_card = self.get_queued_cards().cards[0]
|
||||||
|
@ -125,7 +126,7 @@ class Scheduler(SchedulerBaseWithLegacy):
|
||||||
"Don't use this, it is a stop-gap until this code is refactored."
|
"Don't use this, it is a stop-gap until this code is refactored."
|
||||||
return not self.get_queued_cards().cards
|
return not self.get_queued_cards().cards
|
||||||
|
|
||||||
def counts(self, card: Optional[Card] = None) -> tuple[int, int, int]:
|
def counts(self, card: Card | None = None) -> tuple[int, int, int]:
|
||||||
info = self.get_queued_cards()
|
info = self.get_queued_cards()
|
||||||
return (info.new_count, info.learning_count, info.review_count)
|
return (info.new_count, info.learning_count, info.review_count)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anki.cards
|
import anki.cards
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
@ -721,7 +722,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
||||||
tot = bad + good
|
tot = bad + good
|
||||||
try:
|
try:
|
||||||
pct = good / float(tot) * 100
|
pct = good / float(tot) * 100
|
||||||
except:
|
except Exception:
|
||||||
pct = 0
|
pct = 0
|
||||||
i.append(
|
i.append(
|
||||||
"Correct: <b>%(pct)0.2f%%</b><br>(%(good)d of %(tot)d)"
|
"Correct: <b>%(pct)0.2f%%</b><br>(%(good)d of %(tot)d)"
|
||||||
|
@ -973,7 +974,7 @@ from cards where did in %s"""
|
||||||
else:
|
else:
|
||||||
conf["legend"] = {"container": "#%sLegend" % id, "noColumns": 10}
|
conf["legend"] = {"container": "#%sLegend" % id, "noColumns": 10}
|
||||||
conf["series"] = dict(stack=True)
|
conf["series"] = dict(stack=True)
|
||||||
if not "yaxis" in conf:
|
if "yaxis" not in conf:
|
||||||
conf["yaxis"] = {}
|
conf["yaxis"] = {}
|
||||||
conf["yaxis"]["labelWidth"] = 40
|
conf["yaxis"]["labelWidth"] = 40
|
||||||
if "xaxis" not in conf:
|
if "xaxis" not in conf:
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import anki.collection
|
import anki.collection
|
||||||
import anki.models
|
import anki.models
|
||||||
|
|
|
@ -13,7 +13,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
import re
|
||||||
from typing import Collection, Match, Sequence
|
from collections.abc import Collection, Sequence
|
||||||
|
from typing import Match
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki.collection
|
import anki.collection
|
||||||
|
|
|
@ -28,8 +28,9 @@ template_legacy.py file, using the legacy addHook() system.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Sequence, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.cards
|
import anki.cards
|
||||||
|
|
|
@ -13,9 +13,10 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable, Iterable, Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from anki._legacy import DeprecatedNamesMixinForModule
|
from anki._legacy import DeprecatedNamesMixinForModule
|
||||||
from anki.dbproxy import DBProxy
|
from anki.dbproxy import DBProxy
|
||||||
|
@ -28,7 +29,7 @@ try:
|
||||||
|
|
||||||
to_json_bytes: Callable[[Any], bytes] = orjson.dumps
|
to_json_bytes: Callable[[Any], bytes] = orjson.dumps
|
||||||
from_json_bytes = orjson.loads
|
from_json_bytes = orjson.loads
|
||||||
except:
|
except Exception:
|
||||||
print("orjson is missing; DB operations will be slower")
|
print("orjson is missing; DB operations will be slower")
|
||||||
|
|
||||||
def to_json_bytes(obj: Any) -> bytes:
|
def to_json_bytes(obj: Any) -> bytes:
|
||||||
|
@ -214,7 +215,7 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
|
||||||
info = subprocess.STARTUPINFO() # type: ignore
|
info = subprocess.STARTUPINFO() # type: ignore
|
||||||
try:
|
try:
|
||||||
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||||
except:
|
except Exception:
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
info.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW # type: ignore
|
||||||
else:
|
else:
|
||||||
|
@ -285,7 +286,7 @@ def plat_desc() -> str:
|
||||||
else:
|
else:
|
||||||
theos = system
|
theos = system
|
||||||
break
|
break
|
||||||
except:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return theos
|
return theos
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ def clear_tempfile(tf):
|
||||||
try:
|
try:
|
||||||
tf.close()
|
tf.close()
|
||||||
os.unlink(tf.name)
|
os.unlink(tf.name)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Dict
|
from collections.abc import Callable
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -385,7 +386,7 @@ def test_reviews():
|
||||||
assert "leech" in c.note().tags
|
assert "leech" in c.note().tags
|
||||||
|
|
||||||
|
|
||||||
def review_limits_setup() -> tuple[anki.collection.Collection, Dict]:
|
def review_limits_setup() -> tuple[anki.collection.Collection, dict]:
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
|
|
||||||
parent = col.decks.get(col.decks.id("parent"))
|
parent = col.decks.get(col.decks.id("parent"))
|
||||||
|
|
|
@ -137,7 +137,9 @@ prefix = """\
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.hooks
|
import anki.hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
Code for generating hooks.
|
Code for generating hooks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
sys.path.append("pylib/anki/_vendor")
|
sys.path.append("pylib/anki/_vendor")
|
||||||
|
|
||||||
|
@ -23,19 +24,19 @@ class Hook:
|
||||||
name: str
|
name: str
|
||||||
# string of the typed arguments passed to the callback, eg
|
# string of the typed arguments passed to the callback, eg
|
||||||
# ["kind: str", "val: int"]
|
# ["kind: str", "val: int"]
|
||||||
args: list[str] = None
|
args: list[str] | None = None
|
||||||
# string of the return type. if set, hook is a filter.
|
# string of the return type. if set, hook is a filter.
|
||||||
return_type: Optional[str] = None
|
return_type: str | None = None
|
||||||
# if add-ons may be relying on the legacy hook name, add it here
|
# if add-ons may be relying on the legacy hook name, add it here
|
||||||
legacy_hook: Optional[str] = None
|
legacy_hook: str | None = None
|
||||||
# if legacy hook takes no arguments but the new hook does, set this
|
# if legacy hook takes no arguments but the new hook does, set this
|
||||||
legacy_no_args: bool = False
|
legacy_no_args: bool = False
|
||||||
# if the hook replaces a deprecated one, add its name here
|
# if the hook replaces a deprecated one, add its name here
|
||||||
replaces: Optional[str] = None
|
replaces: str | None = None
|
||||||
# arguments that the hook being replaced took
|
# arguments that the hook being replaced took
|
||||||
replaced_hook_args: Optional[list[str]] = None
|
replaced_hook_args: list[str] | None = None
|
||||||
# docstring to add to hook class
|
# docstring to add to hook class
|
||||||
doc: Optional[str] = None
|
doc: str | None = None
|
||||||
|
|
||||||
def callable(self) -> str:
|
def callable(self) -> str:
|
||||||
"Convert args into a Callable."
|
"Convert args into a Callable."
|
||||||
|
@ -47,7 +48,7 @@ class Hook:
|
||||||
types_str = ", ".join(types)
|
types_str = ", ".join(types)
|
||||||
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
||||||
|
|
||||||
def arg_names(self, args: Optional[list[str]]) -> list[str]:
|
def arg_names(self, args: list[str] | None) -> list[str]:
|
||||||
names = []
|
names = []
|
||||||
for arg in args or []:
|
for arg in args or []:
|
||||||
if not arg:
|
if not arg:
|
||||||
|
@ -126,7 +127,7 @@ class {self.classname()}:
|
||||||
for hook in self._hooks:
|
for hook in self._hooks:
|
||||||
try:
|
try:
|
||||||
hook({", ".join(arg_names)})
|
hook({", ".join(arg_names)})
|
||||||
except:
|
except Exception:
|
||||||
# if the hook fails, remove it
|
# if the hook fails, remove it
|
||||||
self._hooks.remove(hook)
|
self._hooks.remove(hook)
|
||||||
raise
|
raise
|
||||||
|
@ -162,7 +163,7 @@ class {self.classname()}:
|
||||||
for filter in self._hooks:
|
for filter in self._hooks:
|
||||||
try:
|
try:
|
||||||
{arg_names[0]} = filter({", ".join(arg_names)})
|
{arg_names[0]} = filter({", ".join(arg_names)})
|
||||||
except:
|
except Exception:
|
||||||
# if the hook fails, remove it
|
# if the hook fails, remove it
|
||||||
self._hooks.remove(filter)
|
self._hooks.remove(filter)
|
||||||
raise
|
raise
|
||||||
|
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[tool.black]
|
||||||
|
target-version = ["py39", "py310", "py311", "py312"]
|
||||||
|
extend-exclude = "qt/bundle"
|
|
@ -1,9 +1,5 @@
|
||||||
[settings]
|
[settings]
|
||||||
ensure_newline_before_comments=true
|
py_version=39
|
||||||
force_grid_wrap=0
|
profile=black
|
||||||
include_trailing_comma=True
|
|
||||||
known_first_party=anki,aqt
|
known_first_party=anki,aqt
|
||||||
line_length=88
|
extend_skip=aqt/forms,hooks_gen.py
|
||||||
multi_line_output=3
|
|
||||||
skip=aqt/forms,hooks_gen.py
|
|
||||||
use_parentheses=True
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any, Union, cast
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pip_system_certs.wrapt_requests
|
import pip_system_certs.wrapt_requests
|
||||||
|
@ -50,7 +52,6 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
|
||||||
|
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki._backend import RustBackend
|
from anki._backend import RustBackend
|
||||||
|
@ -61,6 +62,7 @@ from anki.utils import checksum, is_lin, is_mac
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.log import setup_logging
|
from aqt.log import setup_logging
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.qt import sip
|
||||||
from aqt.utils import TR, tr
|
from aqt.utils import TR, tr
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -92,8 +94,8 @@ appHelpSite = HELP_SITE
|
||||||
from aqt.main import AnkiQt # isort:skip
|
from aqt.main import AnkiQt # isort:skip
|
||||||
from aqt.profiles import ProfileManager, VideoDriver # isort:skip
|
from aqt.profiles import ProfileManager, VideoDriver # isort:skip
|
||||||
|
|
||||||
profiler: Optional[cProfile.Profile] = None
|
profiler: cProfile.Profile | None = None
|
||||||
mw: Optional[AnkiQt] = None # set on init
|
mw: AnkiQt | None = None # set on init
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
|
||||||
|
@ -154,7 +156,7 @@ class DialogManager:
|
||||||
def allClosed(self) -> bool:
|
def allClosed(self) -> bool:
|
||||||
return not any(x[1] for x in self._dialogs.values())
|
return not any(x[1] for x in self._dialogs.values())
|
||||||
|
|
||||||
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
|
def closeAll(self, onsuccess: Callable[[], None]) -> bool | None:
|
||||||
# can we close immediately?
|
# can we close immediately?
|
||||||
if self.allClosed():
|
if self.allClosed():
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
@ -181,7 +183,7 @@ class DialogManager:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def register_dialog(
|
def register_dialog(
|
||||||
self, name: str, creator: Union[Callable, type], instance: Optional[Any] = None
|
self, name: str, creator: Callable | type, instance: Any | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Allows add-ons to register a custom dialog to be managed by Anki's dialog
|
"""Allows add-ons to register a custom dialog to be managed by Anki's dialog
|
||||||
manager, which ensures that only one copy of the window is open at once,
|
manager, which ensures that only one copy of the window is open at once,
|
||||||
|
@ -219,19 +221,19 @@ dialogs = DialogManager()
|
||||||
|
|
||||||
# A reference to the Qt translator needs to be held to prevent it from
|
# A reference to the Qt translator needs to be held to prevent it from
|
||||||
# being immediately deallocated.
|
# being immediately deallocated.
|
||||||
_qtrans: Optional[QTranslator] = None
|
_qtrans: QTranslator | None = None
|
||||||
|
|
||||||
|
|
||||||
def setupLangAndBackend(
|
def setupLangAndBackend(
|
||||||
pm: ProfileManager,
|
pm: ProfileManager,
|
||||||
app: QApplication,
|
app: QApplication,
|
||||||
force: Optional[str] = None,
|
force: str | None = None,
|
||||||
firstTime: bool = False,
|
firstTime: bool = False,
|
||||||
) -> RustBackend:
|
) -> RustBackend:
|
||||||
global _qtrans
|
global _qtrans
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# add _ and ngettext globals used by legacy code
|
# add _ and ngettext globals used by legacy code
|
||||||
|
@ -288,7 +290,7 @@ def setupLangAndBackend(
|
||||||
class NativeEventFilter(QAbstractNativeEventFilter):
|
class NativeEventFilter(QAbstractNativeEventFilter):
|
||||||
def nativeEventFilter(
|
def nativeEventFilter(
|
||||||
self, eventType: Any, message: Any
|
self, eventType: Any, message: Any
|
||||||
) -> tuple[bool, Optional[sip.voidptr]]:
|
) -> tuple[bool, sip.voidptr | None]:
|
||||||
if eventType == "windows_generic_MSG":
|
if eventType == "windows_generic_MSG":
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
|
@ -563,7 +565,7 @@ def run() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiApp]:
|
def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
|
||||||
"""Start AnkiQt application or reuse an existing instance if one exists.
|
"""Start AnkiQt application or reuse an existing instance if one exists.
|
||||||
|
|
||||||
If the function is invoked with exec=False, the AnkiQt will not enter
|
If the function is invoked with exec=False, the AnkiQt will not enter
|
||||||
|
@ -629,7 +631,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
|
||||||
pmLoadResult = pm.setupMeta()
|
pmLoadResult = pm.setupMeta()
|
||||||
|
|
||||||
Collection.initialize_backend_logging()
|
Collection.initialize_backend_logging()
|
||||||
except:
|
except Exception:
|
||||||
# will handle below
|
# will handle below
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
pm = None
|
pm = None
|
||||||
|
@ -671,11 +673,6 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
|
||||||
# we've signaled the primary instance, so we should close
|
# we've signaled the primary instance, so we should close
|
||||||
return None
|
return None
|
||||||
|
|
||||||
setup_logging(
|
|
||||||
pm.addon_logs(),
|
|
||||||
level=logging.DEBUG if int(os.getenv("ANKIDEV", "0")) else logging.INFO,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not pm:
|
if not pm:
|
||||||
if i18n_setup:
|
if i18n_setup:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
|
@ -687,6 +684,11 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
|
||||||
QMessageBox.critical(None, "Startup Failed", "Unable to create data folder")
|
QMessageBox.critical(None, "Startup Failed", "Unable to create data folder")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
setup_logging(
|
||||||
|
pm.addon_logs(),
|
||||||
|
level=logging.DEBUG if int(os.getenv("ANKIDEV", "0")) else logging.INFO,
|
||||||
|
)
|
||||||
|
|
||||||
# disable icons on mac; this must be done before window created
|
# disable icons on mac; this must be done before window created
|
||||||
if is_mac:
|
if is_mac:
|
||||||
app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus)
|
app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus)
|
||||||
|
@ -719,7 +721,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
|
||||||
# we must have a usable temp dir
|
# we must have a usable temp dir
|
||||||
try:
|
try:
|
||||||
tempfile.gettempdir()
|
tempfile.gettempdir()
|
||||||
except:
|
except Exception:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
None,
|
None,
|
||||||
tr.qt_misc_error(),
|
tr.qt_misc_error(),
|
||||||
|
|
|
@ -5,8 +5,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p
|
from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.utils
|
import aqt.utils
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
|
@ -86,6 +87,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
"Christian Krause",
|
"Christian Krause",
|
||||||
"Christian Rusche",
|
"Christian Rusche",
|
||||||
"Dave Druelinger",
|
"Dave Druelinger",
|
||||||
|
"David Culley",
|
||||||
"David Smith",
|
"David Smith",
|
||||||
"Dmitry Mikheev",
|
"Dmitry Mikheev",
|
||||||
"Dotan Cohen",
|
"Dotan Cohen",
|
||||||
|
@ -194,6 +196,8 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
"Marko Sisovic",
|
"Marko Sisovic",
|
||||||
"Lucas Scharenbroch",
|
"Lucas Scharenbroch",
|
||||||
"Antoine Q.",
|
"Antoine Q.",
|
||||||
|
"Ian Samir Yep Manzano",
|
||||||
|
"Asuka Minato",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Optional
|
from collections.abc import Callable
|
||||||
|
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -54,7 +54,7 @@ class AddCards(QMainWindow):
|
||||||
self.setupButtons()
|
self.setupButtons()
|
||||||
self.col.add_image_occlusion_notetype()
|
self.col.add_image_occlusion_notetype()
|
||||||
self.history: list[NoteId] = []
|
self.history: list[NoteId] = []
|
||||||
self._last_added_note: Optional[Note] = None
|
self._last_added_note: Note | None = None
|
||||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||||
restoreGeom(self, "add")
|
restoreGeom(self, "add")
|
||||||
gui_hooks.add_cards_did_init(self)
|
gui_hooks.add_cards_did_init(self)
|
||||||
|
@ -178,7 +178,7 @@ class AddCards(QMainWindow):
|
||||||
break
|
break
|
||||||
# copy non-empty old fields
|
# copy non-empty old fields
|
||||||
if (
|
if (
|
||||||
not old_field_value in copied_field_names
|
old_field_value not in copied_field_names
|
||||||
and old_note.fields[old_idx]
|
and old_note.fields[old_idx]
|
||||||
):
|
):
|
||||||
new_note.fields[new_idx] = old_note.fields[old_idx]
|
new_note.fields[new_idx] = old_note.fields[old_idx]
|
||||||
|
@ -195,7 +195,7 @@ class AddCards(QMainWindow):
|
||||||
self, old_note.note_type(), new_note.note_type()
|
self, old_note.note_type(), new_note.note_type()
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None:
|
def _load_new_note(self, sticky_fields_from: Note | None = None) -> None:
|
||||||
note = self._new_note()
|
note = self._new_note()
|
||||||
if old_note := sticky_fields_from:
|
if old_note := sticky_fields_from:
|
||||||
flds = note.note_type()["flds"]
|
flds = note.note_type()["flds"]
|
||||||
|
@ -209,7 +209,7 @@ class AddCards(QMainWindow):
|
||||||
self.setAndFocusNote(note)
|
self.setAndFocusNote(note)
|
||||||
|
|
||||||
def on_operation_did_execute(
|
def on_operation_did_execute(
|
||||||
self, changes: OpChanges, handler: Optional[object]
|
self, changes: OpChanges, handler: object | None
|
||||||
) -> None:
|
) -> None:
|
||||||
if (changes.notetype or changes.deck) and handler is not self.editor:
|
if (changes.notetype or changes.deck) and handler is not self.editor:
|
||||||
self.on_notetype_change(
|
self.on_notetype_change(
|
||||||
|
|
|
@ -9,13 +9,16 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Callable, Iterable, Sequence
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, Any, Callable, Iterable, Sequence, Union
|
from typing import IO, Any, Union
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
@ -247,7 +250,7 @@ class AddonManager:
|
||||||
__import__(addon.dir_name)
|
__import__(addon.dir_name)
|
||||||
except AbortAddonImport:
|
except AbortAddonImport:
|
||||||
pass
|
pass
|
||||||
except:
|
except Exception:
|
||||||
name = html.escape(addon.human_name())
|
name = html.escape(addon.human_name())
|
||||||
page = addon.page()
|
page = addon.page()
|
||||||
if page:
|
if page:
|
||||||
|
@ -340,7 +343,7 @@ class AddonManager:
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"json error in add-on {module}:\n{e}")
|
print(f"json error in add-on {module}:\n{e}")
|
||||||
return dict()
|
return dict()
|
||||||
except:
|
except Exception:
|
||||||
# missing meta file, etc
|
# missing meta file, etc
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
|
@ -643,7 +646,7 @@ class AddonManager:
|
||||||
try:
|
try:
|
||||||
with open(path, encoding="utf8") as f:
|
with open(path, encoding="utf8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_config_help_action(self, module: str, action: Callable[[], str]) -> None:
|
def set_config_help_action(self, module: str, action: Callable[[], str]) -> None:
|
||||||
|
@ -1083,9 +1086,11 @@ def download_addon(client: HttpClient, id: int) -> DownloadOk | DownloadError:
|
||||||
|
|
||||||
data = client.stream_content(resp)
|
data = client.stream_content(resp)
|
||||||
|
|
||||||
fname = re.match(
|
match = re.match(
|
||||||
"attachment; filename=(.+)", resp.headers["content-disposition"]
|
"attachment; filename=(.+)", resp.headers["content-disposition"]
|
||||||
).group(1)
|
)
|
||||||
|
assert match is not None
|
||||||
|
fname = match.group(1)
|
||||||
|
|
||||||
meta = extract_meta_from_download_url(resp.url)
|
meta = extract_meta_from_download_url(resp.url)
|
||||||
|
|
||||||
|
@ -1113,11 +1118,14 @@ def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
|
||||||
urlobj = urlparse(url)
|
urlobj = urlparse(url)
|
||||||
query = parse_qs(urlobj.query)
|
query = parse_qs(urlobj.query)
|
||||||
|
|
||||||
|
def get_first_element(elements: list[str]) -> int:
|
||||||
|
return int(elements[0])
|
||||||
|
|
||||||
meta = ExtractedDownloadMeta(
|
meta = ExtractedDownloadMeta(
|
||||||
mod_time=int(query.get("t")[0]),
|
mod_time=get_first_element(query["t"]),
|
||||||
min_point_version=int(query.get("minpt")[0]),
|
min_point_version=get_first_element(query["minpt"]),
|
||||||
max_point_version=int(query.get("maxpt")[0]),
|
max_point_version=get_first_element(query["maxpt"]),
|
||||||
branch_index=int(query.get("bidx")[0]),
|
branch_index=get_first_element(query["bidx"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return meta
|
return meta
|
||||||
|
|
|
@ -6,7 +6,8 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
from typing import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
|
|
@ -13,6 +13,7 @@ import aqt.forms
|
||||||
from anki.collection import SearchNode
|
from anki.collection import SearchNode
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.qt import sip
|
||||||
from aqt.webview import AnkiWebViewKind
|
from aqt.webview import AnkiWebViewKind
|
||||||
|
|
||||||
from ..operations import QueryOp
|
from ..operations import QueryOp
|
||||||
|
|
|
@ -6,7 +6,8 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Callable, Iterable
|
|
||||||
|
|
||||||
from anki.collection import SearchNode
|
from anki.collection import SearchNode
|
||||||
from aqt.theme import ColoredIcon
|
from aqt.theme import ColoredIcon
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Iterable, cast
|
from typing import cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
|
@ -43,6 +44,7 @@ from aqt.operations.tag import (
|
||||||
set_tag_collapsed,
|
set_tag_collapsed,
|
||||||
)
|
)
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.qt import sip
|
||||||
from aqt.theme import ColoredIcon, theme_manager
|
from aqt.theme import ColoredIcon, theme_manager
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
KeyboardModifiersPressed,
|
KeyboardModifiersPressed,
|
||||||
|
@ -147,7 +149,7 @@ class SidebarTreeView(QTreeView):
|
||||||
def op_executed(
|
def op_executed(
|
||||||
self, changes: OpChanges, handler: object | None, focused: bool
|
self, changes: OpChanges, handler: object | None, focused: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
if changes.browser_sidebar and not handler is self:
|
if changes.browser_sidebar and handler is not self:
|
||||||
self._refresh_needed = True
|
self._refresh_needed = True
|
||||||
if focused:
|
if focused:
|
||||||
self.refresh_if_needed()
|
self.refresh_if_needed()
|
||||||
|
@ -266,7 +268,7 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
def update_search(
|
def update_search(
|
||||||
self,
|
self,
|
||||||
*terms: Union[str, SearchNode],
|
*terms: str | SearchNode,
|
||||||
joiner: SearchJoiner = "AND",
|
joiner: SearchJoiner = "AND",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Modify the current search string based on modifier keys, then refresh."""
|
"""Modify the current search string based on modifier keys, then refresh."""
|
||||||
|
@ -524,7 +526,7 @@ class SidebarTreeView(QTreeView):
|
||||||
*,
|
*,
|
||||||
root: SidebarItem,
|
root: SidebarItem,
|
||||||
name: str,
|
name: str,
|
||||||
icon: Union[str, ColoredIcon],
|
icon: str | ColoredIcon,
|
||||||
collapse_key: Config.Bool.V,
|
collapse_key: Config.Bool.V,
|
||||||
type: SidebarItemType | None = None,
|
type: SidebarItemType | None = None,
|
||||||
) -> SidebarItem:
|
) -> SidebarItem:
|
||||||
|
|
|
@ -4,8 +4,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Generator, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Generator, Sequence, Union
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
|
@ -242,7 +243,7 @@ class DataModel(QAbstractTableModel):
|
||||||
self._state = self._state.toggle_state()
|
self._state = self._state.toggle_state()
|
||||||
try:
|
try:
|
||||||
self._search_inner(context)
|
self._search_inner(context)
|
||||||
except:
|
except Exception:
|
||||||
# rollback to prevent inconsistent state
|
# rollback to prevent inconsistent state
|
||||||
self._state = self._state.toggle_state()
|
self._state = self._state.toggle_state()
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from typing import Sequence, cast
|
from collections.abc import Sequence
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from anki.browser import BrowserConfig
|
from anki.browser import BrowserConfig
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.browser
|
import aqt.browser
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.deckconf
|
import aqt.deckconf
|
||||||
|
|
|
@ -5,8 +5,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, Match, Optional, cast
|
from typing import Any, Match, cast
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -50,7 +51,7 @@ class CardLayout(QDialog):
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
note: Note,
|
note: Note,
|
||||||
ord: int = 0,
|
ord: int = 0,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
fill_empty: bool = False,
|
fill_empty: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
||||||
|
@ -509,7 +510,7 @@ class CardLayout(QDialog):
|
||||||
# Preview
|
# Preview
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
_previewTimer: Optional[QTimer] = None
|
_previewTimer: QTimer | None = None
|
||||||
|
|
||||||
def renderPreview(self) -> None:
|
def renderPreview(self) -> None:
|
||||||
# schedule a preview when timing stops
|
# schedule a preview when timing stops
|
||||||
|
@ -590,7 +591,7 @@ class CardLayout(QDialog):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
type_filter = r"\[\[type:.+?\]\]"
|
type_filter = r"\[\[type:.+?\]\]"
|
||||||
repl: Union[str, Callable]
|
repl: str | Callable
|
||||||
|
|
||||||
if type == "q":
|
if type == "q":
|
||||||
repl = "<input id='typeans' type=text value='example' readonly='readonly'>"
|
repl = "<input id='typeans' type=text value='example' readonly='readonly'>"
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.operations
|
import aqt.operations
|
||||||
|
@ -37,12 +35,12 @@ class CustomStudy(QDialog):
|
||||||
def fetch_data_and_show(mw: aqt.AnkiQt) -> None:
|
def fetch_data_and_show(mw: aqt.AnkiQt) -> None:
|
||||||
def fetch_data(
|
def fetch_data(
|
||||||
col: Collection,
|
col: Collection,
|
||||||
) -> Tuple[DeckId, CustomStudyDefaults]:
|
) -> tuple[DeckId, CustomStudyDefaults]:
|
||||||
deck_id = mw.col.decks.get_current_id()
|
deck_id = mw.col.decks.get_current_id()
|
||||||
defaults = col.sched.custom_study_defaults(deck_id)
|
defaults = col.sched.custom_study_defaults(deck_id)
|
||||||
return (deck_id, defaults)
|
return (deck_id, defaults)
|
||||||
|
|
||||||
def show_dialog(data: Tuple[DeckId, CustomStudyDefaults]) -> None:
|
def show_dialog(data: tuple[DeckId, CustomStudyDefaults]) -> None:
|
||||||
deck_id, defaults = data
|
deck_id, defaults = data
|
||||||
CustomStudy(mw=mw, deck_id=deck_id, defaults=defaults)
|
CustomStudy(mw=mw, deck_id=deck_id, defaults=defaults)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -292,7 +294,7 @@ class DebugConsole(QDialog):
|
||||||
try:
|
try:
|
||||||
# pylint: disable=exec-used
|
# pylint: disable=exec-used
|
||||||
exec(text, vars)
|
exec(text, vars)
|
||||||
except:
|
except Exception:
|
||||||
self._output += traceback.format_exc()
|
self._output += traceback.format_exc()
|
||||||
self._captureOutput(False)
|
self._captureOutput(False)
|
||||||
buf = ""
|
buf = ""
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
from anki.decks import DEFAULT_DECK_ID, DeckId
|
from anki.decks import DEFAULT_DECK_ID, DeckId
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.qt import sip
|
||||||
from aqt.utils import HelpPage, shortcut, tr
|
from aqt.utils import HelpPage, shortcut, tr
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -173,8 +173,8 @@ class DeckConf(QDialog):
|
||||||
# Loading
|
# Loading
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def listToUser(self, l: list[Union[int, float]]) -> str:
|
def listToUser(self, l: list[int | float]) -> str:
|
||||||
def num_to_user(n: Union[int, float]) -> str:
|
def num_to_user(n: int | float) -> str:
|
||||||
if n == round(n):
|
if n == round(n):
|
||||||
return str(int(n))
|
return str(int(n))
|
||||||
else:
|
else:
|
||||||
|
@ -267,7 +267,7 @@ class DeckConf(QDialog):
|
||||||
if i == int(i):
|
if i == int(i):
|
||||||
i = int(i)
|
i = int(i)
|
||||||
ret.append(i)
|
ret.append(i)
|
||||||
except:
|
except Exception:
|
||||||
# invalid, don't update
|
# invalid, don't update
|
||||||
showWarning(tr.scheduling_steps_must_be_numbers())
|
showWarning(tr.scheduling_steps_must_be_numbers())
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from typing import Optional
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import aqt.editor
|
import aqt.editor
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
|
@ -37,7 +39,7 @@ class EditCurrent(QMainWindow):
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def on_operation_did_execute(
|
def on_operation_did_execute(
|
||||||
self, changes: OpChanges, handler: Optional[object]
|
self, changes: OpChanges, handler: object | None
|
||||||
) -> None:
|
) -> None:
|
||||||
if changes.note_text and handler is not self.editor:
|
if changes.note_text and handler is not self.editor:
|
||||||
# reload note
|
# reload note
|
||||||
|
|
|
@ -9,14 +9,16 @@ import html
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from typing import Any, Callable, Match, cast
|
from typing import Any, Match, cast
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
import requests
|
import requests
|
||||||
|
@ -59,7 +61,7 @@ from aqt.utils import (
|
||||||
)
|
)
|
||||||
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
||||||
|
|
||||||
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico", "avif")
|
pics = ("jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif")
|
||||||
audio = (
|
audio = (
|
||||||
"3gp",
|
"3gp",
|
||||||
"aac",
|
"aac",
|
||||||
|
@ -294,6 +296,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
disables: bool = True,
|
disables: bool = True,
|
||||||
rightside: bool = True,
|
rightside: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
title_attribute = tip
|
||||||
|
|
||||||
if icon:
|
if icon:
|
||||||
if icon.startswith("qrc:/"):
|
if icon.startswith("qrc:/"):
|
||||||
iconstr = icon
|
iconstr = icon
|
||||||
|
@ -301,47 +305,35 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
iconstr = self.resourceToData(icon)
|
iconstr = self.resourceToData(icon)
|
||||||
else:
|
else:
|
||||||
iconstr = f"/_anki/imgs/{icon}.png"
|
iconstr = f"/_anki/imgs/{icon}.png"
|
||||||
imgelm = f"""<img class="topbut" src="{iconstr}">"""
|
image_element = f'<img class="topbut" src="{iconstr}">'
|
||||||
else:
|
else:
|
||||||
imgelm = ""
|
image_element = ""
|
||||||
if label or not imgelm:
|
|
||||||
labelelm = label or cmd
|
if not label and icon:
|
||||||
|
label_element = ""
|
||||||
|
elif label:
|
||||||
|
label_element = label
|
||||||
else:
|
else:
|
||||||
labelelm = ""
|
label_element = cmd
|
||||||
if id:
|
|
||||||
idstr = f"id={id}"
|
title_attribute = shortcut(title_attribute)
|
||||||
else:
|
cmd_to_toggle_button = "toggleEditorButton(this);" if toggleable else ""
|
||||||
idstr = ""
|
id_attribute_assignment = f"id={id}" if id else ""
|
||||||
if toggleable:
|
class_attribute = "linkb" if rightside else "rounded"
|
||||||
toggleScript = "toggleEditorButton(this);"
|
|
||||||
else:
|
|
||||||
toggleScript = ""
|
|
||||||
tip = shortcut(tip)
|
|
||||||
if rightside:
|
|
||||||
class_ = "linkb"
|
|
||||||
else:
|
|
||||||
class_ = "rounded"
|
|
||||||
if not disables:
|
if not disables:
|
||||||
class_ += " perm"
|
class_attribute += " perm"
|
||||||
return """<button tabindex=-1
|
|
||||||
{id}
|
return f"""<button tabindex=-1
|
||||||
class="{class_}"
|
{id_attribute_assignment}
|
||||||
|
class="{class_attribute}"
|
||||||
type="button"
|
type="button"
|
||||||
title="{tip}"
|
title="{title_attribute}"
|
||||||
onclick="pycmd('{cmd}');{togglesc}return false;"
|
onclick="pycmd('{cmd}');{cmd_to_toggle_button}return false;"
|
||||||
onmousedown="window.event.preventDefault();"
|
onmousedown="window.event.preventDefault();"
|
||||||
>
|
>
|
||||||
{imgelm}
|
{image_element}
|
||||||
{labelelm}
|
{label_element}
|
||||||
</button>""".format(
|
</button>"""
|
||||||
imgelm=imgelm,
|
|
||||||
cmd=cmd,
|
|
||||||
tip=tip,
|
|
||||||
labelelm=labelelm,
|
|
||||||
id=idstr,
|
|
||||||
togglesc=toggleScript,
|
|
||||||
class_=class_,
|
|
||||||
)
|
|
||||||
|
|
||||||
def setupShortcuts(self) -> None:
|
def setupShortcuts(self) -> None:
|
||||||
# if a third element is provided, enable shortcut even when no field selected
|
# if a third element is provided, enable shortcut even when no field selected
|
||||||
|
|
|
@ -3,15 +3,19 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Optional, TextIO, cast
|
import traceback
|
||||||
|
from typing import TYPE_CHECKING, TextIO, cast
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import HelpPage
|
from anki.collection import HelpPage
|
||||||
from anki.errors import BackendError, Interrupted
|
from anki.errors import BackendError, Interrupted
|
||||||
|
from anki.utils import is_win
|
||||||
from aqt.addons import AddonManager, AddonMeta
|
from aqt.addons import AddonManager, AddonMeta
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import openHelp, showWarning, supportText, tooltip, tr
|
from aqt.utils import openHelp, showWarning, supportText, tooltip, tr
|
||||||
|
@ -169,7 +173,7 @@ class ErrorHandler(QObject):
|
||||||
def __init__(self, mw: AnkiQt) -> None:
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
QObject.__init__(self, mw)
|
QObject.__init__(self, mw)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.timer: Optional[QTimer] = None
|
self.timer: QTimer | None = None
|
||||||
qconnect(self.errorTimer, self._setTimer)
|
qconnect(self.errorTimer, self._setTimer)
|
||||||
self.pool = ""
|
self.pool = ""
|
||||||
self._oldstderr = sys.stderr
|
self._oldstderr = sys.stderr
|
||||||
|
|
|
@ -7,7 +7,6 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
@ -35,7 +34,7 @@ class ExportDialog(QDialog):
|
||||||
mw: aqt.main.AnkiQt,
|
mw: aqt.main.AnkiQt,
|
||||||
did: DeckId | None = None,
|
did: DeckId | None = None,
|
||||||
cids: list[CardId] | None = None,
|
cids: list[CardId] | None = None,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
):
|
):
|
||||||
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.operations
|
import aqt.operations
|
||||||
|
@ -32,7 +30,7 @@ class FieldDialog(QDialog):
|
||||||
self,
|
self,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
nt: NotetypeDict,
|
nt: NotetypeDict,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
open_at: int = 0,
|
open_at: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
QDialog.__init__(self, parent or mw)
|
QDialog.__init__(self, parent or mw)
|
||||||
|
@ -62,7 +60,7 @@ class FieldDialog(QDialog):
|
||||||
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
|
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
self.currentIdx: Optional[int] = None
|
self.currentIdx: int | None = None
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.setupSignals()
|
self.setupSignals()
|
||||||
self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
|
@ -125,8 +123,8 @@ class FieldDialog(QDialog):
|
||||||
self.loadField(idx)
|
self.loadField(idx)
|
||||||
|
|
||||||
def _uniqueName(
|
def _uniqueName(
|
||||||
self, prompt: str, ignoreOrd: Optional[int] = None, old: str = ""
|
self, prompt: str, ignoreOrd: int | None = None, old: str = ""
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
|
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
|
||||||
if not txt:
|
if not txt:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_language</string>
|
<string>preferences_language</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>lang</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
|
@ -64,6 +67,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_video_driver</string>
|
<string>preferences_video_driver</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>video_driver</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
|
@ -76,6 +82,19 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="check_for_updates">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>preferences_check_for_updates</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -116,6 +135,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_style</string>
|
<string>preferences_style</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>styleComboBox</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
|
@ -123,6 +145,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_theme</string>
|
<string>preferences_theme</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>theme</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
|
@ -130,6 +155,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_user_interface_size</string>
|
<string>preferences_user_interface_size</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>uiScale</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" colspan="2">
|
<item row="3" column="0" colspan="2">
|
||||||
|
@ -264,33 +292,25 @@
|
||||||
<widget class="QSpinBox" name="lrnCutoff">
|
<widget class="QSpinBox" name="lrnCutoff">
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>60</width>
|
<width>16777215</width>
|
||||||
<height>16777215</height>
|
<height>16777215</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string>preferences_mins</string>
|
||||||
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>999</number>
|
<number>999</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QLabel" name="label_29">
|
|
||||||
<property name="text">
|
|
||||||
<string>preferences_mins</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="label_30">
|
<widget class="QLabel" name="label_30">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_timebox_time_limit</string>
|
<string>preferences_timebox_time_limit</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
<property name="buddy">
|
||||||
</item>
|
<cstring>timeLimit</cstring>
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QLabel" name="label_40">
|
|
||||||
<property name="text">
|
|
||||||
<string>preferences_hours_past_midnight</string>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -298,10 +318,13 @@
|
||||||
<widget class="QSpinBox" name="dayOffset">
|
<widget class="QSpinBox" name="dayOffset">
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>60</width>
|
<width>16777215</width>
|
||||||
<height>16777215</height>
|
<height>16777215</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string>preferences_hours_past_midnight</string>
|
||||||
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>23</number>
|
<number>23</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -312,6 +335,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_learn_ahead_limit</string>
|
<string>preferences_learn_ahead_limit</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>lrnCutoff</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
|
@ -319,17 +345,16 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_next_day_starts_at</string>
|
<string>preferences_next_day_starts_at</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
<property name="buddy">
|
||||||
</item>
|
<cstring>dayOffset</cstring>
|
||||||
<item row="2" column="2">
|
|
||||||
<widget class="QLabel" name="label_39">
|
|
||||||
<property name="text">
|
|
||||||
<string>preferences_mins</string>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QSpinBox" name="timeLimit">
|
<widget class="QSpinBox" name="timeLimit">
|
||||||
|
<property name="suffix">
|
||||||
|
<string>preferences_mins</string>
|
||||||
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>9999</number>
|
<number>9999</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -513,6 +538,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_default_deck</string>
|
<string>preferences_default_deck</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>useCurrent</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -553,6 +581,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_default_search_text</string>
|
<string>preferences_default_search_text</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>default_search_text</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -705,10 +736,16 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_network_timeout</string>
|
<string>preferences_network_timeout</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>network_timeout</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSpinBox" name="network_timeout">
|
<widget class="QSpinBox" name="network_timeout">
|
||||||
|
<property name="suffix">
|
||||||
|
<string>scheduling_seconds</string>
|
||||||
|
</property>
|
||||||
<property name="minimum">
|
<property name="minimum">
|
||||||
<number>30</number>
|
<number>30</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -717,26 +754,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_8">
|
|
||||||
<property name="text">
|
|
||||||
<string>scheduling_seconds</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_4">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -853,6 +870,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_custom_sync_url</string>
|
<string>preferences_custom_sync_url</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>custom_sync_url</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -954,6 +974,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_daily_backups</string>
|
<string>preferences_daily_backups</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>daily_backups</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="4" column="2">
|
||||||
|
@ -968,6 +991,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_monthly_backups</string>
|
<string>preferences_monthly_backups</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>monthly_backups</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
|
@ -975,6 +1001,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_weekly_backups</string>
|
<string>preferences_weekly_backups</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>weekly_backups</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
|
@ -982,6 +1011,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>preferences_minutes_between_backups</string>
|
<string>preferences_minutes_between_backups</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>minutes_between_backups</cstring>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="2">
|
<item row="2" column="2">
|
||||||
|
|
|
@ -7,8 +7,8 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Sequence, Type
|
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.main
|
import aqt.main
|
||||||
|
@ -42,7 +42,7 @@ class ExportDialog(QDialog):
|
||||||
mw: aqt.main.AnkiQt,
|
mw: aqt.main.AnkiQt,
|
||||||
did: DeckId | None = None,
|
did: DeckId | None = None,
|
||||||
nids: Sequence[NoteId] | None = None,
|
nids: Sequence[NoteId] | None = None,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
):
|
):
|
||||||
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
QDialog.__init__(self, parent or mw, Qt.WindowType.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
@ -56,7 +56,7 @@ class ExportDialog(QDialog):
|
||||||
self.open()
|
self.open()
|
||||||
|
|
||||||
def setup(self, did: DeckId | None) -> None:
|
def setup(self, did: DeckId | None) -> None:
|
||||||
self.exporter_classes: list[Type[Exporter]] = [
|
self.exporter_classes: list[type[Exporter]] = [
|
||||||
ApkgExporter,
|
ApkgExporter,
|
||||||
ColpkgExporter,
|
ColpkgExporter,
|
||||||
NoteCsvExporter,
|
NoteCsvExporter,
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Callable
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
import aqt.main
|
import aqt.main
|
||||||
from anki.collection import Collection, Progress
|
from anki.collection import Collection, Progress
|
||||||
|
@ -124,7 +125,7 @@ class JsonImporter(Importer):
|
||||||
ImportDialog(mw, JsonFileArgs(path=path))
|
ImportDialog(mw, JsonFileArgs(path=path))
|
||||||
|
|
||||||
|
|
||||||
IMPORTERS: list[Type[Importer]] = [
|
IMPORTERS: list[type[Importer]] = [
|
||||||
ColpkgImporter,
|
ColpkgImporter,
|
||||||
ApkgImporter,
|
ApkgImporter,
|
||||||
MnemosyneImporter,
|
MnemosyneImporter,
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import anki.importing as importing
|
import anki.importing as importing
|
||||||
import aqt.deckchooser
|
import aqt.deckchooser
|
||||||
|
@ -54,7 +58,7 @@ class ChangeMap(QDialog):
|
||||||
self.frm.fields.setCurrentRow(n)
|
self.frm.fields.setCurrentRow(n)
|
||||||
else:
|
else:
|
||||||
self.frm.fields.setCurrentRow(n + 1)
|
self.frm.fields.setCurrentRow(n + 1)
|
||||||
self.field: Optional[str] = None
|
self.field: str | None = None
|
||||||
|
|
||||||
def getField(self) -> str:
|
def getField(self) -> str:
|
||||||
self.exec()
|
self.exec()
|
||||||
|
@ -230,13 +234,13 @@ class ImportDialog(QDialog):
|
||||||
self.frm.mappingArea.setWidget(self.frame)
|
self.frm.mappingArea.setWidget(self.frame)
|
||||||
self.mapbox = QVBoxLayout(self.frame)
|
self.mapbox = QVBoxLayout(self.frame)
|
||||||
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.mapwidget: Optional[QWidget] = None
|
self.mapwidget: QWidget | None = None
|
||||||
|
|
||||||
def hideMapping(self) -> None:
|
def hideMapping(self) -> None:
|
||||||
self.frm.mappingGroup.hide()
|
self.frm.mappingGroup.hide()
|
||||||
|
|
||||||
def showMapping(
|
def showMapping(
|
||||||
self, keepMapping: bool = False, hook: Optional[Callable] = None
|
self, keepMapping: bool = False, hook: Callable | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if hook:
|
if hook:
|
||||||
hook()
|
hook()
|
||||||
|
|
|
@ -8,10 +8,13 @@ import gc
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
import weakref
|
import weakref
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Any, Literal, Sequence, TypeVar, cast
|
from typing import Any, Literal, TypeVar, cast
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.cards
|
import anki.cards
|
||||||
|
@ -199,7 +202,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.setupUI()
|
self.setupUI()
|
||||||
self.setupAddons(args)
|
self.setupAddons(args)
|
||||||
self.finish_ui_setup()
|
self.finish_ui_setup()
|
||||||
except:
|
except Exception:
|
||||||
showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc()))
|
showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc()))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
# must call this after ui set up
|
# must call this after ui set up
|
||||||
|
@ -350,7 +353,7 @@ class AnkiQt(QMainWindow):
|
||||||
f.profiles.addItems(profs)
|
f.profiles.addItems(profs)
|
||||||
try:
|
try:
|
||||||
idx = profs.index(self.pm.name)
|
idx = profs.index(self.pm.name)
|
||||||
except:
|
except Exception:
|
||||||
idx = 0
|
idx = 0
|
||||||
f.profiles.setCurrentRow(idx)
|
f.profiles.setCurrentRow(idx)
|
||||||
|
|
||||||
|
@ -680,7 +683,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.maybeOptimize()
|
self.maybeOptimize()
|
||||||
if not dev_mode:
|
if not dev_mode:
|
||||||
corrupt = self.col.db.scalar("pragma quick_check") != "ok"
|
corrupt = self.col.db.scalar("pragma quick_check") != "ok"
|
||||||
except:
|
except Exception:
|
||||||
corrupt = True
|
corrupt = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -692,7 +695,7 @@ class AnkiQt(QMainWindow):
|
||||||
force=False,
|
force=False,
|
||||||
wait_for_completion=False,
|
wait_for_completion=False,
|
||||||
)
|
)
|
||||||
except:
|
except Exception:
|
||||||
print("backup on close failed")
|
print("backup on close failed")
|
||||||
self.col.close(downgrade=False)
|
self.col.close(downgrade=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -1428,6 +1431,7 @@ title="{}" {}>{}</button>""".format(
|
||||||
def setup_auto_update(self, _log: list[DownloadLogEntry]) -> None:
|
def setup_auto_update(self, _log: list[DownloadLogEntry]) -> None:
|
||||||
from aqt.update import check_for_update
|
from aqt.update import check_for_update
|
||||||
|
|
||||||
|
if aqt.mw.pm.check_for_updates():
|
||||||
check_for_update()
|
check_for_update()
|
||||||
|
|
||||||
# Timers
|
# Timers
|
||||||
|
|
|
@ -5,8 +5,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Iterable, Sequence, TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.progress
|
import aqt.progress
|
||||||
|
|
|
@ -11,10 +11,10 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from errno import EPROTOTYPE
|
from errno import EPROTOTYPE
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_cors
|
import flask_cors
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from collections.abc import Callable
|
||||||
|
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -16,7 +17,7 @@ class ModelChooser(QHBoxLayout):
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
widget: QWidget,
|
widget: QWidget,
|
||||||
label: bool = True,
|
label: bool = True,
|
||||||
on_activated: Optional[Callable[[], None]] = None,
|
on_activated: Callable[[], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""If provided, on_activated() will be called when the button is clicked,
|
"""If provided, on_activated() will be called when the button is clicked,
|
||||||
and the caller can call .onModelChange() to pull up the dialog when they
|
and the caller can call .onModelChange() to pull up the dialog when they
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, Optional, Sequence
|
from typing import Any
|
||||||
|
|
||||||
import aqt.clayout
|
import aqt.clayout
|
||||||
from anki import stdmodels
|
from anki import stdmodels
|
||||||
|
@ -40,9 +41,9 @@ class Models(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
fromMain: bool = False,
|
fromMain: bool = False,
|
||||||
selected_notetype_id: Optional[NotetypeId] = None,
|
selected_notetype_id: NotetypeId | None = None,
|
||||||
):
|
):
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
parent = parent or mw
|
parent = parent or mw
|
||||||
|
@ -61,6 +62,13 @@ class Models(QDialog):
|
||||||
self.models: Sequence[NotetypeNameIdUseCount] = []
|
self.models: Sequence[NotetypeNameIdUseCount] = []
|
||||||
self.setupModels()
|
self.setupModels()
|
||||||
restoreGeom(self, "models")
|
restoreGeom(self, "models")
|
||||||
|
|
||||||
|
self.setWindowFlags(
|
||||||
|
self.windowFlags()
|
||||||
|
| Qt.WindowType.WindowMaximizeButtonHint
|
||||||
|
| Qt.WindowType.WindowMinimizeButtonHint
|
||||||
|
)
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
|
@ -231,13 +239,13 @@ class Models(QDialog):
|
||||||
|
|
||||||
|
|
||||||
class AddModel(QDialog):
|
class AddModel(QDialog):
|
||||||
model: Optional[NotetypeDict]
|
model: NotetypeDict | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
on_success: Callable[[NotetypeDict], None],
|
on_success: Callable[[NotetypeDict], None],
|
||||||
parent: Optional[QWidget] = None,
|
parent: QWidget | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.parent_ = parent or mw
|
self.parent_ = parent or mw
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
@ -249,9 +257,7 @@ class AddModel(QDialog):
|
||||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
# standard models
|
# standard models
|
||||||
self.notetypes: list[
|
self.notetypes: list[NotetypeDict | Callable[[Collection], NotetypeDict]] = []
|
||||||
Union[NotetypeDict, Callable[[Collection], NotetypeDict]]
|
|
||||||
] = []
|
|
||||||
for name, func in stdmodels.get_stock_notetypes(self.col):
|
for name, func in stdmodels.get_stock_notetypes(self.col):
|
||||||
item = QListWidgetItem(tr.notetypes_add(val=name))
|
item = QListWidgetItem(tr.notetypes_add(val=name))
|
||||||
self.dialog.models.addItem(item)
|
self.dialog.models.addItem(item)
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
# pylint: disable=raise-missing-from
|
# pylint: disable=raise-missing-from
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
@ -38,7 +39,6 @@ import threading
|
||||||
import time
|
import time
|
||||||
from queue import Empty, Full, Queue
|
from queue import Empty, Full, Queue
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from anki.utils import is_win
|
from anki.utils import is_win
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class MPVBase:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
executable = which("mpv")
|
executable = which("mpv")
|
||||||
popenEnv: Optional[dict[str, str]] = None
|
popenEnv: dict[str, str] | None = None
|
||||||
|
|
||||||
default_argv = [
|
default_argv = [
|
||||||
"--idle",
|
"--idle",
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
from anki.models import NotetypeId
|
from anki.models import NotetypeId
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures._base import Future
|
from concurrent.futures._base import Future
|
||||||
from typing import Any, Callable, Generic, Protocol, TypeVar, Union
|
from typing import Any, Generic, Protocol, TypeVar, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.gui_hooks
|
import aqt.gui_hooks
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from anki.cards import CardId
|
from anki.cards import CardId
|
||||||
from anki.collection import OpChangesWithCount
|
from anki.collection import OpChangesWithCount
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue