mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
ignore = [
|
||||
# gix only used by burn-train
|
||||
"RUSTSEC-2024-0350",
|
||||
# pyoxidizer is stuck on an old ring version
|
||||
"RUSTSEC-2025-0009",
|
||||
"RUSTSEC-2025-0010",
|
||||
# burn depends on an unmaintained package 'paste'
|
||||
"RUSTSEC-2024-0436",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
|
2
.version
2
.version
|
@ -1 +1 @@
|
|||
25.02
|
||||
25.02.2
|
||||
|
|
46
Cargo.lock
generated
46
Cargo.lock
generated
|
@ -1288,9 +1288,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
@ -3377,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4166,9 +4166,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.70"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
|
@ -4198,9 +4198,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.105"
|
||||
version = "0.9.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -4768,9 +4768,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.23.4"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
|
||||
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
|
@ -4786,9 +4786,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.23.4"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
|
||||
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
|
@ -4796,9 +4796,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.23.4"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
|
||||
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
|
@ -4806,9 +4806,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.23.4"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
|
||||
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
|
@ -4818,9 +4818,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.23.4"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
|
||||
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
@ -6016,9 +6016,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
|
@ -6202,9 +6202,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.40.0"
|
||||
version = "1.44.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
|
||||
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
@ -6219,9 +6219,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -110,7 +110,7 @@ prost-build = "0.13"
|
|||
prost-reflect = "0.14"
|
||||
prost-types = "0.13"
|
||||
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"
|
||||
regex = "1.11.0"
|
||||
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);
|
||||
}
|
||||
|
||||
fn hide_last_line(&self) -> bool {
|
||||
fn hide_progress(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ pub trait BuildAction {
|
|||
true
|
||||
}
|
||||
|
||||
fn hide_last_line(&self) -> bool {
|
||||
fn hide_progress(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
|
|
@ -271,14 +271,14 @@ impl BuildStatement<'_> {
|
|||
stmt.rule_variables.push(("pool".into(), pool.into()));
|
||||
}
|
||||
if have_n2 {
|
||||
stmt.rule_variables.push((
|
||||
"hide_success".into(),
|
||||
(action.hide_success() as u8).to_string(),
|
||||
));
|
||||
stmt.rule_variables.push((
|
||||
"hide_last_line".into(),
|
||||
(action.hide_last_line() as u8).to_string(),
|
||||
));
|
||||
if action.hide_success() {
|
||||
stmt.rule_variables
|
||||
.push(("hide_success".into(), "1".into()));
|
||||
}
|
||||
if action.hide_progress() {
|
||||
stmt.rule_variables
|
||||
.push(("hide_progress".into(), "1".into()));
|
||||
}
|
||||
}
|
||||
|
||||
stmt
|
||||
|
|
|
@ -252,7 +252,7 @@ impl BuildAction for SvelteCheck {
|
|||
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
|
||||
}
|
||||
|
||||
fn hide_last_line(&self) -> bool {
|
||||
fn hide_progress(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ impl BuildAction for PythonTypecheck {
|
|||
build.add_output_stamp(format!("tests/python_typecheck.{hash}"));
|
||||
}
|
||||
|
||||
fn hide_last_line(&self) -> bool {
|
||||
fn hide_progress(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ impl BuildAction for PythonTest {
|
|||
build.add_output_stamp(format!("tests/python_pytest.{hash}"));
|
||||
}
|
||||
|
||||
fn hide_last_line(&self) -> bool {
|
||||
fn hide_progress(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -757,7 +757,7 @@
|
|||
},
|
||||
{
|
||||
"name": "crossbeam-channel",
|
||||
"version": "0.5.13",
|
||||
"version": "0.5.15",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/crossbeam-rs/crossbeam",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
|
@ -2665,7 +2665,7 @@
|
|||
},
|
||||
{
|
||||
"name": "openssl",
|
||||
"version": "0.10.70",
|
||||
"version": "0.10.72",
|
||||
"authors": "Steven Fackler <sfackler@gmail.com>",
|
||||
"repository": "https://github.com/sfackler/rust-openssl",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -2692,7 +2692,7 @@
|
|||
},
|
||||
{
|
||||
"name": "openssl-sys",
|
||||
"version": "0.9.105",
|
||||
"version": "0.9.107",
|
||||
"authors": "Alex Crichton <alex@alexcrichton.com>|Steven Fackler <sfackler@gmail.com>",
|
||||
"repository": "https://github.com/sfackler/rust-openssl",
|
||||
"license": "MIT",
|
||||
|
@ -4087,7 +4087,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tokio",
|
||||
"version": "1.40.0",
|
||||
"version": "1.44.2",
|
||||
"authors": "Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/tokio",
|
||||
"license": "MIT",
|
||||
|
@ -4096,7 +4096,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tokio-macros",
|
||||
"version": "2.4.0",
|
||||
"version": "2.5.0",
|
||||
"authors": "Tokio Contributors <team@tokio.rs>",
|
||||
"repository": "https://github.com/tokio-rs/tokio",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -14,7 +14,6 @@ from anki.collection import SearchNode
|
|||
from anki.notes import NoteId
|
||||
from aqt.qt import *
|
||||
from aqt.qt import sip
|
||||
from aqt.webview import AnkiWebViewKind
|
||||
|
||||
from ..operations import QueryOp
|
||||
from ..operations.tag import add_tags_to_notes
|
||||
|
@ -52,7 +51,6 @@ class FindDuplicatesDialog(QDialog):
|
|||
self._dupes: list[tuple[str, list[NoteId]]] = []
|
||||
|
||||
# links
|
||||
form.webView.set_kind(AnkiWebViewKind.FIND_DUPLICATES)
|
||||
form.webView.set_bridge_command(self._on_duplicate_clicked, context=self)
|
||||
form.webView.stdHtml("", context=self)
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ from anki.collection import EmptyCardsReport
|
|||
from aqt import gui_hooks
|
||||
from aqt.qt import QDialog, QDialogButtonBox, qconnect
|
||||
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:
|
||||
|
@ -47,7 +46,6 @@ class EmptyCardsDialog(QDialog):
|
|||
self.setWindowTitle(tr.empty_cards_window_title())
|
||||
disable_help_button(self)
|
||||
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)
|
||||
|
||||
gui_hooks.empty_cards_will_show(self)
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AnkiWebView" name="webview" native="true">
|
||||
<widget class="EmptyCardsWebView" name="webview" native="true">
|
||||
<property name="url" stdset="0">
|
||||
<url>
|
||||
<string notr="true">about:blank</string>
|
||||
|
@ -81,7 +81,7 @@
|
|||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AnkiWebView</class>
|
||||
<class>EmptyCardsWebView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">aqt/webview</header>
|
||||
<container>1</container>
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AnkiWebView" name="webView" native="true">
|
||||
<widget class="FindDupesWebView" name="webView" native="true">
|
||||
<property name="url" stdset="0">
|
||||
<url>
|
||||
<string notr="true">about:blank</string>
|
||||
|
@ -98,7 +98,7 @@
|
|||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AnkiWebView</class>
|
||||
<class>FindDupesWebView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">aqt/webview</header>
|
||||
<container>1</container>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AnkiWebView" name="web" native="true">
|
||||
<widget class="StatsWebView" name="web" native="true">
|
||||
<property name="url" stdset="0">
|
||||
<url>
|
||||
<string notr="true">about:blank</string>
|
||||
|
@ -146,7 +146,7 @@
|
|||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AnkiWebView</class>
|
||||
<class>StatsWebView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">aqt/webview</header>
|
||||
<container>1</container>
|
||||
|
|
|
@ -7,7 +7,9 @@ import enum
|
|||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
@ -139,14 +141,17 @@ class MediaServer(threading.Thread):
|
|||
) -> None:
|
||||
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:
|
||||
if page := self._legacy_pages.get(id):
|
||||
if page := self.get_page(id):
|
||||
return page.html
|
||||
else:
|
||||
return 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
|
||||
else:
|
||||
return None
|
||||
|
@ -698,7 +703,6 @@ def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound:
|
|||
def _check_dynamic_request_permissions():
|
||||
if request.method == "GET":
|
||||
return
|
||||
context = _extract_page_context()
|
||||
|
||||
def warn() -> None:
|
||||
show_warning(
|
||||
|
@ -710,24 +714,17 @@ def _check_dynamic_request_permissions():
|
|||
aqt.mw.taskman.run_on_main(warn)
|
||||
abort(403)
|
||||
|
||||
if context in [
|
||||
PageContext.NON_LEGACY_PAGE,
|
||||
PageContext.EDITOR,
|
||||
PageContext.ADDON_PAGE,
|
||||
PageContext.DECK_OPTIONS,
|
||||
]:
|
||||
pass
|
||||
elif context == PageContext.REVIEWER and request.path in (
|
||||
# does page have access to entire API?
|
||||
if _have_api_access():
|
||||
return
|
||||
|
||||
# whitelisted API endpoints for reviewer/previewer
|
||||
if request.path in (
|
||||
"/_anki/getSchedulingStatesWithContext",
|
||||
"/_anki/setSchedulingStates",
|
||||
"/_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
|
||||
else:
|
||||
# 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:
|
||||
id = int(request.args["id"])
|
||||
if html := aqt.mw.mediaServer.get_page_html(id):
|
||||
return Response(html, mimetype="text/html")
|
||||
page = aqt.mw.mediaServer.get_page(id)
|
||||
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:
|
||||
return _text_response(HTTPStatus.NOT_FOUND, "page not found")
|
||||
|
||||
|
||||
def _extract_page_context() -> PageContext:
|
||||
"Get context based on referer header."
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
_APIKEY = "".join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
referer = urlparse(request.headers.get("Referer", ""))
|
||||
if referer.path.startswith("/_anki/pages/") or is_sveltekit_page(referer.path[1:]):
|
||||
return PageContext.NON_LEGACY_PAGE
|
||||
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
|
||||
|
||||
def _have_api_access() -> bool:
|
||||
return request.headers.get("Authorization") == f"Bearer {_APIKEY}"
|
||||
|
||||
|
||||
# this currently only handles a single method; in the future, idempotent
|
||||
|
|
|
@ -141,7 +141,7 @@ class AVPlayer:
|
|||
# audio be stopped?
|
||||
interrupt_current_audio = True
|
||||
# caller key for the current playback (optional)
|
||||
current_caller = None
|
||||
current_caller: Any = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._enqueued: list[AVTag] = []
|
||||
|
@ -165,7 +165,7 @@ class AVPlayer:
|
|||
self._enqueued = []
|
||||
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:
|
||||
self.stop_and_clear_queue()
|
||||
|
||||
|
@ -177,7 +177,7 @@ class AVPlayer:
|
|||
def play_file(self, filename: str) -> None:
|
||||
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.play_file(filename)
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ from aqt.utils import (
|
|||
tooltip,
|
||||
tr,
|
||||
)
|
||||
from aqt.webview import AnkiWebViewKind
|
||||
from aqt.webview import LegacyStatsWebView
|
||||
|
||||
|
||||
class NewDeckStats(QDialog):
|
||||
|
@ -71,7 +71,6 @@ class NewDeckStats(QDialog):
|
|||
maybeHideClose(self.form.buttonBox)
|
||||
addCloseShortcut(self)
|
||||
gui_hooks.stats_dialog_will_show(self)
|
||||
self.form.web.set_kind(AnkiWebViewKind.DECK_STATS)
|
||||
self.form.web.hide_while_preserving_layout()
|
||||
self.show()
|
||||
self.refresh()
|
||||
|
@ -154,6 +153,9 @@ class DeckStats(QDialog):
|
|||
self.name = "deckStats"
|
||||
self.period = 0
|
||||
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.wholeCollection = False
|
||||
self.setMinimumWidth(700)
|
||||
|
@ -232,7 +234,6 @@ class DeckStats(QDialog):
|
|||
stats = self.mw.col.stats()
|
||||
stats.wholeCollection = self.wholeCollection
|
||||
self.report = stats.report(type=self.period)
|
||||
self.form.web.set_kind(AnkiWebViewKind.LEGACY_DECK_STATS)
|
||||
self.form.web.stdHtml(
|
||||
f"<html><body>{self.report}</body></html>",
|
||||
js=["js/vendor/jquery.min.js", "js/vendor/plot.js"],
|
||||
|
|
|
@ -10,7 +10,9 @@ import re
|
|||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
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.lang
|
||||
|
@ -28,36 +30,52 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
|
|||
if TYPE_CHECKING:
|
||||
from aqt.mediasrv import PageContext
|
||||
|
||||
|
||||
# Page for debug messages
|
||||
##########################################################################
|
||||
|
||||
BridgeCommandHandler = Callable[[str], Any]
|
||||
|
||||
|
||||
class AnkiWebPage(QWebEnginePage):
|
||||
def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None:
|
||||
QWebEnginePage.__init__(self)
|
||||
self._onBridgeCmd = onBridgeCmd
|
||||
self._setupBridge()
|
||||
self.open_links_externally = True
|
||||
class AnkiWebViewKind(Enum):
|
||||
"""Enum registry of all web views managed by Anki
|
||||
|
||||
def _setupBridge(self) -> None:
|
||||
class Bridge(QObject):
|
||||
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
|
||||
super().__init__()
|
||||
self.onCmd = bridge_handler
|
||||
The value of each entry corresponds to the web view's title.
|
||||
|
||||
@pyqtSlot(str, result=str) # type: ignore
|
||||
def cmd(self, str: str) -> Any:
|
||||
return json.dumps(self.onCmd(str))
|
||||
When introducing a new web view, please add it to the registry below.
|
||||
"""
|
||||
|
||||
self._bridge = Bridge(self._onCmd)
|
||||
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"
|
||||
|
||||
self._channel = QWebChannel(self)
|
||||
self._channel.registerObject("py", self._bridge)
|
||||
self.setWebChannel(self._channel)
|
||||
|
||||
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):
|
||||
|
@ -90,10 +108,96 @@ class AnkiWebPage(QWebEnginePage):
|
|||
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):
|
||||
def __init__(
|
||||
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._kind = kind
|
||||
self._setupBridge()
|
||||
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:
|
||||
# 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):
|
||||
def __init__(self, bridge_handler: Callable[[str], Any]) -> None:
|
||||
super().__init__()
|
||||
self.onCmd = bridge_handler
|
||||
|
||||
@pyqtSlot(str, result=str) # type: ignore
|
||||
def cmd(self, str: str) -> Any:
|
||||
return json.dumps(self.onCmd(str))
|
||||
|
||||
self._bridge = Bridge(self._onCmd)
|
||||
|
||||
self._channel = QWebChannel(self)
|
||||
self._channel.registerObject("py", self._bridge)
|
||||
self.setWebChannel(self._channel)
|
||||
|
||||
def _inject_user_script(
|
||||
self, profile: QWebEngineProfile, script: QWebEngineScript
|
||||
) -> None:
|
||||
scripts = profile.scripts()
|
||||
assert scripts is not None
|
||||
scripts.insert(script)
|
||||
|
||||
def javaScriptConsoleMessage(
|
||||
|
@ -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):
|
||||
allow_drops = False
|
||||
_kind: AnkiWebViewKind
|
||||
|
@ -286,12 +362,11 @@ class AnkiWebView(QWebEngineView):
|
|||
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
|
||||
) -> None:
|
||||
QWebEngineView.__init__(self, parent=parent)
|
||||
self.set_kind(kind)
|
||||
if title:
|
||||
self.set_title(title)
|
||||
self._page = AnkiWebPage(self._onBridgeCmd)
|
||||
self._kind = kind
|
||||
self.set_title(kind.value)
|
||||
self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self))
|
||||
# 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
|
||||
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
|
||||
|
@ -299,7 +374,6 @@ class AnkiWebView(QWebEngineView):
|
|||
self._domDone = True
|
||||
self._pendingActions: list[tuple[str, Sequence[Any]]] = []
|
||||
self.requiresCol = True
|
||||
self.setPage(self._page)
|
||||
self._disable_zoom = False
|
||||
|
||||
self.resetHandlers()
|
||||
|
@ -320,9 +394,8 @@ class AnkiWebView(QWebEngineView):
|
|||
"""
|
||||
)
|
||||
|
||||
def set_kind(self, kind: AnkiWebViewKind) -> None:
|
||||
self._kind = kind
|
||||
self.set_title(kind.value)
|
||||
def page(self) -> AnkiWebPage:
|
||||
return cast(AnkiWebPage, super().page())
|
||||
|
||||
@property
|
||||
def kind(self) -> AnkiWebViewKind:
|
||||
|
@ -357,7 +430,7 @@ class AnkiWebView(QWebEngineView):
|
|||
return False
|
||||
|
||||
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:
|
||||
w = self.parent()
|
||||
|
@ -822,7 +895,7 @@ html {{ {font} }}
|
|||
Must be done on Windows prior to changing current working directory."""
|
||||
self.requiresCol = False
|
||||
self._domReady = False
|
||||
self._page.setContent(cast(QByteArray, bytes("", "ascii")))
|
||||
self.page().setContent(cast(QByteArray, bytes("", "ascii")))
|
||||
|
||||
def cleanup(self) -> None:
|
||||
try:
|
||||
|
@ -836,14 +909,14 @@ html {{ {font} }}
|
|||
# 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
|
||||
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:
|
||||
# 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"):
|
||||
force_dark_mode = getattr(QWebEngineSettings.WebAttribute, "ForceDarkMode")
|
||||
page_settings = self._page.settings()
|
||||
page_settings = self.page().settings()
|
||||
if page_settings is not None:
|
||||
page_settings.setAttribute(
|
||||
force_dark_mode,
|
||||
|
@ -885,3 +958,53 @@ html {{ {font} }}
|
|||
@deprecated(info="use theme_manager.qcolor() instead")
|
||||
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
|
||||
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.text,
|
||||
)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,6 @@ impl crate::services::ConfigService for Collection {
|
|||
) -> Result<()> {
|
||||
let val: Value = serde_json::from_slice(&input.value_json)?;
|
||||
self.transact_no_undo(|col| col.set_config(input.key.as_str(), &val).map(|_| ()))
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn remove_config(
|
||||
|
|
|
@ -18,9 +18,7 @@ impl BackendImportExportService for Backend {
|
|||
let mut guard = self.lock_open_collection()?;
|
||||
|
||||
let col_inner = guard.take().unwrap();
|
||||
col_inner
|
||||
.export_colpkg(input.out_path, input.include_media, input.legacy)
|
||||
.map(Into::into)
|
||||
col_inner.export_colpkg(input.out_path, input.include_media, input.legacy)
|
||||
}
|
||||
|
||||
fn import_collection_package(
|
||||
|
@ -36,6 +34,5 @@ impl BackendImportExportService for Backend {
|
|||
Path::new(&input.media_db),
|
||||
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 {
|
||||
fn sync_media(&self, input: anki_proto::sync::SyncAuth) -> Result<()> {
|
||||
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> {
|
||||
|
|
|
@ -131,7 +131,7 @@ impl From<Card> for anki_proto::cards::Card {
|
|||
original_due: c.original_due,
|
||||
original_deck_id: c.original_deck_id.0,
|
||||
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),
|
||||
desired_retention: c.desired_retention,
|
||||
custom_data: c.custom_data,
|
||||
|
|
|
@ -21,7 +21,6 @@ impl crate::services::DeckConfigService for Collection {
|
|||
col.add_or_update_deck_config_legacy(&mut conf)?;
|
||||
Ok(anki_proto::deck_config::DeckConfigId { dcid: conf.id.0 })
|
||||
})
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
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<()> {
|
||||
self.transact_no_undo(|col| col.remove_deck_config_inner(input.into()))
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn get_deck_configs_for_update(
|
||||
|
|
|
@ -55,7 +55,7 @@ impl crate::services::SchedulerService for Collection {
|
|||
self.transact_no_undo(|col| {
|
||||
let today = col.current_due_day(0)?;
|
||||
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.review_delta,
|
||||
)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -229,7 +228,6 @@ impl crate::services::SchedulerService for Collection {
|
|||
|
||||
fn upgrade_scheduler(&mut self) -> Result<()> {
|
||||
self.transact_no_undo(|col| col.upgrade_to_v2_scheduler())
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn get_queued_cards(
|
||||
|
|
|
@ -129,7 +129,7 @@ impl crate::services::SearchService for Collection {
|
|||
&mut self,
|
||||
input: generic::Int64,
|
||||
) -> 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,
|
||||
input: anki_proto::stats::GraphPreferences,
|
||||
) -> 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)?
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
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)?)))?
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn add_deck_conf(&self, conf: &mut DeckConfig) -> Result<()> {
|
||||
|
|
|
@ -280,7 +280,7 @@ impl super::SqliteStorage {
|
|||
include_str!("get.sql"),
|
||||
" 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ impl SqliteStorage {
|
|||
pub(crate) fn get_all_revlog_entries(&self, after: TimestampSecs) -> Result<Vec<RevlogEntry>> {
|
||||
self.db
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
|
@ -410,7 +410,7 @@ fn schema_version(db: &Connection) -> Result<(bool, u8)> {
|
|||
|
||||
Ok((
|
||||
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
|
||||
|
||||
use ammonia::Url;
|
||||
use anki_io::metadata;
|
||||
use axum::http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tracing::debug;
|
||||
use tracing::info;
|
||||
|
||||
use crate::config::SchedulerVersion;
|
||||
use crate::prelude::*;
|
||||
|
@ -18,6 +20,7 @@ use crate::sync::error::OrHttpErr;
|
|||
use crate::sync::http_client::HttpSyncClient;
|
||||
use crate::sync::request::IntoSyncRequest;
|
||||
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_10_V2_TIMEZONE;
|
||||
use crate::sync::version::SYNC_VERSION_MAX;
|
||||
|
@ -49,6 +52,8 @@ pub struct SyncMeta {
|
|||
pub v2_scheduler_or_later: bool,
|
||||
#[serde(skip)]
|
||||
pub v2_timezone: bool,
|
||||
#[serde(skip)]
|
||||
pub collection_bytes: u64,
|
||||
}
|
||||
|
||||
impl SyncMeta {
|
||||
|
@ -123,6 +128,7 @@ pub struct MetaRequest {
|
|||
impl Collection {
|
||||
pub fn sync_meta(&self) -> Result<SyncMeta> {
|
||||
let stamps = self.storage.get_collection_timestamps()?;
|
||||
let collection_bytes = metadata(&self.col_path)?.len();
|
||||
Ok(SyncMeta {
|
||||
modified: stamps.collection_change,
|
||||
schema: stamps.schema_change,
|
||||
|
@ -136,6 +142,7 @@ impl Collection {
|
|||
empty: !self.storage.have_at_least_one_card()?,
|
||||
v2_scheduler_or_later: self.scheduler_version() == SchedulerVersion::V2,
|
||||
v2_timezone: self.get_creation_utc_offset().is_some(),
|
||||
collection_bytes,
|
||||
// must be filled in by calling code
|
||||
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")?;
|
||||
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 {
|
||||
meta.server_message = "Your client does not support the v2 scheduler".into();
|
||||
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::http_client::HttpSyncClient;
|
||||
use crate::sync::login::SyncAuth;
|
||||
use crate::sync::request::MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
|
||||
|
||||
pub struct NormalSyncer<'a> {
|
||||
pub(in crate::sync) col: &'a mut Collection,
|
||||
|
@ -68,6 +69,14 @@ impl NormalSyncer<'_> {
|
|||
pub async fn sync(&mut self) -> error::Result<SyncOutput> {
|
||||
debug!("fetching meta...");
|
||||
let local = self.col.sync_meta()?;
|
||||
let local_bytes = local.collection_bytes;
|
||||
let limit = *MAXIMUM_SYNC_PAYLOAD_BYTES_UNCOMPRESSED;
|
||||
if 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?;
|
||||
debug!(?state, "fetched");
|
||||
match state.required {
|
||||
|
|
|
@ -79,7 +79,7 @@ impl ServerMediaManager {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[toolchain]
|
||||
# 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
|
||||
|
||||
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<()> {
|
||||
Command::run("cargo install cargo-deny@0.14.24")?;
|
||||
Command::run("cargo install cargo-deny@0.18.2")?;
|
||||
Command::run("cargo deny check")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue