├── tests ├── __init__.py ├── support │ ├── fake.png │ ├── text-update.txt │ ├── text-tags.txt │ ├── mnemo.db │ ├── anki12.anki │ ├── media.apkg │ ├── update1.apkg │ ├── update2.apkg │ ├── anki12-due.anki │ ├── diffmodels1.anki │ ├── diffmodels2.anki │ ├── suspended12.anki │ ├── anki12-broken.anki │ ├── anki2-alpha.anki2 │ ├── diffmodels2-1.apkg │ ├── diffmodels2-2.apkg │ ├── invalid-ords.anki │ ├── diffmodeltemplates-1.apkg │ ├── diffmodeltemplates-2.apkg │ ├── text-2fields.txt │ └── supermemo1.xml ├── test_stats.py ├── shared.py ├── test_undo.py ├── test_cards.py ├── test_latex.py ├── test_collection.py ├── test_media.py └── test_exporting.py ├── designer ├── .gitignore ├── icons │ ├── .gitignore │ ├── tex.png │ ├── add16.png │ ├── addtag.png │ ├── anki.png │ ├── ankibw.png │ ├── colors.png │ ├── deck16.png │ ├── edit.png │ ├── find.png │ ├── gears.png │ ├── green.png │ ├── help.png │ ├── image.png │ ├── info.png │ ├── kexi.png │ ├── layout.png │ ├── more.png │ ├── none.png │ ├── plus16.png │ ├── rating.png │ ├── star16.png │ ├── addtag16.png │ ├── anki-tag.png │ ├── arrow-up.png │ ├── clock16.png │ ├── contents.png │ ├── delete16.png │ ├── download.png │ ├── go-first.png │ ├── go-last.png │ ├── go-next.png │ ├── kblogger.png │ ├── list-add.png │ ├── pause16.png │ ├── speaker.png │ ├── text-xml.png │ ├── text_sub.png │ ├── arrow-down.png │ ├── clock-icon.png │ ├── configure.png │ ├── contents2.png │ ├── deletetag.png │ ├── deletetag16.png │ ├── edit-find 2.png │ ├── edit-find.png │ ├── edit-redo.png │ ├── edit-rename.png │ ├── edit-undo.png │ ├── editclear.png │ ├── editdelete.png │ ├── fileclose.png │ ├── games-solve.png │ ├── go-previous.png │ ├── help-hint.png │ ├── kbugbuster.png │ ├── khtml_kget.png │ ├── math_matrix.png │ ├── math_sqrt.png │ ├── paperclip.png │ ├── pause_off16.png │ ├── player-time.png │ ├── plus-circle.png │ ├── spreadsheet.png │ ├── star_off16.png │ ├── stock_group.png │ ├── text-speak.png │ ├── text_bold.png │ ├── text_clear.png │ ├── text_cloze.png │ ├── text_italic.png │ ├── text_remove.png │ ├── text_super.png │ ├── text_under.png │ ├── view_text.png │ ├── document-new.png │ ├── folder_image.png │ ├── folder_sound.png │ ├── go-jump-today.png │ ├── help-contents.png │ ├── kpersonalizer.png │ ├── media-record.png │ ├── sqlitebrowser.png │ ├── user-identity.png │ ├── view-pim-news.png │ ├── view-refresh.png │ ├── anki-logo-thin.png │ ├── anki-logo_black.png │ ├── anki-logo_white.png │ ├── application-exit.png │ ├── arrow-up-double.png │ ├── document-export.png │ ├── document-import.png │ ├── emblem-favorite.png │ ├── emblem-important.png │ ├── mail-attachment.png │ ├── product_design.png │ ├── system-shutdown.png │ ├── view-statistics.png │ ├── arrow-down-double.png │ ├── edit-find-replace.png │ ├── package_games_card.png │ ├── preferences-plugin.png │ ├── stock_new_template.png │ ├── view-pim-calendar.png │ ├── emblem-favorite-dark.png │ ├── emblem-favorite-off.png │ ├── format-stroke-color.png │ ├── media-playback-pause.png │ ├── media-playback-start.png │ ├── media-playback-start2.png │ ├── media-playback-stop.png │ ├── view-calendar-tasks.png │ ├── view-sort-ascending.png │ ├── view-sort-descending.png │ ├── stock_new_template_blue.png │ ├── stock_new_template_red.png │ ├── system-software-update.png │ ├── preferences-desktop-font.png │ ├── stock_new_template_green.png │ └── _sources.txt ├── preview.ui ├── debug.ui ├── edithtml.ui ├── editcurrent.ui ├── setlang.ui ├── editaddon.ui ├── studydeck.ui ├── addmodel.ui ├── setgroup.ui ├── changemap.ui ├── about.ui ├── getaddons.ui ├── addcards.ui ├── modelopts.ui ├── models.ui ├── profiles.ui ├── taglimit.ui ├── browserdisp.ui ├── finddupes.ui ├── findreplace.ui ├── reposition.ui ├── exporting.ui ├── browseropts.ui └── addfield.ui ├── anki.png ├── runanki ├── requirements.txt ├── tools ├── anki-wait.bat ├── tests.sh └── build_ui.sh ├── .gitignore ├── anki ├── template │ ├── __init__.py │ ├── README.anki │ ├── hint.py │ ├── furigana.py │ ├── LICENSE │ └── view.py ├── statsbg.py ├── __init__.py ├── errors.py ├── importing │ ├── __init__.py │ ├── base.py │ ├── apkg.py │ └── pauker.py ├── hooks.py ├── consts.py ├── stdmodels.py ├── db.py └── lang.py ├── anki.desktop ├── .travis.yml ├── anki.xml ├── README.md ├── LICENSE.logo ├── aqt ├── sound.py ├── qt.py ├── editcurrent.py ├── mediasrv.py ├── about.py ├── update.py ├── modelchooser.py ├── downloader.py ├── tagedit.py ├── deckchooser.py ├── stats.py ├── taglimit.py ├── dyndeckconf.py └── errors.py ├── Makefile ├── anki.1 ├── README.addons └── README.development /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /tests/support/fake.png: -------------------------------------------------------------------------------- 1 | abc 2 | -------------------------------------------------------------------------------- /tests/support/text-update.txt: -------------------------------------------------------------------------------- 1 | 1 x 2 | -------------------------------------------------------------------------------- /designer/icons/.gitignore: -------------------------------------------------------------------------------- 1 | /.directory 2 | -------------------------------------------------------------------------------- /anki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/anki.png -------------------------------------------------------------------------------- /runanki: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import aqt 4 | aqt.run() 5 | -------------------------------------------------------------------------------- /tests/support/text-tags.txt: -------------------------------------------------------------------------------- 1 | foo bar baz,qux 2 | foo2 bar2 baz2 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | send2trash 3 | httplib2 4 | pyaudio 5 | 6 | -------------------------------------------------------------------------------- /tools/anki-wait.bat: -------------------------------------------------------------------------------- 1 | cd .. 2 | set PYTHONPATH=../lib 3 | python anki 4 | pause 5 | -------------------------------------------------------------------------------- /designer/icons/tex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/tex.png -------------------------------------------------------------------------------- /tests/support/mnemo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/mnemo.db -------------------------------------------------------------------------------- /designer/icons/add16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/add16.png -------------------------------------------------------------------------------- /designer/icons/addtag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/addtag.png -------------------------------------------------------------------------------- /designer/icons/anki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/anki.png -------------------------------------------------------------------------------- /designer/icons/ankibw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/ankibw.png -------------------------------------------------------------------------------- /designer/icons/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/colors.png -------------------------------------------------------------------------------- /designer/icons/deck16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/deck16.png -------------------------------------------------------------------------------- /designer/icons/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit.png -------------------------------------------------------------------------------- /designer/icons/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/find.png -------------------------------------------------------------------------------- /designer/icons/gears.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/gears.png -------------------------------------------------------------------------------- /designer/icons/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/green.png -------------------------------------------------------------------------------- /designer/icons/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/help.png -------------------------------------------------------------------------------- /designer/icons/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/image.png -------------------------------------------------------------------------------- /designer/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/info.png -------------------------------------------------------------------------------- /designer/icons/kexi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/kexi.png -------------------------------------------------------------------------------- /designer/icons/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/layout.png -------------------------------------------------------------------------------- /designer/icons/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/more.png -------------------------------------------------------------------------------- /designer/icons/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/none.png -------------------------------------------------------------------------------- /designer/icons/plus16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/plus16.png -------------------------------------------------------------------------------- /designer/icons/rating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/rating.png -------------------------------------------------------------------------------- /designer/icons/star16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/star16.png -------------------------------------------------------------------------------- /tests/support/anki12.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/anki12.anki -------------------------------------------------------------------------------- /tests/support/media.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/media.apkg -------------------------------------------------------------------------------- /designer/icons/addtag16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/addtag16.png -------------------------------------------------------------------------------- /designer/icons/anki-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/anki-tag.png -------------------------------------------------------------------------------- /designer/icons/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/arrow-up.png -------------------------------------------------------------------------------- /designer/icons/clock16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/clock16.png -------------------------------------------------------------------------------- /designer/icons/contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/contents.png -------------------------------------------------------------------------------- /designer/icons/delete16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/delete16.png -------------------------------------------------------------------------------- /designer/icons/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/download.png -------------------------------------------------------------------------------- /designer/icons/go-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/go-first.png -------------------------------------------------------------------------------- /designer/icons/go-last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/go-last.png -------------------------------------------------------------------------------- /designer/icons/go-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/go-next.png -------------------------------------------------------------------------------- /designer/icons/kblogger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/kblogger.png -------------------------------------------------------------------------------- /designer/icons/list-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/list-add.png -------------------------------------------------------------------------------- /designer/icons/pause16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/pause16.png -------------------------------------------------------------------------------- /designer/icons/speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/speaker.png -------------------------------------------------------------------------------- /designer/icons/text-xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text-xml.png -------------------------------------------------------------------------------- /designer/icons/text_sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_sub.png -------------------------------------------------------------------------------- /tests/support/update1.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/update1.apkg -------------------------------------------------------------------------------- /tests/support/update2.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/update2.apkg -------------------------------------------------------------------------------- /designer/icons/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/arrow-down.png -------------------------------------------------------------------------------- /designer/icons/clock-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/clock-icon.png -------------------------------------------------------------------------------- /designer/icons/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/configure.png -------------------------------------------------------------------------------- /designer/icons/contents2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/contents2.png -------------------------------------------------------------------------------- /designer/icons/deletetag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/deletetag.png -------------------------------------------------------------------------------- /designer/icons/deletetag16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/deletetag16.png -------------------------------------------------------------------------------- /designer/icons/edit-find 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-find 2.png -------------------------------------------------------------------------------- /designer/icons/edit-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-find.png -------------------------------------------------------------------------------- /designer/icons/edit-redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-redo.png -------------------------------------------------------------------------------- /designer/icons/edit-rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-rename.png -------------------------------------------------------------------------------- /designer/icons/edit-undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-undo.png -------------------------------------------------------------------------------- /designer/icons/editclear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/editclear.png -------------------------------------------------------------------------------- /designer/icons/editdelete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/editdelete.png -------------------------------------------------------------------------------- /designer/icons/fileclose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/fileclose.png -------------------------------------------------------------------------------- /designer/icons/games-solve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/games-solve.png -------------------------------------------------------------------------------- /designer/icons/go-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/go-previous.png -------------------------------------------------------------------------------- /designer/icons/help-hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/help-hint.png -------------------------------------------------------------------------------- /designer/icons/kbugbuster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/kbugbuster.png -------------------------------------------------------------------------------- /designer/icons/khtml_kget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/khtml_kget.png -------------------------------------------------------------------------------- /designer/icons/math_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/math_matrix.png -------------------------------------------------------------------------------- /designer/icons/math_sqrt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/math_sqrt.png -------------------------------------------------------------------------------- /designer/icons/paperclip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/paperclip.png -------------------------------------------------------------------------------- /designer/icons/pause_off16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/pause_off16.png -------------------------------------------------------------------------------- /designer/icons/player-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/player-time.png -------------------------------------------------------------------------------- /designer/icons/plus-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/plus-circle.png -------------------------------------------------------------------------------- /designer/icons/spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/spreadsheet.png -------------------------------------------------------------------------------- /designer/icons/star_off16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/star_off16.png -------------------------------------------------------------------------------- /designer/icons/stock_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/stock_group.png -------------------------------------------------------------------------------- /designer/icons/text-speak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text-speak.png -------------------------------------------------------------------------------- /designer/icons/text_bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_bold.png -------------------------------------------------------------------------------- /designer/icons/text_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_clear.png -------------------------------------------------------------------------------- /designer/icons/text_cloze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_cloze.png -------------------------------------------------------------------------------- /designer/icons/text_italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_italic.png -------------------------------------------------------------------------------- /designer/icons/text_remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_remove.png -------------------------------------------------------------------------------- /designer/icons/text_super.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_super.png -------------------------------------------------------------------------------- /designer/icons/text_under.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/text_under.png -------------------------------------------------------------------------------- /designer/icons/view_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view_text.png -------------------------------------------------------------------------------- /tests/support/anki12-due.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/anki12-due.anki -------------------------------------------------------------------------------- /tests/support/diffmodels1.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodels1.anki -------------------------------------------------------------------------------- /tests/support/diffmodels2.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodels2.anki -------------------------------------------------------------------------------- /tests/support/suspended12.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/suspended12.anki -------------------------------------------------------------------------------- /designer/icons/document-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/document-new.png -------------------------------------------------------------------------------- /designer/icons/folder_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/folder_image.png -------------------------------------------------------------------------------- /designer/icons/folder_sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/folder_sound.png -------------------------------------------------------------------------------- /designer/icons/go-jump-today.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/go-jump-today.png -------------------------------------------------------------------------------- /designer/icons/help-contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/help-contents.png -------------------------------------------------------------------------------- /designer/icons/kpersonalizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/kpersonalizer.png -------------------------------------------------------------------------------- /designer/icons/media-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/media-record.png -------------------------------------------------------------------------------- /designer/icons/sqlitebrowser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/sqlitebrowser.png -------------------------------------------------------------------------------- /designer/icons/user-identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/user-identity.png -------------------------------------------------------------------------------- /designer/icons/view-pim-news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-pim-news.png -------------------------------------------------------------------------------- /designer/icons/view-refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-refresh.png -------------------------------------------------------------------------------- /tests/support/anki12-broken.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/anki12-broken.anki -------------------------------------------------------------------------------- /tests/support/anki2-alpha.anki2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/anki2-alpha.anki2 -------------------------------------------------------------------------------- /tests/support/diffmodels2-1.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodels2-1.apkg -------------------------------------------------------------------------------- /tests/support/diffmodels2-2.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodels2-2.apkg -------------------------------------------------------------------------------- /tests/support/invalid-ords.anki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/invalid-ords.anki -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.mo 4 | *\# 5 | .*.swp 6 | .coverage 7 | aqt/forms 8 | locale 9 | .idea 10 | -------------------------------------------------------------------------------- /designer/icons/anki-logo-thin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/anki-logo-thin.png -------------------------------------------------------------------------------- /designer/icons/anki-logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/anki-logo_black.png -------------------------------------------------------------------------------- /designer/icons/anki-logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/anki-logo_white.png -------------------------------------------------------------------------------- /designer/icons/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/application-exit.png -------------------------------------------------------------------------------- /designer/icons/arrow-up-double.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/arrow-up-double.png -------------------------------------------------------------------------------- /designer/icons/document-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/document-export.png -------------------------------------------------------------------------------- /designer/icons/document-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/document-import.png -------------------------------------------------------------------------------- /designer/icons/emblem-favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/emblem-favorite.png -------------------------------------------------------------------------------- /designer/icons/emblem-important.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/emblem-important.png -------------------------------------------------------------------------------- /designer/icons/mail-attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/mail-attachment.png -------------------------------------------------------------------------------- /designer/icons/product_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/product_design.png -------------------------------------------------------------------------------- /designer/icons/system-shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/system-shutdown.png -------------------------------------------------------------------------------- /designer/icons/view-statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-statistics.png -------------------------------------------------------------------------------- /designer/icons/arrow-down-double.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/arrow-down-double.png -------------------------------------------------------------------------------- /designer/icons/edit-find-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/edit-find-replace.png -------------------------------------------------------------------------------- /designer/icons/package_games_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/package_games_card.png -------------------------------------------------------------------------------- /designer/icons/preferences-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/preferences-plugin.png -------------------------------------------------------------------------------- /designer/icons/stock_new_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/stock_new_template.png -------------------------------------------------------------------------------- /designer/icons/view-pim-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-pim-calendar.png -------------------------------------------------------------------------------- /designer/icons/emblem-favorite-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/emblem-favorite-dark.png -------------------------------------------------------------------------------- /designer/icons/emblem-favorite-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/emblem-favorite-off.png -------------------------------------------------------------------------------- /designer/icons/format-stroke-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/format-stroke-color.png -------------------------------------------------------------------------------- /designer/icons/media-playback-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/media-playback-pause.png -------------------------------------------------------------------------------- /designer/icons/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/media-playback-start.png -------------------------------------------------------------------------------- /designer/icons/media-playback-start2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/media-playback-start2.png -------------------------------------------------------------------------------- /designer/icons/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/media-playback-stop.png -------------------------------------------------------------------------------- /designer/icons/view-calendar-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-calendar-tasks.png -------------------------------------------------------------------------------- /designer/icons/view-sort-ascending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-sort-ascending.png -------------------------------------------------------------------------------- /designer/icons/view-sort-descending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/view-sort-descending.png -------------------------------------------------------------------------------- /tests/support/diffmodeltemplates-1.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodeltemplates-1.apkg -------------------------------------------------------------------------------- /tests/support/diffmodeltemplates-2.apkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/tests/support/diffmodeltemplates-2.apkg -------------------------------------------------------------------------------- /designer/icons/stock_new_template_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/stock_new_template_blue.png -------------------------------------------------------------------------------- /designer/icons/stock_new_template_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/stock_new_template_red.png -------------------------------------------------------------------------------- /designer/icons/system-software-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/system-software-update.png -------------------------------------------------------------------------------- /designer/icons/preferences-desktop-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/preferences-desktop-font.png -------------------------------------------------------------------------------- /designer/icons/stock_new_template_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/anki/master/designer/icons/stock_new_template_green.png -------------------------------------------------------------------------------- /tests/support/text-2fields.txt: -------------------------------------------------------------------------------- 1 | # this is a test file 2 | 食べる to eat 3 | 飲む to drink 4 | テスト test 5 | to eat 食べる 6 | 飲む to drink 7 | 多すぎる too many fields 8 | not, enough, fields 9 | 遊ぶ 10 | to play 11 | -------------------------------------------------------------------------------- /anki/template/__init__.py: -------------------------------------------------------------------------------- 1 | from anki.template.template import Template 2 | from anki.template.view import View 3 | 4 | def render(template, context=None, **kwargs): 5 | context = context and context.copy() or {} 6 | context.update(kwargs) 7 | return Template(template, context).render() 8 | -------------------------------------------------------------------------------- /anki.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Anki 3 | Comment=An intelligent spaced-repetition memory training program 4 | GenericName=Flashcards 5 | Exec=anki 6 | TryExec=anki 7 | Icon=anki 8 | Categories=Education;Languages;KDE;Qt; 9 | Terminal=false 10 | Type=Application 11 | Version=1.0 12 | MimeType=application/x-apkg;application/x-anki; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | install: 5 | - sudo apt-get update 6 | - sudo apt-get install portaudio19-dev 7 | - pip install -r requirements.txt 8 | - pip install nose 9 | - pip install coveralls 10 | 11 | script: nosetests ./tests --with-coverage --cover-package=./anki 12 | 13 | after_success: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /designer/icons/_sources.txt: -------------------------------------------------------------------------------- 1 | -Anki icon by Alex Fraser (CC GNU GPL) 2 | -Deck icon: Be Box Icons (non-commercial use) 3 | -Deck due/new icons from: 4 | http://led24.de/iconset 5 | http://p.yusukekamiyamane.com/ 6 | -Other icons obtained from KDE themes (GPL/LGPL) 7 | 8 | Note that some of the icons have been modified to fit in with Anki better 9 | (grayscaled, cropped, etc). 10 | -------------------------------------------------------------------------------- /anki/template/README.anki: -------------------------------------------------------------------------------- 1 | Anki uses a modified version of Pystache to provide Mustache-like syntax. 2 | Behaviour is a little different from standard Mustache: 3 | 4 | - {{text}} returns text verbatim with no HTML escaping 5 | - {{{text}}} does the same and exists for backwards compatibility 6 | - partial rendering is disabled for security reasons 7 | - certain keywords like 'cloze' are treated specially 8 | 9 | -------------------------------------------------------------------------------- /anki/statsbg.py: -------------------------------------------------------------------------------- 1 | # from subtlepatterns.com 2 | bg = """\ 3 | iVBORw0KGgoAAAANSUhEUgAAABIAAAANCAMAAACTkM4rAAAAM1BMVEXy8vLz8/P5+fn19fXt7e329vb4+Pj09PTv7+/u7u739/fw8PD7+/vx8fHr6+v6+vrs7Oz2LjW2AAAAkUlEQVR42g3KyXHAQAwDQYAQj12ItvOP1qqZZwMMPVnd06XToQvz4L2HDQ2iRgkvA7yPPB+JD+OUPnfzZ0JNZh6kkQus5NUmR7g4Jpxv5XN6nYWNmtlq9o3zuK6w3XRsE1pQIEGPIsdtTP3m2cYwlPv6MbL8/QASsKppZefyDmJPbxvxa/NrX1TJ1yp20fhj9D+SiAWWLU8myQAAAABJRU5ErkJggg== 4 | """ 5 | -------------------------------------------------------------------------------- /anki.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Anki 1.2 deck 6 | 7 | 8 | 9 | 10 | Anki 2.0 deck 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /anki/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import sys 6 | 7 | if sys.version_info[0] < 3: 8 | raise Exception("Anki should be run with Python 3") 9 | elif sys.version_info[1] < 4: 10 | raise Exception("Anki requires Python 3.4+") 11 | 12 | version="2.1.0a4" # build scripts grep this line, so preserve formatting 13 | from anki.storage import Collection 14 | __all__ = ["Collection"] 15 | -------------------------------------------------------------------------------- /tools/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Usage: 4 | # tools/tests.sh # run all tests 5 | # tools/tests.sh decks # test only test_decks.py 6 | # coverage=1 tools/tests.sh # run with coverage test 7 | 8 | BIN="$(cd "`dirname "$0"`"; pwd)" 9 | export PYTHONPATH=${BIN}/..:${PYTHONPATH} 10 | 11 | dir=. 12 | 13 | if [ x$1 = x ]; then 14 | lim="tests" 15 | else 16 | lim="tests.test_$1" 17 | fi 18 | 19 | if [ x$coverage != x ]; then 20 | args="--with-coverage" 21 | else 22 | args="" 23 | echo "Call with coverage=1 to run coverage tests" 24 | fi 25 | (cd $dir && nosetests3 -vs $lim $args --cover-package=anki) 26 | 27 | -------------------------------------------------------------------------------- /anki/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | class AnkiError(Exception): 6 | def __init__(self, type, **data): 7 | self.type = type 8 | self.data = data 9 | def __str__(self): 10 | m = self.type 11 | if self.data: 12 | m += ": %s" % repr(self.data) 13 | return m 14 | 15 | class DeckRenameError(Exception): 16 | def __init__(self, description): 17 | self.description = description 18 | def __str__(self): 19 | return "Couldn't rename deck: " + self.description 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Anki 2 | ------------------------------------- 3 | 4 | This is the development branch of Anki. 5 | 6 | For stable builds, please see http://ankisrs.net. 7 | 8 | For non-developers who want to try this development code, 9 | the easiest way is to use a binary package - please see 10 | https://anki.tenderapp.com/discussions/beta-testing 11 | 12 | If you're a developer, you can learn more about building Anki 13 | in README.development. 14 | 15 | [![Build Status](https://travis-ci.org/dae/anki.svg?branch=master)](https://travis-ci.org/dae/anki) 16 | 17 | [![Coverage Status](https://coveralls.io/repos/github/dae/anki/badge.svg?branch=master)](https://coveralls.io/github/dae/anki?branch=master) 18 | 19 | -------------------------------------------------------------------------------- /anki/template/hint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from anki.hooks import addHook 6 | from anki.lang import _ 7 | 8 | def hint(txt, extra, context, tag, fullname): 9 | if not txt.strip(): 10 | return "" 11 | # random id 12 | domid = "hint%d" % id(txt) 13 | return """ 14 | 16 | %s 17 | """ % (domid, _("Show %s") % tag, domid, txt) 18 | 19 | def install(): 20 | addHook('fmod_hint', hint) 21 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | from tests.shared import getEmptyCol 5 | 6 | def test_stats(): 7 | d = getEmptyCol() 8 | f = d.newNote() 9 | f['Front'] = "foo" 10 | d.addNote(f) 11 | c = f.cards()[0] 12 | # card stats 13 | assert d.cardStats(c) 14 | d.reset() 15 | c = d.sched.getCard() 16 | d.sched.answerCard(c, 3) 17 | d.sched.answerCard(c, 2) 18 | assert d.cardStats(c) 19 | 20 | def test_graphs_empty(): 21 | d = getEmptyCol() 22 | assert d.stats().report() 23 | 24 | def test_graphs(): 25 | from anki import Collection as aopen 26 | d = aopen(os.path.expanduser("~/test.anki2")) 27 | g = d.stats() 28 | rep = g.report() 29 | open(os.path.expanduser("~/test.html"), "w").write(rep) 30 | return 31 | -------------------------------------------------------------------------------- /anki/importing/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from anki.importing.csvfile import TextImporter 6 | from anki.importing.apkg import AnkiPackageImporter 7 | from anki.importing.anki2 import Anki2Importer 8 | from anki.importing.supermemo_xml import SupermemoXmlImporter 9 | from anki.importing.mnemo import MnemosyneImporter 10 | from anki.importing.pauker import PaukerImporter 11 | from anki.lang import _ 12 | 13 | Importers = ( 14 | (_("Text separated by tabs or semicolons (*)"), TextImporter), 15 | (_("Packaged Anki Deck (*.apkg *.zip)"), AnkiPackageImporter), 16 | (_("Mnemosyne 2.0 Deck (*.db)"), MnemosyneImporter), 17 | (_("Supermemo XML export (*.xml)"), SupermemoXmlImporter), 18 | (_("Pauker 1.8 Lesson (*.pau.gz)"), PaukerImporter), 19 | ) 20 | -------------------------------------------------------------------------------- /anki/importing/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from anki.utils import maxID 6 | 7 | # Base importer 8 | ########################################################################## 9 | 10 | class Importer(object): 11 | 12 | needMapper = False 13 | needDelimiter = False 14 | 15 | def __init__(self, col, file): 16 | self.file = file 17 | self.log = [] 18 | self.col = col 19 | self.total = 0 20 | 21 | def run(self): 22 | pass 23 | 24 | # Timestamps 25 | ###################################################################### 26 | # It's too inefficient to check for existing ids on every object, 27 | # and a previous import may have created timestamps in the future, so we 28 | # need to make sure our starting point is safe. 29 | 30 | def _prepareTS(self): 31 | self._ts = maxID(self.dst.db) 32 | 33 | def ts(self): 34 | self._ts += 1 35 | return self._ts 36 | -------------------------------------------------------------------------------- /anki/template/furigana.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | # Based off Kieran Clancy's initial implementation. 5 | 6 | import re 7 | from anki.hooks import addHook 8 | 9 | r = r' ?([^ >]+?)\[(.+?)\]' 10 | ruby = r'\1\2' 11 | 12 | def noSound(repl): 13 | def func(match): 14 | if match.group(2).startswith("sound:"): 15 | # return without modification 16 | return match.group(0) 17 | else: 18 | return re.sub(r, repl, match.group(0)) 19 | return func 20 | 21 | def _munge(s): 22 | return s.replace(" ", " ") 23 | 24 | def kanji(txt, *args): 25 | return re.sub(r, noSound(r'\1'), _munge(txt)) 26 | 27 | def kana(txt, *args): 28 | return re.sub(r, noSound(r'\2'), _munge(txt)) 29 | 30 | def furigana(txt, *args): 31 | return re.sub(r, noSound(ruby), _munge(txt)) 32 | 33 | def install(): 34 | addHook('fmod_kanji', kanji) 35 | addHook('fmod_kana', kana) 36 | addHook('fmod_furigana', furigana) 37 | -------------------------------------------------------------------------------- /LICENSE.logo: -------------------------------------------------------------------------------- 1 | Anki's logo is copyright Alex Fraser, and is licensed under the AGPL3 2 | like the rest of Anki's code, but with extra provisions to allow more 3 | liberal use of the logo under limited conditions. 4 | 5 | Under the following conditions, Anki's logo may be included in blogs, 6 | newspaper articles, books, videos and other such material about Anki. 7 | 8 | * The logo must be used to refer to Anki, AnkiMobile or AnkiDroid, 9 | and a link to http://ankisrs.net must be provided. When your 10 | content is focused specifically on AnkiDroid, a link to 11 | http://code.google.com/p/ankidroid/wiki/Index may be provided 12 | instead of the first link. 13 | * The branding of your website or publication must be more prominent 14 | than the Anki logo, to make it clear that the text/video/etc you 15 | are publishing is your own content and not something originating 16 | from the Anki project. 17 | * The logo must be used unmodified - no cropping, changing of colours 18 | or adding or deleting content is allowed. You may resize the image 19 | provided the horizontal and vertical dimensions are resized 20 | equally. 21 | -------------------------------------------------------------------------------- /anki/template/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tools/build_ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # generate python files based on the designer ui files. pyuic5 and pyrcc5 4 | # should be on the path. 5 | # 6 | 7 | if [ ! -d "designer" ] 8 | then 9 | echo "Please run this from the project root" 10 | exit 11 | fi 12 | 13 | mkdir -p aqt/forms 14 | 15 | init=aqt/forms/__init__.py 16 | temp=aqt/forms/scratch 17 | rm -f $init $temp 18 | echo "# This file auto-generated by build_ui.sh. Don't edit." > $init 19 | echo "__all__ = [" >> $init 20 | 21 | echo "Generating forms.." 22 | for i in designer/*.ui 23 | do 24 | base=$(basename $i .ui) 25 | py="aqt/forms/${base}.py" 26 | echo " \"$base\"," >> $init 27 | echo "from . import $base" >> $temp 28 | if [ $i -nt $py ]; then 29 | echo " * "$py 30 | pyuic5 --from-imports $i -o $py 31 | # munge the output to use gettext 32 | perl -pi.bak -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' $py 33 | rm $py.bak 34 | fi 35 | done 36 | echo "]" >> $init 37 | cat $temp >> $init 38 | rm $temp 39 | 40 | echo "Building resources.." 41 | pyrcc5 designer/icons.qrc -o aqt/forms/icons_rc.py 42 | -------------------------------------------------------------------------------- /tests/shared.py: -------------------------------------------------------------------------------- 1 | import tempfile, os, shutil 2 | from anki import Collection as aopen 3 | 4 | def assertException(exception, func): 5 | found = False 6 | try: 7 | func() 8 | except exception: 9 | found = True 10 | assert found 11 | 12 | 13 | # Creating new decks is expensive. Just do it once, and then spin off 14 | # copies from the master. 15 | def getEmptyCol(): 16 | if len(getEmptyCol.master) == 0: 17 | (fd, nam) = tempfile.mkstemp(suffix=".anki2") 18 | os.close(fd) 19 | os.unlink(nam) 20 | col = aopen(nam) 21 | col.db.close() 22 | getEmptyCol.master = nam 23 | (fd, nam) = tempfile.mkstemp(suffix=".anki2") 24 | shutil.copy(getEmptyCol.master, nam) 25 | return aopen(nam) 26 | 27 | getEmptyCol.master = "" 28 | 29 | # Fallback for when the DB needs options passed in. 30 | def getEmptyDeckWith(**kwargs): 31 | (fd, nam) = tempfile.mkstemp(suffix=".anki2") 32 | os.close(fd) 33 | os.unlink(nam) 34 | return aopen(nam, **kwargs) 35 | 36 | def getUpgradeDeckPath(name="anki12.anki"): 37 | src = os.path.join(testDir, "support", name) 38 | (fd, dst) = tempfile.mkstemp(suffix=".anki2") 39 | shutil.copy(src, dst) 40 | return dst 41 | 42 | testDir = os.path.dirname(__file__) 43 | -------------------------------------------------------------------------------- /aqt/sound.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 3 | 4 | from aqt.qt import * 5 | 6 | import time 7 | from anki.sound import Recorder 8 | from aqt.utils import saveGeom, restoreGeom 9 | 10 | def getAudio(parent, encode=True): 11 | "Record and return filename" 12 | # record first 13 | r = Recorder() 14 | mb = QMessageBox(parent) 15 | restoreGeom(mb, "audioRecorder") 16 | mb.setWindowTitle("Anki") 17 | mb.setIconPixmap(QPixmap(":/icons/media-record.png")) 18 | but = QPushButton(_(" Stop")) 19 | but.setIcon(QIcon(":/icons/media-playback-stop.png")) 20 | #but.setIconSize(QSize(32, 32)) 21 | mb.addButton(but, QMessageBox.RejectRole) 22 | t = time.time() 23 | r.start() 24 | QApplication.instance().processEvents() 25 | while not mb.clickedButton(): 26 | txt =_("Recording...
Time: %0.1f") 27 | mb.setText(txt % (time.time() - t)) 28 | mb.show() 29 | QApplication.instance().processEvents() 30 | saveGeom(mb, "audioRecorder") 31 | # ensure at least a second captured 32 | while time.time() - t < 1: 33 | time.sleep(0.1) 34 | r.stop() 35 | # process 36 | r.postprocess(encode) 37 | return r.file() 38 | -------------------------------------------------------------------------------- /aqt/qt.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 3 | 4 | # fixme: make sure not to optimize imports on this file 5 | 6 | import sip 7 | import os 8 | 9 | # fix buggy ubuntu12.04 display of language selector 10 | os.environ["LIBOVERLAY_SCROLLBAR"] = "0" 11 | 12 | from anki.utils import isWin, isMac 13 | 14 | from PyQt5.Qt import * 15 | # trigger explicit message in case of missing libraries 16 | # instead of silently failing to import 17 | from PyQt5.QtWebEngineWidgets import QWebEnginePage 18 | 19 | def debug(): 20 | from PyQt5.QtCore import pyqtRemoveInputHook 21 | from pdb import set_trace 22 | pyqtRemoveInputHook() 23 | set_trace() 24 | 25 | import sys, traceback 26 | 27 | if os.environ.get("DEBUG"): 28 | def info(type, value, tb): 29 | from PyQt5.QtCore import pyqtRemoveInputHook 30 | for line in traceback.format_exception(type, value, tb): 31 | sys.stdout.write(line) 32 | pyqtRemoveInputHook() 33 | from pdb import pm 34 | pm() 35 | sys.excepthook = info 36 | 37 | qtmajor = (QT_VERSION & 0xff0000) >> 16 38 | qtminor = (QT_VERSION & 0x00ff00) >> 8 39 | 40 | if qtmajor < 5 or (qtmajor == 5 and qtminor < 5): 41 | raise Exception("Qt must be 5.5+") 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=/usr 2 | 3 | all: 4 | @echo "You can run Anki with ./runanki" 5 | @echo "If you wish to install it system wide, type 'sudo make install'" 6 | @echo "Uninstall with 'sudo make uninstall'" 7 | 8 | install: 9 | rm -rf ${DESTDIR}${PREFIX}/share/anki 10 | mkdir -p ${DESTDIR}${PREFIX}/share/anki 11 | cp -av * ${DESTDIR}${PREFIX}/share/anki/ 12 | cd ${DESTDIR}${PREFIX}/share/anki && (\ 13 | mv runanki ${DESTDIR}${PREFIX}/local/bin/anki;\ 14 | test -d ${DESTDIR}${PREFIX}/share/pixmaps &&\ 15 | mv anki.xpm anki.png ${DESTDIR}${PREFIX}/share/pixmaps/;\ 16 | mv anki.desktop ${DESTDIR}${PREFIX}/share/applications;\ 17 | mv anki.1 ${DESTDIR}${PREFIX}/share/man/man1/) 18 | xdg-mime install anki.xml --novendor 19 | xdg-mime default anki.desktop application/x-anki 20 | xdg-mime default anki.desktop application/x-apkg 21 | @echo 22 | @echo "Install complete." 23 | 24 | uninstall: 25 | rm -rf ${DESTDIR}${PREFIX}/share/anki 26 | rm -rf ${DESTDIR}${PREFIX}/local/bin/anki 27 | rm -rf ${DESTDIR}${PREFIX}/share/pixmaps/anki.xpm 28 | rm -rf ${DESTDIR}${PREFIX}/share/pixmaps/anki.png 29 | rm -rf ${DESTDIR}${PREFIX}/share/applications/anki.desktop 30 | rm -rf ${DESTDIR}${PREFIX}/share/man/man1/anki.1 31 | -xdg-mime uninstall ${DESTDIR}${PREFIX}/share/mime/packages/anki.xml 32 | @echo 33 | @echo "Uninstall complete." 34 | -------------------------------------------------------------------------------- /designer/preview.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 335 10 | 282 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | Front Preview 24 | 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Back Preview 36 | 37 | 38 | 39 | 0 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /anki/importing/apkg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import zipfile, os 6 | import unicodedata 7 | from anki.utils import tmpfile, json 8 | from anki.importing.anki2 import Anki2Importer 9 | 10 | class AnkiPackageImporter(Anki2Importer): 11 | 12 | def run(self): 13 | # extract the deck from the zip file 14 | self.zip = z = zipfile.ZipFile(self.file) 15 | col = z.read("collection.anki2") 16 | colpath = tmpfile(suffix=".anki2") 17 | open(colpath, "wb").write(col) 18 | self.file = colpath 19 | # we need the media dict in advance, and we'll need a map of fname -> 20 | # number to use during the import 21 | self.nameToNum = {} 22 | for k, v in list(json.loads(z.read("media").decode("utf8")).items()): 23 | self.nameToNum[v] = k 24 | # run anki2 importer 25 | Anki2Importer.run(self) 26 | # import static media 27 | for file, c in list(self.nameToNum.items()): 28 | if not file.startswith("_") and not file.startswith("latex-"): 29 | continue 30 | path = os.path.join(self.col.media.dir(), 31 | unicodedata.normalize("NFC", file)) 32 | if not os.path.exists(path): 33 | open(path, "wb").write(z.read(c)) 34 | 35 | def _srcMediaData(self, fname): 36 | if fname in self.nameToNum: 37 | return self.zip.read(self.nameToNum[fname]) 38 | return None 39 | -------------------------------------------------------------------------------- /designer/debug.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 643 10 | 580 11 | 12 | 13 | 14 | Debug Console 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 1 23 | 24 | 25 | 26 | 27 | 16777215 28 | 100 29 | 30 | 31 | 32 | QPlainTextEdit::NoWrap 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 0 41 | 8 42 | 43 | 44 | 45 | 46 | Courier 47 | 48 | 49 | 50 | Qt::ClickFocus 51 | 52 | 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /designer/edithtml.ui: -------------------------------------------------------------------------------- 1 | 2 | Dialog 3 | 4 | 5 | 6 | 0 7 | 0 8 | 400 9 | 300 10 | 11 | 12 | 13 | HTML Editor 14 | 15 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | QDialogButtonBox::Close|QDialogButtonBox::Help 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | buttonBox 39 | accepted() 40 | Dialog 41 | accept() 42 | 43 | 44 | 248 45 | 254 46 | 47 | 48 | 157 49 | 274 50 | 51 | 52 | 53 | 54 | buttonBox 55 | rejected() 56 | Dialog 57 | reject() 58 | 59 | 60 | 316 61 | 260 62 | 63 | 64 | 286 65 | 274 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /designer/editcurrent.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 3 19 | 20 | 21 | 12 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Qt::Horizontal 30 | 31 | 32 | QDialogButtonBox::Close 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | buttonBox 42 | accepted() 43 | Dialog 44 | accept() 45 | 46 | 47 | 248 48 | 254 49 | 50 | 51 | 157 52 | 274 53 | 54 | 55 | 56 | 57 | buttonBox 58 | rejected() 59 | Dialog 60 | reject() 61 | 62 | 63 | 316 64 | 260 65 | 66 | 67 | 286 68 | 274 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /designer/setlang.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Anki 15 | 16 | 17 | 18 | 19 | 20 | Interface language: 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Horizontal 31 | 32 | 33 | QDialogButtonBox::Ok 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | buttonBox 43 | accepted() 44 | Dialog 45 | accept() 46 | 47 | 48 | 248 49 | 254 50 | 51 | 52 | 157 53 | 274 54 | 55 | 56 | 57 | 58 | buttonBox 59 | rejected() 60 | Dialog 61 | reject() 62 | 63 | 64 | 316 65 | 260 66 | 67 | 68 | 286 69 | 274 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /designer/editaddon.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 753 10 | 475 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | Courier 10 Pitch 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Qt::Horizontal 33 | 34 | 35 | QDialogButtonBox::Cancel|QDialogButtonBox::Save 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | buttonBox 45 | accepted() 46 | Dialog 47 | accept() 48 | 49 | 50 | 248 51 | 254 52 | 53 | 54 | 157 55 | 274 56 | 57 | 58 | 59 | 60 | buttonBox 61 | rejected() 62 | Dialog 63 | reject() 64 | 65 | 66 | 316 67 | 260 68 | 69 | 70 | 286 71 | 274 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/support/supermemo1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3572 4 | 5 | 6 | 1 7 | 8 | Topic 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 40326 18 | 19 | aoeu 20 | 21 | Topic 22 | 23 | 24 | 40327 25 | 26 | 1-400 27 | 28 | Topic 29 | 30 | 31 | 40615 32 | 33 | aoeu 34 | 35 | Topic 36 | 37 | 38 | 10247 39 | 40 | Item 41 | 42 | 43 | aoeu 44 | 45 | aoeu 46 | 47 | 48 | 49 | 1844 50 | 51 | 7 52 | 53 | 0 54 | 55 | 19.09.2002 56 | 57 | 5,701 58 | 59 | 2,452 60 | 61 | 62 | 63 | 64 | 65 | Topic 66 | 67 | 68 | aoeu 69 | 70 | 71 | 72 | 73 | 0 74 | 75 | 0 76 | 77 | 0 78 | 79 | 04.08.2000 80 | 81 | 3,000 82 | 83 | 0,000 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /anki/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | """\ 6 | Hooks - hook management and tools for extending Anki 7 | ============================================================================== 8 | 9 | To find available hooks, grep for runHook and runFilter in the source code. 10 | 11 | Instrumenting allows you to modify functions that don't have hooks available. 12 | If you call wrap() with pos='around', the original function will not be called 13 | automatically but can be called with _old(). 14 | """ 15 | 16 | # Hooks 17 | ############################################################################## 18 | 19 | _hooks = {} 20 | 21 | 22 | def runHook(hook, *args): 23 | """Run all functions on hook.""" 24 | hook = _hooks.get(hook) 25 | if hook: 26 | for func in hook: 27 | func(*args) 28 | 29 | 30 | def runFilter(hook, arg, *args): 31 | hook = _hooks.get(hook) 32 | if hook: 33 | for func in hook: 34 | arg = func(arg, *args) 35 | return arg 36 | 37 | 38 | def addHook(hook, func): 39 | """Add a function to hook. Ignore if already on hook.""" 40 | if not _hooks.get(hook): 41 | _hooks[hook] = [] 42 | if func not in _hooks[hook]: 43 | _hooks[hook].append(func) 44 | 45 | 46 | def remHook(hook, func): 47 | """Remove a function if is on hook.""" 48 | hook = _hooks.get(hook, []) 49 | if func in hook: 50 | hook.remove(func) 51 | 52 | 53 | # Instrumenting 54 | ############################################################################## 55 | 56 | 57 | def wrap(old, new, pos="after"): 58 | """Override an existing function.""" 59 | 60 | def repl(*args, **kwargs): 61 | if pos == "after": 62 | old(*args, **kwargs) 63 | return new(*args, **kwargs) 64 | elif pos == "before": 65 | new(*args, **kwargs) 66 | return old(*args, **kwargs) 67 | else: 68 | return new(_old=old, *args, **kwargs) 69 | 70 | return repl 71 | -------------------------------------------------------------------------------- /designer/studydeck.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Study Deck 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Filter: 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::Horizontal 38 | 39 | 40 | QDialogButtonBox::Cancel|QDialogButtonBox::Help 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | buttonBox 50 | accepted() 51 | Dialog 52 | accept() 53 | 54 | 55 | 248 56 | 254 57 | 58 | 59 | 157 60 | 274 61 | 62 | 63 | 64 | 65 | buttonBox 66 | rejected() 67 | Dialog 68 | reject() 69 | 70 | 71 | 316 72 | 260 73 | 74 | 75 | 286 76 | 274 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /designer/addmodel.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 285 10 | 269 11 | 12 | 13 | 14 | Add Note Type 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | QAbstractItemView::NoEditTriggers 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::Horizontal 40 | 41 | 42 | QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | buttonBox 52 | accepted() 53 | Dialog 54 | accept() 55 | 56 | 57 | 266 58 | 353 59 | 60 | 61 | 157 62 | 274 63 | 64 | 65 | 66 | 67 | buttonBox 68 | rejected() 69 | Dialog 70 | reject() 71 | 72 | 73 | 334 74 | 353 75 | 76 | 77 | 286 78 | 274 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /anki/consts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import os 6 | from anki.lang import _ 7 | 8 | # whether new cards should be mixed with reviews, or shown first or last 9 | NEW_CARDS_DISTRIBUTE = 0 10 | NEW_CARDS_LAST = 1 11 | NEW_CARDS_FIRST = 2 12 | 13 | # new card insertion order 14 | NEW_CARDS_RANDOM = 0 15 | NEW_CARDS_DUE = 1 16 | 17 | # removal types 18 | REM_CARD = 0 19 | REM_NOTE = 1 20 | REM_DECK = 2 21 | 22 | # count display 23 | COUNT_ANSWERED = 0 24 | COUNT_REMAINING = 1 25 | 26 | # media log 27 | MEDIA_ADD = 0 28 | MEDIA_REM = 1 29 | 30 | # dynamic deck order 31 | DYN_OLDEST = 0 32 | DYN_RANDOM = 1 33 | DYN_SMALLINT = 2 34 | DYN_BIGINT = 3 35 | DYN_LAPSES = 4 36 | DYN_ADDED = 5 37 | DYN_DUE = 6 38 | DYN_REVADDED = 7 39 | DYN_DUEPRIORITY = 8 40 | 41 | DYN_MAX_SIZE = 99999 42 | 43 | # model types 44 | MODEL_STD = 0 45 | MODEL_CLOZE = 1 46 | 47 | # deck schema & syncing vars 48 | SCHEMA_VERSION = 11 49 | SYNC_ZIP_SIZE = int(2.5*1024*1024) 50 | SYNC_ZIP_COUNT = 25 51 | SYNC_BASE = "https://ankiweb.net/" 52 | SYNC_MEDIA_BASE = "https://msync.ankiweb.net/" 53 | SYNC_VER = 8 54 | 55 | HELP_SITE="http://ankisrs.net/docs/manual.html" 56 | 57 | # Labels 58 | ########################################################################## 59 | 60 | def newCardOrderLabels(): 61 | return { 62 | 0: _("Show new cards in random order"), 63 | 1: _("Show new cards in order added") 64 | } 65 | 66 | def newCardSchedulingLabels(): 67 | return { 68 | 0: _("Mix new cards and reviews"), 69 | 1: _("Show new cards after reviews"), 70 | 2: _("Show new cards before reviews"), 71 | } 72 | 73 | def alignmentLabels(): 74 | return { 75 | 0: _("Center"), 76 | 1: _("Left"), 77 | 2: _("Right"), 78 | } 79 | 80 | def dynOrderLabels(): 81 | return { 82 | 0: _("Oldest seen first"), 83 | 1: _("Random"), 84 | 2: _("Increasing intervals"), 85 | 3: _("Decreasing intervals"), 86 | 4: _("Most lapses"), 87 | 5: _("Order added"), 88 | 6: _("Order due"), 89 | 7: _("Latest added first"), 90 | 8: _("Relative overdueness"), 91 | } 92 | -------------------------------------------------------------------------------- /designer/setgroup.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 433 10 | 143 11 | 12 | 13 | 14 | Anki 15 | 16 | 17 | 18 | 19 | 20 | Move cards to deck: 21 | 22 | 23 | 24 | 25 | 26 | 27 | Qt::Vertical 28 | 29 | 30 | 31 | 20 32 | 40 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Qt::Horizontal 41 | 42 | 43 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 44 | 45 | 46 | 47 | 48 | 49 | 50 | buttonBox 51 | 52 | 53 | 54 | 55 | buttonBox 56 | accepted() 57 | Dialog 58 | accept() 59 | 60 | 61 | 224 62 | 192 63 | 64 | 65 | 157 66 | 213 67 | 68 | 69 | 70 | 71 | buttonBox 72 | rejected() 73 | Dialog 74 | reject() 75 | 76 | 77 | 292 78 | 198 79 | 80 | 81 | 286 82 | 213 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /anki.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" First parameter, NAME, should be all caps 3 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 4 | .\" other parameters are allowed: see man(7), man(1) 5 | .TH ANKI 1 "August 11, 2007" 6 | .\" Please adjust this date whenever revising the manpage. 7 | .\" 8 | .\" Some roff macros, for reference: 9 | .\" .nh disable hyphenation 10 | .\" .hy enable hyphenation 11 | .\" .ad l left justify 12 | .\" .ad b justify to both left and right margins 13 | .\" .nf disable filling 14 | .\" .fi enable filling 15 | .\" .br insert line break 16 | .\" .sp insert n+1 empty lines 17 | .\" for manpage-specific macros, see man(7) 18 | .SH NAME 19 | anki \- flexible, intelligent flashcard program 20 | .SH DESCRIPTION 21 | \fBAnki\fP is a program designed to help you remember facts (such as words and 22 | phrases in a foreign language) as easily, quickly and efficiently as possible. 23 | To do this, it tracks how well you remember each fact, and uses that 24 | information to optimally schedule review times. With a minimal amount of 25 | effort, you can greatly increase the amount of material you remember, making 26 | study more productive, and more fun. 27 | 28 | Anki is based on a theory called \fIspaced repetition\fP. In simple terms, it means 29 | that each time you review some material, you should wait longer than last time 30 | before reviewing it again. This maximizes the time spent studying difficult 31 | material and minimizes the time spent reviewing things you already know. The 32 | concept is simple, but the vast majority of memory trainers and flashcard 33 | programs out there either avoid the concept all together, or implement 34 | inflexible and suboptimal methods that were originally designed for pen and 35 | paper. 36 | 37 | .SH OPTIONS 38 | .B \-b ~/.anki 39 | Use ~/.anki instead of ~/Anki as Anki's base folder 40 | 41 | .B \-p ProfileName 42 | Load a specific profile 43 | 44 | .B \-l 45 | Start the program in a specific language (de=German, en=English, etc) 46 | .SH SEE ALSO 47 | Anki home page: 48 | .SH AUTHOR 49 | Anki was written by Damien Elmes . 50 | .PP 51 | This manual page was written by Nicholas Breen , 52 | for the Debian project (but may be used by others), and has been 53 | updated for Anki 2 by Damien Elmes. 54 | -------------------------------------------------------------------------------- /designer/changemap.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeMap 4 | 5 | 6 | 7 | 0 8 | 0 9 | 391 10 | 360 11 | 12 | 13 | 14 | Import 15 | 16 | 17 | 18 | 19 | 20 | Target field: 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::Horizontal 34 | 35 | 36 | QDialogButtonBox::Ok 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | buttonBox 46 | accepted() 47 | ChangeMap 48 | accept() 49 | 50 | 51 | 254 52 | 355 53 | 54 | 55 | 157 56 | 274 57 | 58 | 59 | 60 | 61 | buttonBox 62 | rejected() 63 | ChangeMap 64 | reject() 65 | 66 | 67 | 322 68 | 355 69 | 70 | 71 | 286 72 | 274 73 | 74 | 75 | 76 | 77 | fields 78 | doubleClicked(QModelIndex) 79 | ChangeMap 80 | accept() 81 | 82 | 83 | 99 84 | 123 85 | 86 | 87 | 193 88 | 5 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /aqt/editcurrent.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | import aqt.editor 7 | from aqt.utils import saveGeom, restoreGeom 8 | from anki.hooks import addHook, remHook 9 | from anki.utils import isMac 10 | 11 | class EditCurrent(QDialog): 12 | 13 | def __init__(self, mw): 14 | if isMac: 15 | # use a separate window on os x so we can a clean menu 16 | QDialog.__init__(self, None, Qt.Window) 17 | else: 18 | QDialog.__init__(self, mw) 19 | QDialog.__init__(self, None, Qt.Window) 20 | self.mw = mw 21 | self.form = aqt.forms.editcurrent.Ui_Dialog() 22 | self.form.setupUi(self) 23 | self.setWindowTitle(_("Edit Current")) 24 | self.setMinimumHeight(400) 25 | self.setMinimumWidth(500) 26 | self.rejected.connect(self.onSave) 27 | self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut( 28 | QKeySequence("Ctrl+Return")) 29 | self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) 30 | self.editor.setNote(self.mw.reviewer.card.note()) 31 | restoreGeom(self, "editcurrent") 32 | addHook("reset", self.onReset) 33 | self.mw.requireReset() 34 | self.show() 35 | # reset focus after open 36 | self.editor.web.setFocus() 37 | 38 | def onReset(self): 39 | # lazy approach for now: throw away edits 40 | try: 41 | n = self.mw.reviewer.card.note() 42 | n.load() 43 | except: 44 | # card's been deleted 45 | remHook("reset", self.onReset) 46 | self.editor.setNote(None) 47 | self.mw.reset() 48 | aqt.dialogs.close("EditCurrent") 49 | self.close() 50 | return 51 | self.editor.setNote(n) 52 | 53 | def onSave(self): 54 | self.editor.saveNow(self._onSave) 55 | 56 | def _onSave(self): 57 | remHook("reset", self.onReset) 58 | r = self.mw.reviewer 59 | try: 60 | r.card.load() 61 | except: 62 | # card was removed by clayout 63 | pass 64 | else: 65 | self.mw.reviewer.cardQueue.append(self.mw.reviewer.card) 66 | self.mw.moveToState("review") 67 | saveGeom(self, "editcurrent") 68 | aqt.dialogs.close("EditCurrent") 69 | 70 | def canClose(self): 71 | return True 72 | -------------------------------------------------------------------------------- /tests/test_undo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import time 4 | from tests.shared import getEmptyCol 5 | from anki.consts import * 6 | 7 | def test_op(): 8 | d = getEmptyCol() 9 | # should have no undo by default 10 | assert not d.undoName() 11 | # let's adjust a study option 12 | d.save("studyopts") 13 | d.conf['abc'] = 5 14 | # it should be listed as undoable 15 | assert d.undoName() == "studyopts" 16 | # with about 5 minutes until it's clobbered 17 | assert time.time() - d._lastSave < 1 18 | # undoing should restore the old value 19 | d.undo() 20 | assert not d.undoName() 21 | assert 'abc' not in d.conf 22 | # an (auto)save will clear the undo 23 | d.save("foo") 24 | assert d.undoName() == "foo" 25 | d.save() 26 | assert not d.undoName() 27 | # and a review will, too 28 | d.save("add") 29 | f = d.newNote() 30 | f['Front'] = "one" 31 | d.addNote(f) 32 | d.reset() 33 | assert d.undoName() == "add" 34 | c = d.sched.getCard() 35 | d.sched.answerCard(c, 2) 36 | assert d.undoName() == "Review" 37 | 38 | def test_review(): 39 | d = getEmptyCol() 40 | d.conf['counts'] = COUNT_REMAINING 41 | f = d.newNote() 42 | f['Front'] = "one" 43 | d.addNote(f) 44 | d.reset() 45 | assert not d.undoName() 46 | # answer 47 | assert d.sched.counts() == (1, 0, 0) 48 | c = d.sched.getCard() 49 | assert c.queue == 0 50 | d.sched.answerCard(c, 2) 51 | assert c.left == 1001 52 | assert d.sched.counts() == (0, 1, 0) 53 | assert c.queue == 1 54 | # undo 55 | assert d.undoName() 56 | d.undo() 57 | d.reset() 58 | assert d.sched.counts() == (1, 0, 0) 59 | c.load() 60 | assert c.queue == 0 61 | assert c.left != 1001 62 | assert not d.undoName() 63 | # we should be able to undo multiple answers too 64 | f = d.newNote() 65 | f['Front'] = "two" 66 | d.addNote(f) 67 | d.reset() 68 | assert d.sched.counts() == (2, 0, 0) 69 | c = d.sched.getCard() 70 | d.sched.answerCard(c, 2) 71 | c = d.sched.getCard() 72 | d.sched.answerCard(c, 2) 73 | assert d.sched.counts() == (0, 2, 0) 74 | d.undo() 75 | d.reset() 76 | assert d.sched.counts() == (1, 1, 0) 77 | d.undo() 78 | d.reset() 79 | assert d.sched.counts() == (2, 0, 0) 80 | # performing a normal op will clear the review queue 81 | c = d.sched.getCard() 82 | d.sched.answerCard(c, 2) 83 | assert d.undoName() == "Review" 84 | d.save("foo") 85 | assert d.undoName() == "foo" 86 | d.undo() 87 | assert not d.undoName() 88 | 89 | 90 | -------------------------------------------------------------------------------- /aqt/mediasrv.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | from http import HTTPStatus 7 | import http.server 8 | import errno 9 | 10 | class MediaServer(QThread): 11 | 12 | def run(self): 13 | self.port = 10000 14 | self.server = None 15 | while not self.server: 16 | try: 17 | self.server = http.server.HTTPServer( 18 | ("localhost", self.port), RequestHandler) 19 | except OSError as e: 20 | if e.errno == errno.EADDRINUSE: 21 | self.port += 1 22 | continue 23 | raise 24 | break 25 | self.server.serve_forever() 26 | 27 | class RequestHandler(http.server.SimpleHTTPRequestHandler): 28 | 29 | def do_GET(self): 30 | f = self.send_head() 31 | if f: 32 | try: 33 | self.copyfile(f, self.wfile) 34 | except Exception as e: 35 | if os.getenv("ANKIDEV"): 36 | print("http server caught exception:", e) 37 | else: 38 | # swallow it - user likely surfed away from 39 | # review screen before an image had finished 40 | # downloading 41 | pass 42 | finally: 43 | f.close() 44 | 45 | def send_head(self): 46 | path = self.translate_path(self.path) 47 | if os.path.isdir(path): 48 | self.send_error(HTTPStatus.NOT_FOUND, "File not found") 49 | return None 50 | ctype = self.guess_type(path) 51 | try: 52 | f = open(path, 'rb') 53 | except OSError: 54 | self.send_error(HTTPStatus.NOT_FOUND, "File not found") 55 | return None 56 | try: 57 | self.send_response(HTTPStatus.OK) 58 | self.send_header("Content-type", ctype) 59 | fs = os.fstat(f.fileno()) 60 | self.send_header("Content-Length", str(fs[6])) 61 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 62 | self.end_headers() 63 | return f 64 | except: 65 | f.close() 66 | raise 67 | 68 | def log_message(self, format, *args): 69 | if not os.getenv("ANKIDEV"): 70 | return 71 | print("%s - - [%s] %s" % 72 | (self.address_string(), 73 | self.log_date_time_string(), 74 | format%args)) 75 | -------------------------------------------------------------------------------- /aqt/about.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | import aqt.forms 7 | from aqt import appVersion 8 | from aqt.utils import openLink 9 | 10 | def show(mw): 11 | dialog = QDialog(mw) 12 | mw.setupDialogGC(dialog) 13 | abt = aqt.forms.about.Ui_About() 14 | abt.setupUi(dialog) 15 | abouttext = "
" 16 | abouttext += '

