mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4e83c9b82d | ||
![]() |
cf97c925e2 | ||
![]() |
25d5cc5508 | ||
![]() |
bfc87c0427 | ||
![]() |
e9dfb7a13d | ||
![]() |
fa1d6eae84 | ||
![]() |
234fa0c2f4 | ||
![]() |
f1b67a2005 | ||
![]() |
fa3caa472e | ||
![]() |
8d9c8c91b5 | ||
![]() |
218757ca46 | ||
![]() |
832a1c2c3e | ||
![]() |
670c098af2 | ||
![]() |
3f9f3b248e | ||
![]() |
097f9bd138 | ||
![]() |
269fb073e9 | ||
![]() |
0467f717ad | ||
![]() |
2fc6b72460 | ||
![]() |
82f3778340 | ||
![]() |
4bb1698b75 |
39 changed files with 329 additions and 203 deletions
|
@ -5,8 +5,11 @@
|
||||||
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 = [
|
||||||
# gix only used by burn-train
|
# pyoxidizer is stuck on an old ring version
|
||||||
"RUSTSEC-2024-0350",
|
"RUSTSEC-2025-0009",
|
||||||
|
"RUSTSEC-2025-0010",
|
||||||
|
# burn depends on an unmaintained package 'paste'
|
||||||
|
"RUSTSEC-2024-0436",
|
||||||
]
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
|
|
2
.version
2
.version
|
@ -1 +1 @@
|
||||||
25.02
|
25.02.3
|
||||||
|
|
46
Cargo.lock
generated
46
Cargo.lock
generated
|
@ -1288,9 +1288,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.13"
|
version = "0.5.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
@ -3377,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4166,9 +4166,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.70"
|
version = "0.10.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -4198,9 +4198,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.105"
|
version = "0.9.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
|
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -4768,9 +4768,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3"
|
name = "pyo3"
|
||||||
version = "0.23.4"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
|
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
@ -4786,9 +4786,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-build-config"
|
name = "pyo3-build-config"
|
||||||
version = "0.23.4"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
|
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
|
@ -4796,9 +4796,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-ffi"
|
name = "pyo3-ffi"
|
||||||
version = "0.23.4"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
|
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
|
@ -4806,9 +4806,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros"
|
name = "pyo3-macros"
|
||||||
version = "0.23.4"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
|
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-macros-backend",
|
"pyo3-macros-backend",
|
||||||
|
@ -4818,9 +4818,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros-backend"
|
name = "pyo3-macros-backend"
|
||||||
version = "0.23.4"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
|
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -6016,9 +6016,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
|
@ -6202,9 +6202,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.40.0"
|
version = "1.44.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
|
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -6219,9 +6219,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -110,7 +110,7 @@ prost-build = "0.13"
|
||||||
prost-reflect = "0.14"
|
prost-reflect = "0.14"
|
||||||
prost-types = "0.13"
|
prost-types = "0.13"
|
||||||
pulldown-cmark = "0.9.6"
|
pulldown-cmark = "0.9.6"
|
||||||
pyo3 = { version = "0.23.4", features = ["extension-module", "abi3", "abi3-py39"] }
|
pyo3 = { version = "0.24", features = ["extension-module", "abi3", "abi3-py39"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
regex = "1.11.0"
|
regex = "1.11.0"
|
||||||
reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] }
|
reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] }
|
||||||
|
|
|
@ -125,7 +125,7 @@ impl BuildAction for GenPythonProto {
|
||||||
build.add_outputs("", python_outputs);
|
build.add_outputs("", python_outputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_last_line(&self) -> bool {
|
fn hide_progress(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ pub trait BuildAction {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_last_line(&self) -> bool {
|
fn hide_progress(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -271,14 +271,14 @@ impl BuildStatement<'_> {
|
||||||
stmt.rule_variables.push(("pool".into(), pool.into()));
|
stmt.rule_variables.push(("pool".into(), pool.into()));
|
||||||
}
|
}
|
||||||
if have_n2 {
|
if have_n2 {
|
||||||
stmt.rule_variables.push((
|
if action.hide_success() {
|
||||||
"hide_success".into(),
|
stmt.rule_variables
|
||||||
(action.hide_success() as u8).to_string(),
|
.push(("hide_success".into(), "1".into()));
|
||||||
));
|
}
|
||||||
stmt.rule_variables.push((
|
if action.hide_progress() {
|
||||||
"hide_last_line".into(),
|
stmt.rule_variables
|
||||||
(action.hide_last_line() as u8).to_string(),
|
.push(("hide_progress".into(), "1".into()));
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt
|
stmt
|
||||||
|
|
|
@ -252,7 +252,7 @@ impl BuildAction for SvelteCheck {
|
||||||
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
|
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_last_line(&self) -> bool {
|
fn hide_progress(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ impl BuildAction for PythonTypecheck {
|
||||||
build.add_output_stamp(format!("tests/python_typecheck.{hash}"));
|
build.add_output_stamp(format!("tests/python_typecheck.{hash}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_last_line(&self) -> bool {
|
fn hide_progress(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@ impl BuildAction for PythonTest {
|
||||||
build.add_output_stamp(format!("tests/python_pytest.{hash}"));
|
build.add_output_stamp(format!("tests/python_pytest.{hash}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_last_line(&self) -> bool {
|
fn hide_progress(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -757,7 +757,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "crossbeam-channel",
|
"name": "crossbeam-channel",
|
||||||
"version": "0.5.13",
|
"version": "0.5.15",
|
||||||
"authors": null,
|
"authors": null,
|
||||||
"repository": "https://github.com/crossbeam-rs/crossbeam",
|
"repository": "https://github.com/crossbeam-rs/crossbeam",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
@ -2665,7 +2665,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openssl",
|
"name": "openssl",
|
||||||
"version": "0.10.70",
|
"version": "0.10.72",
|
||||||
"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",
|
||||||
|
@ -2692,7 +2692,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openssl-sys",
|
"name": "openssl-sys",
|
||||||
"version": "0.9.105",
|
"version": "0.9.107",
|
||||||
"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",
|
||||||
|
@ -4087,7 +4087,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tokio",
|
"name": "tokio",
|
||||||
"version": "1.40.0",
|
"version": "1.44.2",
|
||||||
"authors": "Tokio Contributors <team@tokio.rs>",
|
"authors": "Tokio Contributors <team@tokio.rs>",
|
||||||
"repository": "https://github.com/tokio-rs/tokio",
|
"repository": "https://github.com/tokio-rs/tokio",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -4096,7 +4096,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tokio-macros",
|
"name": "tokio-macros",
|
||||||
"version": "2.4.0",
|
"version": "2.5.0",
|
||||||
"authors": "Tokio Contributors <team@tokio.rs>",
|
"authors": "Tokio Contributors <team@tokio.rs>",
|
||||||
"repository": "https://github.com/tokio-rs/tokio",
|
"repository": "https://github.com/tokio-rs/tokio",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -14,7 +14,6 @@ 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.qt import sip
|
||||||
from aqt.webview import AnkiWebViewKind
|
|
||||||
|
|
||||||
from ..operations import QueryOp
|
from ..operations import QueryOp
|
||||||
from ..operations.tag import add_tags_to_notes
|
from ..operations.tag import add_tags_to_notes
|
||||||
|
@ -52,7 +51,6 @@ class FindDuplicatesDialog(QDialog):
|
||||||
self._dupes: list[tuple[str, list[NoteId]]] = []
|
self._dupes: list[tuple[str, list[NoteId]]] = []
|
||||||
|
|
||||||
# links
|
# links
|
||||||
form.webView.set_kind(AnkiWebViewKind.FIND_DUPLICATES)
|
|
||||||
form.webView.set_bridge_command(self._on_duplicate_clicked, context=self)
|
form.webView.set_bridge_command(self._on_duplicate_clicked, context=self)
|
||||||
form.webView.stdHtml("", context=self)
|
form.webView.stdHtml("", context=self)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ from anki.collection import EmptyCardsReport
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import QDialog, QDialogButtonBox, qconnect
|
from aqt.qt import QDialog, QDialogButtonBox, qconnect
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip, tr
|
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip, tr
|
||||||
from aqt.webview import AnkiWebViewKind
|
|
||||||
|
|
||||||
|
|
||||||
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
|
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
|
||||||
|
@ -47,7 +46,6 @@ class EmptyCardsDialog(QDialog):
|
||||||
self.setWindowTitle(tr.empty_cards_window_title())
|
self.setWindowTitle(tr.empty_cards_window_title())
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
self.form.keep_notes.setText(tr.empty_cards_preserve_notes_checkbox())
|
self.form.keep_notes.setText(tr.empty_cards_preserve_notes_checkbox())
|
||||||
self.form.webview.set_kind(AnkiWebViewKind.EMPTY_CARDS)
|
|
||||||
self.form.webview.set_bridge_command(self._on_note_link_clicked, self)
|
self.form.webview.set_bridge_command(self._on_note_link_clicked, self)
|
||||||
|
|
||||||
gui_hooks.empty_cards_will_show(self)
|
gui_hooks.empty_cards_will_show(self)
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="AnkiWebView" name="webview" native="true">
|
<widget class="EmptyCardsWebView" name="webview" native="true">
|
||||||
<property name="url" stdset="0">
|
<property name="url" stdset="0">
|
||||||
<url>
|
<url>
|
||||||
<string notr="true">about:blank</string>
|
<string notr="true">about:blank</string>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>AnkiWebView</class>
|
<class>EmptyCardsWebView</class>
|
||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header location="global">aqt/webview</header>
|
<header location="global">aqt/webview</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="AnkiWebView" name="webView" native="true">
|
<widget class="FindDupesWebView" name="webView" native="true">
|
||||||
<property name="url" stdset="0">
|
<property name="url" stdset="0">
|
||||||
<url>
|
<url>
|
||||||
<string notr="true">about:blank</string>
|
<string notr="true">about:blank</string>
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>AnkiWebView</class>
|
<class>FindDupesWebView</class>
|
||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header location="global">aqt/webview</header>
|
<header location="global">aqt/webview</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="AnkiWebView" name="web" native="true">
|
<widget class="StatsWebView" name="web" native="true">
|
||||||
<property name="url" stdset="0">
|
<property name="url" stdset="0">
|
||||||
<url>
|
<url>
|
||||||
<string notr="true">about:blank</string>
|
<string notr="true">about:blank</string>
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>AnkiWebView</class>
|
<class>StatsWebView</class>
|
||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header location="global">aqt/webview</header>
|
<header location="global">aqt/webview</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
|
|
|
@ -7,7 +7,9 @@ import enum
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
|
import string
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -139,14 +141,17 @@ class MediaServer(threading.Thread):
|
||||||
) -> None:
|
) -> None:
|
||||||
self._legacy_pages[id] = LegacyPage(html, context)
|
self._legacy_pages[id] = LegacyPage(html, context)
|
||||||
|
|
||||||
|
def get_page(self, id: int) -> LegacyPage | None:
|
||||||
|
return self._legacy_pages.get(id)
|
||||||
|
|
||||||
def get_page_html(self, id: int) -> str | None:
|
def get_page_html(self, id: int) -> str | None:
|
||||||
if page := self._legacy_pages.get(id):
|
if page := self.get_page(id):
|
||||||
return page.html
|
return page.html
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_page_context(self, id: int) -> PageContext | None:
|
def get_page_context(self, id: int) -> PageContext | None:
|
||||||
if page := self._legacy_pages.get(id):
|
if page := self.get_page(id):
|
||||||
return page.context
|
return page.context
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -698,7 +703,6 @@ def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound:
|
||||||
def _check_dynamic_request_permissions():
|
def _check_dynamic_request_permissions():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return
|
return
|
||||||
context = _extract_page_context()
|
|
||||||
|
|
||||||
def warn() -> None:
|
def warn() -> None:
|
||||||
show_warning(
|
show_warning(
|
||||||
|
@ -710,24 +714,17 @@ def _check_dynamic_request_permissions():
|
||||||
aqt.mw.taskman.run_on_main(warn)
|
aqt.mw.taskman.run_on_main(warn)
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
if context in [
|
# does page have access to entire API?
|
||||||
PageContext.NON_LEGACY_PAGE,
|
if _have_api_access():
|
||||||
PageContext.EDITOR,
|
return
|
||||||
PageContext.ADDON_PAGE,
|
|
||||||
PageContext.DECK_OPTIONS,
|
# whitelisted API endpoints for reviewer/previewer
|
||||||
]:
|
if request.path in (
|
||||||
pass
|
|
||||||
elif context == PageContext.REVIEWER and request.path in (
|
|
||||||
"/_anki/getSchedulingStatesWithContext",
|
"/_anki/getSchedulingStatesWithContext",
|
||||||
"/_anki/setSchedulingStates",
|
"/_anki/setSchedulingStates",
|
||||||
"/_anki/i18nResources",
|
"/_anki/i18nResources",
|
||||||
|
"/_anki/congratsInfo",
|
||||||
):
|
):
|
||||||
# reviewer is only allowed to access custom study methods
|
|
||||||
pass
|
|
||||||
elif (
|
|
||||||
context == PageContext.PREVIEWER or context == PageContext.CARD_LAYOUT
|
|
||||||
) and request.path == "/_anki/i18nResources":
|
|
||||||
# previewers are only allowed to access i18n resources
|
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# other legacy pages may contain third-party JS, so we do not
|
# other legacy pages may contain third-party JS, so we do not
|
||||||
|
@ -746,29 +743,26 @@ def _handle_dynamic_request(req: DynamicRequest) -> Response:
|
||||||
|
|
||||||
def legacy_page_data() -> Response:
|
def legacy_page_data() -> Response:
|
||||||
id = int(request.args["id"])
|
id = int(request.args["id"])
|
||||||
if html := aqt.mw.mediaServer.get_page_html(id):
|
page = aqt.mw.mediaServer.get_page(id)
|
||||||
return Response(html, mimetype="text/html")
|
if page:
|
||||||
|
response = Response(page.html, mimetype="text/html")
|
||||||
|
# Prevent JS in field content from being executed in the editor, as it would
|
||||||
|
# have access to our internal API, and is a security risk.
|
||||||
|
if page.context == PageContext.EDITOR:
|
||||||
|
port = aqt.mw.mediaServer.getPort()
|
||||||
|
response.headers["Content-Security-Policy"] = (
|
||||||
|
f"script-src http://127.0.0.1:{port}/_anki/"
|
||||||
|
)
|
||||||
|
return response
|
||||||
else:
|
else:
|
||||||
return _text_response(HTTPStatus.NOT_FOUND, "page not found")
|
return _text_response(HTTPStatus.NOT_FOUND, "page not found")
|
||||||
|
|
||||||
|
|
||||||
def _extract_page_context() -> PageContext:
|
_APIKEY = "".join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||||
"Get context based on referer header."
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
|
|
||||||
referer = urlparse(request.headers.get("Referer", ""))
|
|
||||||
if referer.path.startswith("/_anki/pages/") or is_sveltekit_page(referer.path[1:]):
|
def _have_api_access() -> bool:
|
||||||
return PageContext.NON_LEGACY_PAGE
|
return request.headers.get("Authorization") == f"Bearer {_APIKEY}"
|
||||||
elif referer.path == "/_anki/legacyPageData":
|
|
||||||
query_params = parse_qs(referer.query)
|
|
||||||
query_id = query_params.get("id")
|
|
||||||
if not query_id:
|
|
||||||
return PageContext.UNKNOWN
|
|
||||||
id = int(query_id[0])
|
|
||||||
page_context = aqt.mw.mediaServer.get_page_context(id)
|
|
||||||
return page_context if page_context else PageContext.UNKNOWN
|
|
||||||
else:
|
|
||||||
return PageContext.UNKNOWN
|
|
||||||
|
|
||||||
|
|
||||||
# this currently only handles a single method; in the future, idempotent
|
# this currently only handles a single method; in the future, idempotent
|
||||||
|
|
|
@ -141,7 +141,7 @@ class AVPlayer:
|
||||||
# audio be stopped?
|
# audio be stopped?
|
||||||
interrupt_current_audio = True
|
interrupt_current_audio = True
|
||||||
# caller key for the current playback (optional)
|
# caller key for the current playback (optional)
|
||||||
current_caller = None
|
current_caller: Any = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._enqueued: list[AVTag] = []
|
self._enqueued: list[AVTag] = []
|
||||||
|
@ -165,7 +165,7 @@ class AVPlayer:
|
||||||
self._enqueued = []
|
self._enqueued = []
|
||||||
self._stop_if_playing()
|
self._stop_if_playing()
|
||||||
|
|
||||||
def stop_and_clear_queue_if_caller(self, caller) -> None:
|
def stop_and_clear_queue_if_caller(self, caller: Any) -> None:
|
||||||
if caller == self.current_caller:
|
if caller == self.current_caller:
|
||||||
self.stop_and_clear_queue()
|
self.stop_and_clear_queue()
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ class AVPlayer:
|
||||||
def play_file(self, filename: str) -> None:
|
def play_file(self, filename: str) -> None:
|
||||||
self.play_tags([SoundOrVideoTag(filename=filename)])
|
self.play_tags([SoundOrVideoTag(filename=filename)])
|
||||||
|
|
||||||
def play_file_with_caller(self, filename: str, caller) -> None:
|
def play_file_with_caller(self, filename: str, caller: Any) -> None:
|
||||||
self.current_caller = caller
|
self.current_caller = caller
|
||||||
self.play_file(filename)
|
self.play_file(filename)
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ from aqt.utils import (
|
||||||
tooltip,
|
tooltip,
|
||||||
tr,
|
tr,
|
||||||
)
|
)
|
||||||
from aqt.webview import AnkiWebViewKind
|
from aqt.webview import LegacyStatsWebView
|
||||||
|
|
||||||
|
|
||||||
class NewDeckStats(QDialog):
|
class NewDeckStats(QDialog):
|
||||||
|
@ -71,7 +71,6 @@ class NewDeckStats(QDialog):
|
||||||
maybeHideClose(self.form.buttonBox)
|
maybeHideClose(self.form.buttonBox)
|
||||||
addCloseShortcut(self)
|
addCloseShortcut(self)
|
||||||
gui_hooks.stats_dialog_will_show(self)
|
gui_hooks.stats_dialog_will_show(self)
|
||||||
self.form.web.set_kind(AnkiWebViewKind.DECK_STATS)
|
|
||||||
self.form.web.hide_while_preserving_layout()
|
self.form.web.hide_while_preserving_layout()
|
||||||
self.show()
|
self.show()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
@ -154,6 +153,9 @@ class DeckStats(QDialog):
|
||||||
self.name = "deckStats"
|
self.name = "deckStats"
|
||||||
self.period = 0
|
self.period = 0
|
||||||
self.form = aqt.forms.stats.Ui_Dialog()
|
self.form = aqt.forms.stats.Ui_Dialog()
|
||||||
|
# Hack: Switch out web views dynamically to avoid maintaining multiple
|
||||||
|
# Qt forms for different versions of the stats dialog.
|
||||||
|
self.form.web = LegacyStatsWebView(self.mw)
|
||||||
self.oldPos = None
|
self.oldPos = None
|
||||||
self.wholeCollection = False
|
self.wholeCollection = False
|
||||||
self.setMinimumWidth(700)
|
self.setMinimumWidth(700)
|
||||||
|
@ -232,7 +234,6 @@ class DeckStats(QDialog):
|
||||||
stats = self.mw.col.stats()
|
stats = self.mw.col.stats()
|
||||||
stats.wholeCollection = self.wholeCollection
|
stats.wholeCollection = self.wholeCollection
|
||||||
self.report = stats.report(type=self.period)
|
self.report = stats.report(type=self.period)
|
||||||
self.form.web.set_kind(AnkiWebViewKind.LEGACY_DECK_STATS)
|
|
||||||
self.form.web.stdHtml(
|
self.form.web.stdHtml(
|
||||||
f"<html><body>{self.report}</body></html>",
|
f"<html><body>{self.report}</body></html>",
|
||||||
js=["js/vendor/jquery.min.js", "js/vendor/plot.js"],
|
js=["js/vendor/jquery.min.js", "js/vendor/plot.js"],
|
||||||
|
|
|
@ -10,7 +10,9 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, Type, cast
|
||||||
|
|
||||||
|
from typing_extensions import TypedDict, Unpack
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki.lang
|
import anki.lang
|
||||||
|
@ -28,21 +30,154 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from aqt.mediasrv import PageContext
|
from aqt.mediasrv import PageContext
|
||||||
|
|
||||||
|
|
||||||
# Page for debug messages
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
BridgeCommandHandler = Callable[[str], Any]
|
BridgeCommandHandler = Callable[[str], Any]
|
||||||
|
|
||||||
|
|
||||||
|
class AnkiWebViewKind(Enum):
|
||||||
|
"""Enum registry of all web views managed by Anki
|
||||||
|
|
||||||
|
The value of each entry corresponds to the web view's title.
|
||||||
|
|
||||||
|
When introducing a new web view, please add it to the registry below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT = "default"
|
||||||
|
MAIN = "main webview"
|
||||||
|
TOP_TOOLBAR = "top toolbar"
|
||||||
|
BOTTOM_TOOLBAR = "bottom toolbar"
|
||||||
|
DECK_OPTIONS = "deck options"
|
||||||
|
EDITOR = "editor"
|
||||||
|
LEGACY_DECK_STATS = "legacy deck stats"
|
||||||
|
DECK_STATS = "deck stats"
|
||||||
|
PREVIEWER = "previewer"
|
||||||
|
CHANGE_NOTETYPE = "change notetype"
|
||||||
|
CARD_LAYOUT = "card layout"
|
||||||
|
BROWSER_CARD_INFO = "browser card info"
|
||||||
|
IMPORT_CSV = "csv import"
|
||||||
|
EMPTY_CARDS = "empty cards"
|
||||||
|
FIND_DUPLICATES = "find duplicates"
|
||||||
|
FIELDS = "fields"
|
||||||
|
IMPORT_LOG = "import log"
|
||||||
|
IMPORT_ANKI_PACKAGE = "anki package import"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthInterceptor(QWebEngineUrlRequestInterceptor):
|
||||||
|
_api_enabled = False
|
||||||
|
|
||||||
|
def __init__(self, parent: QObject | None = None, api_enabled: bool = False):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._api_enabled = api_enabled
|
||||||
|
|
||||||
|
def interceptRequest(self, info):
|
||||||
|
from aqt.mediasrv import _APIKEY
|
||||||
|
|
||||||
|
if self._api_enabled and info.requestUrl().host() == "127.0.0.1":
|
||||||
|
info.setHttpHeader(b"Authorization", f"Bearer {_APIKEY}".encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_bridge_script() -> QWebEngineScript:
|
||||||
|
qwebchannel = ":/qtwebchannel/qwebchannel.js"
|
||||||
|
jsfile = QFile(qwebchannel)
|
||||||
|
if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
|
||||||
|
print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
|
||||||
|
jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
|
||||||
|
jsfile.close()
|
||||||
|
|
||||||
|
script = QWebEngineScript()
|
||||||
|
script.setSourceCode(
|
||||||
|
jstext
|
||||||
|
+ """
|
||||||
|
var pycmd, bridgeCommand;
|
||||||
|
new QWebChannel(qt.webChannelTransport, function(channel) {
|
||||||
|
bridgeCommand = pycmd = function (arg, cb) {
|
||||||
|
var resultCB = function (res) {
|
||||||
|
// pass result back to user-provided callback
|
||||||
|
if (cb) {
|
||||||
|
cb(JSON.parse(res));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.objects.py.cmd(arg, resultCB);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pycmd("domDone");
|
||||||
|
});
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
||||||
|
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
||||||
|
script.setRunsOnSubFrames(False)
|
||||||
|
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
_bridge_script = _create_bridge_script()
|
||||||
|
|
||||||
|
_profile_with_api_access: QWebEngineProfile | None = None
|
||||||
|
_profile_without_api_access: QWebEngineProfile | None = None
|
||||||
|
|
||||||
|
|
||||||
class AnkiWebPage(QWebEnginePage):
|
class AnkiWebPage(QWebEnginePage):
|
||||||
def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None:
|
def __init__(
|
||||||
QWebEnginePage.__init__(self)
|
self,
|
||||||
|
onBridgeCmd: BridgeCommandHandler,
|
||||||
|
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
|
||||||
|
parent: QObject | None = None,
|
||||||
|
) -> None:
|
||||||
|
profile = self._profileForPage(kind)
|
||||||
|
self._inject_user_script(profile, _bridge_script)
|
||||||
|
QWebEnginePage.__init__(self, profile, parent)
|
||||||
self._onBridgeCmd = onBridgeCmd
|
self._onBridgeCmd = onBridgeCmd
|
||||||
|
self._kind = kind
|
||||||
self._setupBridge()
|
self._setupBridge()
|
||||||
self.open_links_externally = True
|
self.open_links_externally = True
|
||||||
|
|
||||||
|
def _profileForPage(self, kind: AnkiWebViewKind) -> QWebEngineProfile:
|
||||||
|
have_api_access = kind in (
|
||||||
|
AnkiWebViewKind.DECK_OPTIONS,
|
||||||
|
AnkiWebViewKind.EDITOR,
|
||||||
|
AnkiWebViewKind.DECK_STATS,
|
||||||
|
AnkiWebViewKind.CHANGE_NOTETYPE,
|
||||||
|
AnkiWebViewKind.BROWSER_CARD_INFO,
|
||||||
|
AnkiWebViewKind.IMPORT_ANKI_PACKAGE,
|
||||||
|
AnkiWebViewKind.IMPORT_CSV,
|
||||||
|
AnkiWebViewKind.IMPORT_LOG,
|
||||||
|
)
|
||||||
|
|
||||||
|
global _profile_with_api_access, _profile_without_api_access
|
||||||
|
|
||||||
|
# Use cached profile if available
|
||||||
|
if have_api_access and _profile_with_api_access is not None:
|
||||||
|
return _profile_with_api_access
|
||||||
|
elif not have_api_access and _profile_without_api_access is not None:
|
||||||
|
return _profile_without_api_access
|
||||||
|
|
||||||
|
# Create a new profile if not cached
|
||||||
|
profile = QWebEngineProfile()
|
||||||
|
|
||||||
|
interceptor = AuthInterceptor(profile, api_enabled=have_api_access)
|
||||||
|
profile.setUrlRequestInterceptor(interceptor)
|
||||||
|
if have_api_access:
|
||||||
|
_profile_with_api_access = profile
|
||||||
|
else:
|
||||||
|
_profile_without_api_access = profile
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
def _setupBridge(self) -> None:
|
def _setupBridge(self) -> None:
|
||||||
|
# Add-on compatibility: For existing add-on callers that override the init
|
||||||
|
# and invoke _setupBridge directly (e.g. in order to use a custom web profile),
|
||||||
|
# we need to ensure that the bridge script is injected into the profile scripts,
|
||||||
|
# if it has yet to be injected.
|
||||||
|
profile = self.profile()
|
||||||
|
assert profile is not None
|
||||||
|
scripts = profile.scripts()
|
||||||
|
assert scripts is not None
|
||||||
|
|
||||||
|
if not scripts.contains(_bridge_script):
|
||||||
|
print("add-on callers should not call _setupBridge directly")
|
||||||
|
self._inject_user_script(profile, _bridge_script)
|
||||||
|
|
||||||
class Bridge(QObject):
|
class Bridge(QObject):
|
||||||
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
|
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -58,40 +193,9 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
self._channel.registerObject("py", self._bridge)
|
self._channel.registerObject("py", self._bridge)
|
||||||
self.setWebChannel(self._channel)
|
self.setWebChannel(self._channel)
|
||||||
|
|
||||||
qwebchannel = ":/qtwebchannel/qwebchannel.js"
|
def _inject_user_script(
|
||||||
jsfile = QFile(qwebchannel)
|
self, profile: QWebEngineProfile, script: QWebEngineScript
|
||||||
if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
|
) -> None:
|
||||||
print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
|
|
||||||
jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
|
|
||||||
jsfile.close()
|
|
||||||
|
|
||||||
script = QWebEngineScript()
|
|
||||||
script.setSourceCode(
|
|
||||||
jstext
|
|
||||||
+ """
|
|
||||||
var pycmd, bridgeCommand;
|
|
||||||
new QWebChannel(qt.webChannelTransport, function(channel) {
|
|
||||||
bridgeCommand = pycmd = function (arg, cb) {
|
|
||||||
var resultCB = function (res) {
|
|
||||||
// pass result back to user-provided callback
|
|
||||||
if (cb) {
|
|
||||||
cb(JSON.parse(res));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.objects.py.cmd(arg, resultCB);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pycmd("domDone");
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
|
|
||||||
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
|
||||||
script.setRunsOnSubFrames(False)
|
|
||||||
|
|
||||||
profile = self.profile()
|
|
||||||
assert profile is not None
|
|
||||||
scripts = profile.scripts()
|
scripts = profile.scripts()
|
||||||
assert scripts is not None
|
assert scripts is not None
|
||||||
scripts.insert(script)
|
scripts.insert(script)
|
||||||
|
@ -247,34 +351,6 @@ class WebContent:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
class AnkiWebViewKind(Enum):
|
|
||||||
"""Enum registry of all web views managed by Anki
|
|
||||||
|
|
||||||
The value of each entry corresponds to the web view's title.
|
|
||||||
|
|
||||||
When introducing a new web view, please add it to the registry below.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT = "default"
|
|
||||||
MAIN = "main webview"
|
|
||||||
TOP_TOOLBAR = "top toolbar"
|
|
||||||
BOTTOM_TOOLBAR = "bottom toolbar"
|
|
||||||
DECK_OPTIONS = "deck options"
|
|
||||||
EDITOR = "editor"
|
|
||||||
LEGACY_DECK_STATS = "legacy deck stats"
|
|
||||||
DECK_STATS = "deck stats"
|
|
||||||
PREVIEWER = "previewer"
|
|
||||||
CHANGE_NOTETYPE = "change notetype"
|
|
||||||
CARD_LAYOUT = "card layout"
|
|
||||||
BROWSER_CARD_INFO = "browser card info"
|
|
||||||
IMPORT_CSV = "csv import"
|
|
||||||
EMPTY_CARDS = "empty cards"
|
|
||||||
FIND_DUPLICATES = "find duplicates"
|
|
||||||
FIELDS = "fields"
|
|
||||||
IMPORT_LOG = "import log"
|
|
||||||
IMPORT_ANKI_PACKAGE = "anki package import"
|
|
||||||
|
|
||||||
|
|
||||||
class AnkiWebView(QWebEngineView):
|
class AnkiWebView(QWebEngineView):
|
||||||
allow_drops = False
|
allow_drops = False
|
||||||
_kind: AnkiWebViewKind
|
_kind: AnkiWebViewKind
|
||||||
|
@ -286,12 +362,11 @@ class AnkiWebView(QWebEngineView):
|
||||||
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
|
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
|
||||||
) -> None:
|
) -> None:
|
||||||
QWebEngineView.__init__(self, parent=parent)
|
QWebEngineView.__init__(self, parent=parent)
|
||||||
self.set_kind(kind)
|
self._kind = kind
|
||||||
if title:
|
self.set_title(kind.value)
|
||||||
self.set_title(title)
|
self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self))
|
||||||
self._page = AnkiWebPage(self._onBridgeCmd)
|
|
||||||
# reduce flicker
|
# reduce flicker
|
||||||
self._page.setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
||||||
|
|
||||||
# in new code, use .set_bridge_command() instead of setting this directly
|
# in new code, use .set_bridge_command() instead of setting this directly
|
||||||
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
||||||
|
@ -299,7 +374,6 @@ class AnkiWebView(QWebEngineView):
|
||||||
self._domDone = True
|
self._domDone = True
|
||||||
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
||||||
self.requiresCol = True
|
self.requiresCol = True
|
||||||
self.setPage(self._page)
|
|
||||||
self._disable_zoom = False
|
self._disable_zoom = False
|
||||||
|
|
||||||
self.resetHandlers()
|
self.resetHandlers()
|
||||||
|
@ -320,9 +394,8 @@ class AnkiWebView(QWebEngineView):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_kind(self, kind: AnkiWebViewKind) -> None:
|
def page(self) -> AnkiWebPage:
|
||||||
self._kind = kind
|
return cast(AnkiWebPage, super().page())
|
||||||
self.set_title(kind.value)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self) -> AnkiWebViewKind:
|
def kind(self) -> AnkiWebViewKind:
|
||||||
|
@ -357,7 +430,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_open_links_externally(self, enable: bool) -> None:
|
def set_open_links_externally(self, enable: bool) -> None:
|
||||||
self._page.open_links_externally = enable
|
self.page().open_links_externally = enable
|
||||||
|
|
||||||
def onEsc(self) -> None:
|
def onEsc(self) -> None:
|
||||||
w = self.parent()
|
w = self.parent()
|
||||||
|
@ -822,7 +895,7 @@ html {{ {font} }}
|
||||||
Must be done on Windows prior to changing current working directory."""
|
Must be done on Windows prior to changing current working directory."""
|
||||||
self.requiresCol = False
|
self.requiresCol = False
|
||||||
self._domReady = False
|
self._domReady = False
|
||||||
self._page.setContent(cast(QByteArray, bytes("", "ascii")))
|
self.page().setContent(cast(QByteArray, bytes("", "ascii")))
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
@ -836,14 +909,14 @@ html {{ {font} }}
|
||||||
# defer page cleanup so that in-flight requests have a chance to complete first
|
# defer page cleanup so that in-flight requests have a chance to complete first
|
||||||
# https://forums.ankiweb.net/t/error-when-exiting-browsing-when-the-software-is-installed-in-the-path-c-program-files-anki/38363
|
# https://forums.ankiweb.net/t/error-when-exiting-browsing-when-the-software-is-installed-in-the-path-c-program-files-anki/38363
|
||||||
mw.progress.single_shot(5000, lambda: mw.mediaServer.clear_page_html(id(self)))
|
mw.progress.single_shot(5000, lambda: mw.mediaServer.clear_page_html(id(self)))
|
||||||
self._page.deleteLater()
|
self.page().deleteLater()
|
||||||
|
|
||||||
def on_theme_did_change(self) -> None:
|
def on_theme_did_change(self) -> None:
|
||||||
# avoid flashes if page reloaded
|
# avoid flashes if page reloaded
|
||||||
self._page.setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS))
|
||||||
if hasattr(QWebEngineSettings.WebAttribute, "ForceDarkMode"):
|
if hasattr(QWebEngineSettings.WebAttribute, "ForceDarkMode"):
|
||||||
force_dark_mode = getattr(QWebEngineSettings.WebAttribute, "ForceDarkMode")
|
force_dark_mode = getattr(QWebEngineSettings.WebAttribute, "ForceDarkMode")
|
||||||
page_settings = self._page.settings()
|
page_settings = self.page().settings()
|
||||||
if page_settings is not None:
|
if page_settings is not None:
|
||||||
page_settings.setAttribute(
|
page_settings.setAttribute(
|
||||||
force_dark_mode,
|
force_dark_mode,
|
||||||
|
@ -885,3 +958,53 @@ html {{ {font} }}
|
||||||
@deprecated(info="use theme_manager.qcolor() instead")
|
@deprecated(info="use theme_manager.qcolor() instead")
|
||||||
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
|
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
|
||||||
return theme_manager.qcolor(colors.CANVAS)
|
return theme_manager.qcolor(colors.CANVAS)
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-configured classes for use in Qt Designer
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class _AnkiWebViewKwargs(TypedDict, total=False):
|
||||||
|
parent: QWidget | None
|
||||||
|
title: str
|
||||||
|
kind: AnkiWebViewKind
|
||||||
|
|
||||||
|
|
||||||
|
def _create_ankiwebview_subclass(
|
||||||
|
name: str,
|
||||||
|
/,
|
||||||
|
**fixed_kwargs: Unpack[_AnkiWebViewKwargs],
|
||||||
|
) -> Type[AnkiWebView]:
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: _AnkiWebViewKwargs) -> None:
|
||||||
|
# user‑supplied kwargs override fixed kwargs
|
||||||
|
merged = cast(_AnkiWebViewKwargs, {**fixed_kwargs, **kwargs})
|
||||||
|
AnkiWebView.__init__(self, *args, **merged)
|
||||||
|
|
||||||
|
__init__.__qualname__ = f"{name}.__init__"
|
||||||
|
if fixed_kwargs:
|
||||||
|
__init__.__doc__ = (
|
||||||
|
f"Auto‑generated wrapper that pre‑sets "
|
||||||
|
f"{', '.join(f'{k}={v!r}' for k, v in fixed_kwargs.items())}."
|
||||||
|
)
|
||||||
|
|
||||||
|
cls: Type[AnkiWebView] = type(name, (AnkiWebView,), {"__init__": __init__})
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
# These subclasses are used in Qt Designer UI files to allow for configuring
|
||||||
|
# web views at initialization time (custom widgets can otherwise only be
|
||||||
|
# initialized with the default constructor)
|
||||||
|
StatsWebView = _create_ankiwebview_subclass(
|
||||||
|
"StatsWebView", kind=AnkiWebViewKind.DECK_STATS
|
||||||
|
)
|
||||||
|
LegacyStatsWebView = _create_ankiwebview_subclass(
|
||||||
|
"LegacyStatsWebView", kind=AnkiWebViewKind.LEGACY_DECK_STATS
|
||||||
|
)
|
||||||
|
EmptyCardsWebView = _create_ankiwebview_subclass(
|
||||||
|
"EmptyCardsWebView", kind=AnkiWebViewKind.EMPTY_CARDS
|
||||||
|
)
|
||||||
|
FindDupesWebView = _create_ankiwebview_subclass(
|
||||||
|
"FindDupesWebView", kind=AnkiWebViewKind.FIND_DUPLICATES
|
||||||
|
)
|
||||||
|
|
|
@ -34,6 +34,5 @@ impl BackendCardRenderingService for Backend {
|
||||||
request.speed,
|
request.speed,
|
||||||
&request.text,
|
&request.text,
|
||||||
)
|
)
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,6 @@ impl crate::services::ConfigService for Collection {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let val: Value = serde_json::from_slice(&input.value_json)?;
|
let val: Value = serde_json::from_slice(&input.value_json)?;
|
||||||
self.transact_no_undo(|col| col.set_config(input.key.as_str(), &val).map(|_| ()))
|
self.transact_no_undo(|col| col.set_config(input.key.as_str(), &val).map(|_| ()))
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_config(
|
fn remove_config(
|
||||||
|
|
|
@ -18,9 +18,7 @@ impl BackendImportExportService for Backend {
|
||||||
let mut guard = self.lock_open_collection()?;
|
let mut guard = self.lock_open_collection()?;
|
||||||
|
|
||||||
let col_inner = guard.take().unwrap();
|
let col_inner = guard.take().unwrap();
|
||||||
col_inner
|
col_inner.export_colpkg(input.out_path, input.include_media, input.legacy)
|
||||||
.export_colpkg(input.out_path, input.include_media, input.legacy)
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_collection_package(
|
fn import_collection_package(
|
||||||
|
@ -36,6 +34,5 @@ impl BackendImportExportService for Backend {
|
||||||
Path::new(&input.media_db),
|
Path::new(&input.media_db),
|
||||||
self.new_progress_handler(),
|
self.new_progress_handler(),
|
||||||
)
|
)
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ impl TryFrom<anki_proto::sync::SyncAuth> for SyncAuth {
|
||||||
impl crate::services::BackendSyncService for Backend {
|
impl crate::services::BackendSyncService for Backend {
|
||||||
fn sync_media(&self, input: anki_proto::sync::SyncAuth) -> Result<()> {
|
fn sync_media(&self, input: anki_proto::sync::SyncAuth) -> Result<()> {
|
||||||
let auth = input.try_into()?;
|
let auth = input.try_into()?;
|
||||||
self.sync_media_in_background(auth, None).map(Into::into)
|
self.sync_media_in_background(auth, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn media_sync_status(&self) -> Result<MediaSyncStatusResponse> {
|
fn media_sync_status(&self) -> Result<MediaSyncStatusResponse> {
|
||||||
|
|
|
@ -131,7 +131,7 @@ impl From<Card> for anki_proto::cards::Card {
|
||||||
original_due: c.original_due,
|
original_due: c.original_due,
|
||||||
original_deck_id: c.original_deck_id.0,
|
original_deck_id: c.original_deck_id.0,
|
||||||
flags: c.flags as u32,
|
flags: c.flags as u32,
|
||||||
original_position: c.original_position.map(Into::into),
|
original_position: c.original_position,
|
||||||
memory_state: c.memory_state.map(Into::into),
|
memory_state: c.memory_state.map(Into::into),
|
||||||
desired_retention: c.desired_retention,
|
desired_retention: c.desired_retention,
|
||||||
custom_data: c.custom_data,
|
custom_data: c.custom_data,
|
||||||
|
|
|
@ -21,7 +21,6 @@ impl crate::services::DeckConfigService for Collection {
|
||||||
col.add_or_update_deck_config_legacy(&mut conf)?;
|
col.add_or_update_deck_config_legacy(&mut conf)?;
|
||||||
Ok(anki_proto::deck_config::DeckConfigId { dcid: conf.id.0 })
|
Ok(anki_proto::deck_config::DeckConfigId { dcid: conf.id.0 })
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_deck_config_legacy(&mut self) -> Result<generic::Json> {
|
fn all_deck_config_legacy(&mut self) -> Result<generic::Json> {
|
||||||
|
@ -62,7 +61,6 @@ impl crate::services::DeckConfigService for Collection {
|
||||||
|
|
||||||
fn remove_deck_config(&mut self, input: anki_proto::deck_config::DeckConfigId) -> Result<()> {
|
fn remove_deck_config(&mut self, input: anki_proto::deck_config::DeckConfigId) -> Result<()> {
|
||||||
self.transact_no_undo(|col| col.remove_deck_config_inner(input.into()))
|
self.transact_no_undo(|col| col.remove_deck_config_inner(input.into()))
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_deck_configs_for_update(
|
fn get_deck_configs_for_update(
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl crate::services::SchedulerService for Collection {
|
||||||
self.transact_no_undo(|col| {
|
self.transact_no_undo(|col| {
|
||||||
let today = col.current_due_day(0)?;
|
let today = col.current_due_day(0)?;
|
||||||
let usn = col.usn()?;
|
let usn = col.usn()?;
|
||||||
col.update_deck_stats(today, usn, input).map(Into::into)
|
col.update_deck_stats(today, usn, input)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,6 @@ impl crate::services::SchedulerService for Collection {
|
||||||
input.new_delta,
|
input.new_delta,
|
||||||
input.review_delta,
|
input.review_delta,
|
||||||
)
|
)
|
||||||
.map(Into::into)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +228,6 @@ impl crate::services::SchedulerService for Collection {
|
||||||
|
|
||||||
fn upgrade_scheduler(&mut self) -> Result<()> {
|
fn upgrade_scheduler(&mut self) -> Result<()> {
|
||||||
self.transact_no_undo(|col| col.upgrade_to_v2_scheduler())
|
self.transact_no_undo(|col| col.upgrade_to_v2_scheduler())
|
||||||
.map(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_queued_cards(
|
fn get_queued_cards(
|
||||||
|
|
|
@ -129,7 +129,7 @@ impl crate::services::SearchService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: generic::Int64,
|
input: generic::Int64,
|
||||||
) -> Result<anki_proto::search::BrowserRow> {
|
) -> Result<anki_proto::search::BrowserRow> {
|
||||||
self.browser_row_for_id(input.val).map(Into::into)
|
self.browser_row_for_id(input.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl crate::services::StatsService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: anki_proto::stats::GraphPreferences,
|
input: anki_proto::stats::GraphPreferences,
|
||||||
) -> error::Result<()> {
|
) -> error::Result<()> {
|
||||||
self.set_graph_preferences(input).map(Into::into)
|
self.set_graph_preferences(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,6 @@ impl SqliteStorage {
|
||||||
.query_and_then([machine_name], row_to_deck)?
|
.query_and_then([machine_name], row_to_deck)?
|
||||||
.next()
|
.next()
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_all_decks(&self) -> Result<Vec<Deck>> {
|
pub(crate) fn get_all_decks(&self) -> Result<Vec<Deck>> {
|
||||||
|
|
|
@ -68,7 +68,6 @@ impl SqliteStorage {
|
||||||
.query_and_then([name], |row| Ok::<_, AnkiError>(DeckConfigId(row.get(0)?)))?
|
.query_and_then([name], |row| Ok::<_, AnkiError>(DeckConfigId(row.get(0)?)))?
|
||||||
.next()
|
.next()
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_deck_conf(&self, conf: &mut DeckConfig) -> Result<()> {
|
pub(crate) fn add_deck_conf(&self, conf: &mut DeckConfig) -> Result<()> {
|
||||||
|
|
|
@ -280,7 +280,7 @@ impl super::SqliteStorage {
|
||||||
include_str!("get.sql"),
|
include_str!("get.sql"),
|
||||||
" WHERE id IN (SELECT nid FROM search_nids)"
|
" WHERE id IN (SELECT nid FROM search_nids)"
|
||||||
))?
|
))?
|
||||||
.query_and_then([], |r| row_to_note(r).map_err(Into::into))?
|
.query_and_then([], row_to_note)?
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ impl SqliteStorage {
|
||||||
pub(crate) fn get_all_revlog_entries(&self, after: TimestampSecs) -> Result<Vec<RevlogEntry>> {
|
pub(crate) fn get_all_revlog_entries(&self, after: TimestampSecs) -> Result<Vec<RevlogEntry>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached(concat!(include_str!("get.sql"), " where id >= ?"))?
|
.prepare_cached(concat!(include_str!("get.sql"), " where id >= ?"))?
|
||||||
.query_and_then([after.0 * 1000], |r| row_to_revlog_entry(r).map(Into::into))?
|
.query_and_then([after.0 * 1000], row_to_revlog_entry)?
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,7 +410,7 @@ fn schema_version(db: &Connection) -> Result<(bool, u8)> {
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
false,
|
false,
|
||||||
db.query_row("select ver from col", [], |r| r.get(0).map_err(Into::into))?,
|
db.query_row("select ver from col", [], |r| r.get(0))?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
// 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
|
||||||
|
|
||||||
use ammonia::Url;
|
use ammonia::Url;
|
||||||
|
use anki_io::metadata;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use crate::config::SchedulerVersion;
|
use crate::config::SchedulerVersion;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -18,6 +20,7 @@ use crate::sync::error::OrHttpErr;
|
||||||
use crate::sync::http_client::HttpSyncClient;
|
use crate::sync::http_client::HttpSyncClient;
|
||||||
use crate::sync::request::IntoSyncRequest;
|
use crate::sync::request::IntoSyncRequest;
|
||||||
use crate::sync::request::SyncRequest;
|
use crate::sync::request::SyncRequest;
|
||||||
|
use crate::sync::request::MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
|
||||||
use crate::sync::version::SYNC_VERSION_09_V2_SCHEDULER;
|
use crate::sync::version::SYNC_VERSION_09_V2_SCHEDULER;
|
||||||
use crate::sync::version::SYNC_VERSION_10_V2_TIMEZONE;
|
use crate::sync::version::SYNC_VERSION_10_V2_TIMEZONE;
|
||||||
use crate::sync::version::SYNC_VERSION_MAX;
|
use crate::sync::version::SYNC_VERSION_MAX;
|
||||||
|
@ -49,6 +52,8 @@ pub struct SyncMeta {
|
||||||
pub v2_scheduler_or_later: bool,
|
pub v2_scheduler_or_later: bool,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub v2_timezone: bool,
|
pub v2_timezone: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub collection_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncMeta {
|
impl SyncMeta {
|
||||||
|
@ -123,6 +128,7 @@ pub struct MetaRequest {
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn sync_meta(&self) -> Result<SyncMeta> {
|
pub fn sync_meta(&self) -> Result<SyncMeta> {
|
||||||
let stamps = self.storage.get_collection_timestamps()?;
|
let stamps = self.storage.get_collection_timestamps()?;
|
||||||
|
let collection_bytes = metadata(&self.col_path)?.len();
|
||||||
Ok(SyncMeta {
|
Ok(SyncMeta {
|
||||||
modified: stamps.collection_change,
|
modified: stamps.collection_change,
|
||||||
schema: stamps.schema_change,
|
schema: stamps.schema_change,
|
||||||
|
@ -136,6 +142,7 @@ impl Collection {
|
||||||
empty: !self.storage.have_at_least_one_card()?,
|
empty: !self.storage.have_at_least_one_card()?,
|
||||||
v2_scheduler_or_later: self.scheduler_version() == SchedulerVersion::V2,
|
v2_scheduler_or_later: self.scheduler_version() == SchedulerVersion::V2,
|
||||||
v2_timezone: self.get_creation_utc_offset().is_some(),
|
v2_timezone: self.get_creation_utc_offset().is_some(),
|
||||||
|
collection_bytes,
|
||||||
// must be filled in by calling code
|
// must be filled in by calling code
|
||||||
media_usn: Usn(0),
|
media_usn: Usn(0),
|
||||||
})
|
})
|
||||||
|
@ -152,6 +159,10 @@ pub fn server_meta(req: MetaRequest, col: &mut Collection) -> HttpResult<SyncMet
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut meta = col.sync_meta().or_internal_err("sync meta")?;
|
let mut meta = col.sync_meta().or_internal_err("sync meta")?;
|
||||||
|
if meta.collection_bytes > *MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED {
|
||||||
|
info!("collection is too large, forcing one-way sync");
|
||||||
|
meta.schema = TimestampMillis::now();
|
||||||
|
}
|
||||||
if meta.v2_scheduler_or_later && req.sync_version < SYNC_VERSION_09_V2_SCHEDULER {
|
if meta.v2_scheduler_or_later && req.sync_version < SYNC_VERSION_09_V2_SCHEDULER {
|
||||||
meta.server_message = "Your client does not support the v2 scheduler".into();
|
meta.server_message = "Your client does not support the v2 scheduler".into();
|
||||||
meta.should_continue = false;
|
meta.should_continue = false;
|
||||||
|
|
|
@ -17,6 +17,7 @@ use crate::sync::collection::protocol::SyncProtocol;
|
||||||
use crate::sync::collection::status::online_sync_status_check;
|
use crate::sync::collection::status::online_sync_status_check;
|
||||||
use crate::sync::http_client::HttpSyncClient;
|
use crate::sync::http_client::HttpSyncClient;
|
||||||
use crate::sync::login::SyncAuth;
|
use crate::sync::login::SyncAuth;
|
||||||
|
use crate::sync::request::MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
|
||||||
|
|
||||||
pub struct NormalSyncer<'a> {
|
pub struct NormalSyncer<'a> {
|
||||||
pub(in crate::sync) col: &'a mut Collection,
|
pub(in crate::sync) col: &'a mut Collection,
|
||||||
|
@ -68,6 +69,14 @@ impl NormalSyncer<'_> {
|
||||||
pub async fn sync(&mut self) -> error::Result<SyncOutput> {
|
pub async fn sync(&mut self) -> error::Result<SyncOutput> {
|
||||||
debug!("fetching meta...");
|
debug!("fetching meta...");
|
||||||
let local = self.col.sync_meta()?;
|
let local = self.col.sync_meta()?;
|
||||||
|
let local_bytes = local.collection_bytes;
|
||||||
|
let limit = *MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
|
||||||
|
if self.server.endpoint.as_str().contains("ankiweb") && local.collection_bytes > limit {
|
||||||
|
return Err(AnkiError::sync_error(
|
||||||
|
format!("{local_bytes} > {limit}"),
|
||||||
|
SyncErrorKind::UploadTooLarge,
|
||||||
|
));
|
||||||
|
}
|
||||||
let state = online_sync_status_check(local, &mut self.server).await?;
|
let state = online_sync_status_check(local, &mut self.server).await?;
|
||||||
debug!(?state, "fetched");
|
debug!(?state, "fetched");
|
||||||
match state.required {
|
match state.required {
|
||||||
|
|
|
@ -79,7 +79,7 @@ impl ServerMediaManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_or_replace_file(path: &Path, data: Vec<u8>) -> error::Result<(), FileIoError> {
|
fn add_or_replace_file(path: &Path, data: Vec<u8>) -> error::Result<(), FileIoError> {
|
||||||
write_file(path, data).map_err(Into::into)
|
write_file(path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(path: &Path) -> error::Result<(), FileIoError> {
|
fn remove_file(path: &Path) -> error::Result<(), FileIoError> {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
# older versions may fail to compile; newer versions may fail the clippy tests
|
# older versions may fail to compile; newer versions may fail the clippy tests
|
||||||
channel = "1.84.0"
|
channel = "1.85.0"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cargo install --git https://github.com/ankitects/n2.git --rev 3b725cf9c321efb90496dd2458cf5c1abbef4dba
|
cargo install --git https://github.com/evmar/n2.git --rev 53ec691df749277104d1d4201a344fe4243d6d0a
|
||||||
|
|
|
@ -209,7 +209,7 @@ fn sveltekit_temp_file(path: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_cargo_deny() -> Result<()> {
|
fn check_cargo_deny() -> Result<()> {
|
||||||
Command::run("cargo install cargo-deny@0.14.24")?;
|
Command::run("cargo install cargo-deny@0.18.2")?;
|
||||||
Command::run("cargo deny check")?;
|
Command::run("cargo deny check")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue