Compare commits
33 Commits
23.10.29.0
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
98e0bb35fb | ||
|
a382fdfe85 | ||
|
8bfb4049dc | ||
|
6eea9f9db4 | ||
|
c89dbf2062 | ||
|
ba67c52d9b | ||
|
cc027f8ab8 | ||
|
dc96cdc76c | ||
|
6a2cc2dec1 | ||
|
172d1fb20f | ||
|
0514569621 | ||
|
81c39a2e42 | ||
|
f52e0c2e24 | ||
|
aab9346deb | ||
|
7c171d6722 | ||
|
306103c618 | ||
|
1c428c8627 | ||
|
b77327fb00 | ||
4837823bce | |||
24cdeeb612 | |||
00dffe579d | |||
|
bbf271c5ac | ||
|
2f7bc2e78e | ||
|
d27f54a4fe | ||
|
2996476e03 | ||
|
510f47fd08 | ||
|
977871257a | ||
|
29260d6a00 | ||
|
a17c4e42da | ||
|
50d062e6ca | ||
2de0798fc1 | |||
2019ba75ed | |||
ab6349ec65 |
89
.github/workflows/tests.yml
vendored
89
.github/workflows/tests.yml
vendored
@ -1,89 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run-tests:
|
|
||||||
name: ${{ matrix.name }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: Anki 2.1.45
|
|
||||||
python: 3.8
|
|
||||||
environment: py38-anki2.1.45
|
|
||||||
- name: Anki 2.1.46
|
|
||||||
python: 3.8
|
|
||||||
environment: py38-anki2.1.46
|
|
||||||
- name: Anki 2.1.47
|
|
||||||
python: 3.8
|
|
||||||
environment: py38-anki2.1.47
|
|
||||||
- name: Anki 2.1.48
|
|
||||||
python: 3.8
|
|
||||||
environment: py38-anki2.1.48
|
|
||||||
- name: Anki 2.1.49
|
|
||||||
python: 3.8
|
|
||||||
environment: py38-anki2.1.49
|
|
||||||
- name: Anki 2.1.50 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.50-qt5
|
|
||||||
- name: Anki 2.1.50 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.50-qt6
|
|
||||||
- name: Anki 2.1.51 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.51-qt5
|
|
||||||
- name: Anki 2.1.51 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.51-qt6
|
|
||||||
- name: Anki 2.1.52 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.52-qt5
|
|
||||||
- name: Anki 2.1.52 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.52-qt6
|
|
||||||
- name: Anki 2.1.53 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.53-qt5
|
|
||||||
- name: Anki 2.1.53 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.53-qt6
|
|
||||||
- name: Anki 2.1.54 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.54-qt5
|
|
||||||
- name: Anki 2.1.54 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.54-qt6
|
|
||||||
- name: Anki 2.1.55 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.55-qt5
|
|
||||||
- name: Anki 2.1.55 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.55-qt6
|
|
||||||
- name: Anki 2.1.56 (Qt5)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.56-qt5
|
|
||||||
- name: Anki 2.1.56 (Qt6)
|
|
||||||
python: 3.9
|
|
||||||
environment: py39-anki2.1.56-qt6
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y pyqt5-dev-tools xvfb jq
|
|
||||||
|
|
||||||
- name: Setup Python ${{ matrix.python }}
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python }}
|
|
||||||
|
|
||||||
- name: Install tox
|
|
||||||
run: pip install 'tox==3.28.0'
|
|
||||||
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: tox -vvve ${{ matrix.environment }} -- --forked --verbose
|
|
1
CONTRIBUTING.md
Normal file
1
CONTRIBUTING.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
See https://foosoft.net/projects/contributing/ for details.
|
691
README.md
691
README.md
@ -920,7 +920,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
#### `deleteDecks`
|
#### `deleteDecks`
|
||||||
|
|
||||||
* Deletes decks with the given names.
|
* Deletes decks with the given names.
|
||||||
The argument `cardsToo` *must* be specified and set to `true`.
|
The argument `cardsToo` *must* be specified and set to `true`.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -1226,6 +1226,10 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
* Invokes the *Card Browser* dialog and searches for a given query. Returns an array of identifiers of the cards that
|
* Invokes the *Card Browser* dialog and searches for a given query. Returns an array of identifiers of the cards that
|
||||||
were found. Query syntax is [documented here](https://docs.ankiweb.net/searching.html).
|
were found. Query syntax is [documented here](https://docs.ankiweb.net/searching.html).
|
||||||
|
|
||||||
|
Optionally, the `reorderCards` property can be provided to reorder the cards shown in the *Card Browser*.
|
||||||
|
This is an array including the `order` and `columnId` objects. `order` can be either `ascending` or `descending` while `columnId` can be one of several column identifiers (as documented in the [Anki source code](https://github.com/ankitects/anki/blob/main/rslib/src/browser_table.rs)).
|
||||||
|
The specified column needs to be visible in the *Card Browser*.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample request:</i></summary>
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
@ -1234,7 +1238,11 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
"action": "guiBrowse",
|
"action": "guiBrowse",
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"query": "deck:current"
|
"query": "deck:current",
|
||||||
|
"reorderCards": {
|
||||||
|
"order": "descending",
|
||||||
|
"columnId": "noteCrt"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -1251,6 +1259,36 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
#### `guiSelectNote`
|
||||||
|
|
||||||
|
* Finds the open instance of the *Card Browser* dialog and selects a note given a note identifier.
|
||||||
|
Returns `true` if the *Card Browser* is open, `false` otherwise.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "guiSelectNote",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"note": 1494723142483
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": true,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
#### `guiSelectedNotes`
|
#### `guiSelectedNotes`
|
||||||
|
|
||||||
* Finds the open instance of the *Card Browser* dialog and returns an array of identifiers of the notes that are
|
* Finds the open instance of the *Card Browser* dialog and returns an array of identifiers of the notes that are
|
||||||
@ -1950,7 +1988,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Samples results:</i></summary>
|
<summary><i>Sample results:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -2019,11 +2057,11 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "apiReflect",
|
"action": "apiReflect",
|
||||||
|
"version": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"scopes": ["actions", "invalidType"],
|
"scopes": ["actions", "invalidType"],
|
||||||
"actions": ["apiReflect", "invalidMethod"]
|
"actions": ["apiReflect", "invalidMethod"]
|
||||||
},
|
}
|
||||||
"version": 6
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
@ -2094,6 +2132,33 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
#### `getActiveProfile`
|
||||||
|
|
||||||
|
* Retrieve the active profile.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "getActiveProfile",
|
||||||
|
"version": 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": "User 1",
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
#### `loadProfile`
|
#### `loadProfile`
|
||||||
|
|
||||||
* Selects the profile specified in request.
|
* Selects the profile specified in request.
|
||||||
@ -2104,10 +2169,10 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "loadProfile",
|
"action": "loadProfile",
|
||||||
|
"version": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"name": "user1"
|
"name": "user1"
|
||||||
},
|
}
|
||||||
"version": 6
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
@ -2323,6 +2388,381 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
#### `findModelsById`
|
||||||
|
|
||||||
|
* Gets a list of models for the provided model IDs from the current user.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "findModelsById",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"modelIds": [1704387367119, 1704387398570]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"id": 1704387367119,
|
||||||
|
"name": "Basic",
|
||||||
|
"type": 0,
|
||||||
|
"mod": 1704387367,
|
||||||
|
"usn": -1,
|
||||||
|
"sortf": 0,
|
||||||
|
"did": null,
|
||||||
|
"tmpls": [
|
||||||
|
{
|
||||||
|
"name": "Card 1",
|
||||||
|
"ord": 0,
|
||||||
|
"qfmt": "{{Front}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": 9176047152973362695
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flds": [
|
||||||
|
{
|
||||||
|
"name": "Front",
|
||||||
|
"ord": 0,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": 2453723143453745216,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Back",
|
||||||
|
"ord": 1,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": -4853200230425436781,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
|
||||||
|
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n",
|
||||||
|
"latexPost": "\\end{document}",
|
||||||
|
"latexsvg": false,
|
||||||
|
"req": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"originalStockKind": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1704387398570,
|
||||||
|
"name": "Basic (and reversed card)",
|
||||||
|
"type": 0,
|
||||||
|
"mod": 1704387398,
|
||||||
|
"usn": -1,
|
||||||
|
"sortf": 0,
|
||||||
|
"did": null,
|
||||||
|
"tmpls": [
|
||||||
|
{
|
||||||
|
"name": "Card 1",
|
||||||
|
"ord": 0,
|
||||||
|
"qfmt": "{{Front}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": 1689886528158874152
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Card 2",
|
||||||
|
"ord": 1,
|
||||||
|
"qfmt": "{{Back}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": -7839609225644824587
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flds": [
|
||||||
|
{
|
||||||
|
"name": "Front",
|
||||||
|
"ord": 0,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": -7787837672455357996,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Back",
|
||||||
|
"ord": 1,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": 6364828289839985081,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
|
||||||
|
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n",
|
||||||
|
"latexPost": "\\end{document}",
|
||||||
|
"latexsvg": false,
|
||||||
|
"req": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"originalStockKind": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
#### `findModelsByName`
|
||||||
|
|
||||||
|
* Gets a list of models for the provided model names from the current user.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "findModelsByName",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"modelNames": ["Basic", "Basic (and reversed card)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"id": 1704387367119,
|
||||||
|
"name": "Basic",
|
||||||
|
"type": 0,
|
||||||
|
"mod": 1704387367,
|
||||||
|
"usn": -1,
|
||||||
|
"sortf": 0,
|
||||||
|
"did": null,
|
||||||
|
"tmpls": [
|
||||||
|
{
|
||||||
|
"name": "Card 1",
|
||||||
|
"ord": 0,
|
||||||
|
"qfmt": "{{Front}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": 9176047152973362695
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flds": [
|
||||||
|
{
|
||||||
|
"name": "Front",
|
||||||
|
"ord": 0,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": 2453723143453745216,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Back",
|
||||||
|
"ord": 1,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": -4853200230425436781,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
|
||||||
|
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n",
|
||||||
|
"latexPost": "\\end{document}",
|
||||||
|
"latexsvg": false,
|
||||||
|
"req": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"originalStockKind": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1704387398570,
|
||||||
|
"name": "Basic (and reversed card)",
|
||||||
|
"type": 0,
|
||||||
|
"mod": 1704387398,
|
||||||
|
"usn": -1,
|
||||||
|
"sortf": 0,
|
||||||
|
"did": null,
|
||||||
|
"tmpls": [
|
||||||
|
{
|
||||||
|
"name": "Card 1",
|
||||||
|
"ord": 0,
|
||||||
|
"qfmt": "{{Front}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": 1689886528158874152
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Card 2",
|
||||||
|
"ord": 1,
|
||||||
|
"qfmt": "{{Back}}",
|
||||||
|
"afmt": "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}",
|
||||||
|
"bqfmt": "",
|
||||||
|
"bafmt": "",
|
||||||
|
"did": null,
|
||||||
|
"bfont": "",
|
||||||
|
"bsize": 0,
|
||||||
|
"id": -7839609225644824587
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flds": [
|
||||||
|
{
|
||||||
|
"name": "Front",
|
||||||
|
"ord": 0,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": -7787837672455357996,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Back",
|
||||||
|
"ord": 1,
|
||||||
|
"sticky": false,
|
||||||
|
"rtl": false,
|
||||||
|
"font": "Arial",
|
||||||
|
"size": 20,
|
||||||
|
"description": "",
|
||||||
|
"plainText": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"excludeFromSearch": false,
|
||||||
|
"id": 6364828289839985081,
|
||||||
|
"tag": null,
|
||||||
|
"preventDeletion": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
|
||||||
|
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n",
|
||||||
|
"latexPost": "\\end{document}",
|
||||||
|
"latexsvg": false,
|
||||||
|
"req": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
"any",
|
||||||
|
[
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"originalStockKind": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
#### `modelFieldNames`
|
#### `modelFieldNames`
|
||||||
|
|
||||||
* Gets the complete list of field names for the provided model name.
|
* Gets the complete list of field names for the provided model name.
|
||||||
@ -2455,13 +2895,13 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
#### `createModel`
|
#### `createModel`
|
||||||
|
|
||||||
* Creates a new model to be used in Anki. User must provide the `modelName`, `inOrderFields` and `cardTemplates` to be
|
* Creates a new model to be used in Anki. User must provide the `modelName`, `inOrderFields` and `cardTemplates` to be
|
||||||
used in the model. There are optional fields `css` and `isCloze`. If not specified, `css` will use the default Anki css and `isCloze` will be equal to `False`. If `isCloze` is `True` then model will be created as Cloze.
|
used in the model. There are optional fields `css` and `isCloze`. If not specified, `css` will use the default Anki css and `isCloze` will be equal to `false`. If `isCloze` is `true` then model will be created as Cloze.
|
||||||
|
|
||||||
Optionally the `Name` field can be provided for each entry of `cardTemplates`. By default the
|
Optionally the `Name` field can be provided for each entry of `cardTemplates`. By default the
|
||||||
card names will be `Card 1`, `Card 2`, and so on.
|
card names will be `Card 1`, `Card 2`, and so on.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample request</i></summary>
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -2485,7 +2925,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample result</i></summary>
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -2575,7 +3015,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample result</i></summary>
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -2613,7 +3053,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample result</i></summary>
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -2773,7 +3213,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "modelTemplateRemove",
|
"action": "modelTemplateReposition",
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"modelName": "Basic",
|
"modelName": "Basic",
|
||||||
@ -3026,7 +3466,7 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "modelFieldSetFont",
|
"action": "modelFieldSetFontSize",
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"params": {
|
"params": {
|
||||||
"modelName": "Basic",
|
"modelName": "Basic",
|
||||||
@ -3179,55 +3619,36 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
#### `addNotes`
|
#### `addNotes`
|
||||||
|
|
||||||
* Creates multiple notes using the given deck and model, with the provided field values and tags. Returns an array of
|
* Creates multiple notes using the given deck and model, with the provided field values and tags. Returns an array of
|
||||||
identifiers of the created notes (notes that could not be created will have a `null` identifier). Please see the
|
identifiers of the created notes. In the event of any errors, all errors are gathered and returned.
|
||||||
documentation for `addNote` for an explanation of objects in the `notes` array.
|
* Please see the documentation for `addNote` for an explanation of objects in the `notes` array.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample request:</i></summary>
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "addNotes",
|
"action":"addNotes",
|
||||||
"version": 6,
|
"version":6,
|
||||||
"params": {
|
"params":{
|
||||||
"notes": [
|
"notes":[
|
||||||
{
|
{
|
||||||
"deckName": "Default",
|
"deckName":"College::PluginDev",
|
||||||
"modelName": "Basic",
|
"modelName":"non_existent_model",
|
||||||
"fields": {
|
"fields":{
|
||||||
"Front": "front content",
|
"Front":"front",
|
||||||
"Back": "back content"
|
"Back":"bak"
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"yomichan"
|
|
||||||
],
|
|
||||||
"audio": [{
|
|
||||||
"url": "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ",
|
|
||||||
"filename": "yomichan_ねこ_猫.mp3",
|
|
||||||
"skipHash": "7e2c2f954ef6051373ba916f000168dc",
|
|
||||||
"fields": [
|
|
||||||
"Front"
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
"video": [{
|
|
||||||
"url": "https://cdn.videvo.net/videvo_files/video/free/2015-06/small_watermarked/Contador_Glam_preview.mp4",
|
|
||||||
"filename": "countdown.mp4",
|
|
||||||
"skipHash": "4117e8aab0d37534d9c8eac362388bbe",
|
|
||||||
"fields": [
|
|
||||||
"Back"
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
"picture": [{
|
|
||||||
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/A_black_cat_named_Tilly.jpg/220px-A_black_cat_named_Tilly.jpg",
|
|
||||||
"filename": "black_cat.jpg",
|
|
||||||
"skipHash": "8d6e4646dfae812bf39651b59d7429ce",
|
|
||||||
"fields": [
|
|
||||||
"Back"
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
}
|
{
|
||||||
|
"deckName":"College::PluginDev",
|
||||||
|
"modelName":"Basic",
|
||||||
|
"fields":{
|
||||||
|
"Front":"front",
|
||||||
|
"Back":"bak"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
@ -3237,8 +3658,8 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"result": [1496198395707, null],
|
"result":null,
|
||||||
"error": null
|
"error":"['model was not found: non_existent_model']"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
@ -3285,6 +3706,70 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
#### `canAddNotesWithErrorDetail`
|
||||||
|
|
||||||
|
* Accepts an array of objects which define parameters for candidate notes (see `addNote`) and returns an array of
|
||||||
|
objects with fields `canAdd` and `error`.
|
||||||
|
|
||||||
|
* `canAdd` indicates whether or not the parameters at the corresponding index could be used to create a new note.
|
||||||
|
* `error` contains an explanation of why a note cannot be added.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "canAddNotesWithErrorDetail",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"deckName": "Default",
|
||||||
|
"modelName": "Basic",
|
||||||
|
"fields": {
|
||||||
|
"Front": "front content",
|
||||||
|
"Back": "back content"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"yomichan"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deckName": "Default",
|
||||||
|
"modelName": "Basic",
|
||||||
|
"fields": {
|
||||||
|
"Front": "front content 2",
|
||||||
|
"Back": "back content 2"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"yomichan"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"canAdd": false,
|
||||||
|
"error": "cannot create note because it is a duplicate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"canAdd": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
#### `updateNoteFields`
|
#### `updateNoteFields`
|
||||||
|
|
||||||
* Modify the fields of an existing note. You can also include audio, video, or picture files which will be added to the note with an
|
* Modify the fields of an existing note. You can also include audio, video, or picture files which will be added to the note with an
|
||||||
@ -3372,6 +3857,45 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": null,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### `updateNoteModel`
|
||||||
|
|
||||||
|
* Update the model, fields, and tags of an existing note.
|
||||||
|
This allows you to change the note's model, update its fields with new content, and set new tags.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "updateNoteModel",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"note": {
|
||||||
|
"id": 1514547547030,
|
||||||
|
"modelName": "NewModel",
|
||||||
|
"fields": {
|
||||||
|
"NewField1": "new field 1",
|
||||||
|
"NewField2": "new field 2",
|
||||||
|
"NewField3": "new field 3"
|
||||||
|
},
|
||||||
|
"tags": ["new", "updated", "tags"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -3404,7 +3928,6 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -3416,7 +3939,6 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
"error": null
|
"error": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
#### `getNoteTags`
|
#### `getNoteTags`
|
||||||
@ -3435,7 +3957,6 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -3447,7 +3968,6 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
"error": null
|
"error": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
#### `addTags`
|
#### `addTags`
|
||||||
@ -3654,8 +4174,8 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
#### `notesInfo`
|
#### `notesInfo`
|
||||||
|
|
||||||
* Returns a list of objects containing for each note ID the note fields, tags, note type and the cards belonging to
|
* Returns a list of objects containing for each note ID the note fields, tags, note type, modification time,the cards belonging to
|
||||||
the note.
|
the note and the profile where the note was created.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><i>Sample request:</i></summary>
|
<summary><i>Sample request:</i></summary>
|
||||||
@ -3679,12 +4199,49 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
"result": [
|
"result": [
|
||||||
{
|
{
|
||||||
"noteId":1502298033753,
|
"noteId":1502298033753,
|
||||||
|
"profile": "User_1",
|
||||||
"modelName": "Basic",
|
"modelName": "Basic",
|
||||||
"tags":["tag","another_tag"],
|
"tags":["tag","another_tag"],
|
||||||
"fields": {
|
"fields": {
|
||||||
"Front": {"value": "front content", "order": 0},
|
"Front": {"value": "front content", "order": 0},
|
||||||
"Back": {"value": "back content", "order": 1}
|
"Back": {"value": "back content", "order": 1}
|
||||||
}
|
},
|
||||||
|
"mod": 1718377864,
|
||||||
|
"cards": [1498938915662]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
s
|
||||||
|
#### `notesModTime`
|
||||||
|
|
||||||
|
* Returns a list of objects containings for each note ID the modification time.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample request:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "notesModTime",
|
||||||
|
"version": 6,
|
||||||
|
"params": {
|
||||||
|
"notes": [1502298033753]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Sample result:</i></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"noteId": 1498938915662,
|
||||||
|
"mod": 1629454092
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"error": null
|
"error": null
|
||||||
@ -3830,8 +4387,8 @@ Search parameters are passed to Anki, check the docs for more information: https
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": null,
|
"result": "<center> lots of HTML here </center>",
|
||||||
"result": "<center> lots of HTML here </center>"
|
"error": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
@ -15,10 +15,11 @@
|
|||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
|
||||||
|
required_anki_version = (23, 10, 0)
|
||||||
anki_version = tuple(int(segment) for segment in aqt.appVersion.split("."))
|
anki_version = tuple(int(segment) for segment in aqt.appVersion.split("."))
|
||||||
|
|
||||||
if anki_version < (2, 1, 45):
|
if anki_version < required_anki_version:
|
||||||
raise Exception("Minimum Anki version supported: 2.1.45")
|
raise Exception(f"Minimum Anki version supported: {required_anki_version[0]}.{required_anki_version[1]}.{required_anki_version[2]}")
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import glob
|
import glob
|
||||||
@ -41,6 +42,7 @@ from anki.exporting import AnkiPackageExporter
|
|||||||
from anki.importing import AnkiPackageImporter
|
from anki.importing import AnkiPackageImporter
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
|
from anki.scheduler.base import ScheduleCardsAsNew
|
||||||
from aqt.qt import Qt, QTimer, QMessageBox, QCheckBox
|
from aqt.qt import Qt, QTimer, QMessageBox, QCheckBox
|
||||||
|
|
||||||
from .web import format_exception_reply, format_success_reply
|
from .web import format_exception_reply, format_success_reply
|
||||||
@ -190,14 +192,14 @@ class AnkiConnect:
|
|||||||
|
|
||||||
|
|
||||||
def getModel(self, modelName):
|
def getModel(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def getField(self, model, fieldName):
|
def getField(self, model, fieldName):
|
||||||
fieldMap = self.collection().models.fieldMap(model)
|
fieldMap = self.collection().models.field_map(model)
|
||||||
if fieldName not in fieldMap:
|
if fieldName not in fieldMap:
|
||||||
raise Exception('field was not found in {}: {}'.format(model['name'], fieldName))
|
raise Exception('field was not found in {}: {}'.format(model['name'], fieldName))
|
||||||
return fieldMap[fieldName][1]
|
return fieldMap[fieldName][1]
|
||||||
@ -214,24 +216,19 @@ class AnkiConnect:
|
|||||||
self.window().requireReset()
|
self.window().requireReset()
|
||||||
|
|
||||||
|
|
||||||
def stopEditing(self):
|
|
||||||
if self.collection() is not None:
|
|
||||||
self.window().maybeReset()
|
|
||||||
|
|
||||||
|
|
||||||
def createNote(self, note):
|
def createNote(self, note):
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
|
|
||||||
model = collection.models.byName(note['modelName'])
|
model = collection.models.by_name(note['modelName'])
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(note['modelName']))
|
raise Exception('model was not found: {}'.format(note['modelName']))
|
||||||
|
|
||||||
deck = collection.decks.byName(note['deckName'])
|
deck = collection.decks.by_name(note['deckName'])
|
||||||
if deck is None:
|
if deck is None:
|
||||||
raise Exception('deck was not found: {}'.format(note['deckName']))
|
raise Exception('deck was not found: {}'.format(note['deckName']))
|
||||||
|
|
||||||
ankiNote = anki.notes.Note(collection, model)
|
ankiNote = anki.notes.Note(collection, model)
|
||||||
ankiNote.model()['did'] = deck['id']
|
ankiNote.note_type()['did'] = deck['id']
|
||||||
if 'tags' in note:
|
if 'tags' in note:
|
||||||
ankiNote.tags = note['tags']
|
ankiNote.tags = note['tags']
|
||||||
|
|
||||||
@ -314,14 +311,14 @@ class AnkiConnect:
|
|||||||
val = note.fields[0]
|
val = note.fields[0]
|
||||||
if not val.strip():
|
if not val.strip():
|
||||||
return 1
|
return 1
|
||||||
csum = anki.utils.fieldChecksum(val)
|
csum = anki.utils.field_checksum(val)
|
||||||
|
|
||||||
# Create dictionary of deck ids
|
# Create dictionary of deck ids
|
||||||
dids = None
|
dids = None
|
||||||
if duplicateScope == 'deck':
|
if duplicateScope == 'deck':
|
||||||
did = deck['id']
|
did = deck['id']
|
||||||
if duplicateScopeDeckName is not None:
|
if duplicateScopeDeckName is not None:
|
||||||
deck2 = collection.decks.byName(duplicateScopeDeckName)
|
deck2 = collection.decks.by_name(duplicateScopeDeckName)
|
||||||
if deck2 is None:
|
if deck2 is None:
|
||||||
# Invalid deck, so cannot be duplicate
|
# Invalid deck, so cannot be duplicate
|
||||||
return 0
|
return 0
|
||||||
@ -362,13 +359,13 @@ class AnkiConnect:
|
|||||||
|
|
||||||
def getCard(self, card_id: int) -> Card:
|
def getCard(self, card_id: int) -> Card:
|
||||||
try:
|
try:
|
||||||
return self.collection().getCard(card_id)
|
return self.collection().get_card(card_id)
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
self.raiseNotFoundError('Card was not found: {}'.format(card_id))
|
self.raiseNotFoundError('Card was not found: {}'.format(card_id))
|
||||||
|
|
||||||
def getNote(self, note_id: int) -> Note:
|
def getNote(self, note_id: int) -> Note:
|
||||||
try:
|
try:
|
||||||
return self.collection().getNote(note_id)
|
return self.collection().get_note(note_id)
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
self.raiseNotFoundError('Note was not found: {}'.format(note_id))
|
self.raiseNotFoundError('Note was not found: {}'.format(note_id))
|
||||||
|
|
||||||
@ -421,14 +418,20 @@ class AnkiConnect:
|
|||||||
msg.setText('"{}" requests permission to use Anki through AnkiConnect. Do you want to give it access?'.format(origin))
|
msg.setText('"{}" requests permission to use Anki through AnkiConnect. Do you want to give it access?'.format(origin))
|
||||||
msg.setInformativeText("By granting permission, you'll allow the website to modify your collection on your behalf, including the execution of destructive actions such as deck deletion.")
|
msg.setInformativeText("By granting permission, you'll allow the website to modify your collection on your behalf, including the execution of destructive actions such as deck deletion.")
|
||||||
msg.setWindowIcon(self.window().windowIcon())
|
msg.setWindowIcon(self.window().windowIcon())
|
||||||
msg.setIcon(QMessageBox.Question)
|
msg.setIcon(QMessageBox.Icon.Question)
|
||||||
msg.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No)
|
||||||
msg.setDefaultButton(QMessageBox.No)
|
msg.setDefaultButton(QMessageBox.StandardButton.No)
|
||||||
msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg))
|
msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg))
|
||||||
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
|
if hasattr(Qt, 'WindowStaysOnTopHint'):
|
||||||
pressedButton = msg.exec_()
|
# Qt5
|
||||||
|
WindowOnTopFlag = Qt.WindowStaysOnTopHint
|
||||||
|
elif hasattr(Qt, 'WindowType') and hasattr(Qt.WindowType, 'WindowStaysOnTopHint'):
|
||||||
|
# Qt6
|
||||||
|
WindowOnTopFlag = Qt.WindowType.WindowStaysOnTopHint
|
||||||
|
msg.setWindowFlags(WindowOnTopFlag)
|
||||||
|
pressedButton = msg.exec()
|
||||||
|
|
||||||
if pressedButton == QMessageBox.Yes:
|
if pressedButton == QMessageBox.StandardButton.Yes:
|
||||||
config = aqt.mw.addonManager.getConfig(__name__)
|
config = aqt.mw.addonManager.getConfig(__name__)
|
||||||
config["webCorsOriginList"] = util.setting('webCorsOriginList')
|
config["webCorsOriginList"] = util.setting('webCorsOriginList')
|
||||||
config["webCorsOriginList"].append(origin)
|
config["webCorsOriginList"].append(origin)
|
||||||
@ -440,7 +443,7 @@ class AnkiConnect:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# if the origin isn't an empty string, the user clicks "No", and the ignore box is checked
|
# if the origin isn't an empty string, the user clicks "No", and the ignore box is checked
|
||||||
elif origin and pressedButton == QMessageBox.No and msg.checkBox().isChecked():
|
elif origin and pressedButton == QMessageBox.StandardButton.No and msg.checkBox().isChecked():
|
||||||
config = aqt.mw.addonManager.getConfig(__name__)
|
config = aqt.mw.addonManager.getConfig(__name__)
|
||||||
config["ignoreOriginList"] = util.setting('ignoreOriginList')
|
config["ignoreOriginList"] = util.setting('ignoreOriginList')
|
||||||
config["ignoreOriginList"].append(origin)
|
config["ignoreOriginList"].append(origin)
|
||||||
@ -454,7 +457,10 @@ class AnkiConnect:
|
|||||||
@util.api()
|
@util.api()
|
||||||
def getProfiles(self):
|
def getProfiles(self):
|
||||||
return self.window().pm.profiles()
|
return self.window().pm.profiles()
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def getActiveProfile(self):
|
||||||
|
return self.window().pm.name
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def loadProfile(self, name):
|
def loadProfile(self, name):
|
||||||
@ -486,7 +492,15 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def sync(self):
|
def sync(self):
|
||||||
self.window().onSync()
|
mw = self.window()
|
||||||
|
auth = mw.pm.sync_auth()
|
||||||
|
if not auth:
|
||||||
|
raise Exception("sync: auth not configured")
|
||||||
|
out = mw.col.sync_collection(auth, mw.pm.media_syncing_enabled())
|
||||||
|
accepted_sync_statuses = [out.NO_CHANGES, out.NORMAL_SYNC]
|
||||||
|
if out.required not in accepted_sync_statuses:
|
||||||
|
raise Exception(f"Sync status {out.required} not one of {accepted_sync_statuses} - see SyncCollectionResponse.ChangesRequired for list of sync statuses: https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/proto/anki/sync.proto#L57-L65")
|
||||||
|
mw.onSync()
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -517,7 +531,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def deckNames(self):
|
def deckNames(self):
|
||||||
return self.decks().allNames()
|
return [x.name for x in self.decks().all_names_and_ids()]
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -545,13 +559,8 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def createDeck(self, deck):
|
def createDeck(self, deck):
|
||||||
try:
|
self.startEditing()
|
||||||
self.startEditing()
|
return self.decks().id(deck)
|
||||||
did = self.decks().id(deck)
|
|
||||||
finally:
|
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
return did
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -559,7 +568,7 @@ class AnkiConnect:
|
|||||||
self.startEditing()
|
self.startEditing()
|
||||||
|
|
||||||
did = self.collection().decks.id(deck)
|
did = self.collection().decks.id(deck)
|
||||||
mod = anki.utils.intTime()
|
mod = anki.utils.int_time()
|
||||||
usn = self.collection().usn()
|
usn = self.collection().usn()
|
||||||
|
|
||||||
# normal cards
|
# normal cards
|
||||||
@ -569,7 +578,6 @@ class AnkiConnect:
|
|||||||
|
|
||||||
# then move into new deck
|
# then move into new deck
|
||||||
self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did)
|
self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did)
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -583,14 +591,11 @@ class AnkiConnect:
|
|||||||
# this is dangerous, so let's raise our own exception
|
# this is dangerous, so let's raise our own exception
|
||||||
raise Exception("Since Anki 2.1.28 it's not possible "
|
raise Exception("Since Anki 2.1.28 it's not possible "
|
||||||
"to delete decks without deleting cards as well")
|
"to delete decks without deleting cards as well")
|
||||||
try:
|
self.startEditing()
|
||||||
self.startEditing()
|
decks = filter(lambda d: d in self.deckNames(), decks)
|
||||||
decks = filter(lambda d: d in self.deckNames(), decks)
|
for deck in decks:
|
||||||
for deck in decks:
|
did = self.decks().id(deck)
|
||||||
did = self.decks().id(deck)
|
self.decks().remove([did])
|
||||||
self.decks().rem(did, cardsToo=cardsToo)
|
|
||||||
finally:
|
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -600,7 +605,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
did = collection.decks.id(deck)
|
did = collection.decks.id(deck)
|
||||||
return collection.decks.confForDid(did)
|
return collection.decks.config_dict_for_deck_id(did)
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -608,13 +613,13 @@ class AnkiConnect:
|
|||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
|
|
||||||
config['id'] = str(config['id'])
|
config['id'] = str(config['id'])
|
||||||
config['mod'] = anki.utils.intTime()
|
config['mod'] = anki.utils.int_time()
|
||||||
config['usn'] = collection.usn()
|
config['usn'] = collection.usn()
|
||||||
if int(config['id']) not in [c['id'] for c in collection.decks.all_config()]:
|
if int(config['id']) not in [c['id'] for c in collection.decks.all_config()]:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
collection.decks.save(config)
|
collection.decks.save(config)
|
||||||
collection.decks.updateConf(config)
|
collection.decks.update_config(config)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -648,8 +653,8 @@ class AnkiConnect:
|
|||||||
if configId not in [c['id'] for c in collection.decks.all_config()]:
|
if configId not in [c['id'] for c in collection.decks.all_config()]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
config = collection.decks.getConf(configId)
|
config = collection.decks.get_config(configId)
|
||||||
return collection.decks.confId(name, config)
|
return collection.decks.add_config_returning_id(name, config)
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -658,7 +663,7 @@ class AnkiConnect:
|
|||||||
if int(configId) not in [c['id'] for c in collection.decks.all_config()]:
|
if int(configId) not in [c['id'] for c in collection.decks.all_config()]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
collection.decks.remConf(configId)
|
collection.decks.remove_config(configId)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -722,10 +727,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def deleteMediaFile(self, filename):
|
def deleteMediaFile(self, filename):
|
||||||
try:
|
self.media().trash_files([filename])
|
||||||
self.media().syncDelete(filename)
|
|
||||||
except AttributeError:
|
|
||||||
self.media().trash_files([filename])
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def getMediaDirPath(self):
|
def getMediaDirPath(self):
|
||||||
@ -740,8 +742,6 @@ class AnkiConnect:
|
|||||||
nCardsAdded = collection.addNote(ankiNote)
|
nCardsAdded = collection.addNote(ankiNote)
|
||||||
if nCardsAdded < 1:
|
if nCardsAdded < 1:
|
||||||
raise Exception('The field values you have provided would make an empty question on all cards.')
|
raise Exception('The field values you have provided would make an empty question on all cards.')
|
||||||
collection.autosave()
|
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
return ankiNote.id
|
return ankiNote.id
|
||||||
|
|
||||||
@ -799,6 +799,17 @@ class AnkiConnect:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def canAddNoteWithErrorDetail(self, note):
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
'canAdd': bool(self.createNote(note))
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'canAdd': False,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def updateNoteFields(self, note):
|
def updateNoteFields(self, note):
|
||||||
@ -809,19 +820,9 @@ class AnkiConnect:
|
|||||||
if name in ankiNote:
|
if name in ankiNote:
|
||||||
ankiNote[name] = value
|
ankiNote[name] = value
|
||||||
|
|
||||||
audioObjectOrList = note.get('audio')
|
self.addMediaFromNote(ankiNote, note)
|
||||||
self.addMedia(ankiNote, audioObjectOrList, util.MediaType.Audio)
|
|
||||||
|
|
||||||
videoObjectOrList = note.get('video')
|
self.collection().update_note(ankiNote, skip_undo_entry=True);
|
||||||
self.addMedia(ankiNote, videoObjectOrList, util.MediaType.Video)
|
|
||||||
|
|
||||||
pictureObjectOrList = note.get('picture')
|
|
||||||
self.addMedia(ankiNote, pictureObjectOrList, util.MediaType.Picture)
|
|
||||||
|
|
||||||
ankiNote.flush()
|
|
||||||
|
|
||||||
self.collection().autosave()
|
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -836,6 +837,57 @@ class AnkiConnect:
|
|||||||
if not updated:
|
if not updated:
|
||||||
raise Exception('Must provide a "fields" or "tags" property.')
|
raise Exception('Must provide a "fields" or "tags" property.')
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def updateNoteModel(self, note):
|
||||||
|
"""
|
||||||
|
Update the model and fields of a given note.
|
||||||
|
|
||||||
|
:param note: A dictionary containing note details, including 'id', 'modelName', 'fields', and 'tags'.
|
||||||
|
"""
|
||||||
|
# Extract and validate the note ID
|
||||||
|
note_id = note.get('id')
|
||||||
|
if not note_id:
|
||||||
|
raise ValueError("Note ID is required")
|
||||||
|
|
||||||
|
# Extract and validate the new model name
|
||||||
|
new_model_name = note.get('modelName')
|
||||||
|
if not new_model_name:
|
||||||
|
raise ValueError("Model name is required")
|
||||||
|
|
||||||
|
# Extract and validate the new fields
|
||||||
|
new_fields = note.get('fields')
|
||||||
|
if not new_fields or not isinstance(new_fields, dict):
|
||||||
|
raise ValueError("Fields must be provided as a dictionary")
|
||||||
|
|
||||||
|
# Extract the new tags
|
||||||
|
new_tags = note.get('tags', [])
|
||||||
|
|
||||||
|
# Get the current note from the collection
|
||||||
|
anki_note = self.getNote(note_id)
|
||||||
|
|
||||||
|
# Get the new model from the collection
|
||||||
|
collection = self.collection()
|
||||||
|
new_model = collection.models.by_name(new_model_name)
|
||||||
|
if not new_model:
|
||||||
|
raise ValueError(f"Model '{new_model_name}' not found")
|
||||||
|
|
||||||
|
# Update the note's model
|
||||||
|
anki_note.mid = new_model['id']
|
||||||
|
anki_note._fmap = collection.models.field_map(new_model)
|
||||||
|
anki_note.fields = [''] * len(new_model['flds'])
|
||||||
|
|
||||||
|
# Update the fields with new values
|
||||||
|
for name, value in new_fields.items():
|
||||||
|
for anki_name in anki_note.keys():
|
||||||
|
if name.lower() == anki_name.lower():
|
||||||
|
anki_note[anki_name] = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Update the tags
|
||||||
|
anki_note.tags = new_tags
|
||||||
|
|
||||||
|
# Update note to ensure changes are saved
|
||||||
|
collection.update_note(anki_note, skip_undo_entry=True);
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def updateNoteTags(self, note, tags):
|
def updateNoteTags(self, note, tags):
|
||||||
@ -859,7 +911,6 @@ class AnkiConnect:
|
|||||||
def addTags(self, notes, tags, add=True):
|
def addTags(self, notes, tags, add=True):
|
||||||
self.startEditing()
|
self.startEditing()
|
||||||
self.collection().tags.bulkAdd(notes, tags, add)
|
self.collection().tags.bulkAdd(notes, tags, add)
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -887,10 +938,10 @@ class AnkiConnect:
|
|||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if note.hasTag(tag_to_replace):
|
if note.has_tag(tag_to_replace):
|
||||||
note.delTag(tag_to_replace)
|
note.remove_tag(tag_to_replace)
|
||||||
note.addTag(replace_with_tag)
|
note.add_tag(replace_with_tag)
|
||||||
note.flush()
|
self.collection().update_note(note, skip_undo_entry=True);
|
||||||
|
|
||||||
self.window().requireReset()
|
self.window().requireReset()
|
||||||
self.window().progress.finish()
|
self.window().progress.finish()
|
||||||
@ -904,10 +955,10 @@ class AnkiConnect:
|
|||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
for nid in collection.db.list('select id from notes'):
|
for nid in collection.db.list('select id from notes'):
|
||||||
note = self.getNote(nid)
|
note = self.getNote(nid)
|
||||||
if note.hasTag(tag_to_replace):
|
if note.has_tag(tag_to_replace):
|
||||||
note.delTag(tag_to_replace)
|
note.remove_tag(tag_to_replace)
|
||||||
note.addTag(replace_with_tag)
|
note.add_tag(replace_with_tag)
|
||||||
note.flush()
|
self.collection().update_note(note, skip_undo_entry=True);
|
||||||
|
|
||||||
self.window().requireReset()
|
self.window().requireReset()
|
||||||
self.window().progress.finish()
|
self.window().progress.finish()
|
||||||
@ -926,7 +977,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
couldSetEaseFactors.append(True)
|
couldSetEaseFactors.append(True)
|
||||||
ankiCard.factor = easeFactors[i]
|
ankiCard.factor = easeFactors[i]
|
||||||
ankiCard.flush()
|
self.collection().update_card(ankiCard, skip_undo_entry=True)
|
||||||
|
|
||||||
return couldSetEaseFactors
|
return couldSetEaseFactors
|
||||||
|
|
||||||
@ -956,7 +1007,7 @@ class AnkiConnect:
|
|||||||
ankiCard = self.getCard(card)
|
ankiCard = self.getCard(card)
|
||||||
for i, key in enumerate(keys):
|
for i, key in enumerate(keys):
|
||||||
setattr(ankiCard, key, newValues[i])
|
setattr(ankiCard, key, newValues[i])
|
||||||
ankiCard.flush()
|
self.collection().update_card(ankiCard, skip_undo_entry=True)
|
||||||
result.append(True)
|
result.append(True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result.append([False, str(e)])
|
result.append([False, str(e)])
|
||||||
@ -993,7 +1044,6 @@ class AnkiConnect:
|
|||||||
scheduler.suspendCards(cards)
|
scheduler.suspendCards(cards)
|
||||||
else:
|
else:
|
||||||
scheduler.unsuspendCards(cards)
|
scheduler.unsuspendCards(cards)
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1055,7 +1105,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelNames(self):
|
def modelNames(self):
|
||||||
return self.collection().models.allNames()
|
return [n.name for n in self.collection().models.all_names_and_ids()]
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -1065,7 +1115,7 @@ class AnkiConnect:
|
|||||||
raise Exception('Must provide at least one field for inOrderFields')
|
raise Exception('Must provide at least one field for inOrderFields')
|
||||||
if len(cardTemplates) == 0:
|
if len(cardTemplates) == 0:
|
||||||
raise Exception('Must provide at least one card for cardTemplates')
|
raise Exception('Must provide at least one card for cardTemplates')
|
||||||
if modelName in self.collection().models.allNames():
|
if modelName in [n.name for n in self.collection().models.all_names_and_ids()]:
|
||||||
raise Exception('Model name already exists')
|
raise Exception('Model name already exists')
|
||||||
|
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
@ -1078,7 +1128,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
# Create fields and add them to Note
|
# Create fields and add them to Note
|
||||||
for field in inOrderFields:
|
for field in inOrderFields:
|
||||||
fm = mm.newField(field)
|
fm = mm.new_field(field)
|
||||||
mm.addField(m, fm)
|
mm.addField(m, fm)
|
||||||
|
|
||||||
# Add shared css to model if exists. Use default otherwise
|
# Add shared css to model if exists. Use default otherwise
|
||||||
@ -1092,7 +1142,7 @@ class AnkiConnect:
|
|||||||
if 'Name' in card:
|
if 'Name' in card:
|
||||||
cardName = card['Name']
|
cardName = card['Name']
|
||||||
|
|
||||||
t = mm.newTemplate(cardName)
|
t = mm.new_template(cardName)
|
||||||
cardCount += 1
|
cardCount += 1
|
||||||
t['qfmt'] = card['Front']
|
t['qfmt'] = card['Front']
|
||||||
t['afmt'] = card['Back']
|
t['afmt'] = card['Back']
|
||||||
@ -1106,11 +1156,33 @@ class AnkiConnect:
|
|||||||
def modelNamesAndIds(self):
|
def modelNamesAndIds(self):
|
||||||
models = {}
|
models = {}
|
||||||
for model in self.modelNames():
|
for model in self.modelNames():
|
||||||
models[model] = int(self.collection().models.byName(model)['id'])
|
models[model] = int(self.collection().models.by_name(model)['id'])
|
||||||
|
|
||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def findModelsById(self, modelIds):
|
||||||
|
models = []
|
||||||
|
for id in modelIds:
|
||||||
|
model = self.collection().models.get(id)
|
||||||
|
if model is None:
|
||||||
|
raise Exception("model was not found: {}".format(id))
|
||||||
|
else:
|
||||||
|
models.append(model)
|
||||||
|
return models
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def findModelsByName(self, modelNames):
|
||||||
|
models = []
|
||||||
|
for name in modelNames:
|
||||||
|
model = self.collection().models.by_name(name)
|
||||||
|
if model is None:
|
||||||
|
raise Exception("model was not found: {}".format(name))
|
||||||
|
else:
|
||||||
|
models.append(model)
|
||||||
|
return models
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelNameFromId(self, modelId):
|
def modelNameFromId(self, modelId):
|
||||||
model = self.collection().models.get(modelId)
|
model = self.collection().models.get(modelId)
|
||||||
@ -1122,7 +1194,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelFieldNames(self, modelName):
|
def modelFieldNames(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
else:
|
else:
|
||||||
@ -1131,7 +1203,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelFieldDescriptions(self, modelName):
|
def modelFieldDescriptions(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
else:
|
else:
|
||||||
@ -1159,7 +1231,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelFieldsOnTemplates(self, modelName):
|
def modelFieldsOnTemplates(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
|
|
||||||
@ -1190,7 +1262,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelTemplates(self, modelName):
|
def modelTemplates(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
|
|
||||||
@ -1203,7 +1275,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def modelStyling(self, modelName):
|
def modelStyling(self, modelName):
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
|
|
||||||
@ -1213,7 +1285,7 @@ class AnkiConnect:
|
|||||||
@util.api()
|
@util.api()
|
||||||
def updateModelTemplates(self, model):
|
def updateModelTemplates(self, model):
|
||||||
models = self.collection().models
|
models = self.collection().models
|
||||||
ankiModel = models.byName(model['name'])
|
ankiModel = models.by_name(model['name'])
|
||||||
if ankiModel is None:
|
if ankiModel is None:
|
||||||
raise Exception('model was not found: {}'.format(model['name']))
|
raise Exception('model was not found: {}'.format(model['name']))
|
||||||
|
|
||||||
@ -1235,7 +1307,7 @@ class AnkiConnect:
|
|||||||
@util.api()
|
@util.api()
|
||||||
def updateModelStyling(self, model):
|
def updateModelStyling(self, model):
|
||||||
models = self.collection().models
|
models = self.collection().models
|
||||||
ankiModel = models.byName(model['name'])
|
ankiModel = models.by_name(model['name'])
|
||||||
if ankiModel is None:
|
if ankiModel is None:
|
||||||
raise Exception('model was not found: {}'.format(model['name']))
|
raise Exception('model was not found: {}'.format(model['name']))
|
||||||
|
|
||||||
@ -1249,13 +1321,13 @@ class AnkiConnect:
|
|||||||
if not modelName:
|
if not modelName:
|
||||||
ankiModel = self.collection().models.allNames()
|
ankiModel = self.collection().models.allNames()
|
||||||
else:
|
else:
|
||||||
model = self.collection().models.byName(modelName)
|
model = self.collection().models.by_name(modelName)
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(modelName))
|
raise Exception('model was not found: {}'.format(modelName))
|
||||||
ankiModel = [modelName]
|
ankiModel = [modelName]
|
||||||
updatedModels = 0
|
updatedModels = 0
|
||||||
for model in ankiModel:
|
for model in ankiModel:
|
||||||
model = self.collection().models.byName(model)
|
model = self.collection().models.by_name(model)
|
||||||
checkForText = False
|
checkForText = False
|
||||||
if css and findText in model['css']:
|
if css and findText in model['css']:
|
||||||
checkForText = True
|
checkForText = True
|
||||||
@ -1344,7 +1416,7 @@ class AnkiConnect:
|
|||||||
model = self.getModel(modelName)
|
model = self.getModel(modelName)
|
||||||
field = self.getField(model, fieldName)
|
field = self.getField(model, fieldName)
|
||||||
|
|
||||||
mm.repositionField(model, field, index)
|
mm.reposition_field(model, field, index)
|
||||||
|
|
||||||
self.save_model(mm, model)
|
self.save_model(mm, model)
|
||||||
|
|
||||||
@ -1355,16 +1427,16 @@ class AnkiConnect:
|
|||||||
model = self.getModel(modelName)
|
model = self.getModel(modelName)
|
||||||
|
|
||||||
# only adds the field if it doesn't already exist
|
# only adds the field if it doesn't already exist
|
||||||
fieldMap = mm.fieldMap(model)
|
fieldMap = mm.field_map(model)
|
||||||
if fieldName not in fieldMap:
|
if fieldName not in fieldMap:
|
||||||
field = mm.newField(fieldName)
|
field = mm.new_field(fieldName)
|
||||||
mm.addField(model, field)
|
mm.addField(model, field)
|
||||||
|
|
||||||
# repositions, even if the field already exists
|
# repositions, even if the field already exists
|
||||||
if index is not None:
|
if index is not None:
|
||||||
fieldMap = mm.fieldMap(model)
|
fieldMap = mm.field_map(model)
|
||||||
newField = fieldMap[fieldName][1]
|
newField = fieldMap[fieldName][1]
|
||||||
mm.repositionField(model, newField, index)
|
mm.reposition_field(model, newField, index)
|
||||||
|
|
||||||
self.save_model(mm, model)
|
self.save_model(mm, model)
|
||||||
|
|
||||||
@ -1375,7 +1447,7 @@ class AnkiConnect:
|
|||||||
model = self.getModel(modelName)
|
model = self.getModel(modelName)
|
||||||
field = self.getField(model, fieldName)
|
field = self.getField(model, fieldName)
|
||||||
|
|
||||||
mm.removeField(model, field)
|
mm.remove_field(model, field)
|
||||||
|
|
||||||
self.save_model(mm, model)
|
self.save_model(mm, model)
|
||||||
|
|
||||||
@ -1438,7 +1510,7 @@ class AnkiConnect:
|
|||||||
if query is None:
|
if query is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return list(map(int, self.collection().findNotes(query)))
|
return list(map(int, self.collection().find_notes(query)))
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -1446,7 +1518,7 @@ class AnkiConnect:
|
|||||||
if query is None:
|
if query is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return list(map(int, self.collection().findCards(query)))
|
return list(map(int, self.collection().find_cards(query)))
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -1455,13 +1527,15 @@ class AnkiConnect:
|
|||||||
for cid in cards:
|
for cid in cards:
|
||||||
try:
|
try:
|
||||||
card = self.getCard(cid)
|
card = self.getCard(cid)
|
||||||
model = card.model()
|
model = card.note_type()
|
||||||
note = card.note()
|
note = card.note()
|
||||||
fields = {}
|
fields = {}
|
||||||
for info in model['flds']:
|
for info in model['flds']:
|
||||||
order = info['ord']
|
order = info['ord']
|
||||||
name = info['name']
|
name = info['name']
|
||||||
fields[name] = {'value': note.fields[order], 'order': order}
|
fields[name] = {'value': note.fields[order], 'order': order}
|
||||||
|
states = self.collection()._backend.get_scheduling_states(card.id)
|
||||||
|
nextReviews = self.collection()._backend.describe_next_states(states)
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
'cardId': card.id,
|
'cardId': card.id,
|
||||||
@ -1485,6 +1559,8 @@ class AnkiConnect:
|
|||||||
'lapses': card.lapses,
|
'lapses': card.lapses,
|
||||||
'left': card.left,
|
'left': card.left,
|
||||||
'mod': card.mod,
|
'mod': card.mod,
|
||||||
|
'nextReviews': list(nextReviews),
|
||||||
|
'flags': card.flags,
|
||||||
})
|
})
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
# Anki will give a NotFoundError if the card ID does not exist.
|
# Anki will give a NotFoundError if the card ID does not exist.
|
||||||
@ -1513,21 +1589,23 @@ class AnkiConnect:
|
|||||||
result.append({})
|
result.append({})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def forgetCards(self, cards):
|
def forgetCards(self, cards):
|
||||||
self.startEditing()
|
self.startEditing()
|
||||||
scids = anki.utils.ids2str(cards)
|
request = ScheduleCardsAsNew(
|
||||||
self.collection().db.execute('update cards set type=0, queue=0, left=0, ivl=0, due=0, odue=0, factor=0 where id in ' + scids)
|
card_ids=cards,
|
||||||
self.stopEditing()
|
log=True,
|
||||||
|
restore_position=True,
|
||||||
|
reset_counts=False,
|
||||||
|
context=None,
|
||||||
|
)
|
||||||
|
self.collection()._backend.schedule_cards_as_new(request)
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def relearnCards(self, cards):
|
def relearnCards(self, cards):
|
||||||
self.startEditing()
|
self.startEditing()
|
||||||
scids = anki.utils.ids2str(cards)
|
scids = anki.utils.ids2str(cards)
|
||||||
self.collection().db.execute('update cards set type=3, queue=1 where id in ' + scids)
|
self.collection().db.execute('update cards set type=3, queue=1 where id in ' + scids)
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
@ -1600,7 +1678,7 @@ class AnkiConnect:
|
|||||||
for nid in notes:
|
for nid in notes:
|
||||||
try:
|
try:
|
||||||
note = self.getNote(nid)
|
note = self.getNote(nid)
|
||||||
model = note.model()
|
model = note.note_type()
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
for info in model['flds']:
|
for info in model['flds']:
|
||||||
@ -1610,9 +1688,11 @@ class AnkiConnect:
|
|||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
'noteId': note.id,
|
'noteId': note.id,
|
||||||
|
'profile': self.window().pm.name,
|
||||||
'tags' : note.tags,
|
'tags' : note.tags,
|
||||||
'fields': fields,
|
'fields': fields,
|
||||||
'modelName': model['name'],
|
'modelName': model['name'],
|
||||||
|
'mod': note.mod,
|
||||||
'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id)
|
'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id)
|
||||||
})
|
})
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
@ -1624,20 +1704,34 @@ class AnkiConnect:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def notesModTime(self, notes):
|
||||||
|
result = []
|
||||||
|
for nid in notes:
|
||||||
|
try:
|
||||||
|
note = self.getNote(nid)
|
||||||
|
result.append({
|
||||||
|
'noteId': note.id,
|
||||||
|
'mod': note.mod
|
||||||
|
})
|
||||||
|
except NotFoundError:
|
||||||
|
# Anki will give a NotFoundError if the note ID does not exist.
|
||||||
|
# Best behavior is probably to add an 'empty card' to the
|
||||||
|
# returned result, so that the items of the input and return
|
||||||
|
# lists correspond.
|
||||||
|
result.append({})
|
||||||
|
return result
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def deleteNotes(self, notes):
|
def deleteNotes(self, notes):
|
||||||
try:
|
self.collection().remove_notes(notes)
|
||||||
self.collection().remNotes(notes)
|
|
||||||
finally:
|
|
||||||
self.stopEditing()
|
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def removeEmptyNotes(self):
|
def removeEmptyNotes(self):
|
||||||
for model in self.collection().models.all():
|
for model in self.collection().models.all():
|
||||||
if self.collection().models.useCount(model) == 0:
|
if self.collection().models.use_count(model) == 0:
|
||||||
self.collection().models.rem(model)
|
self.collection().models.remove(model["id"])
|
||||||
self.window().requireReset()
|
self.window().requireReset()
|
||||||
|
|
||||||
|
|
||||||
@ -1647,7 +1741,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def guiBrowse(self, query=None):
|
def guiBrowse(self, query=None, reorderCards=None):
|
||||||
browser = aqt.dialogs.open('Browser', self.window())
|
browser = aqt.dialogs.open('Browser', self.window())
|
||||||
browser.activateWindow()
|
browser.activateWindow()
|
||||||
|
|
||||||
@ -1658,6 +1752,23 @@ class AnkiConnect:
|
|||||||
else:
|
else:
|
||||||
browser.onSearchActivated()
|
browser.onSearchActivated()
|
||||||
|
|
||||||
|
if reorderCards is not None:
|
||||||
|
if not isinstance(reorderCards, dict):
|
||||||
|
raise Exception('reorderCards should be a dict: {}'.format(reorderCards))
|
||||||
|
if not ('columnId' in reorderCards and 'order' in reorderCards):
|
||||||
|
raise Exception('Must provide a "columnId" and a "order" property"')
|
||||||
|
|
||||||
|
cardOrder = reorderCards['order']
|
||||||
|
if cardOrder not in ('ascending', 'descending'):
|
||||||
|
raise Exception('invalid card order: {}'.format(reorderCards['order']))
|
||||||
|
|
||||||
|
cardOrder = Qt.SortOrder.DescendingOrder if cardOrder == 'descending' else Qt.SortOrder.AscendingOrder
|
||||||
|
columnId = browser.table._model.active_column_index(reorderCards['columnId'])
|
||||||
|
if columnId == None:
|
||||||
|
raise Exception('invalid columnId: {}'.format(reorderCards['columnId']))
|
||||||
|
|
||||||
|
browser.table._on_sort_column_changed(columnId, cardOrder)
|
||||||
|
|
||||||
return self.findCards(query)
|
return self.findCards(query)
|
||||||
|
|
||||||
|
|
||||||
@ -1665,6 +1776,14 @@ class AnkiConnect:
|
|||||||
def guiEditNote(self, note):
|
def guiEditNote(self, note):
|
||||||
Edit.open_dialog_and_show_note_with_id(note)
|
Edit.open_dialog_and_show_note_with_id(note)
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def guiSelectNote(self, note):
|
||||||
|
(creator, instance) = aqt.dialogs._dialogs['Browser']
|
||||||
|
if instance is None:
|
||||||
|
return False
|
||||||
|
instance.table.clear_selection()
|
||||||
|
instance.table.select_single_card(note)
|
||||||
|
return True
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def guiSelectedNotes(self):
|
def guiSelectedNotes(self):
|
||||||
@ -1678,18 +1797,18 @@ class AnkiConnect:
|
|||||||
if note is not None:
|
if note is not None:
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
|
|
||||||
deck = collection.decks.byName(note['deckName'])
|
deck = collection.decks.by_name(note['deckName'])
|
||||||
if deck is None:
|
if deck is None:
|
||||||
raise Exception('deck was not found: {}'.format(note['deckName']))
|
raise Exception('deck was not found: {}'.format(note['deckName']))
|
||||||
|
|
||||||
collection.decks.select(deck['id'])
|
collection.decks.select(deck['id'])
|
||||||
savedMid = deck.pop('mid', None)
|
savedMid = deck.pop('mid', None)
|
||||||
|
|
||||||
model = collection.models.byName(note['modelName'])
|
model = collection.models.by_name(note['modelName'])
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception('model was not found: {}'.format(note['modelName']))
|
raise Exception('model was not found: {}'.format(note['modelName']))
|
||||||
|
|
||||||
collection.models.setCurrent(model)
|
collection.models.set_current(model)
|
||||||
collection.models.update(model)
|
collection.models.update(model)
|
||||||
|
|
||||||
ankiNote = anki.notes.Note(collection, model)
|
ankiNote = anki.notes.Note(collection, model)
|
||||||
@ -1748,7 +1867,7 @@ class AnkiConnect:
|
|||||||
|
|
||||||
reviewer = self.reviewer()
|
reviewer = self.reviewer()
|
||||||
card = reviewer.card
|
card = reviewer.card
|
||||||
model = card.model()
|
model = card.note_type()
|
||||||
note = card.note()
|
note = card.note()
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
@ -1829,7 +1948,7 @@ class AnkiConnect:
|
|||||||
def guiDeckOverview(self, name):
|
def guiDeckOverview(self, name):
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
if collection is not None:
|
if collection is not None:
|
||||||
deck = collection.decks.byName(name)
|
deck = collection.decks.by_name(name)
|
||||||
if deck is not None:
|
if deck is not None:
|
||||||
collection.decks.select(deck['id'])
|
collection.decks.select(deck['id'])
|
||||||
self.window().onOverview()
|
self.window().onOverview()
|
||||||
@ -1910,11 +2029,19 @@ class AnkiConnect:
|
|||||||
@util.api()
|
@util.api()
|
||||||
def addNotes(self, notes):
|
def addNotes(self, notes):
|
||||||
results = []
|
results = []
|
||||||
|
errs = []
|
||||||
|
|
||||||
for note in notes:
|
for note in notes:
|
||||||
try:
|
try:
|
||||||
results.append(self.addNote(note))
|
results.append(self.addNote(note))
|
||||||
except:
|
except Exception as e:
|
||||||
results.append(None)
|
# I specifically chose to continue, so we gather all the errors of all notes (ie not break)
|
||||||
|
errs.append(str(e))
|
||||||
|
|
||||||
|
if errs:
|
||||||
|
# Roll back the changes so on error nothing happens
|
||||||
|
self.deleteNotes(results)
|
||||||
|
raise Exception(str(errs))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -1927,12 +2054,20 @@ class AnkiConnect:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@util.api()
|
||||||
|
def canAddNotesWithErrorDetail(self, notes):
|
||||||
|
results = []
|
||||||
|
for note in notes:
|
||||||
|
results.append(self.canAddNoteWithErrorDetail(note))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
@util.api()
|
@util.api()
|
||||||
def exportPackage(self, deck, path, includeSched=False):
|
def exportPackage(self, deck, path, includeSched=False):
|
||||||
collection = self.collection()
|
collection = self.collection()
|
||||||
if collection is not None:
|
if collection is not None:
|
||||||
deck = collection.decks.byName(deck)
|
deck = collection.decks.by_name(deck)
|
||||||
if deck is not None:
|
if deck is not None:
|
||||||
exporter = AnkiPackageExporter(collection)
|
exporter = AnkiPackageExporter(collection)
|
||||||
exporter.did = deck['id']
|
exporter.did = deck['id']
|
||||||
@ -1952,10 +2087,8 @@ class AnkiConnect:
|
|||||||
importer = AnkiPackageImporter(collection, path)
|
importer = AnkiPackageImporter(collection, path)
|
||||||
importer.run()
|
importer.run()
|
||||||
except:
|
except:
|
||||||
self.stopEditing()
|
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
self.stopEditing()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -2,8 +2,8 @@ import aqt
|
|||||||
import aqt.editor
|
import aqt.editor
|
||||||
import aqt.browser.previewer
|
import aqt.browser.previewer
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import QDialog, Qt, QKeySequence, QShortcut
|
from aqt.qt import Qt, QKeySequence, QShortcut, QCloseEvent, QMainWindow
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip
|
from aqt.utils import restoreGeom, saveGeom, tooltip
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
from anki.consts import QUEUE_TYPE_SUSPENDED
|
from anki.consts import QUEUE_TYPE_SUSPENDED
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
@ -187,15 +187,13 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
# upon a request to open the dialog via `aqt.dialogs.open()`,
|
# upon a request to open the dialog via `aqt.dialogs.open()`,
|
||||||
# the manager will call either the constructor or the `reopen` method
|
# the manager will call either the constructor or the `reopen` method
|
||||||
def __init__(self, note):
|
def __init__(self, note):
|
||||||
QDialog.__init__(self, None, Qt.WindowType.Window)
|
QMainWindow.__init__(self, None, Qt.WindowType.Window)
|
||||||
aqt.mw.garbage_collect_on_dialog_finish(self)
|
|
||||||
self.form = aqt.forms.editcurrent.Ui_Dialog()
|
self.form = aqt.forms.editcurrent.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
self.setWindowTitle("Edit")
|
self.setWindowTitle("Edit")
|
||||||
self.setMinimumWidth(250)
|
self.setMinimumWidth(250)
|
||||||
self.setMinimumHeight(400)
|
self.setMinimumHeight(400)
|
||||||
restoreGeom(self, self.dialog_geometry_tag)
|
restoreGeom(self, self.dialog_geometry_tag)
|
||||||
disable_help_button(self)
|
|
||||||
|
|
||||||
self.form.buttonBox.setVisible(False) # hides the Close button bar
|
self.form.buttonBox.setVisible(False) # hides the Close button bar
|
||||||
self.setup_editor_buttons()
|
self.setup_editor_buttons()
|
||||||
@ -215,14 +213,16 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
self.show_note(note)
|
self.show_note(note)
|
||||||
self.bring_to_foreground()
|
self.bring_to_foreground()
|
||||||
|
|
||||||
def cleanup_and_close(self):
|
def cleanup(self):
|
||||||
gui_hooks.editor_did_load_note.remove(self.editor_did_load_note)
|
gui_hooks.editor_did_load_note.remove(self.editor_did_load_note)
|
||||||
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
|
||||||
|
|
||||||
self.editor.cleanup()
|
self.editor.cleanup()
|
||||||
saveGeom(self, self.dialog_geometry_tag)
|
saveGeom(self, self.dialog_geometry_tag)
|
||||||
aqt.dialogs.markClosed(self.dialog_registry_tag)
|
aqt.dialogs.markClosed(self.dialog_registry_tag)
|
||||||
QDialog.reject(self)
|
|
||||||
|
def closeEvent(self, evt: QCloseEvent) -> None:
|
||||||
|
self.editor.call_after_note_saved(self.cleanup)
|
||||||
|
|
||||||
# This method (mostly) solves (at least on my Windows 10 machine) three issues
|
# This method (mostly) solves (at least on my Windows 10 machine) three issues
|
||||||
# with window activation. Without this not even too hacky a fix,
|
# with window activation. Without this not even too hacky a fix,
|
||||||
@ -283,7 +283,7 @@ class Edit(aqt.editcurrent.EditCurrent):
|
|||||||
try:
|
try:
|
||||||
self.note = history.get_last_note()
|
self.note = history.get_last_note()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.cleanup_and_close()
|
self.cleanup()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.show_note(self.note)
|
self.show_note(self.note)
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
trap '[[ -v SERVER_PID ]] && pkill -P $SERVER_PID' EXIT
|
|
||||||
print_first_group() { perl -snle 'm/$re/; print $1; exit 0' -- -re="$1"; }
|
|
||||||
|
|
||||||
envname="$1"
|
|
||||||
toxworkdir="$2"
|
|
||||||
packages=("${@:3}")
|
|
||||||
|
|
||||||
version=$(print_first_group 'anki([\d\.a-z]+)' <<< "$envname")
|
|
||||||
upload_time=$(curl https://pypi.org/pypi/anki/json \
|
|
||||||
| jq --arg v "$version" -r '.releases[$v][0].upload_time_iso_8601')
|
|
||||||
cutoff_time=$(date --utc -d "$upload_time +1 hour" '+%Y-%m-%dT%H:%M:%S')
|
|
||||||
|
|
||||||
coproc SERVER { "$toxworkdir"/.tox/bin/python -um pypi_timemachine "$cutoff_time"; }
|
|
||||||
index_url=$(print_first_group '(http\S+)' <&"${SERVER[0]}")
|
|
||||||
|
|
||||||
python -m pip install --index-url "$index_url" "anki==$version" "$AQT==$version"
|
|
||||||
python -m pip install "${packages[@]}"
|
|
86
tox.ini
86
tox.ini
@ -1,86 +0,0 @@
|
|||||||
# For testing, you will need:
|
|
||||||
# * PyQt5 dev tools
|
|
||||||
# * tox
|
|
||||||
# * X virtual framebuffer--to test without GUI
|
|
||||||
#
|
|
||||||
# Install these by running:
|
|
||||||
# $ sudo apt install pyqt5-dev-tools xvfb
|
|
||||||
# $ python3 -m pip install --user --upgrade tox
|
|
||||||
#
|
|
||||||
# Then, to run tests against multiple anki versions:
|
|
||||||
# $ tox
|
|
||||||
#
|
|
||||||
# To run tests slightly less safely, but faster:
|
|
||||||
# $ tox -- --no-tear-down-profile-after-each-test
|
|
||||||
#
|
|
||||||
# To run tests more safely, but *much* slower:
|
|
||||||
# $ tox -- --forked
|
|
||||||
|
|
||||||
# Test tool cheat sheet:
|
|
||||||
# * Test several environments in parallel:
|
|
||||||
# $ tox -p auto
|
|
||||||
#
|
|
||||||
# * To activate one of the test environments:
|
|
||||||
# $ source .tox/py38-anki49/bin/activate
|
|
||||||
#
|
|
||||||
# * Stop on first failure:
|
|
||||||
# $ xvfb-run python -m pytest -x
|
|
||||||
#
|
|
||||||
# * See stdout/stderr (doesn't work with --forked!):
|
|
||||||
# $ xvfb-run python -m pytest -s
|
|
||||||
#
|
|
||||||
# * Run some specific tests:
|
|
||||||
# $ xvfb-run python -m pytest -k "test_cards.py or test_guiBrowse"
|
|
||||||
#
|
|
||||||
# * To run with visible GUI in WSL2
|
|
||||||
# (Make sure to disable access control in your X server, such as VcXsrv):
|
|
||||||
# $ DISPLAY=$(ip route list default | awk '{print $3}'):0 python -m pytest
|
|
||||||
#
|
|
||||||
# * Environmental variables of interest:
|
|
||||||
# LIBGL_ALWAYS_INDIRECT=1
|
|
||||||
# QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
|
|
||||||
# QT_DEBUG_PLUGINS=1
|
|
||||||
# ANKIDEV=1
|
|
||||||
|
|
||||||
# Note: pypi packages anki and aqt do not pin their dependencies.
|
|
||||||
# To tests against historically accurate dependencies, we use a “time machine”
|
|
||||||
# that prevents pip from using packages that were uploaded after a specified date.
|
|
||||||
|
|
||||||
[tox]
|
|
||||||
minversion = 3.24
|
|
||||||
skipsdist = true
|
|
||||||
skip_install = true
|
|
||||||
requires =
|
|
||||||
pypi-timemachine
|
|
||||||
envlist =
|
|
||||||
py38-anki2.1.{45,46,47,48,49}
|
|
||||||
py39-anki2.1.{50,51,52,53,54,55,56}-qt{5,6}
|
|
||||||
|
|
||||||
[testenv:.tox]
|
|
||||||
install_command =
|
|
||||||
python -m pip install {packages}
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
install_command =
|
|
||||||
bash tox-install-command {envname} {toxworkdir} {packages}
|
|
||||||
|
|
||||||
commands =
|
|
||||||
env HOME={envtmpdir}/home xvfb-run python -m pytest {posargs}
|
|
||||||
|
|
||||||
setenv =
|
|
||||||
DISABLE_QT5_COMPAT=1
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS=--no-sandbox
|
|
||||||
|
|
||||||
!qt{5,6}: AQT=aqt
|
|
||||||
qt5: AQT=aqt[qt5]
|
|
||||||
qt6: AQT=aqt[qt6]
|
|
||||||
|
|
||||||
allowlist_externals =
|
|
||||||
bash
|
|
||||||
env
|
|
||||||
xvfb-run
|
|
||||||
|
|
||||||
deps =
|
|
||||||
pytest==7.1.1
|
|
||||||
pytest-forked==1.4.0
|
|
||||||
pytest-anki @ git+https://github.com/oakkitten/pytest-anki.git@a0d27aa5
|
|
Loading…
Reference in New Issue
Block a user