' + _("Anki is a friendly, intelligent spaced learning \ 17 | system. It's free and open source.") 18 | abouttext += "

"+_("Anki is licensed under the AGPL3 license. Please see " 19 | "the license file in the source distribution for more information.") 20 | abouttext += '

' + _("Version %s") % appVersion + '
' 21 | abouttext += ("Qt %s PyQt %s
") % (QT_VERSION_STR, PYQT_VERSION_STR) 22 | abouttext += (_("Visit website") % aqt.appWebsite) + \ 23 | "" 24 | abouttext += '

' + _("Written by Damien Elmes, with patches, translation,\ 25 | testing and design from:

%(cont)s") % {'cont': """Aaron Harsh, Ádám Szegi, 26 | Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, C. van Rooyen, Charlene Barina, 27 | Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, 28 | Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail, 29 | Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7, 30 | Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Jussi Määttä, Kieran Clancy, LaC, Laurent Steffan, 31 | Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur, 32 | Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael 33 | Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas 34 | Laxström, Nguyễn Hào Khôi, Norbert Nagold, Ole Guldberg, 35 | Pcsl88, Petr Michalec, Piotr Kubowicz, Richard Colley, Roland Sieker, Samson Melamed, 36 | Stefaan De Pooter, Silja Ijas, Snezana Lukic, Soren Bjornstad, Susanna Björverud, Sylvain Durand, 37 | Tacutu, Timm Preetz, Timo Paulssen, Ursus, Victor Suba, Volker Jansen, 38 | Volodymyr Goncharenko, Xtru, 赵金鹏 and 黃文龍."""} 39 | abouttext += '

' + _("""\ 40 | The icons were obtained from various sources; please see the Anki source 41 | for credits.""") 42 | abouttext += '

' + _("If you have contributed and are not on this list, \ 43 | please get in touch.") 44 | abouttext += '

' + _("A big thanks to all the people who have provided \ 45 | suggestions, bug reports and donations.") 46 | abt.label.setHtml(abouttext) 47 | dialog.adjustSize() 48 | dialog.show() 49 | dialog.exec_() 50 | -------------------------------------------------------------------------------- /aqt/update.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 3 | 4 | import urllib.request, urllib.parse, urllib.error 5 | import urllib.request, urllib.error, urllib.parse 6 | import time 7 | 8 | from aqt.qt import * 9 | import aqt 10 | from aqt.utils import openLink 11 | from anki.utils import json, platDesc 12 | from aqt.utils import showText 13 | 14 | 15 | class LatestVersionFinder(QThread): 16 | 17 | newVerAvail = pyqtSignal(str) 18 | newMsg = pyqtSignal(dict) 19 | clockIsOff = pyqtSignal(float) 20 | 21 | def __init__(self, main): 22 | QThread.__init__(self) 23 | self.main = main 24 | self.config = main.pm.meta 25 | 26 | def _data(self): 27 | d = {"ver": aqt.appVersion, 28 | "os": platDesc(), 29 | "id": self.config['id'], 30 | "lm": self.config['lastMsg'], 31 | "crt": self.config['created']} 32 | return d 33 | 34 | def run(self): 35 | if not self.config['updates']: 36 | return 37 | d = self._data() 38 | d['proto'] = 1 39 | d = urllib.parse.urlencode(d).encode("utf8") 40 | try: 41 | f = urllib.request.urlopen(aqt.appUpdate, d) 42 | resp = f.read() 43 | if not resp: 44 | print("update check load failed") 45 | return 46 | resp = json.loads(resp.decode("utf8")) 47 | except: 48 | # behind proxy, corrupt message, etc 49 | print("update check failed") 50 | return 51 | if resp['msg']: 52 | self.newMsg.emit(resp) 53 | if resp['ver']: 54 | self.newVerAvail.emit(resp['ver']) 55 | diff = resp['time'] - time.time() 56 | if abs(diff) > 300: 57 | self.clockIsOff.emit(diff) 58 | 59 | def askAndUpdate(mw, ver): 60 | baseStr = ( 61 | _('''

Anki Updated

Anki %s has been released.

''') % 62 | ver) 63 | msg = QMessageBox(mw) 64 | msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 65 | msg.setIcon(QMessageBox.Information) 66 | msg.setText(baseStr + _("Would you like to download it now?")) 67 | button = QPushButton(_("Ignore this update")) 68 | msg.addButton(button, QMessageBox.RejectRole) 69 | msg.setDefaultButton(QMessageBox.Yes) 70 | ret = msg.exec_() 71 | if msg.clickedButton() == button: 72 | # ignore this update 73 | mw.pm.meta['suppressUpdate'] = ver 74 | elif ret == QMessageBox.Yes: 75 | openLink(aqt.appWebsite) 76 | 77 | def showMessages(mw, data): 78 | showText(data['msg'], parent=mw, type="html") 79 | mw.pm.meta['lastMsg'] = data['msgId'] 80 | -------------------------------------------------------------------------------- /anki/importing/pauker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Andreas Klauer 3 | # License: BSD-3 4 | 5 | import gzip, math, random, time, cgi 6 | import xml.etree.ElementTree as ET 7 | from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard 8 | from anki.stdmodels import addForwardReverse 9 | 10 | ONE_DAY = 60*60*24 11 | 12 | class PaukerImporter(NoteImporter): 13 | '''Import Pauker 1.8 Lesson (*.pau.gz)''' 14 | 15 | needMapper = False 16 | allowHTML = True 17 | 18 | def run(self): 19 | model = addForwardReverse(self.col) 20 | model['name'] = "Pauker" 21 | self.col.models.save(model) 22 | self.col.models.setCurrent(model) 23 | self.model = model 24 | self.initMapping() 25 | NoteImporter.run(self) 26 | 27 | def fields(self): 28 | '''Pauker is Front/Back''' 29 | return 2 30 | 31 | def foreignNotes(self): 32 | '''Build and return a list of notes.''' 33 | notes = [] 34 | 35 | try: 36 | f = gzip.open(self.file) 37 | tree = ET.parse(f) 38 | lesson = tree.getroot() 39 | assert lesson.tag == "Lesson" 40 | finally: 41 | f.close() 42 | 43 | index = -4 44 | 45 | for batch in lesson.findall('./Batch'): 46 | index += 1 47 | 48 | for card in batch.findall('./Card'): 49 | # Create a note for this card. 50 | front = card.findtext('./FrontSide/Text') 51 | back = card.findtext('./ReverseSide/Text') 52 | note = ForeignNote() 53 | note.fields = [cgi.escape(x.strip()).replace('\n','
').replace(' ','  ') for x in [front,back]] 54 | notes.append(note) 55 | 56 | # Determine due date for cards. 57 | frontdue = card.find('./FrontSide[@LearnedTimestamp]') 58 | backdue = card.find('./ReverseSide[@Batch][@LearnedTimestamp]') 59 | 60 | if frontdue is not None: 61 | note.cards[0] = self._learnedCard(index, int(frontdue.attrib['LearnedTimestamp'])) 62 | 63 | if backdue is not None: 64 | note.cards[1] = self._learnedCard(int(backdue.attrib['Batch']), int(backdue.attrib['LearnedTimestamp'])) 65 | 66 | return notes 67 | 68 | def _learnedCard(self, batch, timestamp): 69 | ivl = math.exp(batch) 70 | now = time.time() 71 | due = ivl - (now - timestamp/1000.0)/ONE_DAY 72 | fc = ForeignCard() 73 | fc.due = self.col.sched.today + int(due+0.5) 74 | fc.ivl = random.randint(int(ivl*0.90), int(ivl+0.5)) 75 | fc.factor = random.randint(1500,2500) 76 | return fc 77 | -------------------------------------------------------------------------------- /designer/about.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | About 4 | 5 | 6 | 7 | 0 8 | 0 9 | 410 10 | 664 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | About Anki 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | 39 | about:blank 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Horizontal 48 | 49 | 50 | QDialogButtonBox::Ok 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | AnkiWebView 59 | QWidget 60 |
aqt/webview
61 | 1 62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | buttonBox 70 | accepted() 71 | About 72 | accept() 73 | 74 | 75 | 248 76 | 254 77 | 78 | 79 | 157 80 | 274 81 | 82 | 83 | 84 | 85 | buttonBox 86 | rejected() 87 | About 88 | reject() 89 | 90 | 91 | 316 92 | 260 93 | 94 | 95 | 286 96 | 274 97 | 98 | 99 | 100 | 101 |
102 | -------------------------------------------------------------------------------- /anki/stdmodels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from anki.lang import _ 6 | from anki.consts import MODEL_CLOZE 7 | 8 | models = [] 9 | 10 | # Basic 11 | ########################################################################## 12 | 13 | def addBasicModel(col): 14 | mm = col.models 15 | m = mm.new(_("Basic")) 16 | fm = mm.newField(_("Front")) 17 | mm.addField(m, fm) 18 | fm = mm.newField(_("Back")) 19 | mm.addField(m, fm) 20 | t = mm.newTemplate(_("Card 1")) 21 | t['qfmt'] = "{{"+_("Front")+"}}" 22 | t['afmt'] = "{{FrontSide}}\n\n
\n\n"+"{{"+_("Back")+"}}" 23 | mm.addTemplate(m, t) 24 | mm.add(m) 25 | return m 26 | 27 | models.append((lambda: _("Basic"), addBasicModel)) 28 | 29 | # Forward & Reverse 30 | ########################################################################## 31 | 32 | def addForwardReverse(col): 33 | mm = col.models 34 | m = addBasicModel(col) 35 | m['name'] = _("Basic (and reversed card)") 36 | t = mm.newTemplate(_("Card 2")) 37 | t['qfmt'] = "{{"+_("Back")+"}}" 38 | t['afmt'] = "{{FrontSide}}\n\n
\n\n"+"{{"+_("Front")+"}}" 39 | mm.addTemplate(m, t) 40 | return m 41 | 42 | models.append((lambda: _("Basic (and reversed card)"), addForwardReverse)) 43 | 44 | # Forward & Optional Reverse 45 | ########################################################################## 46 | 47 | def addForwardOptionalReverse(col): 48 | mm = col.models 49 | m = addBasicModel(col) 50 | m['name'] = _("Basic (optional reversed card)") 51 | av = _("Add Reverse") 52 | fm = mm.newField(av) 53 | mm.addField(m, fm) 54 | t = mm.newTemplate(_("Card 2")) 55 | t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av) 56 | t['afmt'] = "{{FrontSide}}\n\n
\n\n"+"{{"+_("Front")+"}}" 57 | mm.addTemplate(m, t) 58 | return m 59 | 60 | models.append((lambda: _("Basic (optional reversed card)"), 61 | addForwardOptionalReverse)) 62 | 63 | # Cloze 64 | ########################################################################## 65 | 66 | def addClozeModel(col): 67 | mm = col.models 68 | m = mm.new(_("Cloze")) 69 | m['type'] = MODEL_CLOZE 70 | txt = _("Text") 71 | fm = mm.newField(txt) 72 | mm.addField(m, fm) 73 | fm = mm.newField(_("Extra")) 74 | mm.addField(m, fm) 75 | t = mm.newTemplate(_("Cloze")) 76 | fmt = "{{cloze:%s}}" % txt 77 | m['css'] += """ 78 | .cloze { 79 | font-weight: bold; 80 | color: blue; 81 | }""" 82 | t['qfmt'] = fmt 83 | t['afmt'] = fmt + "
\n{{%s}}" % _("Extra") 84 | mm.addTemplate(m, t) 85 | mm.add(m) 86 | return m 87 | 88 | models.append((lambda: _("Cloze"), addClozeModel)) 89 | -------------------------------------------------------------------------------- /designer/getaddons.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 367 10 | 204 11 | 12 | 13 | 14 | Install Add-on 15 | 16 | 17 | 18 | 19 | 20 | To browse add-ons, please click the browse button below.<br><br>When you've found an add-on you like, please paste its code below. 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Vertical 31 | 32 | 33 | 34 | 20 35 | 40 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Code: 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Qt::Horizontal 58 | 59 | 60 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | buttonBox 70 | accepted() 71 | Dialog 72 | accept() 73 | 74 | 75 | 248 76 | 254 77 | 78 | 79 | 157 80 | 274 81 | 82 | 83 | 84 | 85 | buttonBox 86 | rejected() 87 | Dialog 88 | reject() 89 | 90 | 91 | 316 92 | 260 93 | 94 | 95 | 286 96 | 274 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /aqt/modelchooser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | from anki.hooks import addHook, remHook, runHook 7 | from aqt.utils import shortcut 8 | import aqt 9 | 10 | class ModelChooser(QHBoxLayout): 11 | 12 | def __init__(self, mw, widget, label=True): 13 | QHBoxLayout.__init__(self) 14 | self.widget = widget 15 | self.mw = mw 16 | self.deck = mw.col 17 | self.label = label 18 | self.setContentsMargins(0,0,0,0) 19 | self.setSpacing(8) 20 | self.setupModels() 21 | addHook('reset', self.onReset) 22 | self.widget.setLayout(self) 23 | 24 | def setupModels(self): 25 | if self.label: 26 | self.modelLabel = QLabel(_("Type")) 27 | self.addWidget(self.modelLabel) 28 | # models box 29 | self.models = QPushButton() 30 | #self.models.setStyleSheet("* { text-align: left; }") 31 | self.models.setToolTip(shortcut(_("Change Note Type (Ctrl+N)"))) 32 | s = QShortcut(QKeySequence(_("Ctrl+N")), self.widget, activated=self.onModelChange) 33 | self.models.setAutoDefault(False) 34 | self.addWidget(self.models) 35 | self.models.clicked.connect(self.onModelChange) 36 | # layout 37 | sizePolicy = QSizePolicy( 38 | QSizePolicy.Policy(7), 39 | QSizePolicy.Policy(0)) 40 | self.models.setSizePolicy(sizePolicy) 41 | self.updateModels() 42 | 43 | def cleanup(self): 44 | remHook('reset', self.onReset) 45 | 46 | def onReset(self): 47 | self.updateModels() 48 | 49 | def show(self): 50 | self.widget.show() 51 | 52 | def hide(self): 53 | self.widget.hide() 54 | 55 | def onEdit(self): 56 | import aqt.models 57 | aqt.models.Models(self.mw, self.widget) 58 | 59 | def onModelChange(self): 60 | from aqt.studydeck import StudyDeck 61 | current = self.deck.models.current()['name'] 62 | # edit button 63 | edit = QPushButton(_("Manage"), clicked=self.onEdit) 64 | def nameFunc(): 65 | return sorted(self.deck.models.allNames()) 66 | ret = StudyDeck( 67 | self.mw, names=nameFunc, 68 | accept=_("Choose"), title=_("Choose Note Type"), 69 | help="_notes", current=current, parent=self.widget, 70 | buttons=[edit], cancel=True, geomKey="selectModel") 71 | if not ret.name: 72 | return 73 | m = self.deck.models.byName(ret.name) 74 | self.deck.conf['curModel'] = m['id'] 75 | cdeck = self.deck.decks.current() 76 | cdeck['mid'] = m['id'] 77 | self.deck.decks.save(cdeck) 78 | runHook("currentModelChanged") 79 | self.mw.reset() 80 | 81 | def updateModels(self): 82 | self.models.setText(self.deck.models.current()['name']) 83 | -------------------------------------------------------------------------------- /aqt/downloader.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import time, re, traceback 6 | from aqt.qt import * 7 | from anki.sync import httpCon 8 | from aqt.utils import showWarning 9 | from anki.hooks import addHook, remHook 10 | import aqt.sync # monkey-patches httplib2 11 | 12 | def download(mw, code): 13 | "Download addon/deck from AnkiWeb. On success caller must stop progress diag." 14 | # check code is valid 15 | try: 16 | code = int(code) 17 | except ValueError: 18 | showWarning(_("Invalid code.")) 19 | return 20 | # create downloading thread 21 | thread = Downloader(code) 22 | def onRecv(): 23 | try: 24 | mw.progress.update(label="%dKB downloaded" % (thread.recvTotal/1024)) 25 | except NameError: 26 | # some users report the following error on long downloads 27 | # NameError: free variable 'mw' referenced before assignment in enclosing scope 28 | # unsure why this is happening, but guard against throwing the 29 | # error 30 | pass 31 | thread.recv.connect(onRecv) 32 | thread.start() 33 | mw.progress.start(immediate=True) 34 | while not thread.isFinished(): 35 | mw.app.processEvents() 36 | thread.wait(100) 37 | if not thread.error: 38 | # success 39 | return thread.data, thread.fname 40 | else: 41 | mw.progress.finish() 42 | showWarning(_("Download failed: %s") % thread.error) 43 | 44 | class Downloader(QThread): 45 | 46 | recv = pyqtSignal() 47 | 48 | def __init__(self, code): 49 | QThread.__init__(self) 50 | self.code = code 51 | self.error = None 52 | 53 | def run(self): 54 | # setup progress handler 55 | self.byteUpdate = time.time() 56 | self.recvTotal = 0 57 | def canPost(): 58 | if (time.time() - self.byteUpdate) > 0.1: 59 | self.byteUpdate = time.time() 60 | return True 61 | def recvEvent(bytes): 62 | self.recvTotal += bytes 63 | if canPost(): 64 | self.recv.emit() 65 | addHook("httpRecv", recvEvent) 66 | con = httpCon() 67 | try: 68 | resp, cont = con.request( 69 | aqt.appShared + "download/%d" % self.code) 70 | except Exception as e: 71 | exc = traceback.format_exc() 72 | try: 73 | self.error = str(e[0]) 74 | except: 75 | self.error = str(exc) 76 | return 77 | finally: 78 | remHook("httpRecv", recvEvent) 79 | if resp['status'] == '200': 80 | self.error = None 81 | self.fname = re.match("attachment; filename=(.+)", 82 | resp['content-disposition']).group(1) 83 | self.data = cont 84 | elif resp['status'] == '403': 85 | self.error = _("Invalid code.") 86 | else: 87 | self.error = _("Error downloading: %s") % resp['status'] 88 | -------------------------------------------------------------------------------- /aqt/tagedit.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 3 | 4 | from aqt.qt import * 5 | import re 6 | 7 | class TagEdit(QLineEdit): 8 | 9 | lostFocus = pyqtSignal() 10 | 11 | # 0 = tags, 1 = decks 12 | def __init__(self, parent, type=0): 13 | QLineEdit.__init__(self, parent) 14 | self.col = None 15 | self.model = QStringListModel() 16 | self.type = type 17 | if type == 0: 18 | self.completer = TagCompleter(self.model, parent, self) 19 | else: 20 | self.completer = QCompleter(self.model, parent) 21 | self.completer.setCompletionMode(QCompleter.PopupCompletion) 22 | self.completer.setCaseSensitivity(Qt.CaseInsensitive) 23 | self.setCompleter(self.completer) 24 | 25 | def setCol(self, col): 26 | "Set the current col, updating list of available tags." 27 | self.col = col 28 | if self.type == 0: 29 | l = sorted(self.col.tags.all()) 30 | else: 31 | l = sorted(self.col.decks.allNames()) 32 | self.model.setStringList(l) 33 | 34 | def focusInEvent(self, evt): 35 | QLineEdit.focusInEvent(self, evt) 36 | self.showCompleter() 37 | 38 | def keyPressEvent(self, evt): 39 | if evt.key() in (Qt.Key_Enter, Qt.Key_Return): 40 | self.hideCompleter() 41 | QWidget.keyPressEvent(self, evt) 42 | return 43 | QLineEdit.keyPressEvent(self, evt) 44 | if not evt.text(): 45 | # if it's a modifier, don't show 46 | return 47 | if evt.key() not in ( 48 | Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Space, 49 | Qt.Key_Tab, Qt.Key_Backspace, Qt.Key_Delete): 50 | self.showCompleter() 51 | 52 | def showCompleter(self): 53 | self.completer.setCompletionPrefix(self.text()) 54 | self.completer.complete() 55 | 56 | def focusOutEvent(self, evt): 57 | QLineEdit.focusOutEvent(self, evt) 58 | self.lostFocus.emit() 59 | self.completer.popup().hide() 60 | 61 | def hideCompleter(self): 62 | self.completer.popup().hide() 63 | 64 | class TagCompleter(QCompleter): 65 | 66 | def __init__(self, model, parent, edit, *args): 67 | QCompleter.__init__(self, model, parent) 68 | self.tags = [] 69 | self.edit = edit 70 | self.cursor = None 71 | 72 | def splitPath(self, tags): 73 | tags = tags.strip() 74 | tags = re.sub(" +", " ", tags) 75 | self.tags = self.edit.col.tags.split(tags) 76 | self.tags.append("") 77 | p = self.edit.cursorPosition() 78 | self.cursor = tags.count(" ", 0, p) 79 | return [self.tags[self.cursor]] 80 | 81 | def pathFromIndex(self, idx): 82 | if self.cursor is None: 83 | return self.edit.text() 84 | ret = QCompleter.pathFromIndex(self, idx) 85 | self.tags[self.cursor] = ret 86 | try: 87 | self.tags.remove("") 88 | except ValueError: 89 | pass 90 | return " ".join(self.tags) 91 | -------------------------------------------------------------------------------- /anki/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import os 6 | import time 7 | 8 | try: 9 | from pysqlite2 import dbapi2 as sqlite 10 | vi = sqlite.version_info 11 | if vi[0] > 2 or vi[1] > 6: 12 | # latest pysqlite breaks anki 13 | raise ImportError() 14 | except ImportError: 15 | from sqlite3 import dbapi2 as sqlite 16 | 17 | Error = sqlite.Error 18 | 19 | class DB(object): 20 | def __init__(self, path, timeout=0): 21 | self._db = sqlite.connect(path, timeout=timeout) 22 | self._path = path 23 | self.echo = os.environ.get("DBECHO") 24 | self.mod = False 25 | 26 | def execute(self, sql, *a, **ka): 27 | s = sql.strip().lower() 28 | # mark modified? 29 | for stmt in "insert", "update", "delete": 30 | if s.startswith(stmt): 31 | self.mod = True 32 | t = time.time() 33 | if ka: 34 | # execute("...where id = :id", id=5) 35 | res = self._db.execute(sql, ka) 36 | else: 37 | # execute("...where id = ?", 5) 38 | res = self._db.execute(sql, a) 39 | if self.echo: 40 | #print a, ka 41 | print(sql, "%0.3fms" % ((time.time() - t)*1000)) 42 | if self.echo == "2": 43 | print(a, ka) 44 | return res 45 | 46 | def executemany(self, sql, l): 47 | self.mod = True 48 | t = time.time() 49 | self._db.executemany(sql, l) 50 | if self.echo: 51 | print(sql, "%0.3fms" % ((time.time() - t)*1000)) 52 | if self.echo == "2": 53 | print(l) 54 | 55 | def commit(self): 56 | t = time.time() 57 | self._db.commit() 58 | if self.echo: 59 | print("commit %0.3fms" % ((time.time() - t)*1000)) 60 | 61 | def executescript(self, sql): 62 | self.mod = True 63 | if self.echo: 64 | print(sql) 65 | self._db.executescript(sql) 66 | 67 | def rollback(self): 68 | self._db.rollback() 69 | 70 | def scalar(self, *a, **kw): 71 | res = self.execute(*a, **kw).fetchone() 72 | if res: 73 | return res[0] 74 | return None 75 | 76 | def all(self, *a, **kw): 77 | return self.execute(*a, **kw).fetchall() 78 | 79 | def first(self, *a, **kw): 80 | c = self.execute(*a, **kw) 81 | res = c.fetchone() 82 | c.close() 83 | return res 84 | 85 | def list(self, *a, **kw): 86 | return [x[0] for x in self.execute(*a, **kw)] 87 | 88 | def close(self): 89 | self._db.close() 90 | 91 | def set_progress_handler(self, *args): 92 | self._db.set_progress_handler(*args) 93 | 94 | def __enter__(self): 95 | self._db.execute("begin") 96 | return self 97 | 98 | def __exit__(self, exc_type, *args): 99 | self._db.close() 100 | 101 | def totalChanges(self): 102 | return self._db.total_changes 103 | 104 | def interrupt(self): 105 | self._db.interrupt() 106 | -------------------------------------------------------------------------------- /aqt/deckchooser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | from anki.hooks import addHook, remHook 7 | from aqt.utils import shortcut 8 | 9 | class DeckChooser(QHBoxLayout): 10 | 11 | def __init__(self, mw, widget, label=True, start=None): 12 | QHBoxLayout.__init__(self) 13 | self.widget = widget 14 | self.mw = mw 15 | self.deck = mw.col 16 | self.label = label 17 | self.setContentsMargins(0,0,0,0) 18 | self.setSpacing(8) 19 | self.setupDecks() 20 | self.widget.setLayout(self) 21 | addHook('currentModelChanged', self.onModelChange) 22 | 23 | def setupDecks(self): 24 | if self.label: 25 | self.deckLabel = QLabel(_("Deck")) 26 | self.addWidget(self.deckLabel) 27 | # decks box 28 | self.deck = QPushButton(clicked=self.onDeckChange) 29 | self.deck.setToolTip(shortcut(_("Target Deck (Ctrl+D)"))) 30 | s = QShortcut(QKeySequence(_("Ctrl+D")), self.widget, activated=self.onDeckChange) 31 | self.addWidget(self.deck) 32 | # starting label 33 | if self.mw.col.conf.get("addToCur", True): 34 | col = self.mw.col 35 | did = col.conf['curDeck'] 36 | if col.decks.isDyn(did): 37 | # if they're reviewing, try default to current card 38 | c = self.mw.reviewer.card 39 | if self.mw.state == "review" and c: 40 | if not c.odid: 41 | did = c.did 42 | else: 43 | did = c.odid 44 | else: 45 | did = 1 46 | self.deck.setText(self.mw.col.decks.nameOrNone( 47 | did) or _("Default")) 48 | else: 49 | self.deck.setText(self.mw.col.decks.nameOrNone( 50 | self.mw.col.models.current()['did']) or _("Default")) 51 | # layout 52 | sizePolicy = QSizePolicy( 53 | QSizePolicy.Policy(7), 54 | QSizePolicy.Policy(0)) 55 | self.deck.setSizePolicy(sizePolicy) 56 | 57 | def show(self): 58 | self.widget.show() 59 | 60 | def hide(self): 61 | self.widget.hide() 62 | 63 | def cleanup(self): 64 | remHook('currentModelChanged', self.onModelChange) 65 | 66 | def onModelChange(self): 67 | if not self.mw.col.conf.get("addToCur", True): 68 | self.deck.setText(self.mw.col.decks.nameOrNone( 69 | self.mw.col.models.current()['did']) or _("Default")) 70 | 71 | def onDeckChange(self): 72 | from aqt.studydeck import StudyDeck 73 | current = self.deck.text() 74 | ret = StudyDeck( 75 | self.mw, current=current, accept=_("Choose"), 76 | title=_("Choose Deck"), help="addingnotes", 77 | cancel=False, parent=self.widget, geomKey="selectDeck") 78 | self.deck.setText(ret.name) 79 | 80 | def selectedId(self): 81 | # save deck name 82 | name = self.deck.text() 83 | if not name.strip(): 84 | did = 1 85 | else: 86 | did = self.mw.col.decks.id(name) 87 | return did 88 | -------------------------------------------------------------------------------- /designer/addcards.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 453 10 | 366 11 | 12 | 13 | 14 | Add 15 | 16 | 17 | 18 | :/icons/list-add.png:/icons/list-add.png 19 | 20 | 21 | 22 | 3 23 | 24 | 25 | 12 26 | 27 | 28 | 6 29 | 30 | 31 | 12 32 | 33 | 34 | 12 35 | 36 | 37 | 38 | 39 | 6 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | 47 | 48 | 0 49 | 10 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Qt::Horizontal 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 10 72 | 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | Qt::Horizontal 83 | 84 | 85 | QDialogButtonBox::NoButton 86 | 87 | 88 | 89 | 90 | 91 | 92 | buttonBox 93 | 94 | 95 | 96 | 97 | buttonBox 98 | rejected() 99 | Dialog 100 | reject() 101 | 102 | 103 | 301 104 | -1 105 | 106 | 107 | 286 108 | 274 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /designer/modelopts.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 276 13 | 323 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | LaTeX 28 | 29 | 30 | 31 | 32 | 33 | Header 34 | 35 | 36 | 37 | 38 | 39 | 40 | true 41 | 42 | 43 | 44 | 45 | 46 | 47 | Footer 48 | 49 | 50 | 51 | 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Qt::Horizontal 66 | 67 | 68 | QDialogButtonBox::Close|QDialogButtonBox::Help 69 | 70 | 71 | 72 | 73 | 74 | 75 | qtabwidget 76 | buttonBox 77 | latexHeader 78 | latexFooter 79 | 80 | 81 | 82 | 83 | buttonBox 84 | accepted() 85 | Dialog 86 | accept() 87 | 88 | 89 | 275 90 | 442 91 | 92 | 93 | 157 94 | 274 95 | 96 | 97 | 98 | 99 | buttonBox 100 | rejected() 101 | Dialog 102 | reject() 103 | 104 | 105 | 343 106 | 442 107 | 108 | 109 | 286 110 | 274 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /README.addons: -------------------------------------------------------------------------------- 1 | Porting add-ons to Anki 2.1 2 | --------------------------- 3 | 4 | 2.1 is still in alpha and prone to change, so you may wish to wait until it 5 | hits beta before starting to update add-ons. But if you'd like to dive in 6 | straight away, here are some tips on porting. 7 | 8 | Python 3 9 | --------- 10 | 11 | Anki 2.1 requires Python 3.4 or later. After installing Python 3 on your 12 | machine, you can use the 2to3 tool to automatically convert your existing 13 | scripts to Python 3 code on a folder by folder basis, like: 14 | 15 | 2to3-3.5 --output-dir=aqt3 -W -n aqt 16 | mv aqt aqt-old 17 | mv aqt3 aqt 18 | 19 | Most simple code can be converted automatically, but there may be parts of the 20 | code that you need to manually modify. 21 | 22 | Add-ons that don't deal with file access and bytestrings may well work on both 23 | Python 2 and 3 without any special work required. 24 | 25 | Qt5 / PyQt5 26 | ------------ 27 | 28 | The syntax for connecting signals and slots has changed in PyQt5. Recent PyQt4 29 | versions support the new syntax as well, so after updating your add-ons you 30 | may find they still work in Anki 2.0.x as well. 31 | 32 | More info is available at 33 | http://pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html 34 | 35 | Changes in Anki 36 | ---------------- 37 | 38 | Qt 5 has deprecated WebKit in favour of the Chromium-based WebEngine, so 39 | Anki's webviews are now using WebEngine. Of note: 40 | 41 | - WebEngine uses a different method of communicating back to Python. 42 | AnkiWebView() is a wrapper for webviews which provides a pycmd(str) function in 43 | Javascript which will call the ankiwebview's onBridgeCmd(str) method. Various 44 | parts of Anki's UI like reviewer.py and deckbrowser.py have had to be 45 | modified to use this. 46 | - Javascript is evaluated asynchronously, so if you need the result of a JS 47 | expression you can use ankiwebview's evalWithCallback(). 48 | - As a result of this asynchronous behaviour, editor.saveNow() now requires a 49 | callback. If your add-on performs actions in the browser, you likely need to 50 | call editor.saveNow() first and then run the rest of your code in the callback. 51 | Calls to .onSearch() will need to be changed to .search()/.onSearchActivated() 52 | as well. See the browser's .deleteNotes() for an example. 53 | - You can now debug the webviews using an external Chrome instance, by setting 54 | the env var QTWEBENGINE_REMOTE_DEBUGGING to 8080 prior to starting Anki, 55 | then surfing to localhost:8080 in Chrome. If you run into issues, try 56 | connecting with Chrome 49. 57 | 58 | Add-ons without a top level file 59 | --------------------------------- 60 | 61 | Add-ons no longer require a top level file - if you just distribute a single 62 | folder, the folder's __init__.py file will form the entry point. This will not 63 | work in 2.0.x however. 64 | 65 | Sharing updated add-ons 66 | ------------------------ 67 | 68 | If you've succeeded in making an add-on that supports both 2.0.x and 2.1.x at 69 | the same time, please feel free to upload it to the shared add-ons area. If 70 | you've decided to make a separate 2.1.x version, it's probably best to just 71 | post a link to it in your current add-on description or upload it separately. 72 | When we get closer to a release I'll look into adding separate uploads for the 73 | two versions. 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /designer/models.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 396 13 | 255 14 | 15 | 16 | 17 | Note Types 18 | 19 | 20 | 21 | 0 22 | 23 | 24 | 25 | 26 | 6 27 | 28 | 29 | 30 | 31 | 32 | 0 33 | 0 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 12 42 | 43 | 44 | 45 | 46 | Qt::Vertical 47 | 48 | 49 | QDialogButtonBox::Close|QDialogButtonBox::Help 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Qt::Vertical 61 | 62 | 63 | QSizePolicy::Minimum 64 | 65 | 66 | 67 | 20 68 | 6 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | modelsList 77 | 78 | 79 | 80 | 81 | 82 | 83 | buttonBox 84 | accepted() 85 | Dialog 86 | accept() 87 | 88 | 89 | 252 90 | 513 91 | 92 | 93 | 157 94 | 274 95 | 96 | 97 | 98 | 99 | buttonBox 100 | rejected() 101 | Dialog 102 | reject() 103 | 104 | 105 | 320 106 | 513 107 | 108 | 109 | 286 110 | 274 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /anki/lang.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: Damien Elmes 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import os, sys, re 6 | import gettext 7 | import threading 8 | 9 | langs = [ 10 | ("Afrikaans", "af"), 11 | ("Bahasa Melayu", "ms"), 12 | ("Dansk", "da"), 13 | ("Deutsch", "de"), 14 | ("Eesti", "et"), 15 | ("English", "en"), 16 | ("Español", "es"), 17 | ("Esperanto", "eo"), 18 | ("Euskara", "eu"), 19 | ("Français", "fr"), 20 | ("Galego", "gl"), 21 | ("Hrvatski", "hr"), 22 | ("Interlingua", "ia"), 23 | ("Italiano", "it"), 24 | ("Lenga d'òc", "oc"), 25 | ("Magyar", "hu"), 26 | ("Nederlands","nl"), 27 | ("Norsk","nb"), 28 | ("Occitan","oc"), 29 | ("Plattdüütsch", "nds"), 30 | ("Polski", "pl"), 31 | ("Português Brasileiro", "pt_BR"), 32 | ("Português", "pt"), 33 | ("Româneşte", "ro"), 34 | ("Slovenčina", "sk"), 35 | ("Slovenščina", "sl"), 36 | ("Suomi", "fi"), 37 | ("Svenska", "sv"), 38 | ("Tiếng Việt", "vi"), 39 | ("Türkçe", "tr"), 40 | ("Čeština", "cs"), 41 | ("Ελληνικά", "el"), 42 | ("Ελληνικά", "el"), 43 | ("босански", "bs"), 44 | ("Български", "bg"), 45 | ("Монгол хэл","mn"), 46 | ("русский язык", "ru"), 47 | ("Српски", "sr"), 48 | ("українська мова", "uk"), 49 | ("עִבְרִית", "he"), 50 | ("العربية", "ar"), 51 | ("فارسی", "fa"), 52 | ("ภาษาไทย", "th"), 53 | ("日本語", "ja"), 54 | ("简体中文", "zh_CN"), 55 | ("繁體中文", "zh_TW"), 56 | ("한국어", "ko"), 57 | ] 58 | 59 | threadLocal = threading.local() 60 | 61 | # global defaults 62 | currentLang = None 63 | currentTranslation = None 64 | 65 | def localTranslation(): 66 | "Return the translation local to this thread, or the default." 67 | if getattr(threadLocal, 'currentTranslation', None): 68 | return threadLocal.currentTranslation 69 | else: 70 | return currentTranslation 71 | 72 | def _(str): 73 | return localTranslation().gettext(str) 74 | 75 | def ngettext(single, plural, n): 76 | return localTranslation().ngettext(single, plural, n) 77 | 78 | def langDir(): 79 | dir = os.path.join(os.path.dirname( 80 | os.path.abspath(__file__)), "locale") 81 | if not os.path.isdir(dir): 82 | dir = os.path.join(os.path.dirname(sys.argv[0]), "locale") 83 | if not os.path.isdir(dir): 84 | dir = "/usr/share/anki/locale" 85 | return dir 86 | 87 | def setLang(lang, local=True): 88 | trans = gettext.translation( 89 | 'anki', langDir(), languages=[lang], fallback=True) 90 | if local: 91 | threadLocal.currentLang = lang 92 | threadLocal.currentTranslation = trans 93 | else: 94 | global currentLang, currentTranslation 95 | currentLang = lang 96 | currentTranslation = trans 97 | 98 | def getLang(): 99 | "Return the language local to this thread, or the default." 100 | if getattr(threadLocal, 'currentLang', None): 101 | return threadLocal.currentLang 102 | else: 103 | return currentLang 104 | 105 | def noHint(str): 106 | "Remove translation hint from end of string." 107 | return re.sub("(^.*?)( ?\(.+?\))?$", "\\1", str) 108 | 109 | if not currentTranslation: 110 | setLang("en_US", local=False) 111 | -------------------------------------------------------------------------------- /aqt/stats.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | import os, time 7 | from aqt.utils import saveGeom, restoreGeom, maybeHideClose, showInfo, addCloseShortcut 8 | import aqt 9 | 10 | # Deck Stats 11 | ###################################################################### 12 | 13 | class DeckStats(QDialog): 14 | 15 | def __init__(self, mw): 16 | QDialog.__init__(self, mw, Qt.Window) 17 | mw.setupDialogGC(self) 18 | self.mw = mw 19 | self.name = "deckStats" 20 | self.period = 0 21 | self.form = aqt.forms.stats.Ui_Dialog() 22 | self.oldPos = None 23 | self.wholeCollection = False 24 | self.setMinimumWidth(700) 25 | f = self.form 26 | f.setupUi(self) 27 | restoreGeom(self, self.name) 28 | b = f.buttonBox.addButton(_("Save Image"), 29 | QDialogButtonBox.ActionRole) 30 | b.clicked.connect(self.browser) 31 | b.setAutoDefault(False) 32 | f.groups.clicked.connect(lambda: self.changeScope("deck")) 33 | f.groups.setShortcut("g") 34 | f.all.clicked.connect(lambda: self.changeScope("collection")) 35 | f.month.clicked.connect(lambda: self.changePeriod(0)) 36 | f.year.clicked.connect(lambda: self.changePeriod(1)) 37 | f.life.clicked.connect(lambda: self.changePeriod(2)) 38 | maybeHideClose(self.form.buttonBox) 39 | addCloseShortcut(self) 40 | self.refresh() 41 | self.show() 42 | print("fixme: save image support in deck stats") 43 | 44 | def reject(self): 45 | saveGeom(self, self.name) 46 | QDialog.reject(self) 47 | 48 | def browser(self): 49 | name = time.strftime("-%Y-%m-%d@%H-%M-%S.png", 50 | time.localtime(time.time())) 51 | name = "anki-"+_("stats")+name 52 | desktopPath = QStandardPaths.writableLocation( 53 | QStandardPaths.DesktopLocation) 54 | if not os.path.exists(desktopPath): 55 | os.mkdir(desktopPath) 56 | path = os.path.join(desktopPath, name) 57 | p = self.form.web.page() 58 | oldsize = p.viewportSize() 59 | p.setViewportSize(p.mainFrame().contentsSize()) 60 | image = QImage(p.viewportSize(), QImage.Format_ARGB32) 61 | painter = QPainter(image) 62 | p.mainFrame().render(painter) 63 | painter.end() 64 | isOK = image.save(path, "png") 65 | if isOK: 66 | showInfo(_("An image was saved to your desktop.")) 67 | else: 68 | showInfo(_("""\ 69 | Anki could not save the image. Please check that you have permission to write \ 70 | to your desktop.""")) 71 | p.setViewportSize(oldsize) 72 | 73 | def changePeriod(self, n): 74 | self.period = n 75 | self.refresh() 76 | 77 | def changeScope(self, type): 78 | self.wholeCollection = type == "collection" 79 | self.refresh() 80 | 81 | def refresh(self): 82 | self.mw.progress.start(immediate=True) 83 | stats = self.mw.col.stats() 84 | stats.wholeCollection = self.wholeCollection 85 | self.report = stats.report(type=self.period) 86 | self.form.web.stdHtml(""+self.report+"") 87 | self.mw.progress.finish() 88 | -------------------------------------------------------------------------------- /tests/test_cards.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from tests.shared import getEmptyCol 4 | 5 | def test_previewCards(): 6 | deck = getEmptyCol() 7 | f = deck.newNote() 8 | f['Front'] = '1' 9 | f['Back'] = '2' 10 | # non-empty and active 11 | cards = deck.previewCards(f, 0) 12 | assert len(cards) == 1 13 | assert cards[0].ord == 0 14 | # all templates 15 | cards = deck.previewCards(f, 2) 16 | assert len(cards) == 1 17 | # add the note, and test existing preview 18 | deck.addNote(f) 19 | cards = deck.previewCards(f, 1) 20 | assert len(cards) == 1 21 | assert cards[0].ord == 0 22 | # make sure we haven't accidentally added cards to the db 23 | assert deck.cardCount() == 1 24 | 25 | def test_delete(): 26 | deck = getEmptyCol() 27 | f = deck.newNote() 28 | f['Front'] = '1' 29 | f['Back'] = '2' 30 | deck.addNote(f) 31 | cid = f.cards()[0].id 32 | deck.reset() 33 | deck.sched.answerCard(deck.sched.getCard(), 2) 34 | deck.remCards([cid]) 35 | assert deck.cardCount() == 0 36 | assert deck.noteCount() == 0 37 | assert deck.db.scalar("select count() from notes") == 0 38 | assert deck.db.scalar("select count() from cards") == 0 39 | assert deck.db.scalar("select count() from graves") == 2 40 | 41 | def test_misc(): 42 | d = getEmptyCol() 43 | f = d.newNote() 44 | f['Front'] = '1' 45 | f['Back'] = '2' 46 | d.addNote(f) 47 | c = f.cards()[0] 48 | id = d.models.current()['id'] 49 | assert c.template()['ord'] == 0 50 | 51 | def test_genrem(): 52 | d = getEmptyCol() 53 | f = d.newNote() 54 | f['Front'] = '1' 55 | f['Back'] = '' 56 | d.addNote(f) 57 | assert len(f.cards()) == 1 58 | m = d.models.current() 59 | mm = d.models 60 | # adding a new template should automatically create cards 61 | t = mm.newTemplate("rev") 62 | t['qfmt'] = '{{Front}}' 63 | t['afmt'] = "" 64 | mm.addTemplate(m, t) 65 | mm.save(m, templates=True) 66 | assert len(f.cards()) == 2 67 | # if the template is changed to remove cards, they'll be removed 68 | t['qfmt'] = "{{Back}}" 69 | mm.save(m, templates=True) 70 | d.remCards(d.emptyCids()) 71 | assert len(f.cards()) == 1 72 | # if we add to the note, a card should be automatically generated 73 | f.load() 74 | f['Back'] = "1" 75 | f.flush() 76 | assert len(f.cards()) == 2 77 | 78 | def test_gendeck(): 79 | d = getEmptyCol() 80 | cloze = d.models.byName("Cloze") 81 | d.models.setCurrent(cloze) 82 | f = d.newNote() 83 | f['Text'] = '{{c1::one}}' 84 | d.addNote(f) 85 | assert d.cardCount() == 1 86 | assert f.cards()[0].did == 1 87 | # set the model to a new default deck 88 | newId = d.decks.id("new") 89 | cloze['did'] = newId 90 | d.models.save(cloze) 91 | # a newly generated card should share the first card's deck 92 | f['Text'] += '{{c2::two}}' 93 | f.flush() 94 | assert f.cards()[1].did == 1 95 | # and same with multiple cards 96 | f['Text'] += '{{c3::three}}' 97 | f.flush() 98 | assert f.cards()[2].did == 1 99 | # if one of the cards is in a different deck, it should revert to the 100 | # model default 101 | c = f.cards()[1] 102 | c.did = newId 103 | c.flush() 104 | f['Text'] += '{{c4::four}}' 105 | f.flush() 106 | assert f.cards()[3].did == newId 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /designer/profiles.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 352 10 | 283 11 | 12 | 13 | 14 | Profiles 15 | 16 | 17 | 18 | :/icons/anki.png:/icons/anki.png 19 | 20 | 21 | 22 | 23 | 24 | Profile: 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Password: 39 | 40 | 41 | 42 | 43 | 44 | 45 | QLineEdit::Password 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Open 57 | 58 | 59 | 60 | 61 | 62 | 63 | Add 64 | 65 | 66 | 67 | 68 | 69 | 70 | Rename 71 | 72 | 73 | 74 | 75 | 76 | 77 | Delete 78 | 79 | 80 | 81 | 82 | 83 | 84 | Quit 85 | 86 | 87 | 88 | 89 | 90 | 91 | Qt::Vertical 92 | 93 | 94 | 95 | 20 96 | 40 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | profiles 109 | passEdit 110 | login 111 | add 112 | rename 113 | delete_2 114 | quit 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /designer/taglimit.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 361 10 | 394 11 | 12 | 13 | 14 | Selective Study 15 | 16 | 17 | 18 | 19 | 20 | Require one or more of these tags: 21 | 22 | 23 | 24 | 25 | 26 | 27 | false 28 | 29 | 30 | 31 | 0 32 | 2 33 | 34 | 35 | 36 | QAbstractItemView::MultiSelection 37 | 38 | 39 | 40 | 41 | 42 | 43 | Select tags to exclude: 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | 54 | 0 55 | 2 56 | 57 | 58 | 59 | QAbstractItemView::MultiSelection 60 | 61 | 62 | 63 | 64 | 65 | 66 | Qt::Horizontal 67 | 68 | 69 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | buttonBox 79 | accepted() 80 | Dialog 81 | accept() 82 | 83 | 84 | 358 85 | 264 86 | 87 | 88 | 157 89 | 274 90 | 91 | 92 | 93 | 94 | buttonBox 95 | rejected() 96 | Dialog 97 | reject() 98 | 99 | 100 | 316 101 | 260 102 | 103 | 104 | 286 105 | 274 106 | 107 | 108 | 109 | 110 | activeCheck 111 | toggled(bool) 112 | activeList 113 | setEnabled(bool) 114 | 115 | 116 | 133 117 | 18 118 | 119 | 120 | 133 121 | 85 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /designer/browserdisp.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 412 10 | 241 11 | 12 | 13 | 14 | Browser Appearance 15 | 16 | 17 | 18 | 19 | 20 | Override front template: 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Override back template: 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Override font: 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 5 51 | 0 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 6 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Qt::Vertical 69 | 70 | 71 | 72 | 20 73 | 40 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | Qt::Horizontal 82 | 83 | 84 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 85 | 86 | 87 | 88 | 89 | 90 | 91 | qfmt 92 | afmt 93 | font 94 | fontSize 95 | buttonBox 96 | 97 | 98 | 99 | 100 | buttonBox 101 | accepted() 102 | Dialog 103 | accept() 104 | 105 | 106 | 248 107 | 254 108 | 109 | 110 | 157 111 | 274 112 | 113 | 114 | 115 | 116 | buttonBox 117 | rejected() 118 | Dialog 119 | reject() 120 | 121 | 122 | 316 123 | 260 124 | 125 | 126 | 286 127 | 274 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /README.development: -------------------------------------------------------------------------------- 1 | Running from source 2 | -------------------- 3 | 4 | For non-developers who want to try this development code, the easiest way is 5 | to use a binary package - please see: 6 | 7 | https://anki.tenderapp.com/discussions/beta-testing 8 | 9 | You are welcome to run Anki from source instead, but it is expected that you 10 | can sort out all dependencies and issues by yourself - we are not able to 11 | provide support for problems you encounter when running from source. 12 | 13 | Anki requires: 14 | 15 | - Python 3.4+ 16 | - Qt 5.5+ 17 | - PyQt5.6+ 18 | - mplayer 19 | - lame 20 | 21 | It also requires a number of Python packages, which you can grab via pip: 22 | 23 | $ pip3 install -r requirements.txt 24 | 25 | You will also need PyQt development tools (specifically pyrcc5 and pyuic5). 26 | These are often contained in a separate package on Linux, such as 27 | 'pyqt5-dev-tools' on Debian/Ubuntu. 28 | 29 | To use the development version: 30 | 31 | $ git clone https://github.com/dae/anki.git 32 | $ cd anki 33 | $ ./tools/build_ui.sh 34 | 35 | If you get any errors, you will not be able to proceed, so please return to 36 | the top and check the requirements again. 37 | 38 | ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you 39 | will get errors down the road. 40 | 41 | The translations are stored in a bazaar repo for integration with Launchpad's 42 | translation services. If you want to use a language other than English: 43 | 44 | $ cd .. 45 | $ mv anki dtop # i18n code expects anki folder to be called dtop 46 | $ bzr clone lp:anki i18n 47 | $ cd i18n 48 | $ ./update-mos.sh 49 | $ cd ../dtop 50 | 51 | And now you're ready to run Anki: 52 | $ ./runanki 53 | 54 | If you get any errors, please make sure you don't have an older version of 55 | Anki installed in a system location. 56 | 57 | Before contributing code, please read the LICENSE file. 58 | 59 | If you'd like to contribute translations, please see the translations section 60 | of http://ankisrs.net/docs/manual.html#_contributing 61 | 62 | Windows & Mac users 63 | --------------------- 64 | 65 | The following was contributed by users in the past and will need updating 66 | for the latest version. It is left here in case it is any help: 67 | 68 | Windows: 69 | 70 | I have not tested the build scripts on Windows, so you'll need to solve any 71 | problems you encounter on your own. The easiest way is to use a source 72 | tarball instead of git, as that way you don't need to build the UI yourself. 73 | 74 | If you do want to use git, two alternatives have been contributed by users. As 75 | these are not official solutions, I'm afraid we can not provide you with any 76 | support for these. 77 | 78 | A powershell script: 79 | 80 | https://gist.github.com/vermiceli/108fec65759d19645ee3 81 | 82 | Or a way with git bash and perl: 83 | 84 | 1) Install "git bash". 85 | 2) In the tools directory, modify build_ui.sh. Locate the line that reads 86 | "pyuic4 $i -o $py" and alter it to be of the following form: 87 | "" "" $i -o $py 88 | These two paths must point to your python executable, and to pyuic.py, on your 89 | system. Typical paths would be: 90 | = C:\\Python27\\python.exe 91 | = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py 92 | 93 | Mac: 94 | 95 | These instructions may be incomplete as prerequisites may have already been 96 | installed. Most likely you will need to have installed xcode 97 | (https://developer.apple.com/xcode/) 98 | 99 | Install homebrew (http://brew.sh/) and then install Anki prerequisites: 100 | 101 | $ brew install python PyQt mplayer lame portaudio 102 | $ pip install sqlalchemy 103 | 104 | Now you can follow the development commands at the start of this document. 105 | -------------------------------------------------------------------------------- /aqt/taglimit.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html 3 | 4 | import aqt 5 | from aqt.qt import * 6 | from aqt.utils import saveGeom, restoreGeom 7 | 8 | class TagLimit(QDialog): 9 | 10 | def __init__(self, mw, parent): 11 | QDialog.__init__(self, parent, Qt.Window) 12 | self.mw = mw 13 | self.parent = parent 14 | self.deck = self.parent.deck 15 | self.dialog = aqt.forms.taglimit.Ui_Dialog() 16 | self.dialog.setupUi(self) 17 | self.rebuildTagList() 18 | restoreGeom(self, "tagLimit") 19 | self.exec_() 20 | 21 | def rebuildTagList(self): 22 | usertags = self.mw.col.tags.byDeck(self.deck['id'], True) 23 | yes = self.deck.get("activeTags", []) 24 | no = self.deck.get("inactiveTags", []) 25 | yesHash = {} 26 | noHash = {} 27 | for y in yes: 28 | yesHash[y] = True 29 | for n in no: 30 | noHash[n] = True 31 | groupedTags = [] 32 | usertags.sort() 33 | icon = QIcon(":/icons/Anki_Fact.png") 34 | groupedTags.append([icon, usertags]) 35 | self.tags = [] 36 | for (icon, tags) in groupedTags: 37 | for t in tags: 38 | self.tags.append(t) 39 | item = QListWidgetItem(icon, t.replace("_", " ")) 40 | self.dialog.activeList.addItem(item) 41 | if t in yesHash: 42 | mode = QItemSelectionModel.Select 43 | self.dialog.activeCheck.setChecked(True) 44 | else: 45 | mode = QItemSelectionModel.Deselect 46 | idx = self.dialog.activeList.indexFromItem(item) 47 | self.dialog.activeList.selectionModel().select(idx, mode) 48 | # inactive 49 | item = QListWidgetItem(icon, t.replace("_", " ")) 50 | self.dialog.inactiveList.addItem(item) 51 | if t in noHash: 52 | mode = QItemSelectionModel.Select 53 | else: 54 | mode = QItemSelectionModel.Deselect 55 | idx = self.dialog.inactiveList.indexFromItem(item) 56 | self.dialog.inactiveList.selectionModel().select(idx, mode) 57 | 58 | def reject(self): 59 | self.tags = "" 60 | QDialog.reject(self) 61 | 62 | def accept(self): 63 | self.hide() 64 | n = 0 65 | # gather yes/no tags 66 | yes = [] 67 | no = [] 68 | for c in range(self.dialog.activeList.count()): 69 | # active 70 | if self.dialog.activeCheck.isChecked(): 71 | item = self.dialog.activeList.item(c) 72 | idx = self.dialog.activeList.indexFromItem(item) 73 | if self.dialog.activeList.selectionModel().isSelected(idx): 74 | yes.append(self.tags[c]) 75 | # inactive 76 | item = self.dialog.inactiveList.item(c) 77 | idx = self.dialog.inactiveList.indexFromItem(item) 78 | if self.dialog.inactiveList.selectionModel().isSelected(idx): 79 | no.append(self.tags[c]) 80 | # save in the deck for future invocations 81 | self.deck['activeTags'] = yes 82 | self.deck['inactiveTags'] = no 83 | self.mw.col.decks.save(self.deck) 84 | # build query string 85 | self.tags = "" 86 | if yes: 87 | arr = [] 88 | for req in yes: 89 | arr.append("tag:'%s'" % req) 90 | self.tags += "(" + " or ".join(arr) + ")" 91 | if no: 92 | arr = [] 93 | for req in no: 94 | arr.append("-tag:'%s'" % req) 95 | self.tags += " " + " ".join(arr) 96 | saveGeom(self, "tagLimit") 97 | QDialog.accept(self) 98 | -------------------------------------------------------------------------------- /designer/finddupes.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 531 10 | 345 11 | 12 | 13 | 14 | Find Duplicates 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Optional limit: 26 | 27 | 28 | 29 | 30 | 31 | 32 | Look in field: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | QFrame::StyledPanel 45 | 46 | 47 | QFrame::Raised 48 | 49 | 50 | 51 | 0 52 | 53 | 54 | 0 55 | 56 | 57 | 0 58 | 59 | 60 | 0 61 | 62 | 63 | 64 | 65 | 66 | about:blank 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::Horizontal 78 | 79 | 80 | QDialogButtonBox::Close 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | AnkiWebView 89 | QWidget 90 |
aqt/webview
91 | 1 92 |
93 |
94 | 95 | fields 96 | webView 97 | buttonBox 98 | 99 | 100 | 101 | 102 | buttonBox 103 | accepted() 104 | Dialog 105 | accept() 106 | 107 | 108 | 248 109 | 254 110 | 111 | 112 | 157 113 | 274 114 | 115 | 116 | 117 | 118 | buttonBox 119 | rejected() 120 | Dialog 121 | reject() 122 | 123 | 124 | 316 125 | 260 126 | 127 | 128 | 286 129 | 274 130 | 131 | 132 | 133 | 134 |
135 | -------------------------------------------------------------------------------- /anki/template/view.py: -------------------------------------------------------------------------------- 1 | from anki.template import Template 2 | import os.path 3 | import re 4 | 5 | class View(object): 6 | # Path where this view's template(s) live 7 | template_path = '.' 8 | 9 | # Extension for templates 10 | template_extension = 'mustache' 11 | 12 | # The name of this template. If none is given the View will try 13 | # to infer it based on the class name. 14 | template_name = None 15 | 16 | # Absolute path to the template itself. Pystache will try to guess 17 | # if it's not provided. 18 | template_file = None 19 | 20 | # Contents of the template. 21 | template = None 22 | 23 | # Character encoding of the template file. If None, Pystache will not 24 | # do any decoding of the template. 25 | template_encoding = None 26 | 27 | def __init__(self, template=None, context=None, **kwargs): 28 | self.template = template 29 | self.context = context or {} 30 | 31 | # If the context we're handed is a View, we want to inherit 32 | # its settings. 33 | if isinstance(context, View): 34 | self.inherit_settings(context) 35 | 36 | if kwargs: 37 | self.context.update(kwargs) 38 | 39 | def inherit_settings(self, view): 40 | """Given another View, copies its settings.""" 41 | if view.template_path: 42 | self.template_path = view.template_path 43 | 44 | if view.template_name: 45 | self.template_name = view.template_name 46 | 47 | def load_template(self): 48 | if self.template: 49 | return self.template 50 | 51 | if self.template_file: 52 | return self._load_template() 53 | 54 | name = self.get_template_name() + '.' + self.template_extension 55 | 56 | if isinstance(self.template_path, str): 57 | self.template_file = os.path.join(self.template_path, name) 58 | return self._load_template() 59 | 60 | for path in self.template_path: 61 | self.template_file = os.path.join(path, name) 62 | if os.path.exists(self.template_file): 63 | return self._load_template() 64 | 65 | raise IOError('"%s" not found in "%s"' % (name, ':'.join(self.template_path),)) 66 | 67 | 68 | def _load_template(self): 69 | f = open(self.template_file, 'r') 70 | try: 71 | template = f.read() 72 | if self.template_encoding: 73 | template = str(template, self.template_encoding) 74 | finally: 75 | f.close() 76 | return template 77 | 78 | def get_template_name(self, name=None): 79 | """TemplatePartial => template_partial 80 | Takes a string but defaults to using the current class' name or 81 | the `template_name` attribute 82 | """ 83 | if self.template_name: 84 | return self.template_name 85 | 86 | if not name: 87 | name = self.__class__.__name__ 88 | 89 | def repl(match): 90 | return '_' + match.group(0).lower() 91 | 92 | return re.sub('[A-Z]', repl, name)[1:] 93 | 94 | def __contains__(self, needle): 95 | return needle in self.context or hasattr(self, needle) 96 | 97 | def __getitem__(self, attr): 98 | val = self.get(attr, None) 99 | if not val: 100 | raise KeyError("No such key.") 101 | return val 102 | 103 | def get(self, attr, default): 104 | attr = self.context.get(attr, getattr(self, attr, default)) 105 | 106 | if hasattr(attr, '__call__'): 107 | return attr() 108 | else: 109 | return attr 110 | 111 | def render(self, encoding=None): 112 | template = self.load_template() 113 | return Template(template, self).render(encoding=encoding) 114 | 115 | def __str__(self): 116 | return self.render() 117 | -------------------------------------------------------------------------------- /designer/findreplace.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 367 10 | 209 11 | 12 | 13 | 14 | Find and Replace 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <b>Find</b>: 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <b>Replace With</b>: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <b>In</b>: 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Treat input as regular expression 53 | 54 | 55 | 56 | 57 | 58 | 59 | Ignore case 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Qt::Vertical 72 | 73 | 74 | 75 | 20 76 | 40 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Qt::Horizontal 85 | 86 | 87 | QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok 88 | 89 | 90 | 91 | 92 | 93 | 94 | find 95 | replace 96 | field 97 | ignoreCase 98 | re 99 | buttonBox 100 | 101 | 102 | 103 | 104 | buttonBox 105 | accepted() 106 | Dialog 107 | accept() 108 | 109 | 110 | 256 111 | 154 112 | 113 | 114 | 157 115 | 274 116 | 117 | 118 | 119 | 120 | buttonBox 121 | rejected() 122 | Dialog 123 | reject() 124 | 125 | 126 | 290 127 | 154 128 | 129 | 130 | 286 131 | 274 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /aqt/dyndeckconf.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | from aqt.qt import * 6 | import aqt 7 | from aqt.utils import showWarning, openHelp, askUser, saveGeom, restoreGeom 8 | 9 | class DeckConf(QDialog): 10 | def __init__(self, mw, first=False, search="", deck=None): 11 | QDialog.__init__(self, mw) 12 | self.mw = mw 13 | self.deck = deck or self.mw.col.decks.current() 14 | self.search = search 15 | self.form = aqt.forms.dyndconf.Ui_Dialog() 16 | self.form.setupUi(self) 17 | if first: 18 | label = _("Build") 19 | else: 20 | label = _("Rebuild") 21 | self.ok = self.form.buttonBox.addButton( 22 | label, QDialogButtonBox.AcceptRole) 23 | self.mw.checkpoint(_("Options")) 24 | self.setWindowModality(Qt.WindowModal) 25 | self.form.buttonBox.helpRequested.connect(lambda: openHelp("filtered")) 26 | self.setWindowTitle(_("Options for %s") % self.deck['name']) 27 | restoreGeom(self, "dyndeckconf") 28 | self.setupOrder() 29 | self.loadConf() 30 | if search: 31 | self.form.search.setText(search) 32 | self.form.search.selectAll() 33 | self.show() 34 | self.exec_() 35 | saveGeom(self, "dyndeckconf") 36 | 37 | def setupOrder(self): 38 | import anki.consts as cs 39 | self.form.order.addItems(list(cs.dynOrderLabels().values())) 40 | 41 | def loadConf(self): 42 | f = self.form 43 | d = self.deck 44 | search, limit, order = d['terms'][0] 45 | f.search.setText(search) 46 | if d['delays']: 47 | f.steps.setText(self.listToUser(d['delays'])) 48 | f.stepsOn.setChecked(True) 49 | else: 50 | f.steps.setText("1 10") 51 | f.stepsOn.setChecked(False) 52 | f.resched.setChecked(d['resched']) 53 | f.order.setCurrentIndex(order) 54 | f.limit.setValue(limit) 55 | 56 | def saveConf(self): 57 | f = self.form 58 | d = self.deck 59 | d['delays'] = None 60 | if f.stepsOn.isChecked(): 61 | steps = self.userToList(f.steps) 62 | if steps: 63 | d['delays'] = steps 64 | else: 65 | d['delays'] = None 66 | d['terms'][0] = [f.search.text(), 67 | f.limit.value(), 68 | f.order.currentIndex()] 69 | d['resched'] = f.resched.isChecked() 70 | self.mw.col.decks.save(d) 71 | return True 72 | 73 | def reject(self): 74 | self.ok = False 75 | QDialog.reject(self) 76 | 77 | def accept(self): 78 | if not self.saveConf(): 79 | return 80 | if not self.mw.col.sched.rebuildDyn(): 81 | if askUser(_("""\ 82 | The provided search did not match any cards. Would you like to revise \ 83 | it?""")): 84 | return 85 | self.mw.reset() 86 | QDialog.accept(self) 87 | 88 | # Step load/save - fixme: share with std options screen 89 | ######################################################## 90 | 91 | def listToUser(self, l): 92 | return " ".join([str(x) for x in l]) 93 | 94 | def userToList(self, w, minSize=1): 95 | items = str(w.text()).split(" ") 96 | ret = [] 97 | for i in items: 98 | if not i: 99 | continue 100 | try: 101 | i = float(i) 102 | assert i > 0 103 | if i == int(i): 104 | i = int(i) 105 | ret.append(i) 106 | except: 107 | # invalid, don't update 108 | showWarning(_("Steps must be numbers.")) 109 | return 110 | if len(ret) < minSize: 111 | showWarning(_("At least one step is required.")) 112 | return 113 | return ret 114 | -------------------------------------------------------------------------------- /aqt/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | import sys, traceback 5 | import cgi 6 | 7 | from anki.lang import _ 8 | from aqt.qt import * 9 | from aqt.utils import showText, showWarning 10 | 11 | def excepthook(etype,val,tb): 12 | sys.stderr.write("Caught exception:\n%s%s\n" % ( 13 | ''.join(traceback.format_tb(tb)), 14 | '{0}: {1}'.format(etype, val))) 15 | sys.excepthook = excepthook 16 | 17 | class ErrorHandler(QObject): 18 | "Catch stderr and write into buffer." 19 | ivl = 100 20 | 21 | errorTimer = pyqtSignal() 22 | 23 | def __init__(self, mw): 24 | QObject.__init__(self, mw) 25 | self.mw = mw 26 | self.timer = None 27 | self.errorTimer.connect(self._setTimer) 28 | self.pool = "" 29 | sys.stderr = self 30 | 31 | def write(self, data): 32 | # dump to stdout 33 | sys.stdout.write(data) 34 | # save in buffer 35 | self.pool += data 36 | # and update timer 37 | self.setTimer() 38 | 39 | def setTimer(self): 40 | # we can't create a timer from a different thread, so we post a 41 | # message to the object on the main thread 42 | self.errorTimer.emit() 43 | 44 | def _setTimer(self): 45 | if not self.timer: 46 | self.timer = QTimer(self.mw) 47 | self.timer.timeout.connect(self.onTimeout) 48 | self.timer.setInterval(self.ivl) 49 | self.timer.setSingleShot(True) 50 | self.timer.start() 51 | 52 | def tempFolderMsg(self): 53 | return _("""\ 54 | The permissions on your system's temporary folder are incorrect, and Anki is \ 55 | not able to correct them automatically. Please search for 'temp folder' in the \ 56 | Anki manual for more information.""") 57 | 58 | def onTimeout(self): 59 | error = cgi.escape(self.pool) 60 | self.pool = "" 61 | self.mw.progress.clear() 62 | if "abortSchemaMod" in error: 63 | return 64 | if "Pyaudio not" in error: 65 | return showWarning(_("Please install PyAudio")) 66 | if "install mplayer" in error: 67 | return showWarning(_("Please install mplayer")) 68 | if "no default input" in error.lower(): 69 | return showWarning(_("Please connect a microphone, and ensure " 70 | "other programs are not using the audio device.")) 71 | if "invalidTempFolder" in error: 72 | return showWarning(self.tempFolderMsg()) 73 | if "Beautiful Soup is not an HTTP client" in error: 74 | return 75 | if "disk I/O error" in error: 76 | return showWarning(_("""\ 77 | An error occurred while accessing the database. 78 | 79 | Possible causes: 80 | 81 | - Antivirus, firewall, backup, or synchronization software may be \ 82 | interfering with Anki. Try disabling such software and see if the \ 83 | problem goes away. 84 | - Your disk may be full. 85 | - The Documents/Anki folder may be on a network drive. 86 | - Files in the Documents/Anki folder may not be writeable. 87 | - Your hard disk may have errors. 88 | 89 | It's a good idea to run Tools>Check Database to ensure your collection \ 90 | is not corrupt. 91 | """)) 92 | stdText = _("""\ 93 | An error occurred. It may have been caused by a harmless bug,
94 | or your deck may have a problem. 95 |

