From 79e80766858d720b4287637b0c5edf73441a8b7c Mon Sep 17 00:00:00 2001 From: Fabian Wood Date: Fri, 31 Jul 2020 00:56:48 +1000 Subject: [PATCH 01/15] Added typehints for qt profiles * `Any` used for pickle methods, this could probably be improved with some kind of Callable * str used for self.base, though this may be a problem for different OSes. Some type of os.PathLike might be good. * Line 75, type ignored: mypy was complaining about no. of args, and kwargs there didn't seem to be needed. Separate issue to test, though. --- qt/aqt/profiles.py | 73 +++++++++++++++++++++++----------------------- qt/mypy.ini | 2 ++ 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 4580a3e75..c46d7d628 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -72,7 +72,7 @@ class LoadMetaResult: class AnkiRestart(SystemExit): def __init__(self, *args, **kwargs): self.exitcode = kwargs.pop("exitcode", 0) - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # type: ignore class ProfileManager: @@ -83,6 +83,7 @@ class ProfileManager: self.db = None self.profile: Optional[Dict] = None # instantiate base folder + self.base: str self._setBaseFolder(base) def setupMeta(self) -> LoadMetaResult: @@ -92,7 +93,7 @@ class ProfileManager: return res # profile load on startup - def openProfile(self, profile): + def openProfile(self, profile) -> None: if profile: if profile not in self.profiles(): QMessageBox.critical(None, "Error", "Requested profile does not exist.") @@ -105,13 +106,13 @@ class ProfileManager: # Base creation ###################################################################### - def ensureBaseExists(self): + def ensureBaseExists(self) -> None: self._ensureExists(self.base) # Folder migration ###################################################################### - def _oldFolderLocation(self): + def _oldFolderLocation(self) -> str: if isMac: return os.path.expanduser("~/Documents/Anki") elif isWin: @@ -153,7 +154,7 @@ class ProfileManager: confirmation = QMessageBox() confirmation.setIcon(QMessageBox.Warning) confirmation.setWindowIcon(icon) - confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) + confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore confirmation.setWindowTitle(window_title) confirmation.setText( "Anki needs to move its data folder from Documents/Anki to a new location. Proceed?" @@ -168,7 +169,7 @@ class ProfileManager: progress.setWindowTitle(window_title) progress.setText("Please wait...") progress.show() - app.processEvents() + app.processEvents() # type: ignore shutil.move(oldBase, self.base) progress.hide() @@ -198,8 +199,8 @@ class ProfileManager: # Profile load/save ###################################################################### - def profiles(self): - def names(): + def profiles(self) -> List: + def names() -> List: return self.db.list("select name from profiles where name != '_global'") n = names() @@ -209,9 +210,9 @@ class ProfileManager: return n - def _unpickle(self, data): + def _unpickle(self, data) -> Any: class Unpickler(pickle.Unpickler): - def find_class(self, module, name): + def find_class(self, module: str, name: str) -> Any: if module == "PyQt5.sip": try: import PyQt5.sip # pylint: disable=unused-import @@ -234,10 +235,10 @@ class ProfileManager: up = Unpickler(io.BytesIO(data), errors="ignore") return up.load() - def _pickle(self, obj): + def _pickle(self, obj) -> Any: return pickle.dumps(obj, protocol=0) - def load(self, name): + def load(self, name) -> bool: assert name != "_global" data = self.db.scalar( "select cast(data as blob) from profiles where name = ?", name @@ -261,32 +262,32 @@ details have been forgotten.""" self.save() return True - def save(self): + def save(self) -> None: sql = "update profiles set data = ? where name = ?" self.db.execute(sql, self._pickle(self.profile), self.name) self.db.execute(sql, self._pickle(self.meta), "_global") self.db.commit() - def create(self, name): + def create(self, name) -> None: prof = profileConf.copy() self.db.execute( "insert or ignore into profiles values (?, ?)", name, self._pickle(prof) ) self.db.commit() - def remove(self, name): + def remove(self, name) -> None: p = self.profileFolder() if os.path.exists(p): send2trash(p) self.db.execute("delete from profiles where name = ?", name) self.db.commit() - def trashCollection(self): + def trashCollection(self) -> None: p = self.collectionPath() if os.path.exists(p): send2trash(p) - def rename(self, name): + def rename(self, name) -> None: oldName = self.name oldFolder = self.profileFolder() self.name = name @@ -337,19 +338,19 @@ and no other programs are accessing your profile folders, then try again.""" # Folder handling ###################################################################### - def profileFolder(self, create=True): + def profileFolder(self, create=True) -> str: path = os.path.join(self.base, self.name) if create: self._ensureExists(path) return path - def addonFolder(self): + def addonFolder(self) -> str: return self._ensureExists(os.path.join(self.base, "addons21")) - def backupFolder(self): + def backupFolder(self) -> str: return self._ensureExists(os.path.join(self.profileFolder(), "backups")) - def collectionPath(self): + def collectionPath(self) -> str: return os.path.join(self.profileFolder(), "collection.anki2") # Downgrade @@ -377,12 +378,12 @@ and no other programs are accessing your profile folders, then try again.""" # Helpers ###################################################################### - def _ensureExists(self, path): + def _ensureExists(self, path: str) -> str: if not os.path.exists(path): os.makedirs(path) return path - def _setBaseFolder(self, cmdlineBase): + def _setBaseFolder(self, cmdlineBase: None) -> None: if cmdlineBase: self.base = os.path.abspath(cmdlineBase) elif os.environ.get("ANKI_BASE"): @@ -392,7 +393,7 @@ and no other programs are accessing your profile folders, then try again.""" self.maybeMigrateFolder() self.ensureBaseExists() - def _defaultBase(self): + def _defaultBase(self) -> str: if isWin: from aqt.winpaths import get_appdata @@ -419,7 +420,7 @@ and no other programs are accessing your profile folders, then try again.""" result.firstTime = not os.path.exists(path) - def recover(): + def recover() -> None: # if we can't load profile, start with a new one if self.db: try: @@ -471,7 +472,7 @@ create table if not exists profiles ) return result - def _ensureProfile(self): + def _ensureProfile(self) -> None: "Create a new profile if none exists." self.create(_("User 1")) p = os.path.join(self.base, "README.txt") @@ -486,7 +487,7 @@ create table if not exists profiles ###################################################################### # On first run, allow the user to choose the default language - def setDefaultLang(self): + def setDefaultLang(self) -> None: # create dialog class NoCloseDiag(QDialog): def reject(self): @@ -519,20 +520,20 @@ create table if not exists profiles f.lang.setCurrentRow(idx) d.exec_() - def _onLangSelected(self): + def _onLangSelected(self) -> None: f = self.langForm obj = anki.lang.langs[f.lang.currentRow()] code = obj[1] name = obj[0] en = "Are you sure you wish to display Anki's interface in %s?" r = QMessageBox.question( - None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No + None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No # type: ignore ) if r != QMessageBox.Yes: return self.setDefaultLang() self.setLang(code) - def setLang(self, code): + def setLang(self, code) -> None: self.meta["defaultLang"] = code sql = "update profiles set data = ? where name = ?" self.db.execute(sql, self._pickle(self.meta), "_global") @@ -542,10 +543,10 @@ create table if not exists profiles # OpenGL ###################################################################### - def _glPath(self): + def _glPath(self) -> str: return os.path.join(self.base, "gldriver") - def glMode(self): + def glMode(self) -> str: if isMac: return "auto" @@ -562,11 +563,11 @@ create table if not exists profiles return mode return "auto" - def setGlMode(self, mode): + def setGlMode(self, mode) -> None: with open(self._glPath(), "w") as file: file.write(mode) - def nextGlMode(self): + def nextGlMode(self) -> None: mode = self.glMode() if mode == "software": self.setGlMode("auto") @@ -591,7 +592,7 @@ create table if not exists profiles def last_addon_update_check(self) -> int: return self.meta.get("last_addon_update_check", 0) - def set_last_addon_update_check(self, secs): + def set_last_addon_update_check(self, secs) -> None: self.meta["last_addon_update_check"] = secs def night_mode(self) -> bool: @@ -642,7 +643,7 @@ create table if not exists profiles def auto_sync_media_minutes(self) -> int: return self.profile.get("autoSyncMediaMinutes", 15) - def set_auto_sync_media_minutes(self, val: int): + def set_auto_sync_media_minutes(self, val: int) -> None: self.profile["autoSyncMediaMinutes"] = val ###################################################################### diff --git a/qt/mypy.ini b/qt/mypy.ini index 9adcb3d1b..82da08e40 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -88,3 +88,5 @@ check_untyped_defs=true check_untyped_defs=true [mypy-aqt.modelchooser] check_untyped_defs=true +[mypy-aqt.profiles] +check_untyped_defs=true From b4b12f16422ff98d135e36901beb7c60ab1ee51c Mon Sep 17 00:00:00 2001 From: ANH Date: Thu, 30 Jul 2020 21:06:16 +0300 Subject: [PATCH 02/15] add reviewer_will_play_question_sounds and reviewer_will_play_answer_sounds hooks --- qt/aqt/gui_hooks.py | 54 ++++++++++++++++++++++++++++++++++++++++ qt/aqt/reviewer.py | 16 +++++++++--- qt/tools/genhooks_gui.py | 8 ++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 2f30d22e4..8c8ec5fd9 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -2112,6 +2112,60 @@ class _ReviewerWillEndHook: reviewer_will_end = _ReviewerWillEndHook() +class _ReviewerWillPlayAnswerSoundsHook: + _hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = [] + + def append(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: + """(card: Card, tags: List[anki.sound.AVTag])""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def count(self) -> int: + return len(self._hooks) + + def __call__(self, card: Card, tags: List[anki.sound.AVTag]) -> None: + for hook in self._hooks: + try: + hook(card, tags) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +reviewer_will_play_answer_sounds = _ReviewerWillPlayAnswerSoundsHook() + + +class _ReviewerWillPlayQuestionSoundsHook: + _hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = [] + + def append(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: + """(card: Card, tags: List[anki.sound.AVTag])""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def count(self) -> int: + return len(self._hooks) + + def __call__(self, card: Card, tags: List[anki.sound.AVTag]) -> None: + for hook in self._hooks: + try: + hook(card, tags) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +reviewer_will_play_question_sounds = _ReviewerWillPlayQuestionSoundsHook() + + class _ReviewerWillShowContextMenuHook: _hooks: List[Callable[["aqt.reviewer.Reviewer", QMenu], None]] = [] diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 3bc85352f..01d847afe 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -183,10 +183,14 @@ class Reviewer: q = c.q() # play audio? if c.autoplay(): - av_player.play_tags(c.question_av_tags()) + sounds = c.question_av_tags() + gui_hooks.reviewer_will_play_question_sounds(c, sounds) + av_player.play_tags(sounds) else: av_player.clear_queue_and_maybe_interrupt() - + sounds = [] + gui_hooks.reviewer_will_play_question_sounds(c, sounds) + av_player.play_tags(sounds) # render & update bottom q = self._mungeQA(q) q = gui_hooks.card_will_show(q, c, "reviewQuestion") @@ -225,10 +229,14 @@ class Reviewer: a = c.a() # play audio? if c.autoplay(): - av_player.play_tags(c.answer_av_tags()) + sounds = c.answer_av_tags() + gui_hooks.reviewer_will_play_answer_sounds(c, sounds) + av_player.play_tags(sounds) else: av_player.clear_queue_and_maybe_interrupt() - + sounds = [] + gui_hooks.reviewer_will_play_answer_sounds(c, sounds) + av_player.play_tags(sounds) a = self._mungeQA(a) a = gui_hooks.card_will_show(a, c, "reviewAnswer") # render and update bottom diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index f5db67f6f..ea7fe384a 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -90,6 +90,14 @@ hooks = [ legacy_hook="reviewCleanup", doc="Called before Anki transitions from the review screen to another screen.", ), + Hook( + name="reviewer_will_play_question_sounds", + args=["card: Card", "tags: List[anki.sound.AVTag]"], + ), + Hook( + name="reviewer_will_play_answer_sounds", + args=["card: Card", "tags: List[anki.sound.AVTag]"], + ), # Debug ################### Hook( From b4604873c49a29a70cf503e7552a567b525fe2b2 Mon Sep 17 00:00:00 2001 From: ANH Date: Fri, 31 Jul 2020 03:06:13 +0300 Subject: [PATCH 03/15] document hooks --- qt/aqt/gui_hooks.py | 24 ++++++++++++++++++++++++ qt/tools/genhooks_gui.py | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 8c8ec5fd9..8fd74230a 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -2113,6 +2113,18 @@ reviewer_will_end = _ReviewerWillEndHook() class _ReviewerWillPlayAnswerSoundsHook: + """Called before showing the answer/back side. + + `tags` can be used to inspect and manipulate the sounds + that will be played (if any). + + This won't be called when the user manually plays sounds + using `Replay Audio`. + + Not that this hook is called even when the `Automatically play audio` + option is unchecked; This is so as to allow playing custom + sounds regardless of that option.""" + _hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = [] def append(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: @@ -2140,6 +2152,18 @@ reviewer_will_play_answer_sounds = _ReviewerWillPlayAnswerSoundsHook() class _ReviewerWillPlayQuestionSoundsHook: + """Called before showing the question/front side. + + `tags` can be used to inspect and manipulate the sounds + that will be played (if any). + + This won't be called when the user manually plays sounds + using `Replay Audio`. + + Not that this hook is called even when the `Automatically play audio` + option is unchecked; This is so as to allow playing custom + sounds regardless of that option.""" + _hooks: List[Callable[[Card, "List[anki.sound.AVTag]"], None]] = [] def append(self, cb: Callable[[Card, "List[anki.sound.AVTag]"], None]) -> None: diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index ea7fe384a..2fc9d5024 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -93,10 +93,32 @@ hooks = [ Hook( name="reviewer_will_play_question_sounds", args=["card: Card", "tags: List[anki.sound.AVTag]"], + doc="""Called before showing the question/front side. + + `tags` can be used to inspect and manipulate the sounds + that will be played (if any). + + This won't be called when the user manually plays sounds + using `Replay Audio`. + + Not that this hook is called even when the `Automatically play audio` + option is unchecked; This is so as to allow playing custom + sounds regardless of that option.""", ), Hook( name="reviewer_will_play_answer_sounds", args=["card: Card", "tags: List[anki.sound.AVTag]"], + doc="""Called before showing the answer/back side. + + `tags` can be used to inspect and manipulate the sounds + that will be played (if any). + + This won't be called when the user manually plays sounds + using `Replay Audio`. + + Not that this hook is called even when the `Automatically play audio` + option is unchecked; This is so as to allow playing custom + sounds regardless of that option.""", ), # Debug ################### From a56690bc0845300b8f4f50ba2ec76cc0f86fd14c Mon Sep 17 00:00:00 2001 From: Matt Krump <1036969+mkrump@users.noreply.github.com> Date: Thu, 30 Jul 2020 16:49:05 -0600 Subject: [PATCH 04/15] Turn on check_untyped_defs for aqt.webview --- qt/aqt/qt.py | 1 + qt/aqt/webview.py | 4 ++-- qt/mypy.ini | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/qt/aqt/qt.py b/qt/aqt/qt.py index 9c5dffc0e..3ce6e2fca 100644 --- a/qt/aqt/qt.py +++ b/qt/aqt/qt.py @@ -13,6 +13,7 @@ from PyQt5.Qt import * # type: ignore from PyQt5.QtCore import * from PyQt5.QtCore import pyqtRemoveInputHook # pylint: disable=no-name-in-module from PyQt5.QtGui import * # type: ignore +from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtWebEngineWidgets import * from PyQt5.QtWidgets import * diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 88b5b3a40..8220e26d7 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -30,9 +30,9 @@ class AnkiWebPage(QWebEnginePage): self._setupBridge() self.open_links_externally = True - def _setupBridge(self): + def _setupBridge(self) -> None: class Bridge(QObject): - @pyqtSlot(str, result=str) + @pyqtSlot(str, result=str) # type: ignore def cmd(self, str): return json.dumps(self.onCmd(str)) diff --git a/qt/mypy.ini b/qt/mypy.ini index 9adcb3d1b..ed6253bc7 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -88,3 +88,5 @@ check_untyped_defs=true check_untyped_defs=true [mypy-aqt.modelchooser] check_untyped_defs=true +[mypy-aqt.webview] +check_untyped_defs=true From 7d8f8560600c2dea8293da2a10dcf049ac071acc Mon Sep 17 00:00:00 2001 From: Matt Krump <1036969+mkrump@users.noreply.github.com> Date: Thu, 30 Jul 2020 17:54:05 -0600 Subject: [PATCH 05/15] Turn on check_untyped_defs for aqt.addons --- qt/aqt/addons.py | 4 ++-- qt/mypy.ini | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index b5e534cce..5773e8e28 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -179,7 +179,7 @@ class AddonManager: sys.path.insert(0, self.addonsFolder()) # in new code, you may want all_addon_meta() instead - def allAddons(self): + def allAddons(self) -> List[str]: l = [] for d in os.listdir(self.addonsFolder()): path = self.addonsFolder(d) @@ -188,7 +188,7 @@ class AddonManager: l.append(d) l.sort() if os.getenv("ANKIREVADDONS", ""): - l = reversed(l) + l = list(reversed(l)) return l def all_addon_meta(self) -> Iterable[AddonMeta]: diff --git a/qt/mypy.ini b/qt/mypy.ini index ed6253bc7..04415f4cd 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -90,3 +90,5 @@ check_untyped_defs=true check_untyped_defs=true [mypy-aqt.webview] check_untyped_defs=true +[mypy-aqt.addons] +check_untyped_defs=true From 10f2f9c037196f5389d784dccc80907005123506 Mon Sep 17 00:00:00 2001 From: Matt Krump <1036969+mkrump@users.noreply.github.com> Date: Thu, 30 Jul 2020 17:54:42 -0600 Subject: [PATCH 06/15] Turn on check_untyped_defs for aqt.emptycards --- qt/aqt/emptycards.py | 4 ++-- qt/mypy.ini | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 773d1c650..55b02fb6b 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -85,10 +85,10 @@ class EmptyCardsDialog(QDialog): self.mw.taskman.run_in_background(delete, on_done) - def _delete_cards(self, keep_notes): + def _delete_cards(self, keep_notes: bool) -> int: to_delete = [] + note: NoteWithEmptyCards for note in self.report.notes: - note: NoteWithEmptyCards = note if keep_notes and note.will_delete_note: # leave first card to_delete.extend(note.card_ids[1:]) diff --git a/qt/mypy.ini b/qt/mypy.ini index 04415f4cd..31cf898b2 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -92,3 +92,5 @@ check_untyped_defs=true check_untyped_defs=true [mypy-aqt.addons] check_untyped_defs=true +[mypy-aqt.emptycards] +check_untyped_defs=true From 6df4cf765e5d24ed5f40a16675868248f6de1cb0 Mon Sep 17 00:00:00 2001 From: ANH Date: Fri, 31 Jul 2020 04:41:49 +0300 Subject: [PATCH 07/15] fix typo --- qt/tools/genhooks_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 2fc9d5024..1548d18ee 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -101,7 +101,7 @@ hooks = [ This won't be called when the user manually plays sounds using `Replay Audio`. - Not that this hook is called even when the `Automatically play audio` + Note that this hook is called even when the `Automatically play audio` option is unchecked; This is so as to allow playing custom sounds regardless of that option.""", ), @@ -116,7 +116,7 @@ hooks = [ This won't be called when the user manually plays sounds using `Replay Audio`. - Not that this hook is called even when the `Automatically play audio` + Note that this hook is called even when the `Automatically play audio` option is unchecked; This is so as to allow playing custom sounds regardless of that option.""", ), From 9b0d509e744c80f2487a26260d5ba72878ee0a2a Mon Sep 17 00:00:00 2001 From: ANH Date: Fri, 31 Jul 2020 04:46:39 +0300 Subject: [PATCH 08/15] just forgot to regenerate gui_hooks.py to fix typo --- qt/aqt/gui_hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 8fd74230a..6f87684b8 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -2121,7 +2121,7 @@ class _ReviewerWillPlayAnswerSoundsHook: This won't be called when the user manually plays sounds using `Replay Audio`. - Not that this hook is called even when the `Automatically play audio` + Note that this hook is called even when the `Automatically play audio` option is unchecked; This is so as to allow playing custom sounds regardless of that option.""" @@ -2160,7 +2160,7 @@ class _ReviewerWillPlayQuestionSoundsHook: This won't be called when the user manually plays sounds using `Replay Audio`. - Not that this hook is called even when the `Automatically play audio` + Note that this hook is called even when the `Automatically play audio` option is unchecked; This is so as to allow playing custom sounds regardless of that option.""" From f7a7c95fc0e4ceeca03cf384476eca6a3a3ba405 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 31 Jul 2020 09:58:06 +1000 Subject: [PATCH 09/15] fix early reviews not appearing in graph https://forums.ankiweb.net/t/differences-between-new-and-old-stats-2-1-28/1602 --- proto/backend.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/backend.proto b/proto/backend.proto index 7c23a4a74..51f2089b3 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -1004,7 +1004,7 @@ message RevlogEntry { LEARNING = 0; REVIEW = 1; RELEARNING = 2; - EARLY_REVIEW = 4; + EARLY_REVIEW = 3; } int64 id = 1; int64 cid = 2; From 3aba7c0eeea2c08fc24b488d5a8f241065bc3eb8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 30 Jul 2020 20:13:16 -0500 Subject: [PATCH 10/15] tweak card template message also shown when importing an invalid file https://anki.tenderapp.com/discussions/private/4937-importing-decks --- rslib/ftl/card-templates.ftl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rslib/ftl/card-templates.ftl b/rslib/ftl/card-templates.ftl index 7a0a13672..75f76aecf 100644 --- a/rslib/ftl/card-templates.ftl +++ b/rslib/ftl/card-templates.ftl @@ -13,6 +13,4 @@ card-templates-preview-box = Preview card-templates-template-box = Template card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion. card-templates-fill-empty = Fill Empty Fields -card-templates-invalid-template-number = Please correct the problems on card template { $number } first. - - +card-templates-invalid-template-number = Card template { $number } has a problem. From cd72d6807a3c58f813148c162a834f81f87c839f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 30 Jul 2020 20:55:55 -0500 Subject: [PATCH 11/15] refresh tag list after clearing unused --- qt/aqt/browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 33469bc45..72d29ef18 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1655,6 +1655,7 @@ update cards set usn=?, mod=?, did=? where id in """ def _clearUnusedTags(self): self.col.tags.registerNotes() + self.on_tag_list_update() # Suspending ###################################################################### From 02424ac789120fd285ca69c3ec1aca64cc1bf2d1 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 30 Jul 2020 21:16:18 -0500 Subject: [PATCH 12/15] tweaks to print view of graphs --- ts/src/stats/graphs.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ts/src/stats/graphs.scss b/ts/src/stats/graphs.scss index 3b12ff522..716a83050 100644 --- a/ts/src/stats/graphs.scss +++ b/ts/src/stats/graphs.scss @@ -36,6 +36,7 @@ body { margin-left: auto; margin-right: auto; max-width: 80em; + page-break-inside: avoid; } .graph h1 { @@ -67,6 +68,12 @@ body { padding: 0.5em; } +@media print { + .range-box { + position: absolute; + } +} + .range-box-pad { height: 4em; } From 469272659f6345cda888b4bbf724d2c88d89ab8f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 31 Jul 2020 12:29:08 +1000 Subject: [PATCH 13/15] fix hour graph not handling timezones west of UTC https://forums.ankiweb.net/t/statistics-bugs-after-update-2-1-5-to-2-1-29/1620 --- proto/backend.proto | 2 +- rslib/src/stats/graphs.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 51f2089b3..01346b02d 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -996,7 +996,7 @@ message GraphsOut { uint32 next_day_at_secs = 4; uint32 scheduler_version = 5; /// Seconds to add to UTC timestamps to get local time. - uint32 local_offset_secs = 7; + int32 local_offset_secs = 7; } message RevlogEntry { diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index 48c897d34..601de79fe 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -41,7 +41,7 @@ impl Collection { days_elapsed: timing.days_elapsed, next_day_at_secs: timing.next_day_at as u32, scheduler_version: self.sched_ver() as u32, - local_offset_secs: local_offset_secs as u32, + local_offset_secs: local_offset_secs as i32, }) } } From 2bcf9a82d1e55faa1eca8927f30b1e6b9844a4ee Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 31 Jul 2020 12:38:38 +1000 Subject: [PATCH 14/15] fix missing translations in export screen https://forums.ankiweb.net/t/untranslated-strings/1623 --- pylib/anki/exporting.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index b65ae58bc..3d72175f9 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -8,7 +8,7 @@ import shutil import unicodedata import zipfile from io import BufferedWriter -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from zipfile import ZipFile from anki import hooks @@ -20,7 +20,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML class Exporter: includeHTML: Union[bool, None] = None ext: Optional[str] = None - key: Optional[str] = None + key: Union[str, Callable, None] = None includeTags: Optional[bool] = None includeSched: Optional[bool] = None includeMedia: Optional[bool] = None @@ -92,7 +92,7 @@ class Exporter: class TextCardExporter(Exporter): - key = _("Cards in Plain Text") + key = lambda: _("Cards in Plain Text") ext = ".txt" includeHTML = True @@ -122,7 +122,7 @@ class TextCardExporter(Exporter): class TextNoteExporter(Exporter): - key = _("Notes in Plain Text") + key = lambda: _("Notes in Plain Text") ext = ".txt" includeTags = True includeHTML = True @@ -164,7 +164,7 @@ where cards.id in %s)""" class AnkiExporter(Exporter): - key = _("Anki 2.0 Deck") + key = lambda: _("Anki 2.0 Deck") ext = ".anki2" includeSched: Union[bool, None] = False includeMedia = True @@ -313,7 +313,7 @@ class AnkiExporter(Exporter): class AnkiPackageExporter(AnkiExporter): - key = _("Anki Deck Package") + key = lambda: _("Anki Deck Package") ext = ".apkg" def __init__(self, col: Collection) -> None: @@ -394,7 +394,7 @@ class AnkiPackageExporter(AnkiExporter): class AnkiCollectionPackageExporter(AnkiPackageExporter): - key = _("Anki Collection Package") + key = lambda: _("Anki Collection Package") ext = ".colpkg" verbatim = True includeSched = None @@ -426,7 +426,11 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter): def exporters() -> List[Tuple[str, Any]]: def id(obj): - return ("%s (*%s)" % (obj.key, obj.ext), obj) + if callable(obj.key): + key_str = obj.key() + else: + key_str = obj.key + return ("%s (*%s)" % (key_str, obj.ext), obj) exps = [ id(AnkiCollectionPackageExporter), From 0edb043f535de84b19b4eb4b3281530bdced6f10 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 31 Jul 2020 12:41:43 +1000 Subject: [PATCH 15/15] i18n some card template strings https://forums.ankiweb.net/t/untranslated-strings/1623 --- qt/aqt/clayout.py | 4 ++-- rslib/ftl/card-templates.ftl | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 88adc5060..080b153bb 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -750,7 +750,7 @@ Enter deck to place new %s cards in, or leave blank:""" showWarning(str(e)) return self.mw.reset() - tooltip("Changes saved.", parent=self.parent()) + tooltip(tr(TR.CARD_TEMPLATES_CHANGES_SAVED), parent=self.parent()) self.cleanup() gui_hooks.sidebar_should_refresh_notetypes() return QDialog.accept(self) @@ -759,7 +759,7 @@ Enter deck to place new %s cards in, or leave blank:""" def reject(self) -> None: if self.change_tracker.changed(): - if not askUser("Discard changes?"): + if not askUser(tr(TR.CARD_TEMPLATES_DISCARD_CHANGES)): return self.cleanup() return QDialog.reject(self) diff --git a/rslib/ftl/card-templates.ftl b/rslib/ftl/card-templates.ftl index 75f76aecf..1cc96c0e6 100644 --- a/rslib/ftl/card-templates.ftl +++ b/rslib/ftl/card-templates.ftl @@ -14,3 +14,5 @@ card-templates-template-box = Template card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion. card-templates-fill-empty = Fill Empty Fields card-templates-invalid-template-number = Card template { $number } has a problem. +card-templates-changes-saved = Changes saved. +card-templates-discard-changes = Discard changes?