├── LICENSE
├── Makefile
├── README.md
├── src
├── browser_batch_remove_formatting
│ ├── __init__.py
│ └── browser_batch_remove_formatting.py
├── browser_create_duplicate
│ ├── __init__.py
│ └── browser_create_duplicate.py
├── browser_create_filtered_deck
│ ├── __init__.py
│ └── browser_create_filtered_deck.py
├── browser_field_to_tags
│ ├── __init__.py
│ └── browser_field_to_tags.py
├── browser_more_hotkeys
│ ├── __init__.py
│ └── browser_more_hotkeys.py
├── browser_refresh
│ ├── __init__.py
│ └── browser_refresh.py
├── browser_replace_tag
│ ├── __init__.py
│ └── browser_replace_tag.py
├── browser_search_hotkeys
│ ├── __init__.py
│ └── browser_search_hotkeys.py
├── browser_sidebar_tweaks
│ ├── __init__.py
│ └── browser_sidebar_tweaks.py
├── common_context_search
│ ├── __init__.py
│ ├── common_context_search.py
│ ├── config.md
│ ├── config.py
│ └── platform.py
├── common_ctrlf_search
│ ├── common ctrlf search.py
│ └── common_ctrlf_search
│ │ ├── __init__.py
│ │ ├── common_ctrlf_search.py
│ │ ├── config.json
│ │ ├── config.md
│ │ └── manifest.json
├── editor_autocomplete_whitelist
│ ├── __init__.py
│ └── editor_autocomplete_whitelist.py
├── editor_clear_all
│ ├── __init__.py
│ └── editor_clear_all.py
├── editor_cloze_highlighter
│ ├── __init__.py
│ └── editor_cloze_highlighter.py
├── editor_custom_stylesheet
│ ├── __init__.py
│ └── editor_custom_stylesheet.py
├── editor_field_history
│ ├── __init__.py
│ ├── config.md
│ ├── config.schema.json
│ └── editor_field_history.py
├── editor_field_navigation
│ ├── __init__.py
│ └── editor_field_navigation.py
├── editor_indentation_formatter
│ ├── __init__.py
│ └── editor_indentation_formatter.py
├── editor_paste_sources
│ ├── __init__.py
│ └── editor_paste_sources.py
├── editor_preserve_fields_on_switch
│ ├── __init__.py
│ └── editor_preserve_fields_on_switch.py
├── editor_random_list
│ ├── __init__.py
│ ├── config.md
│ └── editor_random_list.py
├── editor_replace_linebreaks
│ ├── __init__.py
│ └── editor_replace_linebreaks.py
├── editor_reverse_toggle
│ ├── __init__.py
│ └── editor_reverse_toggle.py
├── editor_second_addcards_dialog
│ ├── __init__.py
│ └── editor_second_addcards_dialog.py
├── editor_sync_html_cursor
│ ├── __init__.py
│ └── editor_sync_html_cursor.py
├── editor_tag_hotkeys
│ ├── __init__.py
│ └── editor_tag_hotkeys.py
├── main_fullscreen
│ ├── __init__.py
│ └── main_fullscreen.py
├── main_ontop
│ ├── __init__.py
│ └── main_ontop.py
├── overview_browser_shortcuts
│ ├── __init__.py
│ └── overview_browser_shortcuts.py
├── overview_deck_switcher
│ ├── __init__.py
│ └── overview_deck_switcher.py
├── overview_deck_tooltip
│ ├── __init__.py
│ └── overview_deck_tooltip.py
├── overview_refresh_media
│ ├── __init__.py
│ └── overview_refresh_media.py
├── previewer_tag_browser
│ ├── __init__.py
│ └── previewer_tag_browser.py
├── reviewer_auto_rate_hotkey
│ ├── __init__.py
│ └── reviewer_auto_rate_hotkey.py
├── reviewer_browse_creation
│ ├── __init__.py
│ └── reviewer_browse_creation.py
├── reviewer_browse_today
│ ├── __init__.py
│ └── reviewer_browse_today.py
├── reviewer_card_stats
│ ├── __init__.py
│ └── reviewer_card_stats.py
├── reviewer_file_hyperlinks
│ ├── __init__.py
│ └── reviewer_file_hyperlinks.py
├── reviewer_hide_toolbar
│ ├── __init__.py
│ └── reviewer_hide_toolbar.py
├── reviewer_hint_hotkeys
│ ├── __init__.py
│ └── reviewer_hint_hotkeys.py
├── reviewer_letitsnow
│ ├── __init__.py
│ └── reviewer_letitsnow.py
├── reviewer_more_answer_buttons
│ ├── More Answer Buttons.py
│ └── more_answer_buttons
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── config.json
│ │ ├── config.md
│ │ ├── manifest.json
│ │ ├── reviewer_more_answer_buttons_for_20.py
│ │ └── reviewer_more_answer_buttons_for_21.py
├── reviewer_progress_bar
│ ├── __init__.py
│ └── reviewer_progress_bar.py
├── reviewer_refocus_card
│ ├── __init__.py
│ └── reviewer_refocus_card.py
├── reviewer_track_unseen
│ ├── __init__.py
│ └── reviewer_track_unseen.py
├── sched_advanced_newcard_limits
│ ├── __init__.py
│ └── sched_advanced_newcard_limits.py
├── sched_deck_orgactions
│ ├── __init__.py
│ └── sched_deck_orgactions.py
├── sched_filter_dailydue
│ ├── __init__.py
│ └── sched_filter_dailydue.py
├── sched_ignore_lapses_below_ivl
│ ├── __init__.py
│ └── sched_ignore_lapses_below_ivl.py
├── sched_sibling_spacing_whitelist
│ ├── __init__.py
│ └── sched_sibling_spacing_whitelist.py
├── search_last_edited
│ ├── __init__.py
│ └── search_last_edited.py
├── stats_true_retention_extended
│ ├── __init__.py
│ └── stats_true_retention_extended.py
├── tagedit_enhancements
│ ├── __init__.py
│ └── tagedit_enhancements.py
└── tagedit_subtag_completer
│ ├── __init__.py
│ ├── manifest.json
│ └── tagedit_subtag_completer.py
└── tools
├── build_ankiaddon.sh
└── build_zips.sh
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Anki add-ons
2 | #
3 | # Prepares zip file for upload to AnkiWeb
4 | #
5 | # Copyright: (c) 2017-2018 Glutanimate
6 | # License: GNU AGPLv3
7 |
8 | all: clean zip
9 |
10 | zip:
11 | ./tools/build_zips.sh
12 |
13 | ankiaddon:
14 | ./tools/build_ankiaddon.sh
15 |
16 | clean:
17 | rm -rf build
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Miscellaneous Anki Add-ons
2 |
3 | This repository contains most of the smaller Anki add-ons I have written over the years. For more information on each add-on please take a look at the comments in the respective source file, or at its [AnkiWeb description](docs/descriptions.md).
4 |
5 |
6 |
7 | - [Generic Installation Instructions](#generic-installation-instructions)
8 | - [Naming Scheme](#naming-scheme)
9 | - [Credits](#credits)
10 | - [License](#license)
11 | - [Other Anki-related Projects](#other-anki-related-projects)
12 |
13 |
14 |
15 | ## Generic Installation Instructions
16 |
17 | **Installation from AnkiWeb**
18 |
19 | Most add-ons in this repository have been published on [AnkiWeb](https://ankiweb.net/shared/addons/) and may be comfortably installed via Anki's own add-on management system. To correlate each source file with its listing on AnkiWeb please refer to the add-on titles in their [AnkiWeb descriptions](docs/descriptions.md).
20 |
21 | **Manual Installation**
22 |
23 | 1. Open Anki's add-on folder by navigating to *Tools* -> *Add-ons* -> *Open add-ons folder* from Anki's main screen
24 | 2. Download a copy of the [full repository zip archive](https://github.com/glutanimate/anki-addons-misc/archive/master.zip)
25 | 3. Locate the add-ons you want to install in the `src/` directory
26 | 4. Copy the add-on files to Anki's add-on directory
27 |
28 | - in case of *Anki 2.0*: For each of the add-ons, copy all files its folder aside from `__init__.py` into the top-most level of Anki's add-on directory (e.g. for `reviewer_auto_rate_hotkey` you would only copy `reviewer_auto_rate_hotkey.py`)
29 | - in case of *Anki 2.1*: For each of the add-ons, copy the entire add-on folder into the top-most level of Anki's add-on directory (e.g. for `reviewer_auto_rate_hotkey` you would copy the entire `reviewer_auto_rate_hotkey` folder). **Note:** Not all add-ons in this repository have been transitioned to Anki 2.1, yet. To learn more about the compatibility status of each add-on please see [here](docs/anki21.md).
30 |
31 | 5. Restart Anki to see the changes
32 |
33 | ## Naming Scheme
34 |
35 | The source file naming describes which part of Anki each add-on interacts with. As such, the names do not always correspond with their listings on AnkiWeb.
36 |
37 | Add-on file names also have an impact on their loading order. Files with a leading special character are designed to be imported after most other add-ons have been loaded, as they might interact with them.
38 |
39 | ## Credits
40 |
41 | Some of the add-ons found in this repository were either adopted from earlier works by other authors or simply constitute re-uploads of add-ons that disappeared from AnkiWeb, with no original code repository to be found. I have tried to document the development history of each of these add-ons in their source code header, but more detailed information may also be found [here](docs/credits.md).
42 |
43 | ## License
44 |
45 | Most of the add-ons in this repository are licensed under the same license as Anki, the [GNU AGPL, version 3 or later](https://www.gnu.org/licenses/agpl.html). Please check the source code comments for more details on the licensing of each add-on.
46 |
47 | ## Other Anki-related Projects
48 |
49 | Make sure to also check out my larger Anki projects:
50 |
51 | - [Image Occlusion Enhanced](https://github.com/Glutanimate/image-occlusion-enhanced)
52 | - [Cloze Overlapper](https://github.com/glutanimate/cloze-overlapper)
53 | - [Review Heatmap](https://github.com/Glutanimate/review-heatmap)
54 | - [Advanced Previewer](https://github.com/glutanimate/advanced-previewer)
55 | - [Sticky Searches](https://github.com/glutanimate/sticky-searches)
56 | - [Note Organizer](https://github.com/glutanimate/note-organizer)
57 | - [Sequence Inserter](https://github.com/glutanimate/sequence-inserter)
58 | - [HTML Cleaner](https://github.com/glutanimate/html-cleaner)
59 | - [Unified Remote for Anki](https://github.com/Glutanimate/unified-remote-anki)
--------------------------------------------------------------------------------
/src/browser_batch_remove_formatting/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2018 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_batch_remove_formatting
--------------------------------------------------------------------------------
/src/browser_batch_remove_formatting/browser_batch_remove_formatting.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Anki Add-on: Batch-Remove Field Formatting
4 |
5 | Adds a menu entry to the card browser that removes specific
6 | HTML tags from all fields in all selected notes.
7 |
8 | The HTML tags to be removed can be specified in the
9 | user configuration section below this comment header.
10 |
11 | Based on "Clear Field Formatting (HTML) in Bulk" by Felix Esch
12 | (https://github.com/Araeos/ankiplugins)
13 |
14 | Copyright: (c) Felix Esch 2016
15 | (c) Glutanimate 2017
16 | License: GNU AGPLv3 or later
17 | """
18 |
19 | from __future__ import (absolute_import, division,
20 | print_function, unicode_literals)
21 |
22 | ############## USER CONFIGURATION START ##############
23 |
24 | STRIP_TAGS = ['b', 'i', 'u'] # list of html tags to remove
25 |
26 | ############## USER CONFIGURATION END ##############
27 |
28 | from aqt.qt import *
29 | from aqt import mw
30 | from aqt.utils import tooltip
31 | from anki.hooks import addHook
32 | from anki import version as anki_version
33 |
34 | if anki_version.startswith("2.1"):
35 | from bs4 import BeautifulSoup
36 | ANKI21 = True
37 | else:
38 | from BeautifulSoup import BeautifulSoup
39 | ANKI21 = False
40 |
41 |
42 | def stripFormatting(fields):
43 | """
44 | Uses BeautifulSoup to remove STRIP_TAGS from each string in
45 | supplied list.
46 |
47 | Parameters
48 | ----------
49 | fields : list of strings
50 | list containing html of field contents
51 | Returns
52 | -------
53 | stripped_fields : list of strings
54 | processed list of fields
55 | """
56 |
57 | stripped_fields = []
58 |
59 | for html in fields:
60 | soup = BeautifulSoup(html, "html.parser")
61 | for tag in STRIP_TAGS:
62 | for match in soup.findAll(tag):
63 | match.replaceWithChildren()
64 | text = str(soup) if ANKI21 else unicode(soup)
65 | stripped_fields.append(text)
66 |
67 | return stripped_fields
68 |
69 |
70 | def setupMenu(browser):
71 | """
72 | Add the button to the browser menu "edit".
73 | """
74 | a = browser.form.menuEdit.addAction('Batch-Remove Field Formatting')
75 | a.setShortcut(QKeySequence("Ctrl+Alt+Shift+R"))
76 | a.triggered.connect(lambda _, b=browser: onClearFormatting(b))
77 |
78 |
79 | def onClearFormatting(browser):
80 | """
81 | Clears the formatting for every selected note.
82 | Also creates a restore point, allowing a single undo operation.
83 |
84 | Parameters
85 | ----------
86 | browser : Browser
87 | the anki browser from which the function is called
88 | """
89 |
90 | nids = browser.selectedNotes()
91 | if not nids:
92 | tooltip(_("No cards selected."), period=2000)
93 | return
94 |
95 | mw.checkpoint("Batch-Remove Field Formatting")
96 | mw.progress.start()
97 | for nid in nids:
98 | note = mw.col.getNote(nid)
99 | note.fields = stripFormatting(note.fields)
100 | note.flush()
101 | mw.progress.finish()
102 | mw.reset()
103 |
104 | # Hooks
105 |
106 | addHook("browser.setupMenus", setupMenu)
107 |
--------------------------------------------------------------------------------
/src/browser_create_duplicate/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_create_duplicate
--------------------------------------------------------------------------------
/src/browser_create_duplicate/browser_create_duplicate.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Duplicate Selected Notes
5 |
6 | Select any number of cards in the card browser and duplicate their notes
7 |
8 | To use:
9 |
10 | 1) Open the card browser
11 | 2) Select the desired cards
12 | 3) Press CTRL+ALT+C or go to Edit > Duplicate Notes
13 |
14 | A few pointers:
15 |
16 | - All cards generated by each note will be duplicated alongside the note
17 | - All duplicated cards will end up in the deck of the first selected cards
18 | - The duplicated cards should look exactly like the originals
19 | - Tags are preserved in the duplicated notes
20 | - Review history is NOT duplicated to the new cards (they appear as new cards)
21 | - The notes will be marked as duplicates (because they are!)
22 |
23 | This add-on is based on "Create Copy of Selected Cards" by Kealan Hobelmann
24 |
25 | Based on: "Create Copy of Selected Cards"
26 | by Kealan Hobelmann (https://ankiweb.net/shared/info/787914845)
27 |
28 | Copyright: (c) Glutanimate 2016-2017
29 | License: GNU AGPLv3 or later
30 | """
31 |
32 | from aqt.qt import *
33 | from anki.hooks import addHook
34 | from aqt.utils import tooltip
35 | from anki.utils import timestampID
36 |
37 | def createDuplicate(self):
38 | mw = self.mw
39 | # Get deck of first selected card
40 | cids = self.selectedCards()
41 | if not cids:
42 | tooltip(_("No cards selected."), period=2000)
43 | return
44 | did = mw.col.db.scalar(
45 | "select did from cards where id = ?", cids[0])
46 | deck = mw.col.decks.get(did)
47 | if deck['dyn']:
48 | tooltip(_("Cards can't be duplicated when they are in a filtered deck."), period=2000)
49 | return
50 |
51 | # Set checkpoint
52 | mw.progress.start()
53 | mw.checkpoint("Duplicate Notes")
54 | self.model.beginReset()
55 |
56 | # Copy notes
57 | for nid in self.selectedNotes():
58 | # print "Found note: %s" % (nid)
59 | note = mw.col.getNote(nid)
60 | model = note._model
61 |
62 | # Assign model to deck
63 | mw.col.decks.select(deck['id'])
64 | mw.col.decks.get(deck)['mid'] = model['id']
65 | mw.col.decks.save(deck)
66 |
67 | # Assign deck to model
68 | mw.col.models.setCurrent(model)
69 | mw.col.models.current()['did'] = deck['id']
70 | mw.col.models.save(model)
71 |
72 | # Create new note
73 | note_copy = mw.col.newNote()
74 | # Copy tags and fields (all model fields) from original note
75 | note_copy.tags = note.tags
76 | note_copy.fields = note.fields
77 |
78 | # Refresh note and add to database
79 | note_copy.flush()
80 | mw.col.addNote(note_copy)
81 |
82 | # Reset collection and main window
83 | self.model.endReset()
84 | mw.col.reset()
85 | mw.reset()
86 | mw.progress.finish()
87 |
88 | tooltip(_("Notes duplicated."), period=1000)
89 |
90 |
91 | def setupMenu(self):
92 | menu = self.form.menuEdit
93 | menu.addSeparator()
94 |
95 | a = menu.addAction('Create Duplicate')
96 | a.setShortcut(QKeySequence("Ctrl+Alt+C"))
97 | a.triggered.connect(lambda _, b=self: onCreateDuplicate(b))
98 |
99 | def onCreateDuplicate(self):
100 | createDuplicate(self)
101 |
102 | addHook("browser.setupMenus", setupMenu)
103 |
--------------------------------------------------------------------------------
/src/browser_create_filtered_deck/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_create_filtered_deck
--------------------------------------------------------------------------------
/src/browser_create_filtered_deck/browser_create_filtered_deck.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Create Filtered Deck from Browser
5 |
6 | Creates filtered deck based on current search / selected cards
7 |
8 | Copyright: (c) Glutanimate 2016-2018
9 | License: GNU AGPLv3 or later
10 | """
11 |
12 | from __future__ import unicode_literals
13 |
14 | from aqt.qt import *
15 | from anki.hooks import addHook
16 |
17 | def createFilteredDeck(self, from_selected=False):
18 | col = self.mw.col
19 | if from_selected:
20 | cids = self.selectedCards()
21 | search = " OR ".join("cid:{}".format(cid) for cid in cids)
22 | else:
23 | search = self.form.searchEdit.lineEdit().text()
24 | if 'deck:current' in search:
25 | did = col.conf['curDeck']
26 | curDeck = col.decks.get(did)['name']
27 | search = search.replace('deck:current', '"deck:' + curDeck + '"')
28 | self.mw.onCram(search)
29 |
30 | def setupMenu(self):
31 | menu = self.form.menuEdit
32 | menu.addSeparator()
33 | a = menu.addAction('Filtered Deck from Search')
34 | a.setShortcut(QKeySequence("Ctrl+Shift+D"))
35 | a.triggered.connect(lambda _, b=self: createFilteredDeck(b))
36 | a = menu.addAction('Filtered Deck with Selected Cards')
37 | a.setShortcut(QKeySequence("Ctrl+Shift+Alt+D"))
38 | a.triggered.connect(lambda _,
39 | b=self: createFilteredDeck(b, from_selected=True))
40 |
41 | addHook("browser.setupMenus", setupMenu)
42 |
--------------------------------------------------------------------------------
/src/browser_field_to_tags/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_field_to_tags
--------------------------------------------------------------------------------
/src/browser_field_to_tags/browser_field_to_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Convert Field to Tags
5 |
6 | Provides a new Edit menu entry in the Browser that allows you
7 | to convert the contents of a specific field to tags.
8 |
9 | Copyright: (c) Glutanimate 2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | from __future__ import unicode_literals
14 |
15 | ############## USER CONFIGURATION START ##############
16 |
17 | HOTKEY = "Ctrl+Alt+M"
18 | SEPARATOR = "_" # whitespace will be replaced with this character
19 |
20 | ############## USER CONFIGURATION END ##############
21 |
22 | from aqt.qt import *
23 | from aqt.browser import Browser
24 | from aqt.utils import askUser, tooltip, restoreGeom, saveGeom
25 |
26 | from anki import find
27 | from anki.hooks import addHook
28 | from anki.utils import stripHTML
29 |
30 |
31 | def getField(browser, fields):
32 | """Invoke field selection dialog and return field"""
33 | d = QDialog(browser)
34 | l = QVBoxLayout(d)
35 | d.label = QLabel(
36 | "Please select the field you would like to convert to tags", d)
37 | d.fieldSel = QComboBox(d)
38 | d.fieldSel.addItems(fields)
39 | d.buttonBox = QDialogButtonBox(d)
40 | d.buttonBox.setOrientation(Qt.Horizontal)
41 | d.buttonBox.setStandardButtons(
42 | QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
43 | d.buttonBox.accepted.connect(d.accept)
44 | d.buttonBox.rejected.connect(d.reject)
45 | l.addWidget(d.label)
46 | l.addWidget(d.fieldSel)
47 | l.addWidget(d.buttonBox)
48 | d.setWindowModality(Qt.WindowModal)
49 | d.setWindowTitle("Field to Tags")
50 | restoreGeom(d, "fieldtotags")
51 | r = d.exec_()
52 | saveGeom(d, "fieldtotags")
53 | if not r:
54 | return None
55 |
56 | idx = d.fieldSel.currentIndex()
57 | field = fields[idx]
58 |
59 | return field
60 |
61 |
62 | def fieldToTags(self, nids, field):
63 | """Add field contents to to note tags"""
64 | edited = 0
65 | for nid in nids:
66 | note = self.mw.col.getNote(nid)
67 | if field not in note:
68 | continue
69 | html = note[field]
70 | text = stripHTML(html).strip()
71 | if not text:
72 | continue
73 | tag = SEPARATOR.join(text.split())
74 | if note.hasTag(tag):
75 | continue
76 | note.addTag(tag)
77 | note.flush()
78 | edited += 1
79 | return edited
80 |
81 |
82 | def onFieldToTags(self, _):
83 | """Main function"""
84 | nids = self.selectedNotes()
85 | count = len(nids)
86 | if not nids:
87 | tooltip("Please select some cards.")
88 | return
89 | fields = sorted(find.fieldNames(self.col, downcase=False))
90 | if not fields:
91 | tooltip("No fields found."
92 | "Something might be wrong with your collection")
93 | return
94 |
95 | field = getField(self, fields)
96 |
97 | if not field:
98 | return
99 |
100 | q = ("Are you sure you want to convert the '{}' field "
101 | "to tags in {} selected notes?".format(field, count))
102 | ret = askUser(q, parent=self, title="Please confirm your choice")
103 | if not ret:
104 | return
105 |
106 | self.mw.checkpoint("Find and Replace")
107 | self.mw.progress.start()
108 | self.model.beginReset()
109 |
110 | edited = self.fieldToTags(nids, field)
111 |
112 | self.model.endReset()
113 | self.mw.progress.finish()
114 | tooltip("{} out of {} notes updated.".format(edited, count))
115 |
116 |
117 | # Hooks
118 |
119 | def setupMenu(self):
120 | menu = self.form.menuEdit
121 | menu.addSeparator()
122 | a = menu.addAction('Convert Field to Tags...')
123 | a.setShortcut(QKeySequence(HOTKEY))
124 | a.triggered.connect(self.onFieldToTags)
125 |
126 | addHook("browser.setupMenus", setupMenu)
127 | Browser.onFieldToTags = onFieldToTags
128 | Browser.fieldToTags = fieldToTags
--------------------------------------------------------------------------------
/src/browser_more_hotkeys/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_more_hotkeys
--------------------------------------------------------------------------------
/src/browser_more_hotkeys/browser_more_hotkeys.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Simple Anki addon that adds more hotkeys to the card browser
5 |
6 | Copyright: (c) Glutanimate 2016-2017
7 | License: GNU AGPLv3 or later
8 | """
9 |
10 | from aqt.qt import *
11 | from anki.hooks import addHook
12 |
13 | def onBrowserSetupMenus(self):
14 | c = self.connect; f = self.form; s = SIGNAL("triggered()")
15 | self.invCut = QShortcut(QKeySequence("Ctrl+Alt+I"), self)
16 | c(self.invCut, SIGNAL("activated()"), self.invertSelection)
17 |
18 | c = self.connect; f = self.form; s = SIGNAL("triggered()")
19 | self.schedCut = QShortcut(QKeySequence("Ctrl+Alt+Shift+R"), self)
20 | c(self.schedCut, SIGNAL("activated()"), self.reschedule)
21 |
22 | addHook("browser.setupMenus", onBrowserSetupMenus)
--------------------------------------------------------------------------------
/src/browser_refresh/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_refresh
--------------------------------------------------------------------------------
/src/browser_refresh/browser_refresh.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Refresh Browser List
5 |
6 | Refreshes browser view and optionally changes the sorting column
7 | (e.g. to show newly added cards since last search)
8 |
9 | Copyright: (c) Glutanimate 2016-2017
10 | 2018 Arthur Milchior (porting to 2.1)
11 | License: GNU AGPLv3 or later
12 | """
13 |
14 | # Do not modify the following line
15 | from __future__ import unicode_literals
16 | from anki import version as anki_version
17 | anki21 = anki_version.startswith("2.1.")
18 |
19 | ######## USER CONFIGURATION START ########
20 |
21 | SORTING_COLUMN = "noteCrt"
22 | # Custom column sorting applied on hotkey toggle
23 | # - only works if that column is active in the first place
24 | # - set to note creation time by default ("noteCrt")
25 | # - can be disabled by setting SORTING_COLUM = ""
26 | #
27 | # Valid Values (regular browser):
28 | #
29 | # 'question' 'answer' 'template' 'deck' 'noteFld' 'noteCrt' 'noteMod'
30 | # 'cardMod' 'cardDue' 'cardIvl' 'cardEase' 'cardReps' 'cardLapses'
31 | # 'noteTags' 'note'
32 | #
33 | # Additional values (advanced browser):
34 | #
35 | # 'cfirst' 'clast' 'cavgtime' 'ctottime' 'ntags' 'coverdueivl' 'cprevivl'
36 |
37 |
38 | ######## USER CONFIGURATION END ########
39 |
40 | from aqt.qt import *
41 | from aqt.browser import Browser
42 | from anki.hooks import addHook
43 | def debug(t):
44 | #print(t)
45 | pass
46 |
47 | def refreshView(self):
48 | debug("Calling refreshView()")
49 | if anki21:
50 | self.onSearchActivated()
51 | else:
52 | self.onSearch(reset=True)
53 | if SORTING_COLUMN:
54 | try:
55 | col_index = self.model.activeCols.index(SORTING_COLUMN)
56 | self.onSortChanged(col_index, True)
57 | self.form.tableView.selectRow(0)
58 | except ValueError:
59 | pass
60 |
61 | def setupMenu(self):
62 | menu = self.form.menuEdit
63 | menu.addSeparator()
64 | a = menu.addAction('Refresh View')
65 | a.setShortcut(QKeySequence("CTRL+F5" if anki21 else "F5"))
66 | a.triggered.connect(self.refreshView)
67 |
68 | Browser.refreshView = refreshView
69 | addHook("browser.setupMenus", setupMenu)
70 |
--------------------------------------------------------------------------------
/src/browser_replace_tag/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_replace_tag
--------------------------------------------------------------------------------
/src/browser_replace_tag/browser_replace_tag.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Replace tag
5 |
6 | Replace tag in selected notes. Combines tag 'add' and 'remove' dialogs
7 | in one workflow.
8 |
9 | Copyright: (c) Glutanimate 2016-2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | from aqt.qt import *
14 | from aqt.utils import getText, tooltip
15 | from aqt.tagedit import TagEdit
16 | from anki.hooks import addHook
17 | from anki.lang import _
18 |
19 |
20 | def myGetTag(parent, deck, question, tags="user", taglist=None, **kwargs):
21 | te = TagEdit(parent)
22 | te.setCol(deck)
23 | if taglist is not None:
24 | # set tag list manually
25 | te.model.setStringList(taglist)
26 | ret = getText(question, parent, edit=te, **kwargs)
27 | te.hideCompleter()
28 | return ret
29 |
30 | def replaceTag(self):
31 | mw = self.mw
32 | selected = self.selectedNotes()
33 | if not selected:
34 | tooltip("No cards selected.", period=2000)
35 | return
36 |
37 | firstNote = mw.col.getNote(selected[0])
38 | msg = "Which tag would you like to replace? Please select just one."
39 | (oldTag, r) = myGetTag(self, mw.col, msg, taglist=firstNote.tags, title="Choose tag")
40 | if not r or not oldTag.strip():
41 | return
42 | oldTag = oldTag.split()[0]
43 |
44 | msg = "Which tag would you like to replace %s with?" % oldTag
45 | (newTag, r) = myGetTag(self, mw.col, msg, title="Replace Tag", default=oldTag)
46 | if not r or not newTag.strip():
47 | return
48 |
49 | mw.checkpoint("replace tag")
50 | mw.progress.start()
51 | self.model.beginReset()
52 | for nid in selected:
53 | note = mw.col.getNote(nid)
54 | if note.hasTag(oldTag):
55 | note.delTag(oldTag)
56 | note.addTag(newTag)
57 | note.flush()
58 | self.model.endReset()
59 | mw.requireReset()
60 | mw.progress.finish()
61 | mw.reset()
62 | tooltip("Tag replaced. Use 'Check Database' to remove unused tags.")
63 |
64 | def setupMenu(self):
65 | try:
66 | # used by multiple add-ons, so we check for its existence first
67 | menu = self.menuTags
68 | except:
69 | self.menuTags = QMenu(_("Tags"))
70 | action = self.menuBar().insertMenu(self.mw.form.menuTools.menuAction(), self.menuTags)
71 | menu = self.menuTags
72 | menu.addSeparator()
73 | a = menu.addAction('Replace Tag...')
74 | a.setShortcut(QKeySequence("Ctrl+Alt+Shift+T"))
75 | a.triggered.connect(lambda _, b=self: replaceTag(b))
76 |
77 |
78 | addHook("browser.setupMenus", setupMenu)
79 |
--------------------------------------------------------------------------------
/src/browser_search_hotkeys/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_search_hotkeys
--------------------------------------------------------------------------------
/src/browser_search_hotkeys/browser_search_hotkeys.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Browser search hotkeys
5 |
6 | Set up hotkeys for searches in the browser.
7 |
8 | Hotkeys follow this scheme: Ctrl+S –> (_modifier_) + _key_
9 | (hit Ctrl+S to start the key sequence, then the key assigned to
10 | your search, plus an optional modifier.)
11 |
12 | You can use keyboard modifiers to control whether to add a term to the
13 | search, negate it, remove it, or do something else. This follows the same
14 | logic as the default behaviour in Anki when clicking on a search term in
15 | the sidebar.
16 |
17 | For instance, line 2 in the default search_shortcuts dict assigns the search
18 | 'added 1' (cards added today) to 'T'. This defines the following key sequences
19 | in the browser:
20 |
21 | Ctrl+S -> T replace search field with 'added:1'
22 | Ctrl+S -> Ctrl+T add 'added:1' to existing search
23 | Ctrl+S -> Alt+T: replace search field with '-added:1'
24 | Ctrl+S -> Ctrl+Alt+T: add '-added:1' to existing search
25 | Ctrl+S -> Shift+T: add 'or added:1' to existing search
26 |
27 | Copyright: (c) Glutanimate 2016-2017
28 | License: GNU AGPLv3 or later
29 | """
30 |
31 | from __future__ import unicode_literals
32 |
33 | #============USER CONFIGURATION START===============
34 |
35 | # assign hotkeys to searches
36 | search_shortcuts = {
37 | 'A': {'search': ''}, # All together now
38 | 'T': {'search': 'added:1'}, # Today
39 | 'V': {'search': 'rated:1'}, # Viewed
40 | 'G': {'search': 'rated:1:1'}, # aGain today
41 | 'F': {'search': 'card:1'}, # First
42 | 'C': {'search': 'deck:current'},# Current
43 | 'N': {'search': 'is:new'}, # New
44 | 'L': {'search': 'is:learn'}, # Learn
45 | 'R': {'search': 'is:review'}, # Review
46 | 'D': {'search': 'is:due'}, # Due
47 | 'S': {'search': 'is:suspended'},# Suspended
48 | 'B': {'search': 'is:buried'}, # Buried
49 | 'M': {'search': 'tag:marked'}, # Marked
50 | 'E': {'search': 'tag:leech'}, # lEech
51 | }
52 |
53 | # define the sequence starter hotkey
54 | sequence_starter = "Ctrl+S"
55 |
56 | #==============USER CONFIGURATION END==============
57 |
58 | from aqt.qt import *
59 | from aqt.browser import Browser
60 | from anki.hooks import addHook
61 |
62 | search_modifiers = {
63 | '': 'replace',
64 | 'Ctrl+': 'add',
65 | 'Alt+': 'negate',
66 | 'Ctrl+Alt+': 'add-negate',
67 | 'Shift+': 'add-or',
68 | }
69 |
70 | def setSearchField(self, search, action):
71 | cur = self.form.searchEdit.lineEdit().text()
72 | if action == "replace":
73 | pass
74 | elif action == "add":
75 | search = cur + " " + search
76 | elif action == "negate":
77 | search = "-" + search
78 | elif action == "add-negate":
79 | search = cur + " " + "-" + search
80 | elif action == "add-or":
81 | search = cur + " or " + search
82 | self.form.searchEdit.lineEdit().setText(search)
83 | self.onSearch()
84 |
85 | def onSetupMenus(self):
86 | for key, binding in search_shortcuts.items():
87 | search = binding["search"]
88 | for modifier, action in search_modifiers.items():
89 | key_sequence = modifier + key
90 | a = QShortcut(QKeySequence(sequence_starter + ', ' + key_sequence), self)
91 | a.activated.connect(lambda c=search,d=action: self.setSearchField(c,d))
92 |
93 | addHook("browser.setupMenus", onSetupMenus)
94 | Browser.setSearchField = setSearchField
--------------------------------------------------------------------------------
/src/browser_sidebar_tweaks/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import browser_sidebar_tweaks
--------------------------------------------------------------------------------
/src/common_context_search/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import common_context_search
--------------------------------------------------------------------------------
/src/common_context_search/common_context_search.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Context Menu Search Add-on for Anki
4 | #
5 | # Copyright (C) 2015-2019 Aristotelis P.
6 | # (C) 2015 Eddie Blundell
7 | # (C) 2013 Steve AW
8 | #
9 | # This program is free software: you can redistribute it and/or modify
10 | # it under the terms of the GNU Affero General Public License as
11 | # published by the Free Software Foundation, either version 3 of the
12 | # License, or (at your option) any later version, with the additions
13 | # listed at the end of the accompanied license file.
14 | #
15 | # This program is distributed in the hope that it will be useful,
16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | # GNU Affero General Public License for more details.
19 | #
20 | # Any modifications to this file must keep this entire header intact.
21 |
22 | """
23 | Adds context menu entries for searching the card browser and
24 | various online search providers.
25 |
26 | Based on 'OSX Dictionary Lookup' by Eddie Blundell
27 | and 'Search Google Images' by Steve AW.
28 | """
29 |
30 | from __future__ import (absolute_import, division,
31 | print_function, unicode_literals)
32 |
33 | import aqt
34 | from aqt.qt import *
35 | from aqt.utils import openLink
36 | from anki.hooks import addHook
37 |
38 | from .config import config
39 |
40 | # Local search
41 |
42 | def lookupLocal(text):
43 | browser = aqt.dialogs.open("Browser", aqt.mw)
44 | browser.form.searchEdit.lineEdit().setText('"{}"'.format(text))
45 | if ANKI20:
46 | browser.onSearch()
47 | else:
48 | browser.onSearchActivated()
49 |
50 | # Online search
51 |
52 | def lookupOnline(text, idx):
53 | text = " ".join(text.strip().split())
54 | for url in SEARCH_PROVIDERS[idx][1]:
55 | # opt for using custom formatter to avoid errors with user URLs:
56 | url = url.replace("%s", "text")
57 | openLink(url)
58 |
59 |
60 | # Context menu
61 |
62 | def addToContextMenu(view, menu):
63 | """Add 'lookup' action to context menu"""
64 | if USE_CUSTOM_STYLESHEET:
65 | menu.setStyleSheet(stylesheet)
66 | selected = view.page().selectedText()
67 | if not selected:
68 | return
69 |
70 | suffix = (selected[:20] + '..') if len(selected) > 20 else selected
71 | label = u'Search for "%s" in Card &Browser' % suffix
72 | menu.addSeparator()
73 | a = menu.addAction(label)
74 | a.triggered.connect(lambda _, t=selected: lookup_browser(t))
75 |
76 | search_menu = None
77 | if len(SEARCH_PROVIDERS) > 10:
78 | search_menu = menu.addMenu(u'&Search for "%s" with...' % suffix)
79 |
80 | for idx, provider in enumerate(SEARCH_PROVIDERS):
81 | if search_menu:
82 | label = provider[0]
83 | menu = search_menu
84 | else:
85 | label = u'Search for "%s" on %s' % (suffix, provider[0])
86 | a = menu.addAction(label)
87 | a.triggered.connect(lambda _, i=idx,t=selected: lookup_online(t, i))
88 |
89 | # Hooks / patches
90 |
91 | addHook("AnkiWebView.contextMenuEvent", addToContextMenu)
92 | addHook("EditorWebView.contextMenuEvent", addToContextMenu)
93 |
--------------------------------------------------------------------------------
/src/common_context_search/config.md:
--------------------------------------------------------------------------------
1 | **Important**: These settings do not sync and require a restart to apply.
2 |
3 | - `onlineSearchProviders` (dict): Dictionary of search providers to add to the context menu. Each entry consists of a key, the name of the entry, and a value.
4 |
5 | The value may either be:
6 | - a single string, denoting the URL to open (e.g. `"name": "url"`),
7 | - a list of multiple URLs to open at once (e.g. `"name": ["url1", "url2"]`),
8 | - a subdictionary to group multiple menu entries under a separate submenu (e.g. `"submenu": {"name1": "url1", "name2": "url2"}`)
9 |
10 | Any occurences of the placeholder `%s` in your URLs will be replaced with the actual search term.
11 |
12 | For examples on all of the variations above, please refer to the default configuration of the add-on.
13 |
14 | - `localSearchEnabled` (true/false): Whether to show menu entry for searching in card browser. Default: `true`.
15 | - `useCustomStylesheet` (true/false): Whether to use alternate, more compact, menu styling. Might be buggy, so better left off. Default: `false`.
16 |
17 | Created with ❤️ by [Glutanimate](https://glutanimate.com/). If you enjoy this add-on please consider **[supporting me on Patreon](https://www.patreon.com/bePatron?u=7522179)**. Thanks!
--------------------------------------------------------------------------------
/src/common_context_search/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Context Menu Search Add-on for Anki
4 | #
5 | # Copyright (C) 2015-2019 Aristotelis P.
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as
9 | # published by the Free Software Foundation, either version 3 of the
10 | # License, or (at your option) any later version, with the additions
11 | # listed at the end of the accompanied license file.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU Affero General Public License for more details.
17 | #
18 | # Any modifications to this file must keep this entire header intact.
19 |
20 | """
21 | Configuration shim between Anki 2.0 and Anki 2.1
22 | """
23 |
24 | from __future__ import (absolute_import, division,
25 | print_function, unicode_literals)
26 |
27 | import os
28 | import io
29 |
30 | from aqt import mw
31 | from anki.utils import json
32 |
33 | from .platform import ANKI20, PATH_ADDON
34 |
35 | defaults_path = os.path.join(PATH_ADDON, "config.json")
36 | meta_path = os.path.join(PATH_ADDON, "meta.json")
37 |
38 | if not ANKI20:
39 | def getConfig():
40 | return mw.addonManager.getConfig(__name__)
41 |
42 | def writeConfig(config):
43 | mw.addonManager.writeConfig(__name__, config)
44 |
45 | else:
46 | def _addonMeta():
47 | """Get meta dictionary
48 |
49 | Reads in meta.json in add-on folder and returns
50 | resulting dictionary of user-defined metadata values.
51 |
52 | Note:
53 | Anki 2.1 stores both add-on meta data and customized
54 | settings in meta.json. In this module we are only dealing
55 | with the settings part.
56 |
57 | Returns:
58 | dict: config dictionary
59 |
60 | """
61 |
62 | try:
63 | meta = json.load(io.open(meta_path, encoding="utf-8"))
64 | except (IOError, OSError):
65 | meta = None
66 | except ValueError as e:
67 | print("Could not read meta.json: " + str(e))
68 | meta = None
69 |
70 | if not meta:
71 | meta = {"config": _addonConfigDefaults()}
72 | _writeAddonMeta(meta)
73 |
74 | return meta
75 |
76 | def _writeAddonMeta(meta):
77 | """Write meta dictionary
78 |
79 | Writes meta dictionary to meta.json in add-on folder.
80 |
81 | Args:
82 | meta (dict): meta dictionary
83 |
84 | """
85 |
86 | with io.open(meta_path, 'w', encoding="utf-8") as f:
87 | f.write(unicode(json.dumps(meta, indent=4,
88 | sort_keys=True,
89 | ensure_ascii=False)))
90 |
91 | def _addonConfigDefaults():
92 | """Get default config dictionary
93 |
94 | Reads in config.json in add-on folder and returns
95 | resulting dictionary of default config values.
96 |
97 | Returns:
98 | dict: config dictionary
99 |
100 | Raises:
101 | Exception: If config.json cannot be parsed correctly.
102 | (The assumption being that we would end up in an
103 | inconsistent state if we were to return an empty
104 | config dictionary. This should never happen.)
105 |
106 | """
107 |
108 | try:
109 | return json.load(io.open(defaults_path, encoding="utf-8"))
110 | except (IOError, OSError, ValueError) as e:
111 | print("Could not read config.json: " + str(e))
112 | raise Exception("Config file could not be read: " + str(e))
113 |
114 | def getConfig():
115 | """Get user config dictionary
116 |
117 | Merges user's keys into default config dictionary
118 | and returns the result.
119 |
120 | Returns:
121 | dict: config dictionary
122 |
123 | """
124 |
125 | config = _addonConfigDefaults()
126 | meta = _addonMeta()
127 | userConf = meta.get("config", {})
128 | config.update(userConf)
129 | return config
130 |
131 | def writeConfig(config):
132 | """Write user config dictionary
133 |
134 | Saves user's config dictionary via meta.json.
135 |
136 | Args:
137 | config (dict): user config dictionary
138 |
139 | """
140 |
141 | _writeAddonMeta({"config": config})
142 |
143 | # Only read in config once at app start for now
144 | config = getConfig()
145 |
--------------------------------------------------------------------------------
/src/common_context_search/platform.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Libaddon for Anki
4 | #
5 | # Copyright (C) 2018-2019 Aristotelis P.
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as
9 | # published by the Free Software Foundation, either version 3 of the
10 | # License, or (at your option) any later version, with the additions
11 | # listed at the end of the accompanied license file.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU Affero General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU Affero General Public License
19 | # along with this program. If not, see .
20 | #
21 | # NOTE: This program is subject to certain additional terms pursuant to
22 | # Section 7 of the GNU Affero General Public License. You should have
23 | # received a copy of these additional terms immediately following the
24 | # terms and conditions of the GNU Affero General Public License which
25 | # accompanied this program.
26 | #
27 | # If not, please request a copy through one of the means of contact
28 | # listed here: .
29 | #
30 | # Any modifications to this file must keep this entire header intact.
31 |
32 | """
33 | Constants providing information on current system and Anki platform
34 | """
35 |
36 | from __future__ import (absolute_import, division,
37 | print_function, unicode_literals)
38 |
39 | import sys
40 | import os
41 | from aqt import mw
42 | from anki import version
43 | from anki.utils import isMac, isWin
44 |
45 | __all__ = ["PYTHON3", "ANKI20", "SYS_ENCODING", "MODULE_ADDON",
46 | "MODULE_LIBADDON", "DIRECTORY_ADDONS", "JSPY_BRIDGE",
47 | "PATH_ADDON", "PATH_USERFILES", "PLATFORM"]
48 |
49 | PYTHON3 = sys.version_info[0] == 3
50 | ANKI20 = version.startswith("2.0.")
51 | SYS_ENCODING = sys.getfilesystemencoding()
52 |
53 | name_components = __name__.split(".")
54 |
55 | MODULE_ADDON = name_components[0]
56 | MODULE_LIBADDON = name_components[1]
57 |
58 | if ANKI20:
59 | DIRECTORY_ADDONS = mw.pm.addonFolder()
60 | JSPY_BRIDGE = "py.link"
61 | else:
62 | DIRECTORY_ADDONS = mw.addonManager.addonsFolder()
63 | JSPY_BRIDGE = "pycmd"
64 |
65 | PATH_ADDON = os.path.join(DIRECTORY_ADDONS, MODULE_ADDON)
66 | PATH_USERFILES = os.path.join(PATH_ADDON, "user_files")
67 |
68 | if isMac:
69 | PLATFORM = "mac"
70 | elif isWin:
71 | PLATFORM = "win"
72 | else:
73 | PLATFORM = "lin"
74 |
--------------------------------------------------------------------------------
/src/common_ctrlf_search/common ctrlf search.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Ctrl+F Search
5 | Entry point for the add-on into Anki
6 |
7 | Copyright: (c) 2017-2019 Glutanimate
8 |
9 | License: GNU AGPLv3
10 | """
11 |
12 | # IMPORTANT:
13 | #
14 | # If you are reading this and wondering where to find the
15 | # configuration section for Anki 2.0:
16 | #
17 | # As part of porting this add-on to Anki 2.1 I had to move the
18 | # main add-on module to a separate folder. It is now located under
19 | # common_ctrlf_search/common_ctrlf_search.py.
20 | # All configuration options are located
21 | # at the very top of that file.
22 | #
23 | # Please use NotePad++ or another text editor that supports python
24 | # source code files to edit it
25 |
26 | import common_ctrlf_search # noqa: F401
27 |
--------------------------------------------------------------------------------
/src/common_ctrlf_search/common_ctrlf_search/__init__.py:
--------------------------------------------------------------------------------
1 | from . import common_ctrlf_search
2 |
--------------------------------------------------------------------------------
/src/common_ctrlf_search/common_ctrlf_search/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "HOTKEY_SEARCH":"Ctrl+F",
3 | "HOTKEY_SEARCH_BROWSER":"Ctrl+Alt+Shift+F",
4 | "HOTKEY_NEXT":"F3",
5 | "HOTKEY_PREVIOUS":"Shift+F3",
6 | "Show_in_Browser_Context_menu":false
7 | }
8 |
--------------------------------------------------------------------------------
/src/common_ctrlf_search/common_ctrlf_search/config.md:
--------------------------------------------------------------------------------
1 | ### Hotkeys:
2 |
3 | If you use Anki 2.1: By default Anki uses "F3" for adding media. If you want to use the default keybinding of this add-on for "Next result" which is also "F3" in Anki 2.1 you need the add-on [Customize Keyboard Shortcuts](https://ankiweb.net/shared/info/24411424). In the config of this add-on change "editor add media" to a different key.
4 |
5 | You have to restart Anki so that changes take effect.
6 |
7 | - `HOTKEY_SEARCH` (default is `CTRL + F`). This opens a search bar at the bottom of the window which allows you to search the editor. This shortcut works in the Add window and the EditCurrent window (you see the latter if you press "e" during reviews).
8 | - `HOTKEY_SEARCH_BROWSER` (default is `CTRL + Alt + Shift + F`): There is a separate shortcut for the browser because in the browser window there are fewer easy-to-use shortcuts unused.
9 | - `Close search` (default is `Esc`).
10 | - `Next result` (default is `F3`).
11 | - `Previous result` (default is `Shift+F3`).
12 |
--------------------------------------------------------------------------------
/src/common_ctrlf_search/common_ctrlf_search/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ctrl+F Search",
3 | "package": "common_ctrlf_search"
4 | }
5 |
--------------------------------------------------------------------------------
/src/editor_autocomplete_whitelist/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_autocomplete_whitelist
--------------------------------------------------------------------------------
/src/editor_autocomplete_whitelist/editor_autocomplete_whitelist.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf8 -*-
2 | """
3 | Anki Add-on: Editor Autocomplete Whitelist
4 |
5 | This is a slightly modified versions of Editor Autocomplete by
6 | Sartak that switches out the field blacklist with a whitelist.
7 |
8 | Copyright: (c) Sartak 2013
9 | (c) Glutanimate 2016-2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | from aqt import mw, editor
14 | from aqt.qt import *
15 | from anki.hooks import wrap, addHook
16 | from anki.utils import splitFields, stripHTMLMedia, json
17 | import urllib2
18 |
19 | # Modify the following line to add the fields you would like to
20 | # enable autocomplete on (e.g. [ "field1", "field2" ] )
21 | AutocompleteFields = [ ]
22 |
23 | def mySetup(self, note, hide=True, focus=False):
24 | self.prevAutocomplete = ""
25 |
26 | # only initialize the autocompleter on Add Cards not in browser
27 | if self.note and self.addMode:
28 | self.web.eval("""
29 | document.styleSheets[0].addRule('.autocomplete', 'margin: 0.3em 0 1.0em 0; color: blue; text-decoration: underline; cursor: pointer;');
30 |
31 | // every second send the current state over
32 | setInterval(function () {
33 | if (currentField) {
34 | var r = {
35 | text: currentField.innerHTML,
36 | };
37 |
38 | py.run("autocomplete:" + JSON.stringify(r));
39 | }
40 | }, 1000);
41 | """)
42 |
43 | def myBridge(self, str, _old=None):
44 | if str.startswith("autocomplete"):
45 | (type, jsonText) = str.split(":", 1)
46 | result = json.loads(jsonText)
47 | text = self.mungeHTML(result['text'])
48 |
49 | # bail out if the user hasn't actually changed the field
50 | previous = "%d:%s" % (self.currentField, text)
51 | if self.prevAutocomplete == previous:
52 | return
53 | self.prevAutocomplete = previous
54 |
55 | if text == "" or len(text) > 500 or self.note is None:
56 | self.web.eval("$('.autocomplete').remove();");
57 | return
58 |
59 | field = self.note.model()['flds'][self.currentField]
60 |
61 | if not field['name'] in AutocompleteFields:
62 | field['no_autocomplete'] = True
63 | return
64 |
65 | # find a value from the same model and field whose
66 | # prefix is what the user typed so far
67 | query = "'note:%s' '%s:%s*'" % (
68 | self.note.model()['name'],
69 | field['name'],
70 | text)
71 |
72 | col = self.note.col
73 | res = col.findCards(query, order=True)
74 |
75 | if len(res) == 0:
76 | self.web.eval("$('.autocomplete').remove();");
77 | return
78 |
79 | # pull out the full value
80 | value = col.getCard(res[0]).note().fields[self.currentField]
81 |
82 | escaped = json.dumps(value)
83 |
84 | self.web.eval("""
85 | $('.autocomplete').remove();
86 |
87 | if (currentField) {
88 | $('
' + %s + '
').click(function () {
89 | currentField.focus();
90 | currentField.innerHTML = %s;
91 | saveField("key");
92 | }).insertAfter(currentField)
93 | }
94 | """
95 | % (escaped, escaped))
96 | else:
97 | _old(self, str)
98 |
99 | # XXX must figure out how to add noAutocomplete checkbox to form
100 | def myLoadField(self, idx):
101 | fld = self.model['flds'][idx]
102 | f = self.form
103 | if 'no_autocomplete' in fld.keys():
104 | f.noAutocomplete.setChecked(fld['no_autocomplete'])
105 |
106 | def mySaveField(self):
107 | # not initialized yet?
108 | if self.currentIdx is None:
109 | return
110 | idx = self.currentIdx
111 | fld = self.model['flds'][idx]
112 | f = self.form
113 | fld['no_autocomplete'] = f.noAutocomplete.isChecked()
114 |
115 | # apply autocomplete on hotkey toggle
116 | def applyAutocomplete(editor):
117 | editor.web.eval("""
118 | if ($('.autocomplete').text()) {
119 | currentField.focus();
120 | currentField.innerHTML = $('.autocomplete').text();
121 | saveField("key");
122 | }
123 | """)
124 |
125 | # assign hotkey
126 | def onSetupButtons(editor):
127 | # default: Alt + Return
128 | # insert custom key sequences here:
129 | t = QShortcut(QKeySequence(Qt.ALT + Qt.Key_Return), editor.parentWindow)
130 | t.connect(t, SIGNAL("activated()"),
131 | lambda : applyAutocomplete(editor))
132 |
133 | addHook("setupEditorButtons", onSetupButtons)
134 |
135 | editor.Editor.bridge = wrap(editor.Editor.bridge, myBridge, 'around')
136 | editor.Editor.setNote = wrap(editor.Editor.setNote, mySetup, 'after')
137 |
--------------------------------------------------------------------------------
/src/editor_clear_all/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_clear_all
--------------------------------------------------------------------------------
/src/editor_clear_all/editor_clear_all.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Reset Editor fields
5 |
6 | Adds hotkeys to reset the editor by various degrees.
7 |
8 | Based on Clear All Editor Fields add-on by Mirco Kraenz
9 | (https://github.com/proSingularity/anki2-addons)
10 |
11 | Copyright: (c) Glutanimate 2016-2017
12 | License: GNU AGPLv3 or later
13 | """
14 |
15 |
16 | from anki.hooks import addHook
17 | from aqt.qt import *
18 | from aqt import mw, browser
19 |
20 | excluded_from_clearing = ["Quellen"]
21 |
22 | clear_all_shortcut = "Ctrl+Alt+Shift+R"
23 | clear_most_shortcut = "Ctrl+Shift+R"
24 |
25 | def clear_all_editor_fields(self, mode):
26 | u'''Remove text from fields in editor. '''
27 | note = self.note
28 | # enumerate all fieldNames of the current note
29 | for c, field_name in enumerate(self.mw.col.models.fieldNames(note.model())):
30 | if mode == "most" and field_name in excluded_from_clearing:
31 | continue
32 | note[field_name] = ''
33 | self.loadNote()
34 | self.web.setFocus()
35 | self.web.eval("focusField(%d);" % self.currentField)
36 | self.web.eval('saveField("key");')
37 |
38 |
39 | def onSetupButtons(self):
40 | if not isinstance(self.parentWindow, browser.Browser):
41 | # avoid shortcut conflicts in browser
42 | t = QShortcut(QKeySequence(clear_all_shortcut), self.parentWindow)
43 | t.connect(t, SIGNAL("activated()"),
44 | lambda a=self: clear_all_editor_fields(a, "all"))
45 | t = QShortcut(QKeySequence(clear_most_shortcut), self.parentWindow)
46 | t.connect(t, SIGNAL("activated()"),
47 | lambda a=self: clear_all_editor_fields(a, "most"))
48 |
49 | addHook("setupEditorButtons", onSetupButtons)
50 |
--------------------------------------------------------------------------------
/src/editor_cloze_highlighter/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_cloze_highlighter
--------------------------------------------------------------------------------
/src/editor_cloze_highlighter/editor_cloze_highlighter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Highlight All Cloze Sequences
5 |
6 | Modifies Anki's default cloze function in order to provide the ability
7 | to target all clozed sequences through user-provided CSS
8 |
9 | Usage:
10 |
11 | - Use Anki's regular cloze shortcuts as usual
12 | - Update your stylesheet to target the new cloze elements, e.g.:
13 |
14 | ```CSS
15 | /* target all clozed elements: */
16 | .clozed {
17 | color: grey;
18 | }
19 | /* target first clozed element: */
20 | .clozed.c1 {
21 | background: yellow;
22 | }
23 | ```
24 |
25 | Note:
26 |
27 | This add-on is incompatible with Cloze Overlapper and any other add-ons
28 | that overwrite the Editor.onCloze method.
29 |
30 | Copyright: (c) Glutanimate 2017
31 | License: GNU AGPLv3 or later
32 | """
33 |
34 | import re
35 |
36 | from aqt.qt import *
37 | from aqt.editor import Editor
38 |
39 | from aqt.utils import tooltip, showInfo
40 |
41 |
42 | def onCloze(self):
43 | # check that the model is set up for cloze deletion
44 | if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']):
45 | if self.addMode:
46 | tooltip(_("Warning, cloze deletions will not work until "
47 | "you switch the type at the top to Cloze."))
48 | else:
49 | showInfo(_("""\
50 | To make a cloze deletion on an existing note, you need to change it \
51 | to a cloze type first, via Edit>Change Note Type."""))
52 | return
53 | # find the highest existing cloze
54 | highest = 0
55 | for name, val in self.note.items():
56 | m = re.findall("\{\{c(\d+)::", val)
57 | if m:
58 | highest = max(highest, sorted([int(x) for x in m])[-1])
59 | # reuse last?
60 | if not self.mw.app.keyboardModifiers() & Qt.AltModifier:
61 | highest += 1
62 | # must start at 1
63 | highest = max(1, highest)
64 | # wrap cloze items in span
65 | js_cloze = """wrap('{{c%d::', '}}');"""
66 | self.web.eval(js_cloze % (highest, highest))
67 |
68 | # Hooks
69 |
70 | Editor.onCloze = onCloze
--------------------------------------------------------------------------------
/src/editor_custom_stylesheet/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_custom_stylesheet
--------------------------------------------------------------------------------
/src/editor_custom_stylesheet/editor_custom_stylesheet.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Customize Editor CSS
5 |
6 | Allows you to customize the stylesheet of the Editor widget in Anki.
7 |
8 | Copyright: (c) Glutanimate 2017
9 | License: GNU AGPLv3 or later
10 | """
11 |
12 | ######### USER CONFIGURATION START ##########
13 |
14 | # Whether to style editor window by default
15 | DEFAULT_STATE = True # default: True
16 |
17 | # Set a custom tag field background color
18 | # If this is set to an empty string no changes are applied
19 | # (e.g. "#F5F6CE" or "")
20 | TAGS_BACKGROUND = "" # default: ""
21 |
22 | # Disable custom CSS adjustments when Night Mode add-on active
23 | DISABLE_FOR_NIGHTMODE = True # default: True
24 |
25 | ######### USER CONFIGURATION END ##########
26 |
27 | import os
28 |
29 | from aqt.qt import *
30 | from aqt import mw
31 | from aqt import editor
32 |
33 | from anki.hooks import addHook, wrap
34 |
35 | old_html = editor._html
36 | new_html = old_html
37 |
38 | def updateTagsBackground(self):
39 | """Modify tagEdit background color"""
40 | nm_state_on = False
41 | if DISABLE_FOR_NIGHTMODE:
42 | try:
43 | from Night_Mode import nm_state_on
44 | except ImportError:
45 | pass
46 |
47 | if not mw._styleEditor or nm_state_on:
48 | return
49 |
50 | if TAGS_BACKGROUND:
51 | self.tags.setStyleSheet(
52 | """QLineEdit {{ background: {}; }}""".format(
53 | TAGS_BACKGROUND))
54 |
55 |
56 | def profileLoaded():
57 | """Import modified CSS code into editor"""
58 | global old_html
59 | global new_html
60 | media_dir = mw.col.media.dir()
61 | css_path = os.path.join(media_dir, "_editor.css")
62 | if not os.path.isfile(css_path):
63 | return False
64 | with open(css_path, "r") as css_file:
65 | css = css_file.read()
66 | if not css:
67 | return False
68 | editor_style = "".format(css.replace("%","%%"))
69 | old_html = editor._html
70 | editor._html = editor._html + editor_style
71 | new_html = editor._html
72 |
73 |
74 | def onEditorInit(self, *args, **kwargs):
75 | """Apply modified Editor HTML"""
76 | nm_state_on = False
77 | if DISABLE_FOR_NIGHTMODE:
78 | try:
79 | from Night_Mode import nm_state_on
80 | except ImportError:
81 | pass
82 | if not mw._styleEditor or nm_state_on:
83 | editor._html = old_html
84 | else:
85 | editor._html = new_html
86 |
87 |
88 | def onStylingToggle(checked):
89 | """Set mw variable that controls styling state"""
90 | mw._styleEditor = checked
91 |
92 |
93 | # Menu toggle:
94 |
95 | mw._styleEditor = DEFAULT_STATE
96 | action = QAction(mw)
97 | action.setText("Custom Editor Styling")
98 | action.setCheckable(True)
99 | action.setChecked(DEFAULT_STATE)
100 | action.setShortcut(QKeySequence("Shift+E"))
101 | mw.form.menuTools.addAction(action)
102 | action.toggled.connect(onStylingToggle)
103 |
104 | # Hooks:
105 |
106 | addHook("profileLoaded", profileLoaded)
107 |
108 | editor.Editor.__init__ = wrap(editor.Editor.__init__, onEditorInit, "after")
109 | editor.Editor.setupTags = wrap(editor.Editor.setupTags,
110 | updateTagsBackground, "after")
111 |
--------------------------------------------------------------------------------
/src/editor_field_history/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017-2019 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_field_history
--------------------------------------------------------------------------------
/src/editor_field_history/config.md:
--------------------------------------------------------------------------------
1 | **Important**: These settings do not sync and require a restart to apply.
2 |
3 | - `historyWindowShortcut` (string): Hotkey that invokes the history window. Default: `"Ctrl+Alt+H"`.
4 | - `fieldRestoreShortcut` (string): Hotkey that restores current field to last state. Default: `"Alt+Z"`.
5 | - `partialRestoreShortcut` (string): Hotkey that restores fields listed in `partialRestoreFields`. Default: `"Alt+Shift+Z"`.
6 | - `fullRestoreShortcut` (string): Hotkey that restores all fields at once, tags included: `"Ctrl+Alt+Shift+Z"`.
7 | - `partialRestoreFields` (list): List of fields to restore when using `partialRestoreShortcut` (e.g. `["Front", "Sources"]`. Default: `[]`.
8 | - `maxNotes` (int): Maximum number of notes to query when checking field history. Default: `100`
9 |
--------------------------------------------------------------------------------
/src/editor_field_history/config.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "title": "CTRLF5 to Refresh the Browser",
5 | "properties": {
6 | "historyWindowShortcut": {
7 | "type": "string",
8 | "title": "history window shortcut",
9 | "pattern": "^((([Cc][Tt][Rr][Ll]|[Aa][Ll][Tt]|[Ss][Hh][Ii][Ff][Tt])\\+){0,3}([a-zA-Z0-9]|F[1-9]|F1[0-2]))?$",
10 | "default": "Ctrl+Alt+H"
11 | },
12 | "fieldRestoreShortcut": {
13 | "type": "string",
14 | "title": "field restore shortcut",
15 | "pattern": "^((([Cc][Tt][Rr][Ll]|[Aa][Ll][Tt]|[Ss][Hh][Ii][Ff][Tt])\\+){0,3}([a-zA-Z0-9]|F[1-9]|F1[0-2]))?$",
16 | "default": "Alt+Z"
17 | },
18 | "partialRestoreShortcut": {
19 | "title": "partial restore shortcut",
20 | "type": "string",
21 | "pattern": "^((([Cc][Tt][Rr][Ll]|[Aa][Ll][Tt]|[Ss][Hh][Ii][Ff][Tt])\\+){0,3}([a-zA-Z0-9]|F[1-9]|F1[0-2]))?$",
22 | "default": "Ctrl+Alt+Shift+Z"
23 | },
24 | "fullRestoreShortcut": {
25 | "type": "string",
26 | "title": "Full restore shortcut",
27 | "pattern": "^((([Cc][Tt][Rr][Ll]|[Aa][Ll][Tt]|[Ss][Hh][Ii][Ff][Tt])\\+){0,3}([a-zA-Z0-9]|F[1-9]|F1[0-2]))?$",
28 | "default": "Ctrl+Alt+Shift+Z"
29 | },
30 | "partialRestoreFields": {
31 | "title": "Partial restore fields",
32 | "description": "List of fields to restore when using `partialRestoreShortcut`",
33 | "type": "array",
34 | "default": [],
35 | "items": {
36 | "title": "field",
37 | "type": "string"
38 | }
39 | },
40 | "maxNotes": {
41 | "type": "number",
42 | "title": "Max notes",
43 | "description": "Maximum number of notes to query when checking field history.",
44 | "default": 100
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/editor_field_navigation/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_field_navigation
--------------------------------------------------------------------------------
/src/editor_field_navigation/editor_field_navigation.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Quick Field Navigation
5 |
6 | Implements shortcuts that allow you to navigate
7 | through your fields in the card editor.
8 |
9 | Copyright: Glutanimate 2015-2020
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | from anki.hooks import addHook
14 |
15 | def changeFocusTo(self, fldnr):
16 | fldnr = fldnr - 1
17 | fldstot = len(self.note.fields) -1
18 | if fldnr >= fldstot or fldnr < 0:
19 | # ignore onid field (Note Organizer add-on)
20 | if self.note.model()['flds'][-1]['name'] == "onid":
21 | fldnr = fldstot - 1
22 | else:
23 | fldnr = fldstot
24 | elif self.note.model()['flds'][0]['name'] == "ID (hidden)":
25 | # ignore hidden ID field (Image Occlusion Enhanced)
26 | fldnr += 1
27 | self.web.setFocus()
28 | self.web.eval("focusField(%d);" % int(fldnr))
29 |
30 | def onSetupShortcuts(cuts, editor):
31 | cuts.extend(
32 | [(f"Ctrl+{i}", lambda f=i: changeFocusTo(editor, f), True) for i in range(10)]
33 | )
34 | return cuts
35 |
36 | addHook("setupEditorShortcuts", onSetupShortcuts)
--------------------------------------------------------------------------------
/src/editor_indentation_formatter/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_indentation_formatter
--------------------------------------------------------------------------------
/src/editor_indentation_formatter/editor_indentation_formatter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Indentation Formatter
5 |
6 | Extends Anki's note editing toolbar with an "indent" and "outdent" button
7 | that insert indented paragraphs.
8 |
9 | Copyright: (c) Glutanimate 2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | # USER CONFIGURATION START
14 |
15 | # Default html tag to apply when there is no
16 | # pre-existing formatting (e.g. div, p, blockquote)
17 | HTML_TAG = "p"
18 |
19 | # Whether or not to indent existing tags instead of applying
20 | # the tag above
21 | USE_EXISTING_TAGS = False
22 |
23 | # Hotkeys
24 | HOTKEY_INDENT = "Alt+L"
25 | HOTKEY_OUTDENT = "Alt+J"
26 |
27 | # Indentation steps in px (default: 40)
28 | INDENTATION_STEP = 40
29 |
30 | # USER CONFIGURATION END
31 |
32 | from aqt.editor import Editor
33 | from anki.hooks import wrap
34 |
35 |
36 | js_indent = """
37 | function indent(mode){
38 | var elm = window.getSelection().focusNode;
39 | var parent = window.getSelection().focusNode.parentNode;
40 | if (%(exst)s){
41 | var isElm = parent.isContentEditable && elm.toString() !== "[object Text]";
42 | var isParElm = parent.isContentEditable && parent.parentNode.isContentEditable;
43 | } else {
44 | var isElm = elm.tagName == "%(tag)s"
45 | && parent.isContentEditable;
46 | var isParElm = parent.tagName == "%(tag)s"
47 | && parent.parentNode.isContentEditable;
48 | }
49 | var newNode = false
50 |
51 | if (mode == "in" && !isElm && !isParElm){
52 | document.execCommand("formatBlock", false, "%(tag)s");
53 | var elm = window.getSelection().focusNode;
54 | if (elm.tagName !== "%(tag)s") {
55 | elm = elm.parentNode;
56 | }
57 | var marginL = %(step)i
58 | var newNode = true
59 | } else if (isElm || isParElm) {
60 | if (!isElm) {
61 | elm = parent;
62 | }
63 | mleft = parseInt(elm.style.marginLeft)
64 | if (isNaN(mleft)){
65 | mleft = 0;
66 | }
67 | if (mode == "in"){
68 | var marginL = mleft + %(step)i
69 | } else {
70 | var marginL = Math.max(mleft-%(step)i, 0)
71 | }
72 | } else {
73 | return
74 | }
75 |
76 | if (newNode) {
77 | elm.style.margin = "0px";
78 | }
79 | elm.style.marginLeft = marginL + "px";
80 |
81 | }
82 | indent("%%s");
83 | saveField('key');
84 | """ % dict(exst=str(USE_EXISTING_TAGS).lower(), tag=HTML_TAG.upper(),
85 | step=INDENTATION_STEP)
86 |
87 |
88 | def onIndent(self, mode):
89 | self.web.eval(js_indent % mode)
90 |
91 | def setupButtons(self):
92 | self._addButton("OutdentBtn", lambda: self.onIndent("out"),
93 | text=u"←", tip="Outdent ({})".format(HOTKEY_OUTDENT),
94 | key=HOTKEY_OUTDENT)
95 | self._addButton("IndentBtn", lambda: self.onIndent("in"),
96 | text=u"→", tip="Indent ({})".format(HOTKEY_INDENT),
97 | key=HOTKEY_INDENT)
98 |
99 | Editor.onIndent = onIndent
100 | Editor.setupButtons = wrap(Editor.setupButtons, setupButtons)
101 |
--------------------------------------------------------------------------------
/src/editor_paste_sources/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_paste_sources
--------------------------------------------------------------------------------
/src/editor_paste_sources/editor_paste_sources.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Paste Sources into Editor
5 |
6 | Use a hotkey to replace / add to the Sources field from your clipboard
7 |
8 | Based in part on paste_to_my_field by Mirco Kraenz
9 | (https://github.com/proSingularity/anki2-addons)
10 |
11 | Copyright: (c) Glutanimate 2017
12 | License: GNU AGPLv3 or later
13 | """
14 |
15 | from __future__ import unicode_literals
16 |
17 | ############## USER CONFIGURATION START ##############
18 |
19 | SHORTCUT_REPLACE_PASTE = "Alt+Shift+V"
20 | SHORTCUT_ADD_PASTE = "Ctrl+Alt+Shift+V"
21 | SOURCE_FIELD = "Quellen"
22 |
23 | ############## USER CONFIGURATION END ##############
24 |
25 | from PyQt4.QtGui import QClipboard
26 |
27 | from aqt.qt import *
28 | from anki.hooks import addHook
29 |
30 | def pasteIntoField(self, action):
31 | '''Paste clipboard text to field specified by constant SOURCE_FIELD '''
32 | mode = QClipboard.Clipboard
33 | note = self.note
34 | cb = self.mw.app.clipboard().mimeData(mode=mode)
35 | if cb.hasText():
36 | cb_text = cb.text()
37 | if cb_text and SOURCE_FIELD in note:
38 | if action == "replace":
39 | # replace existing contents
40 | note[SOURCE_FIELD] = cb_text
41 | elif action == "add":
42 | # add to existing contents
43 | curr = note[SOURCE_FIELD]
44 | new = curr + " " + cb_text
45 | note[SOURCE_FIELD] = new
46 | self.web.eval("""
47 | if (currentField) {
48 | saveField("key");
49 | }
50 | """)
51 | self.loadNote()
52 |
53 | # assign hotkey
54 | def onSetupButtons(self):
55 | t = QShortcut(QKeySequence(SHORTCUT_REPLACE_PASTE), self.widget)
56 | t.activated.connect(lambda a=self: pasteIntoField(a, "replace"))
57 | t = QShortcut(QKeySequence(SHORTCUT_ADD_PASTE), self.widget)
58 | t.activated.connect(lambda a=self: pasteIntoField(a, "add"))
59 |
60 | addHook("setupEditorButtons", onSetupButtons)
--------------------------------------------------------------------------------
/src/editor_preserve_fields_on_switch/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_preserve_fields_on_switch
--------------------------------------------------------------------------------
/src/editor_preserve_fields_on_switch/editor_preserve_fields_on_switch.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Preserve editor fields on note model switch
5 |
6 | Copies values of identically named fields over to corresponding fields
7 | in new note type.
8 |
9 | Should be implemented by default in the next Anki release.
10 |
11 | Copyright: (c) Glutanimate 2016-2017
12 | License: GNU AGPLv3 or later
13 | """
14 |
15 | from anki.hooks import wrap
16 | from aqt.addcards import AddCards
17 |
18 | def newOnReset(self, _old, model=None, keep=False):
19 | # return _old(self, model=None, keep=False)
20 | oldNote = self.editor.note
21 | note = self.setupNewNote(set=False)
22 | modelName = note.model()['name']
23 | flds = note.model()['flds']
24 | if oldNote:
25 | oldModelName = oldNote.model()['name']
26 | if oldModelName != modelName:
27 | # model changed
28 | oldFields = oldNote.keys()
29 | newFields = note.keys()
30 | for n, f in enumerate(note.model()['flds']):
31 | fieldName = f['name']
32 | try:
33 | oldFieldName = oldNote.model()['flds'][n]['name']
34 | except IndexError:
35 | oldFieldName = None
36 | # copy identical fields
37 | if fieldName in oldFields:
38 | note[fieldName] = oldNote[fieldName]
39 | # set non-identical fields by field index
40 | elif oldFieldName and oldFieldName not in newFields:
41 | try:
42 | note.fields[n] = oldNote.fields[n]
43 | except IndexError:
44 | pass
45 | else:
46 | # model identical
47 | if not keep:
48 | self.removeTempNote(oldNote)
49 | for n in range(len(note.fields)):
50 | try:
51 | if not keep or flds[n]['sticky']:
52 | note.fields[n] = oldNote.fields[n]
53 | else:
54 | note.fields[n] = ""
55 | except IndexError:
56 | break
57 | self.editor.currentField = 0
58 | self.editor.setNote(note, focus=True)
59 |
60 |
61 | AddCards.onReset = wrap(AddCards.onReset, newOnReset, "around")
62 |
--------------------------------------------------------------------------------
/src/editor_random_list/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017-2018 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_random_list
--------------------------------------------------------------------------------
/src/editor_random_list/config.md:
--------------------------------------------------------------------------------
1 | **Important**: Please restart Anki to apply changes to these settings
2 |
3 | - `hotkeyToggleList` (string): Hotkey for the "Insert Randomized List" button. Default: `"Alt+Shift+L"`.
--------------------------------------------------------------------------------
/src/editor_random_list/editor_random_list.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Insert Randomized List
5 |
6 | Inserts an unordered list with the 'shuffle' CSS class.
7 |
8 | This can be used to randomize list items when coupled with
9 | a special card template.
10 |
11 | Copyright: (c) Glutanimate 2017
12 | License: GNU AGPLv3 or later
13 | """
14 |
15 | ############## USER CONFIGURATION START ##############
16 |
17 | # These settings only apply to Anki 2.0. For Anki 2.1 please use
18 | # Anki's built-in add-on configuration menu
19 |
20 | HOTKEY_TOGGLE_LIST = "Alt+Shift+L"
21 |
22 | ############## USER CONFIGURATION END ##############
23 |
24 | from aqt import editor, mw
25 | from anki.hooks import wrap, addHook
26 |
27 | from anki import version
28 |
29 | ANKI21 = version.startswith("2.1")
30 |
31 | if ANKI21:
32 | config = mw.addonManager.getConfig(__name__)
33 | HOTKEY_TOGGLE_LIST = config["hotkeyToggleList"]
34 |
35 | editor_style = """
36 |
41 | """
42 |
43 |
44 | def toggleRandUl(self):
45 | self.web.eval("""
46 | document.execCommand('insertUnorderedList');
47 | var ulElem = window.getSelection().focusNode.parentNode;
48 | if (ulElem !== null) {
49 | var setAttrs = true;
50 | while (ulElem.toString() !== "[object HTMLUListElement]") {
51 | ulElem = ulElem.parentNode;
52 | if (ulElem === null) {
53 | setAttrs = false;
54 | break;
55 | }
56 | }
57 | if (setAttrs) {
58 | ulElem.style.marginLeft = "20px";
59 | ulElem.className = "shuffle"
60 | }
61 | }
62 | """)
63 |
64 |
65 | def setupButtons20(self):
66 | self._addButton("randUlBtn", self.toggleRandUl,
67 | text="RL",
68 | tip="Insert randomized unordered list ({})".format(
69 | HOTKEY_TOGGLE_LIST),
70 | key=HOTKEY_TOGGLE_LIST)
71 |
72 |
73 | def setupButtons21(btns, editor):
74 | btn = editor.addButton(None, "randUlBtn", toggleRandUl,
75 | label="RL", keys=HOTKEY_TOGGLE_LIST,
76 | tip="Insert randomized unordered list ({})".format(
77 | HOTKEY_TOGGLE_LIST))
78 | btns.append(btn)
79 | return btns
80 |
81 | editor._html = editor._html + editor_style
82 |
83 |
84 | if not ANKI21:
85 | editor.Editor.toggleRandUl = toggleRandUl
86 | editor.Editor.setupButtons = wrap(
87 | editor.Editor.setupButtons, setupButtons20)
88 | else:
89 | addHook("setupEditorButtons", setupButtons21)
90 |
--------------------------------------------------------------------------------
/src/editor_replace_linebreaks/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import editor_replace_linebreaks
--------------------------------------------------------------------------------
/src/editor_replace_linebreaks/editor_replace_linebreaks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Replace Linebreaks in Clipboard or Selection
5 |
6 | Adds hotkeys that remove linebreaks in the clipboard or
7 | currently selected text.
8 |
9 | Copyright: (c) Glutanimate 2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | # Do not modify the following line:
14 | from __future__ import unicode_literals
15 |
16 | ############## USER CONFIGURATION START ##############
17 |
18 | # KEY ASSIGNMENTS
19 |
20 | PASTE_HOTKEY = "Alt+P" # Default: Alt+P
21 | EDIT_SELECTION_HOTKEY = "Alt+Shift+P" # Default: Alt+Shift+P
22 |
23 | # OPTIONS
24 |
25 | # try to preserve paragraphs (not 100% exact):
26 | PRESERVE_PARAGRAPHS = True # Default: True
27 | # confirm actions with tooltips
28 | TOOLTIP = True # Default: True
29 |
30 | ############## USER CONFIGURATION END ##############
31 |
32 | import re
33 |
34 | from aqt.qt import *
35 | from aqt.editor import Editor
36 | from aqt.utils import tooltip
37 |
38 | from anki.hooks import wrap
39 | from anki import json
40 |
41 | if PRESERVE_PARAGRAPHS:
42 | re_lb_open = "(
"
140 | if cnt < card.reps:
141 | s += _("""\
142 | Note: Some of the history is missing. For more information, \
143 | please see the browser documentation.""")
144 | return s
145 |
146 | def _update(self):
147 | if not self.shown:
148 | return
149 | txt = ""
150 | r = self.mw.reviewer
151 | d = self.mw.col
152 | cs = CardStats(d, r.card)
153 | cc = r.card
154 | if cc:
155 | txt += _("
"
164 | txt += self._revlogData(lc, cs)
165 | if not txt:
166 | txt = _("No current card or last card.")
167 | style = self._style()
168 | self.web.setHtml("""
169 |
170 |
171 |
%s
"""% (style, txt))
172 |
173 | def _style(self):
174 | from anki import version
175 | if version.startswith("2.0."):
176 | return ""
177 | return "td { font-size: 80%; }"
178 |
179 | _cs = StatsSidebar(mw)
180 |
181 | def cardStats(on):
182 | _cs.toggle()
183 |
184 | action = QAction(mw)
185 | action.setText("Card Stats")
186 | action.setCheckable(True)
187 | action.setShortcut(QKeySequence("Shift+C"))
188 | mw.form.menuTools.addAction(action)
189 | action.toggled.connect(cardStats)
190 |
--------------------------------------------------------------------------------
/src/reviewer_file_hyperlinks/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_file_hyperlinks
--------------------------------------------------------------------------------
/src/reviewer_file_hyperlinks/reviewer_file_hyperlinks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: File Hyperlinks in the Reviewer
5 |
6 | Parses the Reviewer for a custom URL scheme and inserts links
7 | that invoke external programs.
8 |
9 | Copyright: (c) Glutanimate 2016-2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | ############## USER CONFIGURATION START ##############
14 |
15 | # path to external script or app that handles files
16 |
17 | # Windows
18 | external_handler_win = r"C:\Users\AnkiUser\script.exe"
19 |
20 | # Unix (Linux/macOS)
21 | external_handler_unix = r"notify-send"
22 |
23 | ############## USER CONFIGURATION END ##############
24 |
25 | import subprocess
26 | import re
27 |
28 | from aqt.utils import tooltip
29 | from aqt.reviewer import Reviewer
30 | from anki.hooks import wrap, addHook
31 |
32 | from anki.utils import isWin
33 |
34 | from anki import version as anki_version
35 | anki21 = anki_version.startswith("2.1.")
36 |
37 | pycmd = "pycmd" if anki21 else "py.link"
38 |
39 | regex_link = r"(qv.+?\..+?\b(#\b.+?\b)?)"
40 | replacement = r"""\1""".format(pycmd)
41 |
42 |
43 | def openFileHandler(file):
44 | try:
45 | if isWin:
46 | external_handler = external_handler_win
47 | else:
48 | external_handler = external_handler_unix
49 | subprocess.Popen([external_handler, file])
50 | except OSError:
51 | tooltip("External handler produced an error. "
52 | "Please confirm that it is assigned correctly.")
53 |
54 |
55 | def linkHandler(self, url, _old):
56 | if not url.startswith("open"):
57 | return _old(self, url)
58 | (cmd, arg) = url.split(":", 1)
59 | openFileHandler(arg)
60 |
61 |
62 | def linkInserter(html):
63 | return re.sub(regex_link, replacement, html)
64 |
65 |
66 | def onMungeQA(self, buf, _old):
67 | buf = _old(self, buf)
68 | return linkInserter(buf)
69 |
70 |
71 | def profileLoaded():
72 | """Support for Advanced Previewer"""
73 | try:
74 | from advanced_previewer.previewer import Previewer
75 | except ImportError:
76 | return
77 | Previewer.linkHandler = wrap(
78 | Previewer.linkHandler, linkHandler, "around")
79 | addHook("previewerMungeQA", linkInserter)
80 |
81 |
82 | Reviewer._linkHandler = wrap(Reviewer._linkHandler, linkHandler, "around")
83 | Reviewer._mungeQA = wrap(Reviewer._mungeQA, onMungeQA, "around")
84 | addHook("profileLoaded", profileLoaded)
--------------------------------------------------------------------------------
/src/reviewer_hide_toolbar/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_hide_toolbar
--------------------------------------------------------------------------------
/src/reviewer_hide_toolbar/reviewer_hide_toolbar.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Hides the main window's toolbar when reviewing.
4 |
5 | The aim of the addon is to free up some vertical screen space while reviewing
6 | cards. For me this is useful as I have many cards that contain diagrams which can
7 | be fairly large. In addition. modern wide screen displays seem to feel more
8 | cramped vertically than horizontally.
9 |
10 | The toolbar is made visible again when viewing decks and the overview.
11 |
12 | The commands that are usually found on the toolbar are added to a new
13 | main window "Toolbar" menu. Shortcut keys should continue to function
14 |
15 | Copyright: Steve AW
16 | License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
17 |
18 | Support: Use at your own risk. If you do find a problem please email me
19 | or use one the following forums, however there are certain periods
20 | throughout the year when I will not have time to do any work on
21 | these addons.
22 |
23 | Github page: https://github.com/steveaw/anki_addons
24 | Anki addons: https://groups.google.com/forum/?hl=en#!forum/anki-addons
25 | """
26 | from PyQt4.QtCore import SIGNAL
27 | from PyQt4.QtGui import QMenu
28 | from anki.hooks import wrap
29 | from aqt import mw
30 | from aqt.main import AnkiQt
31 |
32 | __author__ = 'Steve'
33 |
34 |
35 | def hide_toolbar_reviewing(self, oldState):
36 | self.toolbar.web.hide()
37 |
38 |
39 | def show_toolbar_not_reviewing(self, state, *args):
40 | #Rather than tracking the state of the toolbar's visibility
41 | #leave it to Qt to handle. m
42 | self.toolbar.web.show()
43 |
44 |
45 | AnkiQt._reviewState = wrap(AnkiQt._reviewState, hide_toolbar_reviewing, "after")
46 | AnkiQt.moveToState = wrap(AnkiQt.moveToState, show_toolbar_not_reviewing, "before")
47 |
48 | toolbar_menu = QMenu("Tool&bar")
49 | action = mw.menuBar().insertMenu(mw.form.menuTools.menuAction(), toolbar_menu)
50 | #Not really the intended purpose of link_handlers, but good enough
51 | for key, value in iter(sorted(mw.toolbar.link_handlers.items())):
52 | a = toolbar_menu.addAction(_(key.title()))
53 | a.connect(a, SIGNAL("triggered()"), value)
54 |
--------------------------------------------------------------------------------
/src/reviewer_hint_hotkeys/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_hint_hotkeys
--------------------------------------------------------------------------------
/src/reviewer_hint_hotkeys/reviewer_hint_hotkeys.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Anki Add-on: Hint Hotkeys
4 |
5 | Adds two hotkeys to the reviewer: 'H' to reveal hints one by one,
6 | 'G' to reveal all hints at once.
7 |
8 | Based on "Hint-peeking Keyboard Bindings" by Ben Lickly
9 |
10 | Copyright: (c) Ben Lickly 2012
11 | (c) Glutanimate 2016-2017
12 | License: GNU AGPLv3 or later
13 | """
14 |
15 | from __future__ import unicode_literals
16 |
17 | ############## USER CONFIGURATION START ##############
18 |
19 | # Shortcuts need to be single keys on Anki 2.0.x
20 | # Key combinations are supported on Anki 2.1.x
21 |
22 | # Shortcut that will reveal the hint fields one by one:
23 | SHORTCUT_INCREMENTAL = "H"
24 | # Shortcut that will reveal all hint fields at once:
25 | SHORTCUT_ALL = "G"
26 |
27 | ############## USER CONFIGURATION END ##############
28 |
29 | from aqt.qt import *
30 | from aqt import mw
31 | from aqt.reviewer import Reviewer
32 |
33 | from anki.hooks import wrap, addHook
34 | from anki import version as ankiversion
35 |
36 |
37 | def _newKeyHandler(self, evt, _old):
38 | """Add shortcuts on Anki 2.0.x"""
39 | if evt.key() == QKeySequence(SHORTCUT_INCREMENTAL)[0]:
40 | _showHint(incremental=True)
41 | elif evt.key() == QKeySequence(SHORTCUT_ALL)[0]:
42 | _showHint()
43 | return _old(self, evt)
44 |
45 |
46 | def _addShortcuts(shortcuts):
47 | """Add shortcuts on Anki 2.1.x"""
48 | additions = (
49 | (SHORTCUT_INCREMENTAL, lambda: _showHint(incremental=True)),
50 | (SHORTCUT_ALL, _showHint)
51 | )
52 | shortcuts += additions
53 |
54 |
55 | def _showHint(incremental=False):
56 | """Show hint by activating corresponding links."""
57 | mw.web.eval("""
58 | var customEvent = document.createEvent('MouseEvents');
59 | customEvent.initEvent('click', false, true);
60 | var arr = document.getElementsByTagName('a');
61 | // Cloze Overlapper support
62 | if (typeof olToggle === "function") {
63 | olToggle();
64 | }
65 | // Image Occlusion Enhanced support
66 | var ioBtn = document.getElementById("io-revl-btn");
67 | if (!(typeof ioBtn === 'undefined' || !ioBtn)) {
68 | ioBtn.click();
69 | }
70 | for (var i=0; i
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_letitsnow
--------------------------------------------------------------------------------
/src/reviewer_letitsnow/reviewer_letitsnow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | This file is part of the Let It Snow! add-on for Anki
5 |
6 | Copyright: (c) 2017 Glutanimate
7 | License: GNU AGPLv3
8 |
9 | Uses snowfall.js by loktar00 (https://github.com/loktar00/JQuery-Snowfall),
10 | licensed under the Apache license 2.0
11 | """
12 |
13 | from aqt.reviewer import Reviewer
14 | from anki.hooks import wrap
15 |
16 | from anki import version as anki_version
17 | anki21 = anki_version.startswith("2.1.")
18 |
19 | js_snow = r"""
20 | if(!Date.now)
21 | Date.now=function(){return new Date().getTime()};(function(){'use strict';var vendors=['webkit','moz'];for(var i=0;i(elHeight)-(this.size+6)){this.reset()}
26 | this.element.style.top=this.y+'px';this.element.style.left=this.x+'px';this.step+=this.stepSize;if(doRatio===!1){this.x+=Math.cos(this.step)}else{this.x+=(doRatio+Math.cos(this.step))}
27 | if(options.collection){if(this.x>this.target.x&&this.xthis.target.y&&this.ythis.target.height){if(curY+this.speed+this.size>this.target.height){while(curY+this.speed+this.size>this.target.height&&this.speed>0){this.speed*=.5}
28 | ctx.fillStyle=defaults.flakeColor;if(colData[parseInt(curX)][parseInt(curY+this.speed+this.size)]==undefined){colData[parseInt(curX)][parseInt(curY+this.speed+this.size)]=1;ctx.fillRect(curX,(curY)+this.speed+this.size,this.size,this.size)}else{colData[parseInt(curX)][parseInt(curY+this.speed)]=1;ctx.fillRect(curX,curY+this.speed,this.size,this.size)}
29 | this.reset()}else{this.speed=1;this.stepSize=0;if(parseInt(curX)+10&&colData[parseInt(curX)-1][parseInt(curY)+1]==undefined){this.x--}else{ctx.fillStyle=defaults.flakeColor;ctx.fillRect(curX,curY,this.size,this.size);colData[parseInt(curX)][parseInt(curY)]=1;this.reset()}}}}}
30 | if(this.x+this.size>(elWidth)-widthOffset||this.x',{'class':'snowfall-canvas'}),collisionData=[];if(bounds.top-collectionHeight>0){$('body').append($canvas);$canvas.css({'position':options.flakePosition,'left':bounds.left+'px','top':bounds.top-collectionHeight+'px'}).prop({width:bounds.width,height:collectionHeight});for(var w=0;w" + js_snow + ""
63 |
64 | if not anki21:
65 | Reviewer._revHtml += ""
66 | else:
67 | Reviewer.revHtml = wrap(Reviewer.revHtml, onRevHtml, pos="around")
68 |
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/More Answer Buttons.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: More Answer Buttons for New Cards
5 |
6 | Entry point for the add-on into Anki
7 | Please do not edit this if you do not know what you are doing.
8 |
9 | Copyright: (c) 2016-2019 Glutanimate
10 | (c) ijgnd 2019
11 |
12 | License: GNU AGPLv3
13 | """
14 |
15 | # IMPORTANT:
16 | #
17 | # If you are reading this and wondering where to find the
18 | # configuration section for Anki 2.0:
19 | #
20 | # As part of porting this add-on to Anki 2.1 I had to move the
21 | # main add-on module to a separate folder. It is now located under
22 | # more_answer_buttons/reviewer_more_answer_buttons_for_20.py.
23 | # All configuration options are located
24 | # at the very top of that file.
25 | #
26 | # Please use NotePad++ or another text editor that supports python
27 | # source code files to edit it
28 |
29 | import more_answer_buttons # noqa: F401
30 |
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/README.md:
--------------------------------------------------------------------------------
1 | ## Configuring the add-on on Anki 2.0
2 |
3 | The configuration options may be found at the top of the file reviewer_more_answer_buttons_for_20.py.
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017-2019 Glutanimate
7 | # (c) 2019 ijgnd
8 | # License: GNU AGPLv3
9 |
10 | from anki import version
11 |
12 | anki20 = version.startswith("2.0")
13 |
14 | if anki20:
15 | from . import reviewer_more_answer_buttons_for_20 # noqa: F401
16 | else:
17 | from . import reviewer_more_answer_buttons_for_21 # noqa: F401
18 |
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extra_buttons": [
3 | {
4 | "Description": "3-4d",
5 | "Label": "3-4",
6 | "ShortCut": "5",
7 | "ReschedMin": 3,
8 | "ReschedMax": 5
9 | },
10 | {
11 | "Description": "5-7d",
12 | "Label": "5-7",
13 | "ShortCut": "6",
14 | "ReschedMin": 5,
15 | "ReschedMax": 7
16 | },
17 | {
18 | "Description": "8-15d",
19 | "Label": "8-15",
20 | "ShortCut": "7",
21 | "ReschedMin": 8,
22 | "ReschedMax": 15
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/config.md:
--------------------------------------------------------------------------------
1 | ### Answer button properties
2 |
3 | You can add **up to four buttons at maximum**, each of which should have the following properties defined (please refer to the default config for the exact syntax and formatting):
4 |
5 | - `Description` [text]: appears above the button
6 | - `Label` [text]: the label of the button
7 | - `ShortCut` [text]: the shortcut key for the button
8 | - `ReschedMin` [number]: same as the lower number in the Browser's "Edit/Rescedule" command
9 | - `ReschedMax` [number]: same as the higher number in the Browser's "Edit/Rescedule" command
10 |
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "More Answer Buttons for New Cards",
3 | "package": "more_answer_buttons"
4 | }
5 |
--------------------------------------------------------------------------------
/src/reviewer_more_answer_buttons/more_answer_buttons/reviewer_more_answer_buttons_for_20.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: More Answer Buttons for New Cards
5 |
6 | Adds extra buttons to the answer button area for new cards
7 |
8 | Copyright: (c) Steve AW 2013
9 | (c) Glutanimate 2016-2017
10 | License: GNU AGPLv3 or later
11 | """
12 |
13 | ############## USER CONFIGURATION START ##############
14 |
15 | # Setup data:
16 | #
17 | # List of dicts, where each item of the list (a dict) is the data for a new button.
18 | # This can be edited to suit, but there can not be more than 4 buttons.
19 | # Description ... appears above the button
20 | # Label ... the label of the button
21 | # ShortCut ... the shortcut key for the button
22 | # ReschedMin ... same as the lower number in the Browser's "Edit/Rescedule" command
23 | # ReschedMax ... same as the higher number in the Browser's "Edit/Rescedule" command
24 |
25 | extra_buttons = [{"Description": "3-4d", "Label": "3-4", "ShortCut": "5",
26 | "ReschedMin": 3, "ReschedMax": 4},
27 | {"Description": "5-7d", "Label": "5-7", "ShortCut": "6",
28 | "ReschedMin": 5, "ReschedMax": 7},
29 | {"Description": "8-15d", "Label": "8-15", "ShortCut": "7",
30 | "ReschedMin": 8, "ReschedMax": 15}]
31 |
32 | ############## USER CONFIGURATION END ##############
33 |
34 | #Must be four or less
35 | assert len(extra_buttons) <= 4
36 |
37 | from aqt.reviewer import Reviewer
38 | from anki.hooks import wrap
39 | from aqt.utils import tooltip
40 |
41 | #Anki uses a single digit to track which button has been clicked.
42 | #We will use 6 and above to track the extra buttons.
43 | INTERCEPT_EASE_BASE = 6
44 |
45 | #todo: brittle. Replaces existing function
46 | def _answerButtons(self):
47 | times = []
48 | default = self._defaultEase()
49 |
50 | def but(i, label):
51 | if i == default:
52 | extra = "id=defease"
53 | else:
54 | extra = ""
55 | due = self._buttonTime(i)
56 | return '''
57 |
%s
''' % (due, extra, _("Shortcut key: %s") % i, i, label)
59 |
60 | buf = "
"
61 | for ease, label in self._answerButtonList():
62 | buf += but(ease, label)
63 | #swAdded start ====>
64 | #Only for cards in the new queue
65 | if self.card.type in (0, 1, 3): # New, Learn, Day learning
66 | #Check that the number of answer buttons is as expected.
67 | assert self.mw.col.sched.answerButtons(self.card) == 3
68 | #python lists are 0 based
69 | for i, buttonItem in enumerate(extra_buttons):
70 | buf += '''
71 |
%s
''' % (buttonItem["Description"], buttonItem["ShortCut"], i + INTERCEPT_EASE_BASE, buttonItem["Label"])
73 | #swAdded end
74 | buf += "
"
75 | script = """
76 | """
77 | return buf + script
78 |
79 | #This wraps existing Reviewer._answerCard function.
80 | def answer_card_intercepting(self, actual_ease, _old):
81 | ease = actual_ease
82 | was_new_card = self.card.type in (0, 1, 3)
83 | is_extra_button = was_new_card and actual_ease >= INTERCEPT_EASE_BASE
84 | if is_extra_button:
85 | #Make sure this is as expected.
86 | assert self.mw.col.sched.answerButtons(self.card) == 3
87 | #So this is one of our buttons. First answer the card as if "Easy" clicked.
88 | ease = 3
89 | #We will need this to reschedule it.
90 | prev_card_id = self.card.id
91 | #
92 | ret = _old(self, ease)
93 |
94 | if is_extra_button:
95 | buttonItem = extra_buttons[actual_ease - INTERCEPT_EASE_BASE]
96 | #Do the reschedule.
97 | self.mw.col.sched.reschedCards([prev_card_id], buttonItem["ReschedMin"], buttonItem["ReschedMax"])
98 | tooltip("
"
77 | script = """
78 | """
79 | return buf + script
80 |
81 |
82 |
83 | #This wraps existing Reviewer._answerCard function.
84 | def answer_card_intercepting21(self, actual_ease, _old):
85 | ease = actual_ease
86 | #in 2.1 this function is also called when you are in the deck overview screen where self.card does not exit
87 | if hasattr(self,"card"):
88 | was_new_card = self.card.type in (0, 1, 3)
89 | is_extra_button = was_new_card and actual_ease >= INTERCEPT_EASE_BASE
90 | if is_extra_button:
91 | #Make sure this is as expected.
92 | if self.mw.col.sched.name == "std":
93 | assert self.mw.col.sched.answerButtons(self.card) == 3
94 | #So this is one of our buttons. First answer the card as if "Easy" clicked.
95 | ease = 3
96 | if self.mw.col.sched.name == "std2":
97 | assert self.mw.col.sched.answerButtons(self.card) == 4
98 | #So this is one of our buttons. First answer the card as if "Easy" clicked.
99 | ease = 4
100 | #We will need this to reschedule it.
101 | prev_card_id = self.card.id
102 | #
103 | ret = _old(self, ease)
104 |
105 | if is_extra_button:
106 | extra_buttons = getConfig().get("extra_buttons", None)
107 | buttonItem = extra_buttons[actual_ease - INTERCEPT_EASE_BASE]
108 | #Do the reschedule.
109 | self.mw.col.sched.reschedCards([prev_card_id], buttonItem["ReschedMin"], buttonItem["ReschedMax"])
110 | tooltip("
")
111 | return ret
112 |
113 |
114 | def addShortcuts21(shortcuts):
115 | additions = []
116 | extra_buttons = getConfig().get("extra_buttons", None)
117 | for i, buttonItem in enumerate(extra_buttons):
118 | key = str( buttonItem["ShortCut"] )
119 | additions.append([key , lambda a=i: mw.reviewer._answerCard(a + INTERCEPT_EASE_BASE)])
120 | shortcuts += additions
121 | addHook("reviewStateShortcuts", addShortcuts21)
122 |
123 |
124 | Reviewer._answerButtons = _answerButtons21
125 | Reviewer._answerCard = wrap(Reviewer._answerCard, answer_card_intercepting21, "around")
126 |
--------------------------------------------------------------------------------
/src/reviewer_progress_bar/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_progress_bar
--------------------------------------------------------------------------------
/src/reviewer_refocus_card/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2018 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_refocus_card
--------------------------------------------------------------------------------
/src/reviewer_refocus_card/reviewer_refocus_card.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Refocus Card when Reviewing
5 |
6 | Refocuses card during reviews, so that you can use controls like
7 | Page up / Page down.
8 |
9 | Copyright: (c) 2013 Edgar Simo-Serra
10 | (c) 2018 Glutanimate
11 | License: GNU AGPLv3 or later
12 | """
13 |
14 | from anki.hooks import addHook
15 | from aqt import mw
16 |
17 | def refocusInterface():
18 | mw.web.setFocus()
19 |
20 | addHook("showQuestion", refocusInterface)
21 | addHook("showAnswer", refocusInterface)
22 |
--------------------------------------------------------------------------------
/src/reviewer_track_unseen/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import reviewer_track_unseen
--------------------------------------------------------------------------------
/src/reviewer_track_unseen/reviewer_track_unseen.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Track Unseen Cards
5 |
6 | Original Description:
7 |
8 | I am using this in conjunction with cramming. It uses tags (nsN,ns1,ns2,nsX where X is
9 | the card's ordinal number) to identify a set of cards, and then as the cards are viewed,
10 | it automatically removes that card's tag from its note. The idea behind it is: Say I have
11 | an exam in a week. My goal is to review all my cards that relate to the exam over the
12 | coming week, doing bits and pieces wherever I can. In reality, I often don't get the time
13 | to do all of them, but if each day I can do a random smattering of 50-100 extra cards,
14 | that is generally good enough.
15 |
16 | I never found a good way, with the inbuilt cramming/study more tools,
17 | to reliably keep track of what cards had been crammed when cramming across
18 | multiple sessions (the changed field/ "Last reviewed" gets updated when cards are added
19 | to dynamic decks which makes it useless for my needs). This addon allows me to add the
20 | tags to those cards I want to see. I can then create dynamic decks for those tags
21 | (tag:ns*), and the tags only get removed from cards that are seen. It gives me confidence
22 | that I am not re-cramming a card, and also allows me to see how many are still "unseen".
23 | I will see how it goes this semester, but I think it is going to finally give me a good
24 | way to systematically cram.
25 |
26 | Copyright: (c) Steve AW 2013
27 | (c) Glutanimate 2016-2017
28 | License: GNU AGPLv3 or later
29 | """
30 |
31 | from PyQt4.QtCore import SIGNAL
32 | from PyQt4.QtGui import QMenu, QKeySequence
33 | from anki.hooks import addHook, wrap
34 | from anki.sched import Scheduler
35 | from aqt.reviewer import Reviewer
36 | from aqt.utils import tooltip
37 |
38 |
39 | __author__ = 'Steve'
40 |
41 | unseen_tag = ".unseen"
42 |
43 | def add_unseen_tags_to_selected(self):
44 | #self is browser
45 | selected_cids = self.selectedCards()
46 | if not selected_cids:
47 | tooltip(_("No cards selected."), period=2000)
48 | return
49 | self.mw.progress.start()
50 | self.mw.checkpoint("Add Unseen Tags")
51 | self.model.beginReset()
52 | for cid in selected_cids:
53 | unseen_card = self.col.getCard(cid)
54 | unseen_note = unseen_card.note()
55 | unseen_note.addTag(unseen_tag)
56 | unseen_note.flush()
57 | self.model.endReset()
58 | self.mw.requireReset()
59 | self.mw.progress.finish()
60 | tooltip("Added " + unseen_tag + " to notes")
61 |
62 |
63 | def remove_unseen_tags_from_selected(self):
64 | #self is browser
65 | selected_cids = self.selectedCards()
66 | if not selected_cids:
67 | tooltip(_("No cards selected."), period=2000)
68 | return
69 | self.mw.progress.start()
70 | self.mw.checkpoint("Remove Unseen Tags")
71 | self.model.beginReset()
72 | for cid in selected_cids:
73 | unseen_card = self.col.getCard(cid)
74 | unseen_note = unseen_card.note()
75 | unseen_note.delTag(unseen_tag)
76 | unseen_note.flush()
77 | self.model.endReset()
78 | self.mw.requireReset()
79 | self.mw.progress.finish()
80 | tooltip("Removed " + unseen_tag + " from notes")
81 |
82 |
83 | def change_background_color(self):
84 | pot_unseen_card = self.card
85 | pot_unseen_note = pot_unseen_card.note()
86 | if pot_unseen_note.hasTag(unseen_tag):
87 | #We have to remove this, and play nicely with other addons that may fiddle with background
88 | self.web.eval('document.body.style.backgroundColor = "#FFFBBF"')
89 |
90 | def wipe_background_for_nextCard(self):
91 | #do this early enough so that any addon applying colour changes
92 | #does it after
93 | self.web.eval('document.body.style.backgroundColor = "#FFFFFF"')
94 |
95 | def _remove_unseen_tags_for_card_and_note(pot_unseen_card, pot_unseen_note):
96 | if pot_unseen_note.hasTag(unseen_tag):
97 | pot_unseen_note.delTag(unseen_tag)
98 | pot_unseen_note.flush()
99 |
100 |
101 | def _remove_unseen_tags(self):
102 | pot_unseen_card = self.card
103 | pot_unseen_note = pot_unseen_card.note()
104 | _remove_unseen_tags_for_card_and_note(pot_unseen_card, pot_unseen_note)
105 |
106 |
107 | def answer_card_removing_unseen_tags(self, ease):
108 | #self is reviewer
109 | #Keep these guards in place ... not sure why?
110 | if self.mw.state != "review":
111 | return
112 | if self.state != "answer":
113 | return
114 | _remove_unseen_tags(self)
115 |
116 |
117 | def suspend_cards_removing_unseen_tags(self, ids):
118 | #self is Scheduler
119 | for cid in ids:
120 | sus_card = self.col.getCard(cid)
121 | sus_note = sus_card.note()
122 | _remove_unseen_tags_for_card_and_note(sus_card, sus_note)
123 |
124 |
125 | def show_all_unseen_cards(self):
126 | #self is browser
127 | self.form.searchEdit.lineEdit().setText("tag:" + unseen_tag)
128 | self.onSearch()
129 |
130 |
131 | def setup_browser_menu(self):
132 | #self is browser
133 | try:
134 | # used by multiple add-ons, so we check for its existence first
135 | menu = self.menuTags
136 | except:
137 | self.menuTags = QMenu(_("Tags"))
138 | action = self.menuBar().insertMenu(self.mw.form.menuTools.menuAction(), self.menuTags)
139 | menu = self.menuTags
140 | menu.addSeparator()
141 | unseen_menu = menu.addMenu("Unseen Card Tracking")
142 | a = unseen_menu.addAction('Add "Unseen" Tags to Selected Cards')
143 | a.setShortcut(QKeySequence("Ctrl+U"))
144 | self.connect(a, SIGNAL("triggered()"), lambda b=self: add_unseen_tags_to_selected(b))
145 | a = unseen_menu.addAction('Remove "Unseen" Tags from Selected Cards')
146 | a.setShortcut(QKeySequence("Ctrl+Shift+U"))
147 | self.connect(a, SIGNAL("triggered()"), lambda b=self: remove_unseen_tags_from_selected(b))
148 | a = unseen_menu.addAction('Show all Unseen Cards')
149 | a.setShortcut(QKeySequence("Ctrl+Alt+U"))
150 | self.connect(a, SIGNAL("triggered()"), lambda b=self: show_all_unseen_cards(b))
151 |
152 | #todo: menu action to set search string to show all
153 | addHook("browser.setupMenus", setup_browser_menu)
154 | Reviewer._answerCard = wrap(Reviewer._answerCard, answer_card_removing_unseen_tags, "before")
155 | Reviewer._showQuestion = wrap(Reviewer._showQuestion, change_background_color, "after")
156 | # have to call this again on the answer side because of JS Booster add-on
157 | Reviewer._showAnswer = wrap(Reviewer._showAnswer, change_background_color, "after")
158 |
159 | Reviewer.nextCard = wrap(Reviewer.nextCard, wipe_background_for_nextCard, "before")
160 |
161 | #By wrapping this, we cover suspending cards/notes etc
162 | Scheduler.suspendCards = wrap(Scheduler.suspendCards, suspend_cards_removing_unseen_tags, "before")
163 |
--------------------------------------------------------------------------------
/src/sched_advanced_newcard_limits/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import sched_advanced_newcard_limits
--------------------------------------------------------------------------------
/src/sched_advanced_newcard_limits/sched_advanced_newcard_limits.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Anki Add-on: Advanced New Card Limits
4 |
5 | Allows you to restrict new cards to less than one per day.
6 |
7 | The deck options need to be set to 1 new card per day for
8 | the advanced limits to apply.
9 |
10 | Otherwise, the deck can be given to every deck using the same deck.
11 |
12 | Copyright: (c) Glutanimate 2017 and Arthur Milchior arthur@milchior.fr
13 | License: GNU AGPLv3 or later
14 | """
15 |
16 | ############## USER CONFIGURATION START ##############
17 |
18 | # Syntax: "deck_name": days_between_new_cards
19 | deck_limits = {
20 | "Default": 2, # show 1 new card every 2 days in "Default" deck
21 | u"Very Slöw": 7 # deck names with unicode need to be prepended with u
22 | }
23 |
24 | option_limits={
25 | "hard": 5,
26 | "Chapter": 2,
27 | }
28 | #In ANKI21, you can edit the options in the configuration manager,
29 | #instead of here. Thus, the configurations will be kept when the
30 | #add-on is updated.
31 |
32 | ############## USER CONFIGURATION END ##############
33 | from anki import version as anki_version
34 | import aqt
35 | anki21 = anki_version.startswith("2.1.")
36 | if anki21:
37 | userOption = aqt.mw.addonManager.getConfig(__name__)
38 | deck_limits.update(userOption["deck limits"])
39 | option_limits.update(userOption["option limits"])
40 |
41 | from anki.sched import Scheduler
42 | from anki.schedv2 import Scheduler as SchedulerV2
43 |
44 | def debug(t):
45 | print(t)
46 | pass
47 | def myDeckNewLimitSingle(self, g):
48 | """Limit for deck without parent limits,
49 | modified to only show cards every n days"""
50 | debug(f"Calling myDeckNewLimitSingle({g})")
51 | if g['dyn']:
52 | return self.reportLimit
53 | did = g['id']
54 | c = self.col.decks.confForDid(did)
55 | cname = c['name']
56 | deck = self.col.decks.nameOrNone(did)
57 | per_day = c['new']['perDay']
58 | lim = max(0, per_day - g['newToday'][1])
59 | if cname in option_limits:
60 | our_limit = option_limits[cname]
61 | elif per_day <= 1 and deck in deck_limits and deck_limits[deck] != 1:
62 | our_limit = deck_limits[deck]
63 | else:
64 | return lim
65 | dsel = "cid in (select id from cards where did = %s)" % did
66 | last = self.col.db.scalar(
67 | "select id from revlog where type = 0 and ivl > 0 and %s order by id desc limit 1" % dsel)
68 | if not last:
69 | return lim
70 | last_new = last/1000
71 | last_cutoff = self.dayCutoff - 86400
72 | # days between last graduation and yesterday's cutoff:
73 | ddays = -(-(last_cutoff - last_new) // 86400.0) # ceil
74 | if ddays < our_limit:
75 | lim = 0
76 | # print ddays
77 | return lim
78 |
79 | Scheduler._deckNewLimitSingle = myDeckNewLimitSingle
80 | SchedulerV2._deckNewLimitSingle = myDeckNewLimitSingle
81 |
--------------------------------------------------------------------------------
/src/sched_deck_orgactions/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import sched_deck_orgactions
--------------------------------------------------------------------------------
/src/sched_deck_orgactions/sched_deck_orgactions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Deck Organization Actions
5 |
6 | Allows users to assign various deck organization tasks
7 | to a simple Tools menu entry that can be invoked via Shift+O.
8 |
9 | Copyright: (c) 2017 Glutanimate
10 | License: GNU AGPLv3
11 | """
12 |
13 | from __future__ import unicode_literals
14 |
15 | ############## USER CONFIGURATION START ##############
16 |
17 | # Tuple of task dictionaries describing organization tasks
18 | # to be performed on toggling menu entry
19 | org_tasks = (
20 | {"action": "move", "orig": "source deck 1", "dest": "destination deck",
21 | "count": 2, "order": "added", "invert": True},
22 | {"action": "move", "orig": "source deck 2", "dest": "destination deck",
23 | "count": 2, "order": "due", "invert": False}
24 | )
25 | # dictinary keys and possible values:
26 | # "action": "move" (default) # = move new cards, only action so far
27 | # "orig": string, name of old deck
28 | # "dest": string, name of new deck
29 | # "count": integer, number of cards
30 | # "order": "due" (default) / "added" / "random"
31 | # "invert": True / False (default) # whether to invert card order
32 |
33 | # Whether or not to warn when executing task more than once a day
34 | WARN_ON_MULTIPLE_EXECUTIONS = True
35 |
36 | ############## USER CONFIGURATION END ##############
37 |
38 | import time
39 | import random
40 |
41 | from aqt.qt import *
42 | from aqt.utils import tooltip, askUser
43 | from aqt import mw
44 |
45 | VALID_ACTIONS = ("move")
46 | VALID_ORDERS = ("due", "added", "random")
47 |
48 |
49 | def moveCardsAction(task):
50 | """Move cards between decks"""
51 | orig = task.get("orig", None)
52 | dest = task.get("dest", None)
53 | count = task.get("count", None)
54 | order = task.get("order", "due")
55 | invert = task.get("invert", False)
56 |
57 | try:
58 | assert orig and dest and count
59 | assert order in VALID_ORDERS
60 | except AssertionError:
61 | return "invalid"
62 |
63 | orig_did = mw.col.decks.id(orig, create=False)
64 | if not orig_did:
65 | return "invalid"
66 |
67 | dest_did = mw.col.decks.id(dest, create=False)
68 |
69 | if dest_did and mw.col.decks.isDyn(dest_did):
70 | # cards can't be moved into filtered deck
71 | return "dynamic"
72 |
73 |
74 | cids = mw.col.decks.cids(orig_did)
75 | if not cids:
76 | return "nocids"
77 |
78 | if order in ("random", "added"):
79 | cmd = "select id from cards where did = ? and type=0 order by id"
80 | elif order == "due":
81 | cmd = "select id from cards where did = ? and type=0 order by due"
82 |
83 | scids = mw.col.db.list(cmd, orig_did)
84 |
85 | if order == "random":
86 | scids = random.sample(scids, count)
87 | elif invert:
88 | scids = scids[-count:]
89 | else:
90 | scids = scids[:count]
91 |
92 | if not scids:
93 | return "nocids"
94 |
95 | if not dest_did:
96 | # create destination deck now
97 | dest_did = mw.col.decks.id(dest)
98 |
99 | # remove any cards from filtered deck first
100 | mw.col.sched.remFromDyn(scids)
101 | # then move into new deck
102 | mw.col.decks.setDeck(scids, dest_did)
103 |
104 |
105 | def performDeckOrgActions():
106 | """Parse org_tasks dictionary for tasks and perform them"""
107 | returncodes = []
108 |
109 | mw.checkpoint("Deck Organization Tasks")
110 |
111 | for task in org_tasks:
112 |
113 | action = task.get("action", "move")
114 |
115 | try:
116 | assert action in VALID_ACTIONS
117 | except AssertionError:
118 | print("Invalid task. Skipping")
119 | returncodes.append("invalid")
120 | continue
121 |
122 | ret = moveCardsAction(task)
123 | if ret:
124 | returncodes.append(ret)
125 |
126 | mw.reset()
127 |
128 | msg = ["Deck organization tasks complete."]
129 | if "invalid" in returncodes:
130 | msg.append("Warning: Your task dictionary contained invalid"
131 | " tasks that had to be skipped.")
132 | if "dynamic" in returncodes:
133 | msg.append("Warning: Some of your decks are filtered decks."
134 | " Moving cards into filtered decks is not supported")
135 | if "nocids" in returncodes:
136 | msg.append("Warning: Some of the origin decks did not contain"
137 | "any transferable cards. Actions were skipped in those"
138 | " instances.")
139 |
140 | info = "
".join(msg)
141 | tooltip(info)
142 |
143 |
144 | def onOrganizeTask():
145 | """Main function. Invoked on clicking menu entry"""
146 | conf = mw.col.conf
147 | today = mw.col.sched.today # today in days since col creation time
148 |
149 | if WARN_ON_MULTIPLE_EXECUTIONS:
150 | # check if we've already performed tasks today:
151 | last = conf.get("deckOrgLast", None)
152 | if last and last == today:
153 | q = ("You have already performed this task today."
154 | " Are you sure you want to proceed?")
155 | ret = askUser(q)
156 | if not ret:
157 | return False
158 |
159 | performDeckOrgActions()
160 |
161 | conf["deckOrgLast"] = today
162 | mw.col.setMod()
163 |
164 |
165 | # Menu / Hooks
166 |
167 | action = QAction(mw)
168 | action.setText("Perform Deck Organization Tasks")
169 | action.setShortcut(QKeySequence("Shift+O"))
170 | mw.form.menuTools.addAction(action)
171 | action.triggered.connect(onOrganizeTask)
--------------------------------------------------------------------------------
/src/sched_filter_dailydue/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import sched_filter_dailydue
--------------------------------------------------------------------------------
/src/sched_filter_dailydue/sched_filter_dailydue.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Create Filtered Deck of All Cards Scheduled for Today
5 |
6 | Adds the search term "is:today" to the filtered deck creation dialog
7 | which includes the following cards:
8 |
9 | - all cards due for today, according to each deck's review limit
10 | - all new cards "due for today", according to each deck's new card limit
11 |
12 | Also supports limiting to / excluding specific decks by combining
13 | "is:today" with "deck:" phrases.
14 |
15 | Copyright: (c) 2017 Glutanimate
16 | License: GNU AGPLv3
17 | """
18 |
19 | import random
20 |
21 | from anki.sched import Scheduler
22 | from anki.hooks import wrap
23 |
24 | def onUpdateStats(self, card, type, cnt=1):
25 | """Increment card counts for original decks"""
26 | # only applies to filtered decks using our special syntax
27 | key = type+"Today"
28 | if card.odid:
29 | conf = self.col.decks.confForDid(card.did)
30 | if "is:today" not in conf["terms"][0][0]:
31 | return
32 | for g in ([self.col.decks.get(card.odid)] +
33 | self.col.decks.parents(card.odid)):
34 | # add
35 | g[key][1] += cnt
36 | self.col.decks.save(g)
37 |
38 | def myFillDyn(self, deck, _old):
39 | """Fill filtered deck using custom filter"""
40 | search, limit, order = deck['terms'][0]
41 | if "is:today" in search:
42 | ids = self.dynToday(search, order=order)
43 | # move the cards over
44 | self.col.log(deck['id'], ids)
45 | self._moveToDyn(deck['id'], ids)
46 | return ids
47 | else:
48 | return _old(self, deck)
49 |
50 | def dynToday(self, search, order=False):
51 | """Find all cards that are scheduled for today"""
52 | tokens = search.split()
53 | dids = [int(i) for i in self.col.decks.allIds()]
54 | idids = []
55 | sdids = []
56 | for t in tokens:
57 | # set decks according to search phrase
58 | if t.startswith("-deck:"):
59 | l = idids
60 | elif t.startswith("deck:"):
61 | l = sdids
62 | else:
63 | continue
64 | name = ":".join(t.split(":")[1:]).replace('"', '').replace("'", "")
65 | deck = self.col.decks.byName(name)
66 | if not deck:
67 | continue
68 | did = deck["id"]
69 | if did not in l:
70 | l += [did] + [a[1] for a in self.col.decks.children(did)]
71 | if sdids:
72 | dids = sdids
73 | if idids:
74 | dids = [did for did in dids if did not in idids]
75 | ids = []
76 | for did in dids:
77 | newlim = self._deckNewLimit(did)
78 | revlim = self._deckRevLimit(did)
79 | # new cards according to deck limits
80 | ids += self.col.db.list("""
81 | select id from cards where did = ? and
82 | queue = 0 order by due limit ?""", did, newlim)
83 | # due cards according to deck limits
84 | ids += self.col.db.list("""
85 | select id from cards where did = %s and
86 | (queue in (2,3) and due <= %d) or
87 | (queue = 1 and due <= %d)
88 | order by due limit %d""" % (did, self.today, self.dayCutoff, revlim))
89 | # randomize if option set
90 | if order == 1:
91 | random.shuffle(ids)
92 | return ids
93 |
94 | Scheduler.dynToday = dynToday
95 | Scheduler._fillDyn = wrap(Scheduler._fillDyn, myFillDyn, "around")
96 | Scheduler._updateStats = wrap(Scheduler._updateStats, onUpdateStats, "after")
--------------------------------------------------------------------------------
/src/sched_ignore_lapses_below_ivl/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import sched_ignore_lapses_below_ivl
--------------------------------------------------------------------------------
/src/sched_ignore_lapses_below_ivl/sched_ignore_lapses_below_ivl.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Ignore Lapses Below Interval
5 |
6 | Copyright: (c) 2018 Glutanimate
7 | License: GNU AGPLv3
8 | """
9 |
10 | ############## USER CONFIGURATION START ##############
11 |
12 | IVL_THRESHOLD = 4 # Interval threshold in days [integer].
13 | # Only lapses above this interval will
14 | # be registered as such.
15 |
16 | ############## USER CONFIGURATION END ##############
17 |
18 | import time
19 | from heapq import *
20 | from anki.sched import Scheduler
21 |
22 |
23 | def myRescheduleLapse(self, card):
24 | conf = self._lapseConf(card)
25 | card.lastIvl = card.ivl
26 | if self._resched(card):
27 | # ==== MODIFICATIONS START ====
28 | if card.lastIvl > IVL_THRESHOLD:
29 | card.lapses += 1
30 | # ==== MODIFICATIONS END ====
31 | card.ivl = self._nextLapseIvl(card, conf)
32 | card.factor = max(1300, card.factor-200)
33 | card.due = self.today + card.ivl
34 | # if it's a filtered deck, update odue as well
35 | if card.odid:
36 | card.odue = card.due
37 | # if suspended as a leech, nothing to do
38 | delay = 0
39 | if self._checkLeech(card, conf) and card.queue == -1:
40 | return delay
41 | # if no relearning steps, nothing to do
42 | if not conf['delays']:
43 | return delay
44 | # record rev due date for later
45 | if not card.odue:
46 | card.odue = card.due
47 | delay = self._delayForGrade(conf, 0)
48 | card.due = int(delay + time.time())
49 | card.left = self._startingLeft(card)
50 | # queue 1
51 | if card.due < self.dayCutoff:
52 | self.lrnCount += card.left // 1000
53 | card.queue = 1
54 | heappush(self._lrnQueue, (card.due, card.id))
55 | else:
56 | # day learn queue
57 | ahead = ((card.due - self.dayCutoff) // 86400) + 1
58 | card.due = self.today + ahead
59 | card.queue = 3
60 | return delay
61 |
62 | # Hooks
63 |
64 | Scheduler._rescheduleLapse = myRescheduleLapse
--------------------------------------------------------------------------------
/src/sched_sibling_spacing_whitelist/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import sched_sibling_spacing_whitelist
--------------------------------------------------------------------------------
/src/sched_sibling_spacing_whitelist/sched_sibling_spacing_whitelist.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | #-*- coding: utf-8 -*-
3 | # ---------------------------------------------------------------------------
4 | # Sibling Spacing is an addon for Anki 2 - http://ankisrs.net
5 | # ---------------------------------------------------------------------------
6 | # Original Author: Andreas Klauer (Andreas Klauer@metamorpher.de)
7 | # Modified by: Glutanimate 2016 (https://github.com/Glutanimate)
8 | # Version: 0.02 (2016-03-17)
9 | # License: GPL
10 | # ---------------------------------------------------------------------------
11 | #
12 | # This modified version of the Sibling Spacing add-on follows a whitelist
13 | # approach when adjusting scheduling of siblings. Only note models defined in
14 | # the 'enabledModels' list will be processed.
15 |
16 | # --- Imports: ---
17 |
18 | from anki.hooks import addHook, wrap
19 | from aqt import *
20 | from aqt.utils import showInfo, tooltip
21 |
22 | # --- Globals: ---
23 |
24 | enabled = True
25 | debug = True
26 |
27 | # edit this list with the note models you want to enable
28 | # sibling spacing on
29 | enabledModels = [ "Erweitert - Mehrzweck", "Kunst - Bildende Kunst" ]
30 |
31 | # --- Functions: ---
32 |
33 | def siblingIvl(self, card, idealIvl, _old):
34 | origIvl = _old(self, card, idealIvl)
35 |
36 | if not enabled:
37 | return origIvl
38 |
39 | modelName = card.model()["name"]
40 |
41 | if modelName not in enabledModels:
42 | # if debug:
43 | # print "Sibling Spacing not enabled for %s. No adjustment." % modelName
44 | return origIvl
45 |
46 | ivl = origIvl
47 |
48 | # Penalty
49 | minIvl = self.col.db.scalar('''SELECT MIN(ivl) FROM cards WHERE ivl > 0 AND nid = ? AND id != ? AND queue = 2''',
50 | card.nid, card.id)
51 |
52 | while minIvl and minIvl > 0 and ivl > minIvl*4:
53 | ivl = max(1, int(ivl/2.0))
54 |
55 | # Boost
56 | delta = max(1, int(ivl * 0.15))
57 | boost = max(1, int(ivl * 0.5))
58 |
59 | siblings = True
60 |
61 | while siblings:
62 | siblings = self.col.db.scalar('''SELECT count() FROM cards WHERE due >= ? AND due <= ? AND nid = ? AND id != ? AND queue = 2''',
63 | self.today + ivl - delta, self.today + ivl + delta, card.nid, card.id)
64 | if siblings:
65 | ivl += boost
66 |
67 | if debug:
68 | if minIvl and minIvl > 0:
69 | print "Sibling Spacing %d%+d = %d days for card %d (sibling has %d days)" % (origIvl,ivl-origIvl,ivl,card.id,minIvl,)
70 | tooltip("Sibling Spacing:
%d%+d days
" % (origIvl,ivl-origIvl))
71 | else:
72 | print "Sibling Spacing == %d (orig:%d) days for card %d without visible siblings" % (ivl,origIvl,card.id,)
73 |
74 | return ivl
75 |
76 | def toggle():
77 | global enabled
78 |
79 | enabled = not enabled
80 |
81 | if enabled:
82 | showInfo("Sibling Spacing is now ON")
83 | else:
84 | showInfo("Sibling Spacing is now OFF")
85 |
86 | def toggle_debug():
87 | global debug
88 |
89 | debug = not debug
90 |
91 | if debug:
92 | showInfo("Sibling Spacing Debug is now ON")
93 | else:
94 | showInfo("Sibling Spacing Debug is now OFF")
95 |
96 | def siblingMenu():
97 | '''Extend the addon menu with toggle.'''
98 | m = None
99 |
100 | for action in mw.form.menuPlugins.actions():
101 | menu = action.menu()
102 | if menu and menu.title() == "anki-sibling-spacing-whitelist":
103 | m = menu
104 | break
105 |
106 | if not m:
107 | return
108 |
109 | a = QAction(_("Toggle ON/OFF..."), mw)
110 | mw.connect(a, SIGNAL("triggered()"), toggle)
111 | m.addAction(a)
112 | a = QAction(_("Debug ON/OFF..."), mw)
113 | mw.connect(a, SIGNAL("triggered()"), toggle_debug)
114 | m.addAction(a)
115 |
116 | def profileLoaded():
117 | # add menu entry
118 | mw.addonManager.rebuildAddonsMenu = wrap(mw.addonManager.rebuildAddonsMenu,
119 | siblingMenu)
120 | mw.addonManager.rebuildAddonsMenu()
121 |
122 | # add scheduler
123 | anki.sched.Scheduler._adjRevIvl = wrap(anki.sched.Scheduler._adjRevIvl,
124 | siblingIvl, "around")
125 |
126 | # --- Hooks: ---
127 |
128 | addHook("profileLoaded", profileLoaded)
129 |
130 | # --- End of file. ---
131 |
--------------------------------------------------------------------------------
/src/search_last_edited/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import search_last_edited
--------------------------------------------------------------------------------
/src/search_last_edited/search_last_edited.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Anki Add-on: Search by Edit Date
5 |
6 | Adds two new properties to Anki's search module:
7 |
8 | "edited:x" – will list all cards edited over the last x days
9 | "editedon:x" – will list all cards edited x days ago
10 |
11 | These can be used across all search interfaces that Anki provides
12 | (Browser, Filtered Decks Creation, etc.)
13 |
14 | Copyright: (c) Glutanimate 2017
15 | License: GNU AGPLv3 or later
16 | """
17 |
18 | from anki.find import Finder
19 | from anki.utils import ids2str
20 | from anki.hooks import wrap
21 |
22 |
23 | def findLastEdited(self, val, exact=False):
24 | """Find cards by edit date"""
25 | # self is find.Finder
26 | try:
27 | days = int(val[0])
28 | except ValueError:
29 | return
30 | days = max(days, 0)
31 |
32 | # first cutoff at x days ago
33 | cutoff1 = (self.col.sched.dayCutoff - 86400*days)
34 |
35 | if exact:
36 | # second cutoff at x-1 days ago
37 | cutoff2 = cutoff1 + 86400
38 | # select notes that were edited on that day
39 | nids = self.col.db.list(
40 | "select id from notes where mod between {} and {}".format(
41 | cutoff1, cutoff2))
42 | else:
43 | # select notes that were edited since then
44 | nids = self.col.db.list(
45 | "select id from notes where mod > {}".format(cutoff1))
46 |
47 | return ("c.nid in {}".format(ids2str(nids)))
48 |
49 |
50 | def addFinder(self, col):
51 | """Add custom finders to search dictionary"""
52 | self.search["edited"] = self.findLastEdited
53 | self.search["editedon"] = lambda *x: self.findLastEdited(*x, exact=True)
54 |
55 |
56 | # Hooks
57 |
58 | Finder.findLastEdited = findLastEdited
59 | Finder.__init__ = wrap(Finder.__init__, addFinder, "after")
60 |
--------------------------------------------------------------------------------
/src/stats_true_retention_extended/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Entry point for the add-on into Anki
4 | # Please do not edit this if you do not know what you are doing.
5 | #
6 | # Copyright: (c) 2017 Glutanimate
7 | # License: GNU AGPLv3
8 |
9 | from . import stats_true_retention_extended
--------------------------------------------------------------------------------
/src/stats_true_retention_extended/stats_true_retention_extended.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | True Retention Add-on for Anki (extended)
5 |
6 | Based on True Retention by Strider (?)
7 | (https://ankiweb.net/shared/info/613684242)
8 |
9 | Copyright: (c) 2016 Strider (?)
10 | (c) 2017 Glutanimate (https://github.com/Glutanimate)
11 | License: GNU AGPLv3 or later
12 | """
13 |
14 | from __future__ import unicode_literals
15 |
16 | ############## USER CONFIGURATION START ##############
17 |
18 | MATURE_IVL = 21 # mature card interval in days
19 |
20 | ############## USER CONFIGURATION END ##############
21 |
22 | import anki.stats
23 |
24 | from anki.utils import fmtTimeSpan
25 | from anki.lang import _, ngettext
26 | from anki import version as anki_version
27 |
28 |
29 | # Types: 0 - new today; 1 - review; 2 - relearn; 3 - (cram?) [before the answer was pressed]
30 | # "Learning" corresponds to New|Relearn. "Review" corresponds to Young|Mature.
31 | # Ease: 1 - flunk button; 2 - second; 3 - third; 4 - fourth (easy) [which button was pressed]
32 | # Intervals: -60 <1m -600 10m etc; otherwise days
33 | def _line_now(self, i, a, b, bold=True):
34 | colon = _(":")
35 | if bold:
36 | i.append(("