To confirm it's not a problem with your deck, please run 96 | Tools > Check Database. 97 |

If that doesn't fix the problem, please copy the following
98 | into a bug report:""") 99 | pluginText = _("""\ 100 | An error occurred in an add-on.
101 | Please post on the add-on forum:
%s
""") 102 | pluginText %= "https://anki.tenderapp.com/discussions/add-ons" 103 | if "addon" in error: 104 | txt = pluginText 105 | else: 106 | txt = stdText 107 | # show dialog 108 | txt = txt + "

" + error + "
" 109 | showText(txt, type="html") 110 | -------------------------------------------------------------------------------- /tests/test_latex.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | import shutil 6 | 7 | from tests.shared import getEmptyCol 8 | from anki.utils import stripHTML 9 | 10 | def test_latex(): 11 | d = getEmptyCol() 12 | # change latex cmd to simulate broken build 13 | import anki.latex 14 | anki.latex.latexCmds[0][0] = "nolatex" 15 | # add a note with latex 16 | f = d.newNote() 17 | f['Front'] = "[latex]hello[/latex]" 18 | d.addNote(f) 19 | # but since latex couldn't run, there's nothing there 20 | assert len(os.listdir(d.media.dir())) == 0 21 | # check the error message 22 | msg = f.cards()[0].q() 23 | assert "executing nolatex" in msg 24 | assert "installed" in msg 25 | # check if we have latex installed, and abort test if we don't 26 | if not shutil.which("latex") or not shutil.which("dvipng"): 27 | print("aborting test; latex or dvipng is not installed") 28 | return 29 | # fix path 30 | anki.latex.latexCmds[0][0] = "latex" 31 | # check media db should cause latex to be generated 32 | d.media.check() 33 | assert len(os.listdir(d.media.dir())) == 1 34 | assert ".png" in f.cards()[0].q() 35 | # adding new notes should cause generation on question display 36 | f = d.newNote() 37 | f['Front'] = "[latex]world[/latex]" 38 | d.addNote(f) 39 | f.cards()[0].q() 40 | assert len(os.listdir(d.media.dir())) == 2 41 | # another note with the same media should reuse 42 | f = d.newNote() 43 | f['Front'] = " [latex]world[/latex]" 44 | d.addNote(f) 45 | assert len(os.listdir(d.media.dir())) == 2 46 | oldcard = f.cards()[0] 47 | assert ".png" in oldcard.q() 48 | # if we turn off building, then previous cards should work, but cards with 49 | # missing media will show the latex 50 | anki.latex.build = False 51 | f = d.newNote() 52 | f['Front'] = "[latex]foo[/latex]" 53 | d.addNote(f) 54 | assert len(os.listdir(d.media.dir())) == 2 55 | assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]" 56 | assert ".png" in oldcard.q() 57 | # turn it on again so other test don't suffer 58 | anki.latex.build = True 59 | 60 | def test_bad_latex_command_write18(): 61 | (result, msg) = _test_includes_bad_command("\\write18") 62 | assert result, msg 63 | 64 | def test_bad_latex_command_readline(): 65 | (result, msg) = _test_includes_bad_command("\\readline") 66 | assert result, msg 67 | 68 | def test_bad_latex_command_input(): 69 | (result, msg) = _test_includes_bad_command("\\input") 70 | assert result, msg 71 | 72 | def test_bad_latex_command_include(): 73 | (result, msg) = _test_includes_bad_command("\\include") 74 | assert result, msg 75 | 76 | def test_bad_latex_command_catcode(): 77 | (result, msg) = _test_includes_bad_command("\\catcode") 78 | assert result, msg 79 | 80 | def test_bad_latex_command_openout(): 81 | (result, msg) = _test_includes_bad_command("\\openout") 82 | assert result, msg 83 | 84 | def test_bad_latex_command_write(): 85 | (result, msg) = _test_includes_bad_command("\\write") 86 | assert result, msg 87 | 88 | def test_bad_latex_command_loop(): 89 | (result, msg) = _test_includes_bad_command("\\loop") 90 | assert result, msg 91 | 92 | def test_bad_latex_command_def(): 93 | (result, msg) = _test_includes_bad_command("\\def") 94 | assert result, msg 95 | 96 | def test_bad_latex_command_shipout(): 97 | (result, msg) = _test_includes_bad_command("\\shipout") 98 | assert result, msg 99 | 100 | def test_good_latex_command_works(): 101 | # inserting commands beginning with a bad name should not raise an error 102 | (result, msg) = _test_includes_bad_command("\\defeq") 103 | assert not result, msg 104 | # normal commands should not either 105 | (result, msg) = _test_includes_bad_command("\\emph") 106 | assert not result, msg 107 | 108 | def _test_includes_bad_command(bad): 109 | d = getEmptyCol() 110 | f = d.newNote() 111 | f['Front'] = '[latex]%s[/latex]' % bad; 112 | d.addNote(f) 113 | q = f.cards()[0].q() 114 | return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q) 115 | -------------------------------------------------------------------------------- /designer/reposition.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 272 10 | 229 11 | 12 | 13 | 14 | Reposition New Cards 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Start position: 30 | 31 | 32 | 33 | 34 | 35 | 36 | -20000000 37 | 38 | 39 | 200000000 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | 47 | 48 | 49 | Step: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 1 57 | 58 | 59 | 10000 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Randomize order 69 | 70 | 71 | 72 | 73 | 74 | 75 | Shift position of existing cards 76 | 77 | 78 | true 79 | 80 | 81 | 82 | 83 | 84 | 85 | Qt::Vertical 86 | 87 | 88 | 89 | 20 90 | 40 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Qt::Horizontal 99 | 100 | 101 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 102 | 103 | 104 | 105 | 106 | 107 | 108 | start 109 | step 110 | randomize 111 | shift 112 | buttonBox 113 | 114 | 115 | 116 | 117 | buttonBox 118 | accepted() 119 | Dialog 120 | accept() 121 | 122 | 123 | 248 124 | 254 125 | 126 | 127 | 157 128 | 274 129 | 130 | 131 | 132 | 133 | buttonBox 134 | rejected() 135 | Dialog 136 | reject() 137 | 138 | 139 | 316 140 | 260 141 | 142 | 143 | 286 144 | 274 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /designer/exporting.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ExportDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 295 10 | 202 11 | 12 | 13 | 14 | Export 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 100 24 | 0 25 | 26 | 27 | 28 | <b>Export format</b>: 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <b>Include</b>: 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Include scheduling information 53 | 54 | 55 | true 56 | 57 | 58 | 59 | 60 | 61 | 62 | Include media 63 | 64 | 65 | true 66 | 67 | 68 | 69 | 70 | 71 | 72 | Include tags 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Qt::Vertical 85 | 86 | 87 | 88 | 20 89 | 40 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Qt::Horizontal 98 | 99 | 100 | QDialogButtonBox::Cancel 101 | 102 | 103 | 104 | 105 | 106 | 107 | format 108 | deck 109 | includeSched 110 | includeMedia 111 | includeTags 112 | buttonBox 113 | 114 | 115 | 116 | 117 | buttonBox 118 | accepted() 119 | ExportDialog 120 | accept() 121 | 122 | 123 | 248 124 | 254 125 | 126 | 127 | 157 128 | 274 129 | 130 | 131 | 132 | 133 | buttonBox 134 | rejected() 135 | ExportDialog 136 | reject() 137 | 138 | 139 | 316 140 | 260 141 | 142 | 143 | 286 144 | 274 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /designer/browseropts.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 288 10 | 195 11 | 12 | 13 | 14 | Browser Options 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <b>Font</b>: 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | <b>Font Size</b>: 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 75 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | <b>Line Size</b>: 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Qt::Horizontal 64 | 65 | 66 | 67 | 40 68 | 20 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Search within formatting (slow) 79 | 80 | 81 | 82 | 83 | 84 | 85 | Qt::Vertical 86 | 87 | 88 | 89 | 20 90 | 40 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Qt::Horizontal 99 | 100 | 101 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 102 | 103 | 104 | 105 | 106 | 107 | 108 | fontCombo 109 | fontSize 110 | lineSize 111 | fullSearch 112 | buttonBox 113 | 114 | 115 | 116 | 117 | buttonBox 118 | accepted() 119 | Dialog 120 | accept() 121 | 122 | 123 | 248 124 | 254 125 | 126 | 127 | 157 128 | 274 129 | 130 | 131 | 132 | 133 | buttonBox 134 | rejected() 135 | Dialog 136 | reject() 137 | 138 | 139 | 316 140 | 260 141 | 142 | 143 | 286 144 | 274 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /designer/addfield.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 434 10 | 186 11 | 12 | 13 | 14 | Add Field 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Front 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 6 33 | 34 | 35 | 200 36 | 37 | 38 | 39 | 40 | 41 | 42 | Field: 43 | 44 | 45 | 46 | 47 | 48 | 49 | Font: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Size: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Qt::Vertical 70 | 71 | 72 | 73 | 20 74 | 40 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | Back 83 | 84 | 85 | 86 | 87 | 88 | 89 | Add to: 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Qt::Vertical 99 | 100 | 101 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 102 | 103 | 104 | 105 | 106 | 107 | 108 | fields 109 | font 110 | size 111 | radioQ 112 | radioA 113 | buttonBox 114 | 115 | 116 | 117 | 118 | buttonBox 119 | accepted() 120 | Dialog 121 | accept() 122 | 123 | 124 | 248 125 | 254 126 | 127 | 128 | 157 129 | 274 130 | 131 | 132 | 133 | 134 | buttonBox 135 | rejected() 136 | Dialog 137 | reject() 138 | 139 | 140 | 316 141 | 260 142 | 143 | 144 | 286 145 | 274 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /tests/test_collection.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os, tempfile 4 | from tests.shared import assertException, getEmptyCol 5 | from anki.stdmodels import addBasicModel 6 | 7 | from anki import Collection as aopen 8 | 9 | newPath = None 10 | newMod = None 11 | 12 | def test_create(): 13 | global newPath, newMod 14 | (fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew") 15 | try: 16 | os.close(fd) 17 | os.unlink(path) 18 | except OSError: 19 | pass 20 | deck = aopen(path) 21 | # for open() 22 | newPath = deck.path 23 | deck.close() 24 | newMod = deck.mod 25 | del deck 26 | 27 | def test_open(): 28 | deck = aopen(newPath) 29 | assert deck.mod == newMod 30 | deck.close() 31 | 32 | def test_openReadOnly(): 33 | # non-writeable dir 34 | assertException(Exception, 35 | lambda: aopen("/attachroot.anki2")) 36 | # reuse tmp file from before, test non-writeable file 37 | os.chmod(newPath, 0) 38 | assertException(Exception, 39 | lambda: aopen(newPath)) 40 | os.chmod(newPath, 0o666) 41 | os.unlink(newPath) 42 | 43 | def test_noteAddDelete(): 44 | deck = getEmptyCol() 45 | # add a note 46 | f = deck.newNote() 47 | f['Front'] = "one"; f['Back'] = "two" 48 | n = deck.addNote(f) 49 | assert n == 1 50 | # test multiple cards - add another template 51 | m = deck.models.current(); mm = deck.models 52 | t = mm.newTemplate("Reverse") 53 | t['qfmt'] = "{{Back}}" 54 | t['afmt'] = "{{Front}}" 55 | mm.addTemplate(m, t) 56 | mm.save(m) 57 | # the default save doesn't generate cards 58 | assert deck.cardCount() == 1 59 | # but when templates are edited such as in the card layout screen, it 60 | # should generate cards on close 61 | mm.save(m, templates=True) 62 | assert deck.cardCount() == 2 63 | # creating new notes should use both cards 64 | f = deck.newNote() 65 | f['Front'] = "three"; f['Back'] = "four" 66 | n = deck.addNote(f) 67 | assert n == 2 68 | assert deck.cardCount() == 4 69 | # check q/a generation 70 | c0 = f.cards()[0] 71 | assert "three" in c0.q() 72 | # it should not be a duplicate 73 | assert not f.dupeOrEmpty() 74 | # now let's make a duplicate 75 | f2 = deck.newNote() 76 | f2['Front'] = "one"; f2['Back'] = "" 77 | assert f2.dupeOrEmpty() 78 | # empty first field should not be permitted either 79 | f2['Front'] = " " 80 | assert f2.dupeOrEmpty() 81 | 82 | def test_fieldChecksum(): 83 | deck = getEmptyCol() 84 | f = deck.newNote() 85 | f['Front'] = "new"; f['Back'] = "new2" 86 | deck.addNote(f) 87 | assert deck.db.scalar( 88 | "select csum from notes") == int("c2a6b03f", 16) 89 | # changing the val should change the checksum 90 | f['Front'] = "newx" 91 | f.flush() 92 | assert deck.db.scalar( 93 | "select csum from notes") == int("302811ae", 16) 94 | 95 | def test_addDelTags(): 96 | deck = getEmptyCol() 97 | f = deck.newNote() 98 | f['Front'] = "1" 99 | deck.addNote(f) 100 | f2 = deck.newNote() 101 | f2['Front'] = "2" 102 | deck.addNote(f2) 103 | # adding for a given id 104 | deck.tags.bulkAdd([f.id], "foo") 105 | f.load(); f2.load() 106 | assert "foo" in f.tags 107 | assert "foo" not in f2.tags 108 | # should be canonified 109 | deck.tags.bulkAdd([f.id], "foo aaa") 110 | f.load() 111 | assert f.tags[0] == "aaa" 112 | assert len(f.tags) == 2 113 | 114 | def test_timestamps(): 115 | deck = getEmptyCol() 116 | assert len(deck.models.models) == 4 117 | for i in range(100): 118 | addBasicModel(deck) 119 | assert len(deck.models.models) == 104 120 | 121 | def test_furigana(): 122 | deck = getEmptyCol() 123 | mm = deck.models 124 | m = mm.current() 125 | # filter should work 126 | m['tmpls'][0]['qfmt'] = '{{kana:Front}}' 127 | mm.save(m) 128 | n = deck.newNote() 129 | n['Front'] = 'foo[abc]' 130 | deck.addNote(n) 131 | c = n.cards()[0] 132 | assert c.q().endswith("abc") 133 | # and should avoid sound 134 | n['Front'] = 'foo[sound:abc.mp3]' 135 | n.flush() 136 | assert "sound:" in c.q(reload=True) 137 | # it shouldn't throw an error while people are editing 138 | m['tmpls'][0]['qfmt'] = '{{kana:}}' 139 | mm.save(m) 140 | c.q(reload=True) 141 | -------------------------------------------------------------------------------- /tests/test_media.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import tempfile 4 | import os 5 | import time 6 | 7 | from .shared import getEmptyCol, testDir 8 | 9 | 10 | # copying files to media folder 11 | def test_add(): 12 | d = getEmptyCol() 13 | dir = tempfile.mkdtemp(prefix="anki") 14 | path = os.path.join(dir, "foo.jpg") 15 | open(path, "w").write("hello") 16 | # new file, should preserve name 17 | assert d.media.addFile(path) == "foo.jpg" 18 | # adding the same file again should not create a duplicate 19 | assert d.media.addFile(path) == "foo.jpg" 20 | # but if it has a different md5, it should 21 | open(path, "w").write("world") 22 | assert d.media.addFile(path) == "foo (1).jpg" 23 | 24 | def test_strings(): 25 | d = getEmptyCol() 26 | mf = d.media.filesInStr 27 | mid = list(d.models.models.keys())[0] 28 | assert mf(mid, "aoeu") == [] 29 | assert mf(mid, "aoeuao") == ["foo.jpg"] 30 | assert mf(mid, "aoeuao") == ["foo.jpg"] 31 | assert mf(mid, "aoeuao") == [ 32 | "foo.jpg", "bar.jpg"] 33 | assert mf(mid, "aoeuao") == ["foo.jpg"] 34 | assert mf(mid, "") == ["one", "two"] 35 | assert mf(mid, "aoeuao") == ["foo.jpg"] 36 | assert mf(mid, "aoeuao") == [ 37 | "foo.jpg", "fo"] 38 | assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"] 39 | sp = d.media.strip 40 | assert sp("aoeu") == "aoeu" 41 | assert sp("aoeu[sound:foo.mp3]aoeu") == "aoeuaoeu" 42 | assert sp("aoeu") == "aoeu" 43 | es = d.media.escapeImages 44 | assert es("aoeu") == "aoeu" 45 | assert es("") == "" 46 | assert es('') == '' 47 | 48 | def test_deckIntegration(): 49 | d = getEmptyCol() 50 | # create a media dir 51 | d.media.dir() 52 | # put a file into it 53 | file = str(os.path.join(testDir, "support/fake.png")) 54 | d.media.addFile(file) 55 | # add a note which references it 56 | f = d.newNote() 57 | f['Front'] = "one"; f['Back'] = "" 58 | d.addNote(f) 59 | # and one which references a non-existent file 60 | f = d.newNote() 61 | f['Front'] = "one"; f['Back'] = "" 62 | d.addNote(f) 63 | # and add another file which isn't used 64 | open(os.path.join(d.media.dir(), "foo.jpg"), "w").write("test") 65 | # check media 66 | ret = d.media.check() 67 | assert ret[0] == ["fake2.png"] 68 | assert ret[1] == ["foo.jpg"] 69 | 70 | def test_changes(): 71 | d = getEmptyCol() 72 | assert d.media._changed() 73 | def added(): 74 | return d.media.db.execute("select fname from media where csum is not null") 75 | def removed(): 76 | return d.media.db.execute("select fname from media where csum is null") 77 | assert not list(added()) 78 | assert not list(removed()) 79 | # add a file 80 | dir = tempfile.mkdtemp(prefix="anki") 81 | path = os.path.join(dir, "foo.jpg") 82 | open(path, "w").write("hello") 83 | time.sleep(1) 84 | path = d.media.addFile(path) 85 | # should have been logged 86 | d.media.findChanges() 87 | assert list(added()) 88 | assert not list(removed()) 89 | # if we modify it, the cache won't notice 90 | time.sleep(1) 91 | open(path, "w").write("world") 92 | assert len(list(added())) == 1 93 | assert not list(removed()) 94 | # but if we add another file, it will 95 | time.sleep(1) 96 | open(path+"2", "w").write("yo") 97 | d.media.findChanges() 98 | assert len(list(added())) == 2 99 | assert not list(removed()) 100 | # deletions should get noticed too 101 | time.sleep(1) 102 | os.unlink(path+"2") 103 | d.media.findChanges() 104 | assert len(list(added())) == 1 105 | assert len(list(removed())) == 1 106 | 107 | def test_illegal(): 108 | d = getEmptyCol() 109 | aString = "a:b|cd\\e/f\0g*h" 110 | good = "abcdefgh" 111 | assert d.media.stripIllegal(aString) == good 112 | for c in aString: 113 | bad = d.media.hasIllegal("somestring"+c+"morestring") 114 | if bad: 115 | assert(c not in good) 116 | else: 117 | assert(c in good) 118 | -------------------------------------------------------------------------------- /tests/test_exporting.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import nose, os, tempfile 4 | from anki import Collection as aopen 5 | from anki.exporting import * 6 | from anki.importing import Anki2Importer 7 | from .shared import getEmptyCol 8 | 9 | deck = None 10 | ds = None 11 | testDir = os.path.dirname(__file__) 12 | 13 | def setup1(): 14 | global deck 15 | deck = getEmptyCol() 16 | f = deck.newNote() 17 | f['Front'] = "foo"; f['Back'] = "bar"; f.tags = ["tag", "tag2"] 18 | deck.addNote(f) 19 | # with a different deck 20 | f = deck.newNote() 21 | f['Front'] = "baz"; f['Back'] = "qux" 22 | f.model()['did'] = deck.decks.id("new deck") 23 | deck.addNote(f) 24 | 25 | ########################################################################## 26 | 27 | @nose.with_setup(setup1) 28 | def test_export_anki(): 29 | # create a new deck with its own conf to test conf copying 30 | did = deck.decks.id("test") 31 | dobj = deck.decks.get(did) 32 | confId = deck.decks.confId("newconf") 33 | conf = deck.decks.getConf(confId) 34 | conf['new']['perDay'] = 5 35 | deck.decks.save(conf) 36 | deck.decks.setConf(dobj, confId) 37 | # export 38 | e = AnkiExporter(deck) 39 | fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") 40 | newname = str(newname) 41 | os.close(fd) 42 | os.unlink(newname) 43 | e.exportInto(newname) 44 | # exporting should not have changed conf for original deck 45 | conf = deck.decks.confForDid(did) 46 | assert conf['id'] != 1 47 | # connect to new deck 48 | d2 = aopen(newname) 49 | assert d2.cardCount() == 2 50 | # as scheduling was reset, should also revert decks to default conf 51 | did = d2.decks.id("test", create=False) 52 | assert did 53 | conf2 = d2.decks.confForDid(did) 54 | assert conf2['new']['perDay'] == 20 55 | dobj = d2.decks.get(did) 56 | # conf should be 1 57 | assert dobj['conf'] == 1 58 | # try again, limited to a deck 59 | fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") 60 | newname = str(newname) 61 | os.close(fd) 62 | os.unlink(newname) 63 | e.did = 1 64 | e.exportInto(newname) 65 | d2 = aopen(newname) 66 | assert d2.cardCount() == 1 67 | 68 | @nose.with_setup(setup1) 69 | def test_export_ankipkg(): 70 | # add a test file to the media folder 71 | open(os.path.join(deck.media.dir(), "今日.mp3"), "w").write("test") 72 | n = deck.newNote() 73 | n['Front'] = '[sound:今日.mp3]' 74 | deck.addNote(n) 75 | e = AnkiPackageExporter(deck) 76 | fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg") 77 | newname = str(newname) 78 | os.close(fd) 79 | os.unlink(newname) 80 | e.exportInto(newname) 81 | 82 | @nose.with_setup(setup1) 83 | def test_export_anki_due(): 84 | deck = getEmptyCol() 85 | f = deck.newNote() 86 | f['Front'] = "foo" 87 | deck.addNote(f) 88 | deck.crt -= 86400*10 89 | deck.sched.reset() 90 | c = deck.sched.getCard() 91 | deck.sched.answerCard(c, 2) 92 | deck.sched.answerCard(c, 2) 93 | # should have ivl of 1, due on day 11 94 | assert c.ivl == 1 95 | assert c.due == 11 96 | assert deck.sched.today == 10 97 | assert c.due - deck.sched.today == 1 98 | # export 99 | e = AnkiExporter(deck) 100 | e.includeSched = True 101 | fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") 102 | newname = str(newname) 103 | os.close(fd) 104 | os.unlink(newname) 105 | e.exportInto(newname) 106 | # importing into a new deck, the due date should be equivalent 107 | deck2 = getEmptyCol() 108 | imp = Anki2Importer(deck2, newname) 109 | imp.run() 110 | c = deck2.getCard(c.id) 111 | deck2.sched.reset() 112 | assert c.due - deck2.sched.today == 1 113 | 114 | # @nose.with_setup(setup1) 115 | # def test_export_textcard(): 116 | # e = TextCardExporter(deck) 117 | # f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) 118 | # os.unlink(f) 119 | # e.exportInto(f) 120 | # e.includeTags = True 121 | # e.exportInto(f) 122 | 123 | @nose.with_setup(setup1) 124 | def test_export_textnote(): 125 | e = TextNoteExporter(deck) 126 | fd, f = tempfile.mkstemp(prefix="ankitest") 127 | f = str(f) 128 | os.close(fd) 129 | os.unlink(f) 130 | e.exportInto(f) 131 | e.includeTags = True 132 | e.exportInto(f) 133 | 134 | def test_exporters(): 135 | assert "*.apkg" in str(exporters()) 136 | --------------------------------------------------------------------------------