This project will explain how to create a web-app that can be used as an addon for Anki to generate decks. The resulting addon can be then used with Anki, AnkiDroid and AnkiMobile.
Let's start with an example: an app that let's you create basic question-answer cards in the browser.
https://infinyte7.github.io/Create-Addon-in-browser-for-Anki
Usage
- Add text for the front and back side of a card
- Click the add button to save the node
- Click download to export a deck containing the saved notes
- Fork this repository
- All the setup-files are in docs folder
index.html
is the front page for sending note data from html/js to pyodide internallyindex.js
contains code for object conversion between javaScript and pyodidedeck-export.js
contains code for generating and downloading Anki decks
- Note data is sent from html/js to pyodide in a tab-separated format
- When you click the
add button
pyodide writes the data tooutput-all-notes.txt
- When you click the
download button
genanki
generates Anki decks fromoutput-all-notes.txt
and downloads it
- Runs on smartphones as well as on desktop computers
- You can import the generated decks to any version of Anki
- The loading time is high
- Some python modules are not available (for example opencv)
- One needs to understand object conversion between javaScript and python
-
Anki Anki is a free and open-source flashcard program using spaced repetition, a technique from cognitive science for fast and long-lasting memorization.
-
Pyodide brings the Python 3.8 runtime to the browser via WebAssembly, along with the Python scientific stack including NumPy, Pandas, Matplotlib, SciPy, and scikit-learn. The packages directory lists over 75 packages which are currently available. In addition it's possible to install pure Python wheels from PyPi.
Pyodide provides transparent conversion of objects between Javascript and Python. When used inside a browser, Python has full access to the Web APIs.
-
genanki
allows you to programmatically generate decks in Python 3 for Anki, a popular spaced-repetition flashcard program.
<script type="text/javascript">
window.languagePluginUrl = 'pyodide/dev/full/';
</script>
<script src="pyodide/dev/full/pyodide.js"></script>
...
...
<script src="js/deck-export.js"></script>
<!-- Note Form -->
<!-- Front Back -->
<div id="add-note">
<div class="input-note" style="padding-top: 60px;">Front
<hr class="thin">
<textarea id="noteFront" class="input-add-note" type="text" placeholder="Front..." required></textarea>
</div>
<div class="input-note" style="padding-top: 30px;">Back
<hr class="thin">
<textarea id="noteBack" class="input-add-note" type="text" placeholder="Back..." required></textarea>
</div>
</div>
<div id="done-export-all" class="toolbar-button" style="float: right; display: block;" onclick="exportAll();"><i
class="material-icons">get_app</i></div>
<div id="done-btn" class="toolbar-button" style="float: right;" onclick="addNoteToList();"><i
class="material-icons">add</i></div>
// add note data to pyodide output text file for deck export
var textToExport = "";
var textFileName = "";
function addNoteToList() {
textFileName = "output-all-notes.txt";
var container = document.getElementById("add-note");
// tab separated value for genanki
for (i = 0; i < container.childElementCount; i++) {
textToExport += container.children[i].children[1].value.trim() + "\t";
}
// remove last space, tab...
textToExport = textToExport.trim();
// write to file using pyodide
pyodide.runPython("textFileName = js.textFileName")
pyodide.runPython("textToExport = js.textToExport")
pyodide.runPython(`with open(textFileName, 'a', encoding='utf-8') as f:
f.write(textToExport)`)
}
// export and download deck
function exportAll() {
document.getElementById('statusMsg').innerHTML = "Wait, deck generating...";
setTimeout(function () { document.getElementById('statusMsg').innerHTML = ""; }, 2000);
exportDeck();
downloadDeck();
}
languagePluginLoader.then(() => {
return pyodide.loadPackage(['micropip'])
}).then(() => {
pyodide.runPython(pythonCode);
document.getElementById("loading").style.display = "none";
document.getElementById("statusMsg").innerHTML = "";
showSnackbar("Ready to import file");
})
languagePluginLoader.then(function () {
console.log('Ready');
});
import genanki
# front side
front = """
<div>{{Front}}</div>
"""
# styling
style = """
.card {
font-family: arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
}
"""
# back side
back = """
<div>{{Front}}</div>
<hr>
<div>{{Back}}</div>
"""
t_fields = [{"name": "Front"}, {"name": "Back"}]
# print(self.fields)
anki_model = genanki.Model(
model_id,
anki_model_name,
fields=t_fields,
templates=[
{
"name": "Card 1",
"qfmt": front,
"afmt": back,
},
],
css=style
)
anki_notes = []
with open(data_filename, "r", encoding="utf-8") as csv_file:
csv_reader = csv.reader(csv_file, delimiter="\\t")
for row in csv_reader:
flds = []
for i in range(len(row)):
flds.append(row[i])
anki_note = genanki.Note(
model=anki_model,
fields=flds,
)
anki_notes.append(anki_note)
anki_deck = genanki.Deck(model_id, anki_deck_title)
anki_package = genanki.Package(anki_deck)
for anki_note in anki_notes:
anki_deck.add_note(anki_note)
anki_package.write_to_file(deck_filename)
deck_export_msg = "Deck generated with {} flashcards".format(len(anki_deck.notes))
js.showSnackbar(deck_export_msg)
Front and back should contain fields that are present in t_fields
.
t_fields
should contain fields in the following manner.
{"name": "Field name"}
Select image using input
function addImage() {
var imgHeight;
var imgWidth;
var selectedFile = event.target.files[0];
var reader = new FileReader();
var imgtag = document.getElementById("uploadPreview");
imgtag.title = selectedFile.name;
imgtag.type = selectedFile.type;
reader.onload = function (event) {
imgtag.src = event.target.result;
imgtag.onload = function () {
imgHeight = this.height;
imgWidth = this.width;
};
};
reader.readAsDataURL(selectedFile);
}
Saving selected image to internal. In this code base64 of image passed and written to file.
function saveSelectedImageToInternal() {
pyodide.runPython("import os")
pyodide.runPython("import js")
pyodide.runPython("import base64")
pyodide.runPython("if not os.path.exists('images'): os.mkdir('images')")
// src tag contains base64 of image data
pyodide.runPython("blob = js.document.getElementById('uploadPreview').getAttribute('src')")
pyodide.runPython("name = js.document.getElementById('uploadPreview').getAttribute('title')")
// base64 data
// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEECAAAAAAjIz....
// split data and write later part
// iVBORw0KGgoAAAANSUhEUgAAAZAAAAEECAAAAAAjIz....
pyodide.runPython("blob = blob.split(',')[1]")
pyodide.runPython("imgData = base64.b64decode(blob)")
pyodide.runPython(`with open('images/' + name, 'wb') as f:
f.write(imgData)`)
}
Appending images to genanki Anki packages.
# add media
files = []
for ext in ('*.gif', '*.png', '*.jpg', '*.jpeg', '*.bmp', '*.svg'):
files.extend(glob(join("images", ext)))
anki_package.media_files = files
Same goes for other media files.
View usage in image occlusion in browser
import micropip
# from GitHub using CDN
micropip.install("https://cdn.jsdelivr.net/gh/infinyte7/Anki-Export-Deck-tkinter/docs/py-whl/frozendict-1.2-py3-none-any.whl")
micropip.install("https://cdn.jsdelivr.net/gh/infinyte7/Anki-Export-Deck-tkinter/docs/py-whl/pystache-0.5.4-py3-none-any.whl")
micropip.install("https://cdn.jsdelivr.net/gh/infinyte7/Anki-Export-Deck-tkinter/docs/py-whl/PyYAML-5.3.1-cp38-cp38-win_amd64.whl")
micropip.install("https://cdn.jsdelivr.net/gh/infinyte7/Anki-Export-Deck-tkinter/docs/py-whl/cached_property-1.5.2-py2.py3-none-any.whl")
micropip.install("https://cdn.jsdelivr.net/gh/infinyte7/Anki-Export-Deck-tkinter/docs/py-whl/genanki-0.8.0-py3-none-any.whl")
function downloadDeck() {
let txt = pyodide.runPython(`
with open('/output.apkg', 'rb') as fh:
out = fh.read()
out
`);
const blob = new Blob([txt], { type: 'application/zip' });
let url = window.URL.createObjectURL(blob);
var downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = "Export-Deck.apkg";
document.body.appendChild(downloadLink);
downloadLink.click();
}
function exportText() {
let txt = pyodide.runPython(`
with open('/output-all-notes.txt', 'r') as fh:
out = fh.read()
out
`);
console.log(txt);
const blob = new Blob([txt], { type: 'text/plain' });
let url = window.URL.createObjectURL(blob);
var downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = "output.txt";
document.body.appendChild(downloadLink);
downloadLink.click();
}
If you want to use backslashes \ in pythonCode
you have to escape them with another backslash \\, otherwise it will not work or show an error.
csv_reader = csv.reader(csv_file, delimiter="\t")
^^^^^
csv_reader = csv.reader(csv_file, delimiter="\\t")
For every deck generation, genanki use random model id which leads to creation of multiple Note Type
. So to avoid this use constant model id in pythonCode
in deck-export.js
anki_model_name = "addon-in-browser"
# model_id = random.randrange(1 << 30, 1 << 31)
# constant model id for this project
model_id = 1716551648
Create a variable deckName
in js file and access it in pyodide
import js
new_title = js.deckName
anki_deck_title = "Addon in browser"
if new_title != None:
anki_deck_title = new_title